fix(windows): crop window captures without padding

This commit is contained in:
webadderall
2026-09-05 14:11:27 +10:00
parent d404be1fdc
commit 498800a3e2
7 changed files with 61 additions and 32 deletions
+3 -4
View File
@@ -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_");
});
});
+1 -4
View File
@@ -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);
@@ -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",
Binary file not shown.
+4
View File
@@ -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;
+49 -20
View File
@@ -11,6 +11,7 @@
#include <iostream>
#include <chrono>
#include <algorithm>
#include <cmath>
// 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<int>(width);
const int nextHeight = static_cast<int>(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<double>(desktopX - monitorBounds_.left) /
static_cast<double>(monitorWidth);
return std::clamp(
static_cast<LONG>(std::llround(normalized * framePoolWidth_)),
0L,
static_cast<LONG>(framePoolWidth_));
};
const auto mapY = [this, monitorHeight](LONG desktopY) {
const double normalized = static_cast<double>(desktopY - monitorBounds_.top) /
static_cast<double>(monitorHeight);
return std::clamp(
static_cast<LONG>(std::llround(normalized * framePoolHeight_)),
0L,
static_cast<LONG>(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<int>(mappedWidth);
captureHeight_ = static_cast<int>(mappedHeight);
D3D11_TEXTURE2D_DESC desc{};
desc.Width = static_cast<UINT>(nextWidth);
desc.Height = static_cast<UINT>(nextHeight);
desc.Width = static_cast<UINT>(captureWidth_);
desc.Height = static_cast<UINT>(captureHeight_);
desc.MipLevels = 1;
desc.ArraySize = 1;
desc.Format = DXGI_FORMAT_B8G8R8A8_UNORM;
@@ -234,13 +260,16 @@ bool WgcSession::updateWindowCropRect() {
ComPtr<ID3D11Texture2D> 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<LONG>(framePoolWidth_ - captureWidth_));
top = std::clamp(top, 0L, static_cast<LONG>(framePoolHeight_ - captureHeight_));
cropRect_ = {left, top, left + captureWidth_, top + captureHeight_};
return true;
}
@@ -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(