Skip to content

Fix Game Mode EcoQoS opt-out + cleanup batch for v2.0.5#14

Merged
Layellie merged 1 commit into
mainfrom
fix/game-mode-throttle-class
Jun 26, 2026
Merged

Fix Game Mode EcoQoS opt-out + cleanup batch for v2.0.5#14
Layellie merged 1 commit into
mainfrom
fix/game-mode-throttle-class

Conversation

@Layellie

Copy link
Copy Markdown
Owner

Summary

v2.0.5 ships one critical bug fix (Game Mode silently wasn't doing its main job on Win 11) plus four cleanup wins from an internal code audit.

#1 — Fix Game Mode EcoQoS opt-out (regression bug)

ProcessOptimizationService.ApplyTimerResolutionOptOut was calling SetProcessInformation(handle, 34, …). PROCESS_INFORMATION_CLASS::ProcessPowerThrottling is 4, not 34 — the exact typo that was already fixed in App.OnStartup. With 34 the kernel returns ERROR_INVALID_PARAMETER (87) and IGNORE_TIMER_RESOLUTION never applies, so any game added to Game Mode kept getting EcoQoS-throttled to 1–15.6 ms in the background. The 0.5 ms global lock simply didn't reach those processes.

Fix: replaced both literals with NativeMethods.ProcessPowerThrottling = 4 (new named constant) so the magic number can't be re-introduced.

#2 — Import Settings now applies live

SettingsViewModel.ImportSettings was writing to the registry but not re-applying values to the running session — users had to restart for an import to take effect. Now it raises a SettingsImported event with the loaded snapshot; MainViewModel subscribes and re-runs ApplySettings. The "restart to apply" success string is updated accordingly.

#3 — Dead code removed

  • TimerResolutionService.MeasureActualResolutionMs — unused since the watchdog reads current directly from NtSetTimerResolution.
  • NativeMethods.GetSystemTimeAsFileTime — only existed for MeasureActualResolutionMs.
  • NativeMethods.TimeBeginPeriod / TimeEndPeriod — explicit comment in ApplyTimerRequest says these must never be called (Win 11 clamps Nt request to 1 ms).

#4 — AVRT handle leak in ApplyTimerRequest

The activation path was calling AvSetMmThreadCharacteristics("Pro Audio", …) on the caller thread (the dispatcher), then discarding the returned handle. That permanently boosted the UI thread to MMCSS and leaked one avrt handle per Activate/Deactivate cycle. The dedicated watchdog thread already does AVRT correctly (stores handle, reverts in finally), and the kernel request is per-process — not per-thread — so the caller-thread call had no benefit. Removed.

#5 — Split SyncMonitorSettings

SyncMonitorSettings() was rebuilding the entire GamePaths list (Select(...).ToList()) on every keystroke into the threshold textboxes, even though the games list hadn't changed. Split into SyncMonitorThresholds() (called on every textbox change) and SyncMonitorGames() (called only when the Games collection actually mutates).

Version bump

2.0.4 → 2.0.5 in csproj, Setup.iss, build-installer.ps1.

Test plan

  • Release build clean: 0 warnings, 0 errors with EnforceCodeStyleInBuild
  • Game Mode adds a game → process is marked with IGNORE_TIMER_RESOLUTION. Confirmable via Process Hacker → process properties → "Power throttling: Disabled"
  • Export settings → edit JSON → Import → values appear in UI immediately, no restart
  • Typing in Standby/Free threshold inputs no longer triggers GamePaths list rebuild (verify by setting a breakpoint in SyncMonitorGames)
  • Timer still locks at 0.500 ms after Activate/Deactivate cycles (no AVRT handle accumulation in handle table)

#1 (critical bug): ProcessOptimizationService.cs:80 was calling
SetProcessInformation with processInformationClass=34 — the exact
typo that App.OnStartup already had fixed to 4. With 34 the call
returns ERROR_INVALID_PARAMETER and IGNORE_TIMER_RESOLUTION never
applies, so games added to Game Mode kept getting EcoQoS-throttled
to 1–15.6 ms in the background, defeating the whole point of the
0.5 ms global lock for those processes. Replaced both call sites
(this one and App.xaml.cs) with a new
NativeMethods.ProcessPowerThrottling = 4 named constant so the
magic number can't be re-introduced.

#2: SettingsViewModel.ImportSettings now raises a SettingsImported
event with the freshly loaded snapshot; MainViewModel subscribes and
re-applies it via ApplySettings, so the running session updates
without a restart. The localized "restart to apply" success string
is now obsolete and reads just "Settings imported".

#3: Removed dead code — MeasureActualResolutionMs is no longer
called anywhere (the watchdog reads `current` directly from
NtSetTimerResolution), and its three P/Invokes
(GetSystemTimeAsFileTime, TimeBeginPeriod, TimeEndPeriod) went
with it. The legacy timeBeginPeriod path had an explicit warning
comment about why it must never be re-enabled.

#4: ApplyTimerRequest used to register the *caller* thread as AVRT
"Pro Audio" and drop the returned handle on the floor — that
permanently boosted the dispatcher to MMCSS and leaked one handle
per Activate cycle. The dedicated watchdog thread already does
MMCSS registration AND reverts it on exit (handle stored in a
local, AvRevertMmThreadCharacteristics called in finally), so the
caller-thread call was both wrong (per-process kernel request,
not per-thread) and leaky. Removed.

#5: SyncMonitorSettings split into SyncMonitorThresholds (called on
every TextBox keystroke) and SyncMonitorGames (called only when the
Games collection mutates). Stops rebuilding the GamePaths list on
every digit the user types into a threshold input.

Bumped to 2.0.5 in csproj, Setup.iss and build-installer.ps1.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
@Layellie Layellie merged commit b5a7e8b into main Jun 26, 2026
1 check passed
@Layellie Layellie deleted the fix/game-mode-throttle-class branch June 26, 2026 21:55
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant