diff --git a/build.bat b/build.bat index 65c99a1e..be6f213a 100644 --- a/build.bat +++ b/build.bat @@ -30,7 +30,7 @@ cl /nologo /std:c++17 /EHsc /O2 /W4 /DUNICODE /D_UNICODE ^ src\*.cpp ^ /Fe:Wind.exe ^ /link Magnification.lib Dwmapi.lib user32.lib shell32.lib gdi32.lib ^ - d3d11.lib dxgi.lib dxguid.lib d3dcompiler.lib windowscodecs.lib ole32.lib ^ + d3d11.lib dxgi.lib dxguid.lib dcomp.lib d3dcompiler.lib windowscodecs.lib ole32.lib ^ /MANIFEST:EMBED /MANIFESTUAC:NO /MANIFESTINPUT:Wind.manifest /SUBSYSTEM:WINDOWS exit /b %errorlevel% @@ -42,7 +42,7 @@ cl /nologo /std:c++17 /EHsc /O2 /W4 /DUNICODE /D_UNICODE ^ src\*.cpp ^ /Fe:Wind.exe ^ /link Magnification.lib Dwmapi.lib user32.lib shell32.lib gdi32.lib ^ - d3d11.lib dxgi.lib dxguid.lib d3dcompiler.lib windowscodecs.lib ole32.lib ^ + d3d11.lib dxgi.lib dxguid.lib dcomp.lib d3dcompiler.lib windowscodecs.lib ole32.lib ^ /MANIFEST:EMBED /MANIFESTUAC:NO /MANIFESTINPUT:Wind.uiaccess.manifest /SUBSYSTEM:WINDOWS exit /b %errorlevel% diff --git a/src/config.cpp b/src/config.cpp index 867524e0..5d5b0068 100644 --- a/src/config.cpp +++ b/src/config.cpp @@ -31,6 +31,7 @@ Config ParseConfig(const std::string& text) { else if (key == "tickHzCap") c.tickHzCap = std::stoi(val); 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 == "diagnostics") c.diagnostics = std::stoi(val); else if (key == "cursorSensitivity") c.cursorSensitivity = std::stod(val); else if (key == "cursorSmoothing") c.cursorSmoothing = std::stod(val); @@ -74,6 +75,9 @@ Config LoadConfig(const std::wstring& path) { "vsync=1\n" "; dwmFlush: 1=pace the zoomed loop to DWM's composition (smooth, default); 0=old vsync pacing\n" "dwmFlush=1\n" + "; present: blt=default (blt swapchain + DwmFlush); dcomp=flip-model via DirectComposition\n" + "; (experimental, vsync-paced; restart to change)\n" + "present=blt\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 87da515f..47042226 100644 --- a/src/config.h +++ b/src/config.h @@ -21,6 +21,10 @@ struct Config { // compositor. Fixes the blt-model microstutter (phase mismatch between our Present and DWM's // composite) - default ON. 1 = on (overrides vsync while zoomed), 0 = old vsync pacing. int dwmFlush = 1; + // Presentation backend (restart to change): "blt" = blt-model swapchain paced by DwmFlush + // (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"; 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 f51c6992..e81068fd 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -42,6 +42,7 @@ static int DetectRefreshHz() { struct TickState { RenderEngine& renderEngine; int sw, sh; + bool useDComp; // flip-model backend (fixed at init; not hot-reloaded) Config cfg; ZoomController zoom; CursorMapper mapper; @@ -55,7 +56,7 @@ struct TickState { double diagAccum = 0.0, diagSumDt = 0.0, diagMaxDt = 0.0; int diagFrames = 0, diagHitches = 0; TickState(RenderEngine& re, int w, int h, const Config& c) - : renderEngine(re), sw(w), sh(h), cfg(c), + : renderEngine(re), sw(w), sh(h), useDComp(c.present == "dcomp"), cfg(c), zoom(1.0, c.maxLevel, c.fullRangeSeconds), mapper(w, h, c.cursorSensitivity, c.cursorSmoothing) {} }; @@ -141,8 +142,9 @@ static void RunTick(TickState& t) { p.motionBlurStrength = t.cfg.motionBlurStrength; p.brightness = t.cfg.brightness; p.cursorMode = CursorModeFromCfg(t.cfg); - // In DwmFlush mode we present immediately (no vsync block) and let DwmFlush() pace. - p.vsync = (t.cfg.vsync != 0 && t.cfg.dwmFlush == 0); + // dcomp backend: flip-model is paced natively by vsync Present(1,0). blt backend: in + // DwmFlush mode present immediately (no vsync block) and let DwmFlush() pace. + p.vsync = t.useDComp ? true : (t.cfg.vsync != 0 && t.cfg.dwmFlush == 0); t.renderEngine.renderFrame(p); // Reveal AFTER the live frame is presented: setVisible flips the layer alpha over the // now-current front buffer, so the overlay never shows its retained previous-session @@ -253,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)) { + if (!renderEngine.initialize(sw, sh, cfg.zorderBand, cfg.hdrTonemap != 0, cfg.present == "dcomp")) { MessageBoxW(nullptr, L"Could not start the renderer (Direct3D 11 / Desktop Duplication " L"unavailable on this system).", L"Wind", MB_ICONERROR); g_input.stop(); @@ -375,8 +377,8 @@ int WINAPI wWinMain(HINSTANCE hInst, HINSTANCE, PWSTR, int) { // - else: Present(0,0) doesn't block, so the timer paces at tickHzCap. // Idle at 1x uses the timer. bool zoomed = ts.prevLvl > 1.0; - bool dwmPaces = zoomed && ts.cfg.dwmFlush != 0; - bool renderPresentPaces = zoomed && ts.cfg.vsync != 0 && !dwmPaces; + bool dwmPaces = zoomed && !ts.useDComp && ts.cfg.dwmFlush != 0; + bool renderPresentPaces = zoomed && (ts.useDComp || (ts.cfg.vsync != 0 && !dwmPaces)); if (!renderPresentPaces && !dwmPaces) { if (timer) { SetWaitableTimer(timer, &due, 0, nullptr, nullptr, FALSE); diff --git a/src/render_engine.cpp b/src/render_engine.cpp index e0ababca..7eb441cd 100644 --- a/src/render_engine.cpp +++ b/src/render_engine.cpp @@ -2,6 +2,7 @@ #include #include #include +#include #include #include #include @@ -14,6 +15,7 @@ #pragma comment(lib, "d3d11.lib") #pragma comment(lib, "dxgi.lib") +#pragma comment(lib, "dcomp.lib") #pragma comment(lib, "d3dcompiler.lib") #pragma comment(lib, "windowscodecs.lib") #pragma comment(lib, "Magnification.lib") @@ -136,7 +138,8 @@ float4 PSMain(VSOut i) : SV_TARGET { c.rgb = LinearToSrgb(max(c.rgb, 0.0) * scRgbScale); } c.rgb *= brightness; // optional fine-tune (default 1.0) - return c; + return float4(c.rgb, 1.0); // opaque: the DComp backend uses per-pixel alpha + // (blt ignores it via LWA_ALPHA), so force alpha 1 } )"; @@ -231,7 +234,13 @@ struct RenderEngine::State { HWND hwnd = nullptr; ID3D11Device* device = nullptr; ID3D11DeviceContext* ctx = nullptr; - IDXGISwapChain* swap = nullptr; // blt-model (layered window needs the redirection surface) + IDXGISwapChain* swap = nullptr; // blt-model, OR flip-model composed via DirectComposition + // Opt-in flip-model backend (present=dcomp): keeps WS_EX_LAYERED for click-through but presents a + // flip-model swapchain through a DComp visual (smoother native pacing). Null members = blt backend. + bool useDComp = false; + IDCompositionDevice* dcomp = nullptr; + IDCompositionTarget* dcompTarget = nullptr; + IDCompositionVisual* dcompVisual = nullptr; ID3D11RenderTargetView* rtv = nullptr; int lastClickX = INT_MIN, lastClickY = INT_MIN; // skip redundant SetCursorPos unsigned long long lastTopmostMs = 0; // last HWND_TOPMOST re-assert (throttled) @@ -438,11 +447,12 @@ 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 RenderEngine::initialize(int screenW, int screenH, int zorderBand, bool hdrTonemap, bool useDComp) { s_->sw = screenW; s_->sh = screenH; s_->wantHdrTonemap = hdrTonemap; // read before recreateDupl decides the capture format - RLog("=== initialize sw=%d sh=%d band=%d hdrTonemap=%d ===", screenW, screenH, zorderBand, (int)hdrTonemap); + s_->useDComp = useDComp; + RLog("=== initialize sw=%d sh=%d band=%d hdrTonemap=%d dcomp=%d ===", screenW, screenH, zorderBand, (int)hdrTonemap, (int)useDComp); // --- Overlay window: fullscreen, borderless, topmost, click-through, no-activate --- static const wchar_t* kClass = L"WindRenderOverlay"; @@ -454,8 +464,8 @@ bool RenderEngine::initialize(int screenW, int screenH, int zorderBand, bool hdr ATOM atom = RegisterClassW(&wc); // WS_EX_LAYERED is required for true cross-process click-through (WS_EX_TRANSPARENT + // HTTRANSPARENT alone only forwards to same-thread windows, so clicks to other apps were - // being eaten). LAYERED rules out a flip swapchain, so we use a blt-model swapchain below - // (verified to display via the redirection surface). LWA_ALPHA 255 = fully opaque. + // being eaten). Kept for BOTH backends - a spike confirmed WS_EX_LAYERED coexists with a + // DirectComposition flip-model swapchain (click-through + display both work). const DWORD exStyle = WS_EX_LAYERED | WS_EX_TRANSPARENT | WS_EX_TOPMOST | WS_EX_NOACTIVATE | WS_EX_TOOLWINDOW; s_->hwnd = nullptr; @@ -479,12 +489,11 @@ bool RenderEngine::initialize(int screenW, int screenH, int zorderBand, bool hdr 0, 0, screenW, screenH, nullptr, nullptr, wc.hInstance, nullptr); } if (!s_->hwnd) return false; - // Start fully transparent (alpha 0 = invisible). We toggle the layer alpha to show/hide - // instead of SW_HIDE/SW_SHOW: a layered window that is hidden and re-shown makes DWM cache - // and re-display the frame from when it was last visible, which flashed the previous zoom - // session's window on the next zoom-in (most visibly right after an alt-tab). Keeping the - // window always shown lets a reveal just flip alpha over the already-current front buffer. - SetLayeredWindowAttributes(s_->hwnd, 0, 0, LWA_ALPHA); + // Hide/show without SW_HIDE/SW_SHOW (which makes DWM re-display a cached last-visible frame + // -> the alt-tab flash). blt backend: toggle the layer alpha 0/255 (per-pixel alpha is + // ignored under LWA_ALPHA). dcomp backend: keep the layer opaque (255) and hide by presenting + // a fully transparent frame (per-pixel alpha). Either way the window stays shown. + SetLayeredWindowAttributes(s_->hwnd, 0, useDComp ? 255 : 0, LWA_ALPHA); // CRITICAL: exclude our own overlay from screen capture, or Desktop Duplication captures // our presented frame and we magnify our own output -> a feedback loop (degenerates to // black). WDA_EXCLUDEFROMCAPTURE (Win10 2004+) keeps the window visible on screen but @@ -500,39 +509,74 @@ bool RenderEngine::initialize(int screenW, int screenH, int zorderBand, bool hdr &s_->device, &got, &s_->ctx); if (FAILED(hr)) return false; - // --- Blt-model swapchain on the layered overlay HWND (flip can't target layered) --- + // --- Swapchain: blt-model on the HWND (default), or flip-model via DirectComposition (opt-in) --- 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 IDXGIAdapter* adapter = nullptr; dxgiDev->GetAdapter(&adapter); - IDXGIFactory* factory = nullptr; - if (adapter) adapter->GetParent(__uuidof(IDXGIFactory), (void**)&factory); - SafeRelease(dxgiDev); - SafeRelease(adapter); - if (!factory) return false; - - DXGI_SWAP_CHAIN_DESC scd{}; - scd.BufferDesc.Width = screenW; - scd.BufferDesc.Height = screenH; - scd.BufferDesc.Format = DXGI_FORMAT_B8G8R8A8_UNORM; - scd.SampleDesc.Count = 1; - scd.BufferUsage = DXGI_USAGE_RENDER_TARGET_OUTPUT; - scd.BufferCount = 1; - scd.OutputWindow = s_->hwnd; - scd.Windowed = TRUE; - scd.SwapEffect = DXGI_SWAP_EFFECT_DISCARD; - hr = factory->CreateSwapChain(s_->device, &scd, &s_->swap); - factory->MakeWindowAssociation(s_->hwnd, DXGI_MWA_NO_ALT_ENTER); - SafeRelease(factory); - if (FAILED(hr)) return false; - // --- Render target view from back-buffer 0 --- + if (useDComp) { + // Flip-model swapchain composed onto the (still layered) HWND via DirectComposition. + IDXGIFactory2* factory2 = nullptr; + if (adapter) adapter->GetParent(__uuidof(IDXGIFactory2), (void**)&factory2); + if (!factory2) { SafeRelease(dxgiDev); SafeRelease(adapter); return false; } + DXGI_SWAP_CHAIN_DESC1 d{}; + d.Width = screenW; d.Height = screenH; d.Format = DXGI_FORMAT_B8G8R8A8_UNORM; + d.SampleDesc.Count = 1; d.BufferUsage = DXGI_USAGE_RENDER_TARGET_OUTPUT; d.BufferCount = 2; + d.SwapEffect = DXGI_SWAP_EFFECT_FLIP_SEQUENTIAL; d.AlphaMode = DXGI_ALPHA_MODE_PREMULTIPLIED; + d.Scaling = DXGI_SCALING_STRETCH; + IDXGISwapChain1* sc1 = nullptr; + hr = factory2->CreateSwapChainForComposition(s_->device, &d, nullptr, &sc1); + SafeRelease(factory2); + if (FAILED(hr) || !sc1) { SafeRelease(dxgiDev); SafeRelease(adapter); RLog("CreateSwapChainForComposition hr=0x%08lX", (unsigned long)hr); return false; } + s_->swap = sc1; // IDXGISwapChain1 is-a IDXGISwapChain + hr = DCompositionCreateDevice(dxgiDev, __uuidof(IDCompositionDevice), (void**)&s_->dcomp); + if (SUCCEEDED(hr) && s_->dcomp && + SUCCEEDED(s_->dcomp->CreateTargetForHwnd(s_->hwnd, TRUE, &s_->dcompTarget)) && + SUCCEEDED(s_->dcomp->CreateVisual(&s_->dcompVisual))) { + s_->dcompVisual->SetContent(s_->swap); + s_->dcompTarget->SetRoot(s_->dcompVisual); + hr = s_->dcomp->Commit(); + } + SafeRelease(dxgiDev); SafeRelease(adapter); + if (FAILED(hr)) { RLog("DComp setup hr=0x%08lX", (unsigned long)hr); return false; } + } else { + IDXGIFactory* factory = nullptr; + if (adapter) adapter->GetParent(__uuidof(IDXGIFactory), (void**)&factory); + SafeRelease(dxgiDev); + SafeRelease(adapter); + if (!factory) return false; + DXGI_SWAP_CHAIN_DESC scd{}; + scd.BufferDesc.Width = screenW; + scd.BufferDesc.Height = screenH; + scd.BufferDesc.Format = DXGI_FORMAT_B8G8R8A8_UNORM; + scd.SampleDesc.Count = 1; + scd.BufferUsage = DXGI_USAGE_RENDER_TARGET_OUTPUT; + scd.BufferCount = 1; + scd.OutputWindow = s_->hwnd; + scd.Windowed = TRUE; + scd.SwapEffect = DXGI_SWAP_EFFECT_DISCARD; + hr = factory->CreateSwapChain(s_->device, &scd, &s_->swap); + factory->MakeWindowAssociation(s_->hwnd, DXGI_MWA_NO_ALT_ENTER); + SafeRelease(factory); + if (FAILED(hr)) return false; + } + + // --- Render target view from back-buffer 0 (reusable across Presents in D3D11) --- ID3D11Texture2D* back = nullptr; if (FAILED(s_->swap->GetBuffer(0, __uuidof(ID3D11Texture2D), (void**)&back))) return false; hr = s_->device->CreateRenderTargetView(back, nullptr, &s_->rtv); SafeRelease(back); if (FAILED(hr)) return false; + // dcomp backend: present an initial transparent frame so the overlay starts see-through + // (the layer is opaque at 255; per-pixel alpha provides invisibility until we draw). + if (useDComp) { + const float clearT[4] = { 0, 0, 0, 0 }; + s_->ctx->OMSetRenderTargets(1, &s_->rtv, nullptr); + s_->ctx->ClearRenderTargetView(s_->rtv, clearT); + s_->swap->Present(0, 0); + } // --- Desktop copy texture (SRV-able; DDA frames are copied into this). Starts BGRA8; // capture() re-creates it to match the acquired format (e.g. FP16 scRGB on HDR). --- @@ -619,9 +663,21 @@ bool RenderEngine::initialize(int screenW, int screenH, int zorderBand, bool hdr void RenderEngine::setVisible(bool visible) { if (!s_ || !s_->hwnd) return; - // Reveal/hide via layer alpha over the always-shown window (see initialize): flip alpha - // rather than SW_HIDE/SW_SHOW, so a reveal shows the already-current front buffer instead - // of DWM's cached last-visible frame. Callers present the live frame BEFORE revealing. + // Always-shown window (no SW_HIDE/SW_SHOW, which flashes DWM's cached frame). blt backend: + // toggle the layer alpha 0/255. dcomp backend: the layer stays opaque (255) - hide by + // presenting one fully transparent frame (per-pixel alpha); show is implicit (next renderFrame). + if (s_->useDComp) { + if (visible) { + s_->prevSrcValid = false; + SetWindowPos(s_->hwnd, HWND_TOPMOST, 0, 0, 0, 0, SWP_NOMOVE | SWP_NOSIZE | SWP_NOACTIVATE); + } else if (s_->ctx && s_->rtv && s_->swap) { + const float clearT[4] = { 0, 0, 0, 0 }; + s_->ctx->OMSetRenderTargets(1, &s_->rtv, nullptr); + s_->ctx->ClearRenderTargetView(s_->rtv, clearT); + s_->swap->Present(0, 0); + } + return; + } SetLayeredWindowAttributes(s_->hwnd, 0, visible ? 255 : 0, LWA_ALPHA); if (visible) { s_->prevSrcValid = false; // don't motion-blur the jump into zoom @@ -826,6 +882,9 @@ void RenderEngine::shutdown() { SafeRelease(s_->dupl); SafeRelease(s_->desktopCopy); SafeRelease(s_->rtv); + SafeRelease(s_->dcompVisual); + SafeRelease(s_->dcompTarget); + SafeRelease(s_->dcomp); SafeRelease(s_->swap); SafeRelease(s_->ctx); SafeRelease(s_->device); diff --git a/src/render_engine.h b/src/render_engine.h index 262bd0a4..1f01d357 100644 --- a/src/render_engine.h +++ b/src/render_engine.h @@ -33,7 +33,10 @@ class RenderEngine { // CreateWindowInBand (needs UIAccess; e.g. 16 = ZBID_SYSTEM_TOOLS, above the shell so the // Start menu / taskbar flyouts don't show an unmagnified copy). Falls back to a normal // window if the band can't be used. - bool initialize(int screenW, int screenH, int zorderBand = 0, bool hdrTonemap = false); + // 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 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 0c177613..89b76ca9 100644 --- a/tests/test_config.cpp +++ b/tests/test_config.cpp @@ -32,6 +32,11 @@ TEST_CASE("renderer knobs have sane defaults") { CHECK(c.vsync == 1); // vsync on by default 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 +} +TEST_CASE("present backend selects blt or dcomp") { + CHECK(ParseConfig("present=dcomp\n").present == "dcomp"); + CHECK(ParseConfig("present=blt\n").present == "blt"); } TEST_CASE("vsync, dwmFlush, tickHzCap can be set") { CHECK(ParseConfig("vsync=0\n").vsync == 0);