diff --git a/electron/ipc/recording/windows.test.ts b/electron/ipc/recording/windows.test.ts index 5d691cb8..5881eecc 100644 --- a/electron/ipc/recording/windows.test.ts +++ b/electron/ipc/recording/windows.test.ts @@ -115,12 +115,11 @@ describe("native Windows window capture", () => { expect(windowsCaptureSource).toContain("CopySubresourceRegion(cropTexture_"); }); - it("resizes the crop texture when the selected window size changes", () => { - expect(windowsCaptureSource).toContain( - "nextWidth != captureWidth_ || nextHeight != captureHeight_", - ); + it("maps desktop bounds into WGC texture coordinates and keeps the encoder size fixed", () => { + expect(windowsCaptureSource).toContain("normalized * framePoolWidth_"); expect(windowsCaptureSource).toContain( "d3dDevice_->CreateTexture2D(&desc, nullptr, &resizedTexture)", ); + expect(windowsCaptureSource).not.toContain("nextWidth != captureWidth_"); }); }); diff --git a/electron/ipc/register/recording.ts b/electron/ipc/register/recording.ts index 05373691..438b6eee 100644 --- a/electron/ipc/register/recording.ts +++ b/electron/ipc/register/recording.ts @@ -562,13 +562,10 @@ export function registerRecordingHandlers( setWindowsCaptureStopRequested(false); setWindowsCapturePaused(false); - // The native helper currently does not declare DPI awareness in its own - // manifest or process setup, so we keep the compatibility flag here until - // scaled-display capture is verified without it on Windows. wcProc = spawn(exePath, [JSON.stringify(config)], { cwd: recordingsDir, stdio: ["pipe", "pipe", "pipe"], - env: { ...process.env, __COMPAT_LAYER: "HighDpiAware" }, + env: process.env, }); setWindowsCaptureProcess(wcProc); attachWindowsCaptureLifecycle(wcProc); diff --git a/electron/native/bin/win32-x64/helpers-manifest.json b/electron/native/bin/win32-x64/helpers-manifest.json index 62ee78b5..5fe298ae 100644 --- a/electron/native/bin/win32-x64/helpers-manifest.json +++ b/electron/native/bin/win32-x64/helpers-manifest.json @@ -5,10 +5,10 @@ "helpers": { "wgc-capture": { "binaryName": "wgc-capture.exe", - "binarySha256": "4d89fdff8e3343998c7a3b4d75d964c1f75f93594aa097c6681e477d25ec9f01", + "binarySha256": "c0dca95216a46474f1c71e2274228cbc78dd09c49e1539333a76183b115b1c5a", "sourceDir": "electron/native/wgc-capture", - "sourceFingerprint": "c6dac250c9d16f7aa881998353441b4ae3fb59731b802e3b8491a136e79726b0", - "updatedAt": "2026-07-11T11:58:45.856Z" + "sourceFingerprint": "9d8ba313407468ddff200317b210e74953e683c9ccd2e27104dd9e94b8efa7cb", + "updatedAt": "2026-09-04T21:09:08.014Z" }, "cursor-monitor": { "binaryName": "cursor-monitor.exe", diff --git a/electron/native/bin/win32-x64/wgc-capture.exe b/electron/native/bin/win32-x64/wgc-capture.exe index a882ee87..5ea3098e 100644 Binary files a/electron/native/bin/win32-x64/wgc-capture.exe and b/electron/native/bin/win32-x64/wgc-capture.exe differ diff --git a/electron/native/wgc-capture/src/main.cpp b/electron/native/wgc-capture/src/main.cpp index 0f007826..c5a463c6 100644 --- a/electron/native/wgc-capture/src/main.cpp +++ b/electron/native/wgc-capture/src/main.cpp @@ -280,6 +280,10 @@ int main(int argc, char* argv[]) { return 1; } + // Keep Win32 monitor/window rectangles in physical pixels. Without + // per-monitor awareness, mixed-DPI desktops can report virtualized bounds + // that do not line up with the WGC monitor texture. + SetProcessDpiAwarenessContext(DPI_AWARENESS_CONTEXT_PER_MONITOR_AWARE_V2); winrt::init_apartment(winrt::apartment_type::multi_threaded); CaptureConfig config; diff --git a/electron/native/wgc-capture/src/wgc_session.cpp b/electron/native/wgc-capture/src/wgc_session.cpp index e9c53f31..95dd17da 100644 --- a/electron/native/wgc-capture/src/wgc_session.cpp +++ b/electron/native/wgc-capture/src/wgc_session.cpp @@ -11,6 +11,7 @@ #include #include #include +#include // IDirect3DDxgiInterfaceAccess is a COM interface for getting the DXGI interface // from a WinRT IDirect3DSurface @@ -201,30 +202,55 @@ bool WgcSession::initializeWindowCrop(HWND hwnd) { if (!GetMonitorInfoW(MonitorFromWindow(hwnd, MONITOR_DEFAULTTONEAREST), &monitorInfo)) return false; monitorBounds_ = monitorInfo.rcMonitor; - RECT windowBounds{}; - if (FAILED(DwmGetWindowAttribute(hwnd, DWMWA_EXTENDED_FRAME_BOUNDS, &windowBounds, sizeof(windowBounds))) && - !GetWindowRect(hwnd, &windowBounds)) return false; - RECT clipped{}; - if (!IntersectRect(&clipped, &windowBounds, &monitorBounds_)) return false; - return updateWindowCropRect(); + return updateWindowCropRect(true); } -bool WgcSession::updateWindowCropRect() { +bool WgcSession::updateWindowCropRect(bool initializeSize) { RECT windowBounds{}; if (FAILED(DwmGetWindowAttribute(windowHandle_, DWMWA_EXTENDED_FRAME_BOUNDS, &windowBounds, sizeof(windowBounds))) && !GetWindowRect(windowHandle_, &windowBounds)) return false; RECT clipped{}; if (!IntersectRect(&clipped, &windowBounds, &monitorBounds_)) return false; - const LONG width = (clipped.right - clipped.left) & ~1L; - const LONG height = (clipped.bottom - clipped.top) & ~1L; - if (width < 2 || height < 2) return false; - const int nextWidth = static_cast(width); - const int nextHeight = static_cast(height); - if (!cropTexture_ || nextWidth != captureWidth_ || nextHeight != captureHeight_) { + const LONG monitorWidth = monitorBounds_.right - monitorBounds_.left; + const LONG monitorHeight = monitorBounds_.bottom - monitorBounds_.top; + if (monitorWidth <= 0 || monitorHeight <= 0 || framePoolWidth_ < 2 || framePoolHeight_ < 2) return false; + + // WGC textures are in capture-surface pixels, while Win32 monitor/window + // rectangles can be DPI-virtualized. Map both edges into texture space + // instead of assuming those coordinate systems are identical. + const auto mapX = [this, monitorWidth](LONG desktopX) { + const double normalized = static_cast(desktopX - monitorBounds_.left) / + static_cast(monitorWidth); + return std::clamp( + static_cast(std::llround(normalized * framePoolWidth_)), + 0L, + static_cast(framePoolWidth_)); + }; + const auto mapY = [this, monitorHeight](LONG desktopY) { + const double normalized = static_cast(desktopY - monitorBounds_.top) / + static_cast(monitorHeight); + return std::clamp( + static_cast(std::llround(normalized * framePoolHeight_)), + 0L, + static_cast(framePoolHeight_)); + }; + + LONG left = mapX(clipped.left); + LONG top = mapY(clipped.top); + LONG right = mapX(clipped.right); + LONG bottom = mapY(clipped.bottom); + const LONG mappedWidth = (right - left) & ~1L; + const LONG mappedHeight = (bottom - top) & ~1L; + if (mappedWidth < 2 || mappedHeight < 2) return false; + + if (initializeSize) { + captureWidth_ = static_cast(mappedWidth); + captureHeight_ = static_cast(mappedHeight); + D3D11_TEXTURE2D_DESC desc{}; - desc.Width = static_cast(nextWidth); - desc.Height = static_cast(nextHeight); + desc.Width = static_cast(captureWidth_); + desc.Height = static_cast(captureHeight_); desc.MipLevels = 1; desc.ArraySize = 1; desc.Format = DXGI_FORMAT_B8G8R8A8_UNORM; @@ -234,13 +260,16 @@ bool WgcSession::updateWindowCropRect() { ComPtr resizedTexture; if (FAILED(d3dDevice_->CreateTexture2D(&desc, nullptr, &resizedTexture))) return false; cropTexture_ = resizedTexture; - captureWidth_ = nextWidth; - captureHeight_ = nextHeight; } - const LONG left = clipped.left - monitorBounds_.left; - const LONG top = clipped.top - monitorBounds_.top; - cropRect_ = {left, top, left + width, top + height}; + if (!cropTexture_) return false; + + // The encoder's dimensions are fixed for the lifetime of the MP4. Keep the + // crop texture fixed too: reallocating it after a resize made the encoder + // pad the changed frame with black bars. + left = std::clamp(left, 0L, static_cast(framePoolWidth_ - captureWidth_)); + top = std::clamp(top, 0L, static_cast(framePoolHeight_ - captureHeight_)); + cropRect_ = {left, top, left + captureWidth_, top + captureHeight_}; return true; } diff --git a/electron/native/wgc-capture/src/wgc_session.h b/electron/native/wgc-capture/src/wgc_session.h index c2786265..3bc0968e 100644 --- a/electron/native/wgc-capture/src/wgc_session.h +++ b/electron/native/wgc-capture/src/wgc_session.h @@ -64,7 +64,7 @@ private: winrt::Windows::Graphics::Capture::GraphicsCaptureItem createCaptureItemForMonitor(HMONITOR monitor); bool initializeWithItem(int fps); bool initializeWindowCrop(HWND hwnd); - bool updateWindowCropRect(); + bool updateWindowCropRect(bool initializeSize = false); bool recreateFramePoolIfNeeded( winrt::Windows::Graphics::SizeInt32 const& contentSize); void onFrameArrived(