From ed07530c004ac8a596db150123e45d893cdc7622 Mon Sep 17 00:00:00 2001 From: Max <112043822+Maxaubert@users.noreply.github.com> Date: Mon, 25 May 2026 22:12:39 +0200 Subject: [PATCH] feat: bump GPU scheduling priority so zoom stays responsive vs background apps (#26) Less-responsive (laggy, not stuttery) zoom when a GPU-heavy app (a game, even paused) runs in the background is GPU contention: our render engine does GPU work every zoomed frame and queues behind the background app's. IDXGIDevice:: SetGPUThreadPriority lets the GPU scheduler favor the magnifier. New gpuPriority config (default 7, clamped -7..7; 0=OS default), applied at init. Lower it if it hurts a foreground game while zoomed. Won't match the Windows Magnifier (privileged hardware-scanout path), but narrows the gap. build.bat test: 32 cases / 97 assertions; render log confirms it's applied. Co-Authored-By: Claude Opus 4.7 (1M context) --- src/config.cpp | 4 ++++ src/config.h | 5 +++++ src/main.cpp | 2 +- src/render_engine.cpp | 7 ++++++- src/render_engine.h | 3 ++- tests/test_config.cpp | 5 ++++- 6 files changed, 22 insertions(+), 4 deletions(-) diff --git a/src/config.cpp b/src/config.cpp index 5d5b0068..03349187 100644 --- a/src/config.cpp +++ b/src/config.cpp @@ -32,6 +32,7 @@ Config ParseConfig(const std::string& text) { else if (key == "vsync") c.vsync = std::stoi(val); else if (key == "dwmFlush") c.dwmFlush = std::stoi(val); else if (key == "present") c.present = val; + else if (key == "gpuPriority") c.gpuPriority = std::stoi(val); else if (key == "diagnostics") c.diagnostics = std::stoi(val); else if (key == "cursorSensitivity") c.cursorSensitivity = std::stod(val); else if (key == "cursorSmoothing") c.cursorSmoothing = std::stod(val); @@ -78,6 +79,9 @@ Config LoadConfig(const std::wstring& path) { "; present: blt=default (blt swapchain + DwmFlush); dcomp=flip-model via DirectComposition\n" "; (experimental, vsync-paced; restart to change)\n" "present=blt\n" + "; gpuPriority: GPU scheduling priority (-7..7). Higher keeps the zoom responsive when a\n" + "; game runs in the background; lower if it hurts a foreground game (restart to apply)\n" + "gpuPriority=7\n" "; diagnostics=1 logs frame timing to %TEMP%\\wind_diag.log (restart to apply)\n" "diagnostics=0\n" "; cursorSensitivity: pan speed per raw count\n" diff --git a/src/config.h b/src/config.h index 47042226..7e62fbca 100644 --- a/src/config.h +++ b/src/config.h @@ -25,6 +25,11 @@ struct Config { // (default); "dcomp" = flip-model swapchain via DirectComposition, paced by vsync Present // (experimental A/B - native flip pacing + lower latency, still click-through via layered). std::string present = "blt"; + // GPU scheduling priority for the magnifier's device (IDXGIDevice::SetGPUThreadPriority, + // clamped to [-7,7]). Higher = the GPU scheduler favors us over other apps, so the zoom + // stays responsive when a GPU-heavy app (a game) runs in the background. 0 = OS default. + // Lower it if it hurts a foreground game while zoomed. + int gpuPriority = 7; int diagnostics = 0; // 1 = log frame-timing to wind_diag.log // --- Own GPU renderer --------------------------------------------------- diff --git a/src/main.cpp b/src/main.cpp index e81068fd..7e85bd41 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -255,7 +255,7 @@ int WINAPI wWinMain(HINSTANCE hInst, HINSTANCE, PWSTR, int) { // --- Own GPU renderer (DXGI Desktop Duplication + D3D11) --- RenderEngine renderEngine; - if (!renderEngine.initialize(sw, sh, cfg.zorderBand, cfg.hdrTonemap != 0, cfg.present == "dcomp")) { + if (!renderEngine.initialize(sw, sh, cfg.zorderBand, cfg.hdrTonemap != 0, cfg.present == "dcomp", cfg.gpuPriority)) { MessageBoxW(nullptr, L"Could not start the renderer (Direct3D 11 / Desktop Duplication " L"unavailable on this system).", L"Wind", MB_ICONERROR); g_input.stop(); diff --git a/src/render_engine.cpp b/src/render_engine.cpp index 7eb441cd..ef39f1df 100644 --- a/src/render_engine.cpp +++ b/src/render_engine.cpp @@ -447,7 +447,7 @@ void RenderEngine::debugHdr(unsigned& ddaFormat, int& colorSpace, int& bitsPerCo ddaFormat = s_->ddaFormat; colorSpace = s_->outColorSpace; bitsPerColor = s_->outBitsPerColor; } -bool RenderEngine::initialize(int screenW, int screenH, int zorderBand, bool hdrTonemap, bool useDComp) { +bool RenderEngine::initialize(int screenW, int screenH, int zorderBand, bool hdrTonemap, bool useDComp, int gpuPriority) { s_->sw = screenW; s_->sh = screenH; s_->wantHdrTonemap = hdrTonemap; // read before recreateDupl decides the capture format @@ -513,6 +513,11 @@ bool RenderEngine::initialize(int screenW, int screenH, int zorderBand, bool hdr IDXGIDevice1* dxgiDev = nullptr; if (FAILED(s_->device->QueryInterface(__uuidof(IDXGIDevice1), (void**)&dxgiDev))) return false; dxgiDev->SetMaximumFrameLatency(1); // cap input-to-photon latency to ~1 frame + if (gpuPriority != 0) { // favor the magnifier on the GPU vs background apps + int p = gpuPriority < -7 ? -7 : (gpuPriority > 7 ? 7 : gpuPriority); + dxgiDev->SetGPUThreadPriority(p); + RLog("SetGPUThreadPriority(%d)", p); + } IDXGIAdapter* adapter = nullptr; dxgiDev->GetAdapter(&adapter); diff --git a/src/render_engine.h b/src/render_engine.h index 1f01d357..ff1117b0 100644 --- a/src/render_engine.h +++ b/src/render_engine.h @@ -36,7 +36,8 @@ class RenderEngine { // useDComp: false = blt-model swapchain on the layered HWND (default, paced by DwmFlush); // true = flip-model swapchain composed via DirectComposition (still WS_EX_LAYERED for // click-through), paced by vsync Present. Opt-in A/B backend. - bool initialize(int screenW, int screenH, int zorderBand = 0, bool hdrTonemap = false, bool useDComp = false); + bool initialize(int screenW, int screenH, int zorderBand = 0, bool hdrTonemap = false, + bool useDComp = false, int gpuPriority = 0); bool renderFrame(const RenderFrameParams& p); // capture (if changed) + scale + cursor + present void setVisible(bool visible); // show/hide the overlay (hidden at 1x) // Force the next frame to grab a fresh full-desktop capture (release+recreate the diff --git a/tests/test_config.cpp b/tests/test_config.cpp index 89b76ca9..3c1e9d78 100644 --- a/tests/test_config.cpp +++ b/tests/test_config.cpp @@ -33,10 +33,13 @@ TEST_CASE("renderer knobs have sane defaults") { CHECK(c.dwmFlush == 1); // DwmFlush pacing on by default (the smooth path) CHECK(c.tickHzCap == 0); // 0 = auto-detect display refresh rate CHECK(c.present == "blt"); // blt+DwmFlush backend by default + CHECK(c.gpuPriority == 7); // favor the magnifier on the GPU by default } -TEST_CASE("present backend selects blt or dcomp") { +TEST_CASE("present backend and gpuPriority parse") { CHECK(ParseConfig("present=dcomp\n").present == "dcomp"); CHECK(ParseConfig("present=blt\n").present == "blt"); + CHECK(ParseConfig("gpuPriority=0\n").gpuPriority == 0); + CHECK(ParseConfig("gpuPriority=-7\n").gpuPriority == -7); } TEST_CASE("vsync, dwmFlush, tickHzCap can be set") { CHECK(ParseConfig("vsync=0\n").vsync == 0);