Sunday, July 26, 2026

Psychic Fatigue, Part I: The Rabbit Hole

First post in our series: Our family took a trip to Colorado and Utah a couple of weeks ago.  And I could say, I absolutely loved it.  But there is a huge twist...

Hey!  Today I'm starting a series of articles based on my experience debugging for an open source project on a trip.

For the trip itself: I'm like a Texan cowboy, ya' know, so Colorado is kind of cold, but that much cold never really bothered me [0], especially during summer.  So, I got to enjoy both states' breathtaking beauty and let the fresh air just sink into my skin, all while I enjoyed the vast, open, and grand, yet slightly mystical landscapes in both states' national parks.

Yet, I got no more relaxed at the end of the trip.  In fact, I got burnt out, and did exactly nothing for the next 2 days when I came home.  But why?

(DISCLAIMER:  This is an issue of my own making.  Anyone mentioned below did not do anything wrong, or cause this situation either directly or indirectly.)  I am also not affiliated with any project or entity mentioned below, and do not have influence over their future direction.

The technical details

I'm a semi-frequent contributor to a free-and-open-source project named BeeWare, despite the fact that I'm not affiliated with it.  In their official branding guidelines, they describe themselves as

BeeWare is a collection of tools and libraries to aid in writing application user interfaces in Python. It enables developers to write applications for iPhone, Android, Linux, macOS and Windows that are indistinguishable from natively written applications.

And to aid in writing UIs, we need a GUI toolkit.  BeeWare has its own, named Toga, which is an abstraction over the platforms' native GUI toolkits.

Toga uses WinForms on Windows currently.  From my perspective, it would benefit from a migration to WinUI3 [1].  I saw that a contributor has been working on this, but cannot make statements about this work's further status.  In the meantime, however, this ticket showed up on Toga's issue tracker.

When moving a [T]oga window from one monitor to another one (with a different [DPI] scale) on [W]indows [the WinForms backend], the layout is resized in an inconsistent way. It leads to:

  • font sizes increased / decreased
  • window size increased / decreased in width or height (not proportionally)
  • layouts with fixed width / height cutting words

... and after a few details were clarified, it seems that the font is magnified doubly in some cases when moving a window from one screen to another; that is the main issue, although the other window size concerns do exist.

The code

I first saw the mentioned issue on our first at Moab, Utah.  That afternoon when we arrived there from Colorado National Monument, we drove in the great landscapes of Arches National Park.  I got bored in the car during the drive, and scrolled through the list of issues, and found that ticket.  A maintainer called the DPI scaling a "complicated area of code" on the ticket, so of course, liking to untangle complicated things, I started digging in the codebase of Toga's WinForms DPI scaling with the slow-but-usable visitor center WiFi and the occasional 2-or-3-grid 5G signals.

It didn't take me much more than 30 minutes in total to figure out how the code was structured.  But working on a small phone and scrolling rapidly through cross-references certainly hurt my neck.  The layout was done in roughly this manner:

  1. The project can work with either the latest version of .NET Framework or .NET Core.
  2. Before any other GUI elements are declared or used, SetProcessDpiAwarenessContext is called with DPI_AWARENESS_CONTEXT_PER_MONITOR_AWARE_V2 to enable per-monitor DPI scaling.
  3. All layout math is done manually, and absolute positioning of controls is used (i.e., using .Location and .Size, except for menu- and toolbars (I'm going to say toolbars below) which are docked to the window.
  4. Each control has a font created and assigned explicitly; the properties (family, size, and style) of the font are taken from MessageBoxFont, instead of the default because of a historical WinForms issue (https://github.com/dotnet/winforms/issues/524).
  5. Since we don't use an app.config file to set DPI awareness, the WinForms event DpiChanged seems to be not firing; thus, DPI changes are determined based on whether the DPI changes in Form.ResizeForm.LocationChanged, and SystemEvents.DisplaySettingsChanged.
  6. The DPI of a window is obtained by using MonitorFromWindow to get a monitor, and then using GetDpiForMonitor from shcore.dll to obtain the DPI value.
  7. When the DPI does change, first the font sizes of all controls are recomputed based on the new scale factor, and then all the layout math is redone to use the new scale, and then the position, size, etc. are re-evaluated and assigned.  A WinForms font is stored with the logical size, and then when DPI changes, the font is scaled and reassigned to the widget.  Initial font sizes are specified in Points.
  8. Since we explicitly set position and size, there is no usage of AutoScale nor AutoSize; after we scale the font of a control, remeasure its preferred dimensions, scale it out by the DPI, rerun our layout, and then scale the control dimensions in by the DPI again to assign to position and size properties.

So after I figured out all this worked and had at least a semi-coherent mental model, I kept looking at the details while we kept driving.

A hack

It wasn't long before I noticed this hack sitting in the font scaling code (item 7 in above list):

# At a global level:
# It looks like something is caching the initial scale of the primary screen, and # scaling all font sizes by it. Experiments show that this cache is at the level of the # app, not the window. initial_dpi_scale = ScreenImpl(WinForms.Screen.PrimaryScreen).dpi_scale

# [omitted] # In the Window class for scaling a predeclared WinForms Font to a specific DPI:
def scale_font(self, native_font): return WinFont( native_font.FontFamily, native_font.Size * (self.dpi_scale / initial_dpi_scale), native_font.Style, )

Now, since the font sizes scaled doubly, I figured that the font scaling behavior requiring the initial_dpi_scale workaround may not be just limited to the initial scale of the primary screen, and that something is also happening that causes this scaling to happen when the DPI is updated as well.  I asked Gemini how to resolve this, and it told me to switch the font sizing to use Pixels, not Points, which required a small conversion.  Well, I was going to let it go for now, but then, since we found Arches was too hot to hike right then, we drove into our hotel at early-to-mid afternoon.  So I thought I should do some more investigation on this.

Let it Go

Well, first thing that I needed to do was to set up a testing environment so I could get things tested faster instead of bothering the reporter.  I virtualized Windows ARM64 on my M1 Pro Macbook from the early 2020's, so after asking Gemini, what I needed was a Virtual Display Driver that supported ARM64 Windows, which was rare.

Well, then.  I looked into https://github.com/VirtualDrivers/Virtual-Display-Driver, and immediately downloaded the "Virtual Driver Control" GUI.

... and turns out it supports x86_64 only!  Installing the driver manually required me to use "Add Driver..." in Device Manager, which did not succeed.  I then spent the next 40 minutes or so uninstalling VDD's GUI, cleaning up files, reinstalling, restarting Windows... all without success [2].

Then I tried using UTM's settings to create new monitors.  Most of the time, it straight up did not work.  The few times it did, Windows only detected one screen to display on.  A few times it even blue-screened.

And when I finished, it was already time to revisit Arches National Park to do a couple of hikes; we ended up watching the stars there without going back to the hotel, so I didn't even get time to post my (still untested) findings until 10pm that night (which was really late, as the next morning we were going to Canyonlands.)  Surprisingly, one of the locations I hiked was named (not Microsoft, sorry) Windows arch (along with a viewpoint to Delicate Arch).

This was all a sign that I should've just let it go sooner, and started working on the actual Proof-of-Concept fix.

Experimentation

Unfortunately but expectedly, I woke up later than I wanted to the next morning, so I had to go straight down for breakfast.  It took me a while to find the correct route, because the hotel was simply a maze; but when I went down alone to get breakfast first, I took my phone and started probing Google Gemini like crazy on the DPI issue.  This is probably the craziest thing I've done in this time, but still, it gave me enough confidence that the Pixel sizing will remove the workaround documented in the code to get me to stop thinking about it.  At least, for a while.

I also checked my GitHub account, and the reporter of the issue told me that it would be permissible for me to send branch references for them to test.  With this done, the few hours I did yesterday proved completely futile, yet it further motivated me to provide a proof-of-concept fix with my findings.  At this point, the puzzle became impossible to put down for me (because of my own compulsive tendencies, not because of the issue itself was captivating), so I knew I had to try it later.

That morning, we visited Canyonlands.  There were 4 districts, so I did our preliminary research, and we went to Island in the Sky.  And on the car, while the scenery spilled into my eyes, images of the UI of the reproduction case started flashing through my head.

And from here, I've officially entered the first stage of an addiction: experimentation.

To be continued...

... and will the fix prove to be correct?  Find out in the next post of this series!

Footnotes and References

[0] ...anyways. [slams door]
[1] One may say I'm just wasting time writing about WinForms in the great '26, but if you look at the aesthetics of this website, this is clearly a 2010-themed blog, so it's only natural that I cover 2010-themed issues here.
[2] I ended up finding I forgot test signing.  Duh!

No comments:

Post a Comment

Psychic Fatigue, Part I: The Rabbit Hole

First post in our series: Our family took a trip to Colorado and Utah a couple of weeks ago.  And I could say, I absolutely loved it.  But t...