Stop resolution changes stranding the desktop and spamming dialogs (#114, #132) - #159
Open
SwatX18 wants to merge 2 commits into
Open
Stop resolution changes stranding the desktop and spamming dialogs (#114, #132)#159SwatX18 wants to merge 2 commits into
SwatX18 wants to merge 2 commits into
Conversation
…uv#114, juv#132) A failing resolution change popped a modal MessageBox from inside the foreground callback, on the UI thread the WinEvent hook runs on. It reappeared on every alt-tab and blocked the hook while it was up. There is now no MessageBox anywhere the hook can reach. A failure logs once per device and failure code, raises one tray balloon, and stops being attempted after a bound - 3 attempts to enter a game's resolution, 10 to leave it, since giving up on the way out leaves the desktop stuck at the game's resolution. Success was read from the wrong call. CDS_NORESET only staged the change; a second, unconditional call applied it with its return value discarded, so success could be reported for a mode that never landed. The sequence is now CDS_TEST then CDS_UPDATEREGISTRY - one authoritative result, and a mode that fails validation is never written to the registry. The mode is read back after the apply; a driver that reports success without changing anything now yields AppliedUnverified rather than a plain failure, since the mode most likely did change and reporting it failed would strand the desktop at the game's resolution. The captured Windows resolution was never refreshed after startup, so changing the desktop resolution in Windows got quietly undone the next time a game exited. It now refreshes on SystemEvents.DisplaySettingsChanged, while keeping the captured mode for any screen a resolution change is currently applied on. The resolution-needed check compared DmDisplayFixedOutput, which dmFields never declared, so a scaling-qualified mode could never satisfy the guard and re-fired a real mode set and registry write on every foreground event. The guard now compares only the four fields a mode change controls, and dmFields declares what it sets without dropping bits (DM_POSITION above all) EnumDisplaySettings already returned. Adds an IDisplayModeDevice seam between this state/failure-handling logic and the real display driver, so a fake device can drive the same apply/revert/failure/bound logic without ever touching a real display.
) ResolutionChangeFixture drives ChangeResolutionEx and IsResolutionChangeNeeded through the IDisplayModeDevice seam against a fake device, covering the two-call CDS_TEST/CDS_UPDATEREGISTRY sequence, the dmFields mask, every DispChange failure code, the apply/revert failure bounds, and the revert path - 51 checks, no hardware, no live display, ever. Run via vibrance.GUI.exe --selftest-resolution, wired the same way as the other early-return, no-driver-needed self tests in Main.
This was referenced Aug 27, 2026
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Four defects behind #114 and #132, both "Changing the resolution failed: DispChangeBadFlags". Split into two commits so the fixes can be taken without the testing convention — see "About the second commit".
There is no flags bug to find
Worth saying first, because it shapes the whole change.
DISP_CHANGE_BADFLAGSmeans "an invalid set of flags was passed in", so that is where I looked, and ruled it out:CDS_UPDATEREGISTRY | CDS_NORESETthen a global commit is the pattern MSDN documents; both documented BADFLAGS rules are satisfied,hwndandlParamcorrectly NULLDevmodestruct layoutsizeof(DEVMODEA)on x86; the display arm of the union is exactly the size of the printer arm, so every following member sits at the right offsetdmSizeMarshal.SizeOfgives 156, set before the call, and the API overwrites it anywayCharSeton anyDllImport, soAnsi, binding the...Aentry points — consistent withCharSet.AnsionDevmodeSo
-4originates inside win32k for a reason this source cannot express — most plausibly a device name that cannot take a mode set (mirroring or indirect-display drivers, hybrid-laptop render adapters). This change therefore targets everything around the failure, which is where the user harm actually is. Afterwards a persistent-4costs three attempts, one balloon, one log line and no dialog — and the log names the device and the code, which is the reproduction data neither issue ever provided.1. A modal dialog from inside the foreground callback
ResolutionHelper.cs:122callsMessageBox.Showon failure, reached viaPerformResolutionChangefrom both proxies'OnWinEventHook— the UI thread theSetWinEventHookcallback runs on. It reappears on every foreground change, forever, and blocks the hook while open. That is #132's "it keeps on saying that", and the user cannot dismiss their way out.No
MessageBoxis now reachable from the hook. A failure logs once per (device, failure code), raises one tray balloon, and stops being attempted after a bound.2. Success is read from the wrong call
CDS_NORESETonly stages the change. The second call applies it, and its return value is discarded — so the helper can report success for a mode that never landed.Now
CDS_TESTthenCDS_UPDATEREGISTRY: one authoritative result, and a mode that fails validation is never written to the registry. That also dissolves the rollback question — there is no longer a window where the registry holds a mode that was never applied. Which matters, becauseCDS_UPDATEREGISTRYgets no 15-second revert-if-unconfirmed safety net.The mode is then read back. A driver reporting success without changing anything yields
AppliedUnverifiedrather than a plain failure — because the mode probably did change, and reporting failure would clear the flag the revert path depends on, leaving the desktop at the game's resolution while telling the user the opposite.3. The frozen snapshot — likely #114's actual complaint
_windowsResolutionSettingsis built once in theVibranceGUIconstructor and never refreshed, and nothing in the codebase reacts to a display change (zero hits forSystemEvents,DisplaySettingsChanged,WM_DISPLAYCHANGE).So: change your desktop resolution in Windows, and vibranceGUI's idea of "the Windows resolution" is still the startup one. The next time a game exits, the revert path drags the desktop back. With every API call returning
DISP_CHANGE_SUCCESSFUL. That is #114's "i tried changing it back" verbatim, and no amount of investigating BADFLAGS would have found it.It now refreshes on
SystemEvents.DisplaySettingsChanged, keeping the captured Windows mode while one of ours is applied — so a game going fullscreen cannot become the mode we later restore to.One subtlety worth flagging for review: the handler checks
IsDisposed || !IsHandleCreatedbeforeInvokeRequired.Control.InvokeRequiredreturns false when the control has no handle, so without that check the dictionary mutation would run on theSystemEventsthread during the constructor — which is exactly when autostart-at-logon fires the event as monitors settle.4. The retry storm
IsResolutionChangeNeededcompares viaResolutionModeWrapper.Equals, which includesDmDisplayFixedOutput— butdmFieldsnever declaredDM_DISPLAYFIXEDOUTPUT, so drivers may ignore it. A user selecting a "(Center)" mode gets a change that succeeds on the four real fields yet can never satisfy the guard, re-firing a real mode set and registry write on every foreground event.The guard now compares only the four fields a mode change controls, and
dmFieldsdeclares what it sets — OR-ed into whatEnumDisplaySettingsreturned, never overwritten, since dropping the enumeratedDM_POSITIONwould let a multi-monitor desktop rearrange itself.Asymmetric failure bounds — deliberate
Three attempts to enter a game's resolution, ten to leave it. Giving up on the way in costs a feature; giving up on the way out leaves someone's desktop at 1280x720.
Removed
ChangeResolution(verified zero callers onmasterbefore deleting), theChangeDisplaySettingsP/Invoke, and theChangeDisplaySettingsExoverload used only by the discarded global commit.Verification
Warnings unchanged.
masterbuilds with 2 warnings (ResolutionModeWrapperCS0659,WinEventHook.csCS0168); this branch builds with the same 2, both configurations. Neither is touched here.About the second commit
The repo has no test project, so the second commit adds
--selftest-resolutionand aResolutionChangeFixture— 51 checks behind anIDisplayModeDeviceseam with a fake backed by an in-memory mode table and a recorded call log. It covers the two-call sequence, thedmFieldsmask, all six failure codes, the fixed-output fallback, both bounds, the revert cycle and the no-repeat-notification behaviour.This introduces a convention that does not exist here yet. It is a separate commit specifically so you can drop it and take only the fixes.
It is real evidence rather than decoration: every one of the 15 check methods was individually broken — one production line at a time — and confirmed to produce a clean
[FAIL]before being restored and hash-verified. ReintroducingCDS_NORESETbreaks check 1; dropping thedmFieldsmask breaks check 2; bypassing theCDS_TESTgate breaks check 3; reintroducingDmDisplayFixedOutputinto the guard breaks check 12 — that last one is exactly the #132 regression.There is no hardware variant of this fixture and there must never be one. A display mode has no guaranteed undo, and a mode the panel cannot show leaves the user unable to see any dialog asking them to confirm it. That is literally #114.
Known limits
-4has not been reproduced on real hardware. Two of the three ranked hypotheses are environment facts invisible from here. I would not claim this makes-4disappear — it makes it survivable and diagnosable.DisplaySettingsChangedrefresh has no automated test. The fixture drivesResolutionHelper; the dictionary rebuild lives inVibranceGUIbehindScreen.AllScreenswith no seam. Verified by reading and independently traced, not covered by a fixture.-p:TargetFrameworkVersion=v4.8. The csproj still declaresv4.0and was not edited.isResolutionChangeAppliedis added toVibranceInfo— defect 3 needs to know a change is currently applied so the refresh does not capture the game's own mode as the desktop resolution.Happy to drop the fixture commit, split this into separate PRs, or adjust anything else you would prefer.