06. First pass on real GPU hardware

Date: 2026-08-09 Verdict: The five items no sandbox could reach are now verified. One new defect, D13, found and fixed. The published estimate of the interface freeze was wrong by a factor of three, in the app's favour.

This is the first report not produced in a Windows Sandbox. Every earlier pass ran against Microsoft Remote Display Adapter, a virtual driver that publishes one mode and accepts every resolution without applying any. Sections 1 to 5 below are the items docs/audits/README.md lists as untestable under that condition.


Environment

OS Windows 11 Pro, build 26200
Python 3.12.10
Tk 8.6
GPU NVIDIA Quadro RTX 4000, driver 32.0.15.7365
Display LG Electronics LG TV over HDMI, native 2560x1440
Desktop mode during tests 1920x1080 at 50 Hz, 32 bpp
Session console, not Remote Desktop
Run from source, not a packaged build

The session type matters. An earlier attempt at this pass was made over RDP, where the desktop is carried by the remote display adapter and the NVIDIA driver is never on the path. SystemInformation.TerminalServerSession returned False for this pass, and the remote adapter was absent from the adapter list.

The panel is a 60 Hz television, not a high refresh gaming monitor. That shapes what section 1 can and cannot prove. It is stated there rather than buried here.


0. Registering the custom resolutions

QUICK_LIST holds nine resolutions. Before the pass the driver published 108 modes across 20 distinct resolutions, and six of the nine tiles were covered.

Adding a custom resolution in NVIDIA Control Panel is not sufficient on its own. 1600x1080 was created and checked in the Customize dialog, and did not appear in EnumDisplaySettingsW output. It appeared only under EDS_RAWMODE, which is the flag that returns modes the driver knows but does not publish. It became visible to the normal enumeration only after being applied once, through Apply and the Keep changes prompt.

That is worth knowing before anyone concludes the app is broken. A resolution can exist in the GPU control panel and still be invisible to ChangeDisplaySettingsW.

Coverage at the end of the pass, against the published list:

Tile Published Tile Published
1920x1080 yes 1600x1080 yes, after an Apply
2560x1440 no, RAWMODE only 1280x1080 yes
1920x1440 no 1350x1080 no
1440x1080 yes 1280x1024 yes
1280x960 yes

2560x1440 is the panel's native resolution and it still refuses to publish, including after a successful Test in the Edit Custom Resolution dialog. Not diagnosed. It is a driver and EDID question, not an application one, and the app's behaviour when a mode is absent is exactly what section 2 verifies.

1350x1080 being absent was left in place deliberately. It gave section 2 a real rejection to measure.


1. Refresh rate preservation

The headline correctness fix. Never exercised before, because the virtual display reported 32 Hz and offered a single mode.

Method. Set the desktop to 1920x1080 at 50 Hz through Windows Settings. Call set_resolution(1440, 1080). Read back with EnumDisplaySettingsW(ENUM_CURRENT_SETTINGS).

start                : 1920x1080 @ 50 Hz
set_resolution(1440, 1080)
returned             : True
message              : Now running 1440 x 1080 at 50 Hz
independent readback : 1440x1080 @ 50 Hz

Pass. The rate survived the switch.

The test discriminates. 1440x1080 publishes 23, 24, 25, 29, 30, 50, 59 and 60 Hz on this panel, so 60 Hz was available and was not chosen. An implementation that dropped DM_DISPLAYFREQUENCY, or wrote 60, would have produced 60 Hz here.

What this does not prove. The panel tops out at 60 Hz. The verified gap is 50 against 60. The scenario in the source comment, a 240 Hz panel silently falling to 60 mid-match, remains unverified at that magnitude. The mechanism is the same and the field carries the same way, but the claim that should be made is "preserves a non-default rate on real hardware", not "verified on a high refresh panel".

sizeof(DEVMODE) was confirmed at 220 in the same run.


2. A real DISP_CHANGE_BADMODE

Every previous observation of this path came from injection, because the virtual driver returned 0 for everything.

Method. Request 1350x1080, which the driver does not publish and does not know even under EDS_RAWMODE.

returned  : False
message   : That resolution isn't registered with your GPU driver. Add it in
            your GPU control panel first (see the FAQ). (code -2)
readback  : 1920x1080 @ 50 Hz, unchanged

Pass. Code -2 from a real driver, the mapped message, and the display did not move. The last point matters as much as the code. A rejection that left the screen in an intermediate state would be worse than the error itself.


3. Restore native

Previously exercised only on a display that had never actually changed.

Method. Switch to 1600x1080, then call ChangeDisplaySettingsW(NULL, 0).

result code : 0
readback    : 1920x1080 @ 50 Hz

Pass. Returned to the registry mode from a display that had genuinely moved.


4. How long the interface freezes

docs/audits/README.md item 6 states 1 to 3 seconds on real hardware. That figure was an estimate, never measured, and it is wrong.

Method. Ten cycles. Each cycle switches to 1440x1080, back to 1920x1080, then requests the unregistered 1350x1080. One second of settling between switches. Timing wraps the set_resolution() call only, so it is the interval the single threaded interface is blocked.

Transition n Median Min Max
1920x1080 to 1440x1080 10 462 ms 436 529
1440x1080 to 1920x1080 10 629 ms 512 769
Rejection, code -2 10 97 ms 94 104

Conditions, because a bare number here caused a problem once already. Quadro RTX 4000, driver 32.0.15.7365, LG television over HDMI, 50 Hz throughout, run from source under Python 3.12.10, all three modes sharing the same physical link.

Recommendation: no spinner and no thread. The worst single measurement was 769 ms. At that duration the click does not read as a hang, and a thread would put synchronisation into an application whose entire pitch is that one reading of one file is enough to trust it. The cost is not worth the benefit.

Two caveats to carry. Every mode measured here runs over the same HDMI link at the same rate, so nothing renegotiated the link. A switch to a mode that changes the link, 2560x1440 being the obvious candidate, was not measurable because that mode does not publish. And a rejection at 97 ms means an unregistered tile answers almost immediately, which is the common case for a new user who has not created custom resolutions yet.


5. DPI at 125 and 150 percent

Report 04 measured the status strip at these scales through forced tk scaling. That is how the clipping at wraplength=px(288) was found. Nothing had ever been rendered on a display actually set to them.

Method. Windows Settings, System, Display, Scale. Application relaunched after each change, since Metrics is computed once at startup. Windows captured, window rectangles read with a DPI aware probe.

Scale System DPI Main window FAQ window
125% 120 531 x 660 518 x 697
150% 144 636 x 790 622 x 836

636 / 531 is 1.198 and 790 / 660 is 1.197, against an expected 1.2. Metrics.px() scales linearly across the step.

Checked at both scales and clean at both:

The DISP_CHANGE_BADMODE message, which is the reason the strip is sized the way it is, renders in two lines at both 125 and 150 percent and is not clipped. It was produced by clicking the 1350x1080 tile, so it is the real message from a real rejection, not injected text. The px(52) reserved height and the px(320) wraplength hold as a pair on hardware.

Changing scale under a running instance

Not on the checklist, taken because the opportunity was there. With the application open, scaling was changed from 125 to 150 percent.

The FAQ toplevel was resized by Windows to exactly 1.2 times its previous size, 518 x 697 to 622 x 836, while its content kept the metrics computed at 125 percent. The result is an oversized window with slack around the content. Nothing is clipped and every control stays mapped. The main window did not resize at all.

Benign, and it resolves on restart. Recorded rather than filed as a defect, because the application computes Metrics once by design and handling WM_DPICHANGED would mean a full reflow for a case that costs the user a relaunch.


6. D13. The FAQ window shipped Tk's icon

Found at 125 percent, present at every scale, and not DPI related.

The main window carries the red mark. The FAQ window carried the blue Tk feather.

Cause, in _set_window_icon():

self.iconbitmap(resource_path("icon.ico"))
return

Tk's iconbitmap without default= applies the icon to that window alone. The return on success means the fallback below it, iconphoto(True, ...), is never reached, and that is the call that would have propagated to new toplevels. So the defect only appears when icon.ico loads correctly. Before the icon was baked into the .spec, the failing path was taken and every window got the same fallback icon.

Same family as D12, where both builds shipped PyInstaller's blue floppy disk. An icon that is right on the window everyone looks at and wrong on the one they open second.

Fixed by passing default=, which applies to the window and to every toplevel created after it. Verified by relaunching and reopening the FAQ. The red mark is now on both windows at 125 and 150 percent.


What this pass did not test

  1. A high refresh panel. Section 1 verified 50 Hz against an available 60. Nothing above 60 Hz exists on this display.
  2. 2560x1440, 1920x1440 and 1350x1080. Three of the nine tiles have no registered mode. 1350x1080 was used as the rejection case rather than fixed.
  3. A mode change that renegotiates the physical link. All timings in section 4 share one link at one rate.
  4. Multi-monitor. One display, and the app uses ChangeDisplaySettingsW, which has no device parameter.
  5. The packaged build. This pass ran from source. D13 is a source change and has not been rebuilt or reverified through PyInstaller.
  6. Network, process and registry surface. Not resampled here. Report 05 covers it on the one-directory build and nothing in this pass touched that surface.
  7. 100 percent scaling. The display was at 125 percent when the pass began and was not returned to 100 for a render. Report 04 covers it.
  8. MSIX. Still nothing. No makeappx, no certificate, no local install, no Windows App Certification Kit.

Corrections carried to earlier reports