From 9dc0fcd961969db4d358776e38982f26ab649ad3 Mon Sep 17 00:00:00 2001 From: Views2k <292889387+Views2k@users.noreply.github.com> Date: Thu, 15 Oct 2026 22:27:42 +0000 Subject: [PATCH 1/8] Keep Wisp's GPU scheduling priority at the game's level The renderer raised the process scheduling class to HIGH and the device thread priority to +1 at every hardware device creation. That lets the overlay's presents preempt the game's GPU work on each frame, which matches a report of frame drops, stutter and short audio glitches while Wisp is on screen. The renderer now only reads the effective priorities back for diagnostics; the contract tests require that creation leaves the process class unchanged. --- .../NativeContractTests.cpp | 29 +++++++------------ .../Wisp.NativeRenderer.cpp | 19 ++++++------ src/Wisp.NativeRenderer/Wisp.NativeRenderer.h | 4 ++- 3 files changed, 24 insertions(+), 28 deletions(-) diff --git a/src/Wisp.NativeRenderer/NativeContractTests.cpp b/src/Wisp.NativeRenderer/NativeContractTests.cpp index 83b0fd72..a094a881 100644 --- a/src/Wisp.NativeRenderer/NativeContractTests.cpp +++ b/src/Wisp.NativeRenderer/NativeContractTests.cpp @@ -266,9 +266,9 @@ static_assert(sizeof(WispWaitMetrics) == 48 && sizeof(WispWaitMetricsV2) == 72 & static bool IsUnattempted(const WispGpuPriorityStatus& status) { - return status.attempted == 0 && status.requestedProcessClass == D3DKMT_SCHEDULINGPRIORITYCLASS_HIGH && + return status.attempted == 0 && status.requestedProcessClass == D3DKMT_SCHEDULINGPRIORITYCLASS_NORMAL && status.processSetStatus == INT32_MIN && status.processReadStatus == INT32_MIN && - status.effectiveProcessClass == -1 && status.requestedDevicePriority == 1 && + status.effectiveProcessClass == -1 && status.requestedDevicePriority == 0 && status.deviceSetHResult == S_FALSE && status.deviceReadHResult == S_FALSE && status.effectiveDevicePriority == INT32_MIN; } @@ -279,21 +279,22 @@ static void CheckGpuPriority(void* renderer, bool cpuRendering, WispGpuPriorityStatus status{}; Require(WispRendererGetGpuPriorityStatus(renderer, &status) == S_OK, "GPU priority initialization snapshot is available on owner thread"); - Require(status.requestedProcessClass == D3DKMT_SCHEDULINGPRIORITYCLASS_HIGH && - status.requestedDevicePriority == 1, "GPU priority requests HIGH and relative plus one only"); + Require(status.requestedProcessClass == D3DKMT_SCHEDULINGPRIORITYCLASS_NORMAL && + status.requestedDevicePriority == 0, "GPU priority never requests more than the defaults"); + Require(status.processSetStatus == INT32_MIN && status.deviceSetHResult == S_FALSE, + "GPU priority setters are never called"); auto currentClass = D3DKMT_SCHEDULINGPRIORITYCLASS_NORMAL; const NTSTATUS currentRead = D3DKMTGetProcessSchedulingPriorityClass(GetCurrentProcess(), ¤tClass); + if (beforeProcessRead >= 0 && currentRead >= 0) + Require(currentClass == beforeProcessClass, "renderer creation leaves the process GPU priority unchanged"); if (cpuRendering) { - Require(IsUnattempted(status), "WARP skips priority setters and readbacks without reporting success"); - if (beforeProcessRead >= 0 && currentRead >= 0) - Require(currentClass == beforeProcessClass, "WARP leaves existing process GPU priority unchanged"); + Require(IsUnattempted(status), "WARP skips priority readbacks without reporting success"); } else { - Require(status.attempted == 1 && status.processSetStatus != INT32_MIN && - status.processReadStatus != INT32_MIN && status.deviceSetHResult != S_FALSE && - status.deviceReadHResult != S_FALSE, "hardware initialization records all four priority call results"); + Require(status.attempted == 1 && status.processReadStatus != INT32_MIN && + status.deviceReadHResult != S_FALSE, "hardware initialization records both priority readbacks"); if (status.processReadStatus >= 0) { Require(status.effectiveProcessClass >= D3DKMT_SCHEDULINGPRIORITYCLASS_IDLE && @@ -302,19 +303,11 @@ static void CheckGpuPriority(void* renderer, bool cpuRendering, if (currentRead >= 0) Require(status.effectiveProcessClass == static_cast(currentClass), "stored process priority agrees with independent OS readback"); - if (status.processSetStatus == 0) - Require(status.effectiveProcessClass == status.requestedProcessClass, - "successful process priority set and readback agree"); } else Require(status.effectiveProcessClass == -1, "failed process readback retains unknown class"); if (status.deviceReadHResult == S_OK) - { Require(status.effectiveDevicePriority >= -7 && status.effectiveDevicePriority <= 7, "successful device priority readback is a relative priority"); - if (status.deviceSetHResult == S_OK) - Require(status.effectiveDevicePriority == status.requestedDevicePriority, - "successful device priority set and readback agree"); - } else Require(status.effectiveDevicePriority == INT32_MIN, "failed device readback retains unknown priority"); } std::printf("gpuPriorityAttempted=%u;processSet=0x%08lX;processRead=0x%08lX;processEffective=%d;deviceSet=0x%08lX;deviceRead=0x%08lX;deviceEffective=%d\n", diff --git a/src/Wisp.NativeRenderer/Wisp.NativeRenderer.cpp b/src/Wisp.NativeRenderer/Wisp.NativeRenderer.cpp index 2c968b35..a850c0bb 100644 --- a/src/Wisp.NativeRenderer/Wisp.NativeRenderer.cpp +++ b/src/Wisp.NativeRenderer/Wisp.NativeRenderer.cpp @@ -36,10 +36,13 @@ namespace constexpr UINT MaximumFrameLatency = 2; constexpr uint64_t MaximumDialCacheBytes = 16ull * 1024 * 1024; + // Wisp never raises its GPU scheduling priority: the game keeps the GPU, and + // the overlay accepts whatever time is left. The requested values are the + // untouched defaults; only the readbacks are attempted on hardware. constexpr WispGpuPriorityStatus UnattemptedGpuPriorityStatus() noexcept { - return {0, D3DKMT_SCHEDULINGPRIORITYCLASS_HIGH, INT32_MIN, INT32_MIN, -1, - 1, S_FALSE, S_FALSE, INT32_MIN}; + return {0, D3DKMT_SCHEDULINGPRIORITYCLASS_NORMAL, INT32_MIN, INT32_MIN, -1, + 0, S_FALSE, S_FALSE, INT32_MIN}; } bool ValidSize(uint32_t width, uint32_t height) noexcept @@ -667,19 +670,17 @@ namespace pacingStatus = S_OK; } - void ConfigureGpuPriority(IDXGIDevice* dxgiDevice) noexcept + void ObserveGpuPriority(IDXGIDevice* dxgiDevice) noexcept { gpuPriorityStatus.attempted = 1; - // Best-effort test: neither failure prevents rendering, and no - // elevation, absolute priority or realtime class is requested. - gpuPriorityStatus.processSetStatus = D3DKMTSetProcessSchedulingPriorityClass( - GetCurrentProcess(), D3DKMT_SCHEDULINGPRIORITYCLASS_HIGH); + // Read-only: a raised process class or device priority would let the + // overlay's presents preempt the game's GPU work on every frame. The + // setters are never called; the readbacks only feed diagnostics. auto processClass = D3DKMT_SCHEDULINGPRIORITYCLASS_NORMAL; gpuPriorityStatus.processReadStatus = D3DKMTGetProcessSchedulingPriorityClass( GetCurrentProcess(), &processClass); if (gpuPriorityStatus.processReadStatus >= 0) gpuPriorityStatus.effectiveProcessClass = static_cast(processClass); - gpuPriorityStatus.deviceSetHResult = dxgiDevice->SetGPUThreadPriority(1); INT devicePriority = INT32_MIN; gpuPriorityStatus.deviceReadHResult = dxgiDevice->GetGPUThreadPriority(&devicePriority); if (gpuPriorityStatus.deviceReadHResult == S_OK) @@ -850,7 +851,7 @@ namespace context->ClearRenderTargetView(renderTarget.Get(), clear); CHECK_HR(InitialPresentationResult(PresentChain(swapChain.Get(), swapChainFlags, 0))); CHECK_HR(composition->Commit()); - if (!cpuRendering) ConfigureGpuPriority(dxgiDevice.Get()); + if (!cpuRendering) ObserveGpuPriority(dxgiDevice.Get()); return S_OK; } diff --git a/src/Wisp.NativeRenderer/Wisp.NativeRenderer.h b/src/Wisp.NativeRenderer/Wisp.NativeRenderer.h index 07cc983b..ef15e3a9 100644 --- a/src/Wisp.NativeRenderer/Wisp.NativeRenderer.h +++ b/src/Wisp.NativeRenderer/Wisp.NativeRenderer.h @@ -40,7 +40,9 @@ struct WispWaitMetricsV2 // Stored initialization results, not a live query. Process statuses are raw // NTSTATUS values; INT32_MIN means not attempted. Device statuses are HRESULTs // (S_FALSE when not attempted). Effective values stay -1/INT32_MIN if unread. -// attempted is zero for WARP; requested values describe the hardware test only. +// attempted is zero for WARP. The renderer never sets a priority: requested +// values stay at the defaults (NORMAL, 0) and only the readbacks run on hardware, +// so the set statuses always report "not attempted". struct WispGpuPriorityStatus { uint32_t attempted; From 88c679d815e9e719523ccd725908307ae0623025 Mon Sep 17 00:00:00 2001 From: Views2k <292889387+Views2k@users.noreply.github.com> Date: Thu, 15 Oct 2026 22:58:19 +0000 Subject: [PATCH 2/8] Make the HUD's GPU priority a setting, off by default Renderer creation no longer raises the process scheduling class; a new WispRendererSetGpuPriority export raises HIGH / +1 or restores NORMAL / 0 on the render thread, and the render worker applies the user's choice whenever it changes. The setting lives in HUD settings as "Prioritize HUD updates on the GPU", is reported in the Tools environment snapshot as hudGpuPriority, and can be paused from Feature controls as gpu-priority. --- src/Wisp.App/AppController.FeatureControls.cs | 1 + src/Wisp.App/AppController.cs | 22 +++++++ src/Wisp.App/AppSettings.cs | 2 + src/Wisp.App/ControlPanelWindow.cs | 7 +++ src/Wisp.App/DiagnosticsViewModel.cs | 16 +++++ src/Wisp.App/LegacyMainWindow.xaml | 9 +++ src/Wisp.App/MainWindow.xaml | 12 ++++ .../NativeRendering/AnalogHudRenderWorker.cs | 24 ++++++++ .../DirectCompositionAnalogHost.cs | 30 +++++++++- .../DirectCompositionDevice.cs | 18 +++++- src/Wisp.App/NativeRendering/HudNativeHost.cs | 1 + .../Supplementary/FeatureControlPolicy.cs | 2 +- .../Supplementary/SupplementaryEnvironment.cs | 2 + .../NativeContractTests.cpp | 40 +++++++++++++ .../Wisp.NativeRenderer.cpp | 33 ++++++++++ src/Wisp.NativeRenderer/Wisp.NativeRenderer.h | 10 +++- .../CpuRenderingSettingsTests.cs | 21 ++++++- .../FeatureControls/service-schema-1.json | 3 +- .../HudGpuPrioritySettingsTests.cs | 60 +++++++++++++++++++ .../SupplementaryEnvironmentTests.cs | 2 + 20 files changed, 306 insertions(+), 9 deletions(-) create mode 100644 tests/Wisp.App.Tests/HudGpuPrioritySettingsTests.cs diff --git a/src/Wisp.App/AppController.FeatureControls.cs b/src/Wisp.App/AppController.FeatureControls.cs index 912bf737..37feff85 100644 --- a/src/Wisp.App/AppController.FeatureControls.cs +++ b/src/Wisp.App/AppController.FeatureControls.cs @@ -72,6 +72,7 @@ internal Task ApplyFeatureControlsAsync(FeatureControlState state) _ = ConfigureClipShortcut(false, settings.ToggleShortcutEnabled, settings.ToggleShortcut); _ = ConfigureClipShortcut(true, settings.SaveShortcutEnabled, settings.SaveShortcut); } + ApplyHudGpuPriority(); FeatureControlsChanged?.Invoke(this, EventArgs.Empty); UpdateShiftCueObservation(); Overlay?.RefreshFeatureControls(); diff --git a/src/Wisp.App/AppController.cs b/src/Wisp.App/AppController.cs index 515ad1e7..3adba74c 100644 --- a/src/Wisp.App/AppController.cs +++ b/src/Wisp.App/AppController.cs @@ -280,6 +280,28 @@ public void SetCpuRenderingEnabled(bool enabled) ViewModel.UpdateCpuRendering(Settings.CpuRenderingEnabled, saved); } + public void SetHudGpuPriorityEnabled(bool enabled) + { + if (_disposed) return; + var previous = Settings.HudGpuPriority; + Settings.HudGpuPriority = enabled; + var saved = SaveSettings(); + if (!saved) Settings.HudGpuPriority = previous; + PublishSupplementarySettings(); + ApplyHudGpuPriority(saved); + } + + // Effective only when the user turned it on and Tools has not paused it for this version. + internal void ApplyHudGpuPriority(bool saved = true) + { + var active = Settings.HudGpuPriority && FeatureAllowed("gpu-priority"); + ViewModel.UpdateHudGpuPriority(Settings.HudGpuPriority, active, + !saved ? "Could not save the renderer option. Try again." + : Settings.HudGpuPriority && !active + ? FeatureControls.Reason("gpu-priority") ?? "GPU priority is temporarily paused." + : ""); + } + public async Task SetDebugLoggingEnabledAsync(bool enabled) { if (_disposed) diff --git a/src/Wisp.App/AppSettings.cs b/src/Wisp.App/AppSettings.cs index e8122e8a..5f23600b 100644 --- a/src/Wisp.App/AppSettings.cs +++ b/src/Wisp.App/AppSettings.cs @@ -127,6 +127,8 @@ public double TorqueGaugeScale public bool AutomaticApplicationUpdateChecks { get; set; } = true; public DateTimeOffset? LastApplicationUpdateCheckUtc { get; set; } public bool CpuRenderingEnabled { get; set; } + // Off by default: the game keeps the GPU unless the user chooses otherwise. + public bool HudGpuPriority { get; set; } public bool DebugLoggingEnabled { get; set; } public DateTimeOffset? DebugLoggingExpiresAtUtc { get; set; } public string ColorTheme { get; set; } = AppColorThemes.DefaultName; diff --git a/src/Wisp.App/ControlPanelWindow.cs b/src/Wisp.App/ControlPanelWindow.cs index eeec37d9..7597f3cf 100644 --- a/src/Wisp.App/ControlPanelWindow.cs +++ b/src/Wisp.App/ControlPanelWindow.cs @@ -73,6 +73,7 @@ public abstract partial class ControlPanelWindow : Window private Button OverlayHotkeyCaptureButton => FindControl