Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions build.bat
Original file line number Diff line number Diff line change
Expand Up @@ -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%

Expand All @@ -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%

Expand Down
4 changes: 4 additions & 0 deletions src/config.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down Expand Up @@ -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"
Expand Down
4 changes: 4 additions & 0 deletions src/config.h
Original file line number Diff line number Diff line change
Expand Up @@ -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 ---------------------------------------------------
Expand Down
14 changes: 8 additions & 6 deletions src/main.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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) {}
};
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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();
Expand Down Expand Up @@ -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);
Expand Down
133 changes: 96 additions & 37 deletions src/render_engine.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
#include <windows.h>
#include <d3d11.h>
#include <dxgi1_6.h>
#include <dcomp.h>
#include <d3dcompiler.h>
#include <wincodec.h>
#include <magnification.h>
Expand All @@ -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")
Expand Down Expand Up @@ -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
}
)";

Expand Down Expand Up @@ -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)
Expand Down Expand Up @@ -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";
Expand All @@ -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;
Expand All @@ -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
Expand All @@ -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). ---
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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);
Expand Down
5 changes: 4 additions & 1 deletion src/render_engine.h
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
5 changes: 5 additions & 0 deletions tests/test_config.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down
Loading