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 dcomp.lib d3dcompiler.lib windowscodecs.lib ole32.lib ^
d3d11.lib dxgi.lib dxguid.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 dcomp.lib d3dcompiler.lib windowscodecs.lib ole32.lib ^
d3d11.lib dxgi.lib dxguid.lib d3dcompiler.lib windowscodecs.lib ole32.lib ^
/MANIFEST:EMBED /MANIFESTUAC:NO /MANIFESTINPUT:Wind.uiaccess.manifest /SUBSYSTEM:WINDOWS
exit /b %errorlevel%

Expand Down
8 changes: 0 additions & 8 deletions src/config.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -31,8 +31,6 @@ 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 == "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);
Expand Down Expand Up @@ -76,12 +74,6 @@ 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"
"; 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"
Expand Down
9 changes: 0 additions & 9 deletions src/config.h
Original file line number Diff line number Diff line change
Expand Up @@ -21,15 +21,6 @@ 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";
// 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 ---------------------------------------------------
Expand Down
14 changes: 6 additions & 8 deletions src/main.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,6 @@ 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 @@ -56,7 +55,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), useDComp(c.present == "dcomp"), cfg(c),
: renderEngine(re), sw(w), sh(h), cfg(c),
zoom(1.0, c.maxLevel, c.fullRangeSeconds),
mapper(w, h, c.cursorSensitivity, c.cursorSmoothing) {}
};
Expand Down Expand Up @@ -142,9 +141,8 @@ static void RunTick(TickState& t) {
p.motionBlurStrength = t.cfg.motionBlurStrength;
p.brightness = t.cfg.brightness;
p.cursorMode = CursorModeFromCfg(t.cfg);
// 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);
// In DwmFlush mode we present immediately (no vsync block) and let DwmFlush() pace.
p.vsync = (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 @@ -255,7 +253,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", cfg.gpuPriority)) {
if (!renderEngine.initialize(sw, sh, cfg.zorderBand, cfg.hdrTonemap != 0)) {
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 @@ -377,8 +375,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.useDComp && ts.cfg.dwmFlush != 0;
bool renderPresentPaces = zoomed && (ts.useDComp || (ts.cfg.vsync != 0 && !dwmPaces));
bool dwmPaces = zoomed && ts.cfg.dwmFlush != 0;
bool renderPresentPaces = zoomed && ts.cfg.vsync != 0 && !dwmPaces;
if (!renderPresentPaces && !dwmPaces) {
if (timer) {
SetWaitableTimer(timer, &due, 0, nullptr, nullptr, FALSE);
Expand Down
138 changes: 37 additions & 101 deletions src/render_engine.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,6 @@
#include <windows.h>
#include <d3d11.h>
#include <dxgi1_6.h>
#include <dcomp.h>
#include <d3dcompiler.h>
#include <wincodec.h>
#include <magnification.h>
Expand All @@ -15,7 +14,6 @@

#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 @@ -138,8 +136,7 @@ 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 float4(c.rgb, 1.0); // opaque: the DComp backend uses per-pixel alpha
// (blt ignores it via LWA_ALPHA), so force alpha 1
return c;
}
)";

Expand Down Expand Up @@ -234,13 +231,7 @@ struct RenderEngine::State {
HWND hwnd = nullptr;
ID3D11Device* device = nullptr;
ID3D11DeviceContext* ctx = nullptr;
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;
IDXGISwapChain* swap = nullptr; // blt-model (layered window needs the redirection surface)
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 @@ -447,12 +438,11 @@ 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, int gpuPriority) {
bool RenderEngine::initialize(int screenW, int screenH, int zorderBand, bool hdrTonemap) {
s_->sw = screenW;
s_->sh = screenH;
s_->wantHdrTonemap = hdrTonemap; // read before recreateDupl decides the capture format
s_->useDComp = useDComp;
RLog("=== initialize sw=%d sh=%d band=%d hdrTonemap=%d dcomp=%d ===", screenW, screenH, zorderBand, (int)hdrTonemap, (int)useDComp);
RLog("=== initialize sw=%d sh=%d band=%d hdrTonemap=%d ===", screenW, screenH, zorderBand, (int)hdrTonemap);

// --- Overlay window: fullscreen, borderless, topmost, click-through, no-activate ---
static const wchar_t* kClass = L"WindRenderOverlay";
Expand All @@ -464,8 +454,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). Kept for BOTH backends - a spike confirmed WS_EX_LAYERED coexists with a
// DirectComposition flip-model swapchain (click-through + display both work).
// 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.
const DWORD exStyle = WS_EX_LAYERED | WS_EX_TRANSPARENT | WS_EX_TOPMOST |
WS_EX_NOACTIVATE | WS_EX_TOOLWINDOW;
s_->hwnd = nullptr;
Expand All @@ -489,11 +479,12 @@ 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;
// 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);
// 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);
// 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 @@ -509,79 +500,39 @@ bool RenderEngine::initialize(int screenW, int screenH, int zorderBand, bool hdr
&s_->device, &got, &s_->ctx);
if (FAILED(hr)) return false;

// --- Swapchain: blt-model on the HWND (default), or flip-model via DirectComposition (opt-in) ---
// --- Blt-model swapchain on the layered overlay HWND (flip can't target layered) ---
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);
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;

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) ---
// --- Render target view from back-buffer 0 ---
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 @@ -668,21 +619,9 @@ bool RenderEngine::initialize(int screenW, int screenH, int zorderBand, bool hdr

void RenderEngine::setVisible(bool visible) {
if (!s_ || !s_->hwnd) return;
// 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;
}
// 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.
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 @@ -887,9 +826,6 @@ 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
6 changes: 1 addition & 5 deletions src/render_engine.h
Original file line number Diff line number Diff line change
Expand Up @@ -33,11 +33,7 @@ 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.
// 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, int gpuPriority = 0);
bool initialize(int screenW, int screenH, int zorderBand = 0, bool hdrTonemap = 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
Loading
Loading