From e35e8b2da19a2a2d2eeb46a5efd20f8e8ef42e78 Mon Sep 17 00:00:00 2001 From: Etienne Lescot Date: Sun, 19 Jul 2026 10:34:48 +0200 Subject: [PATCH] fix(recording): round window-capture dimensions up to even for WGC MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Fixes #60: recording a single window (as opposed to a full display) on Windows produced a solid black video for the entire clip. Root cause: GraphicsCaptureItem::Size() for a window capture item reports the window's real client-area pixel dimensions verbatim, which are frequently odd (arbitrary resize, DPI rounding) — unlike monitor/display capture items, which are always even. H.264 encoding (and the RGB32->NV12 conversion feeding it) requires even width/height. The frame pool and captureWidth()/ captureHeight() (which configures the encoder's input media type) both used the raw, possibly-odd item size, so an odd-dimensioned window capture fed the Media Foundation H.264 encoder a size it can't encode correctly, producing black output. Fix: round window capture dimensions up to the nearest even value once, in WgcSession::createCaptureItem(HWND), and use that same rounded size (not the raw item size) for both the Direct3D11CaptureFramePool buffer and captureWidth()/captureHeight(), so every consumer of the session agrees on one even-dimensioned size. Monitor/display capture is untouched (always even in practice). Verified: built wgc-capture.exe via CMake/Ninja (VS 18 Insiders x64), opened a real Notepad window resized to an odd 789x595 client area, and drove the helper directly against it (sourceType:"window", real HWND). - Original (pre-fix) binary: 5.4KB output, ffmpeg blackdetect flags the entire clip as black (black_duration ~= full clip length). - Fixed binary: 36.5KB output (consistent with real, compressible frame content vs. a solid-color clip), ffmpeg blackdetect reports zero black segments across the same clip. - Sanity-checked normal display/monitor capture is unaffected (unchanged code path). --- .../native/wgc-capture/src/wgc_session.cpp | 30 ++++++++++++++++--- 1 file changed, 26 insertions(+), 4 deletions(-) diff --git a/electron/native/wgc-capture/src/wgc_session.cpp b/electron/native/wgc-capture/src/wgc_session.cpp index e20096c4ea..89f0b55fe0 100644 --- a/electron/native/wgc-capture/src/wgc_session.cpp +++ b/electron/native/wgc-capture/src/wgc_session.cpp @@ -32,6 +32,28 @@ int64_t timeSpanToHns(wf::TimeSpan const& value) { return value.count(); } +// H.264 encoding (and the RGB32->NV12 conversion feeding it) requires even +// frame dimensions. Monitor resolutions are always even in practice, so +// CreateForMonitor items never hit this. Windows, however, frequently have +// odd client-area dimensions (arbitrary drag-resize, DPI rounding), and +// GraphicsCaptureItem::Size() reports the window's *actual* size verbatim. +// If we requested a Direct3D11CaptureFramePool sized to that odd value while +// the rest of the pipeline (main.cpp's bitrate calc, MFEncoder) rounds down +// to even, the frame pool's real DXGI textures end up one pixel wider/taller +// than the staging texture the encoder allocates. ID3D11DeviceContext:: +// CopyResource silently no-ops on a size mismatch (it only emits a debug- +// layer warning), so the staging texture never receives pixel data and the +// output is solid black for the entire recording -- or, if the mismatch +// trips up the video MFT's input negotiation, SetInputMediaType fails +// outright. Rounding up to the nearest even size here, and using that +// rounded size (not the raw item size) for both the frame pool and +// `captureWidth()`/`captureHeight()`, keeps every consumer of this session +// looking at the exact same dimensions as the real captured texture. +int roundUpToEven(int value) { + const int clamped = std::max(2, value); + return (clamped % 2 == 0) ? clamped : clamped + 1; +} + } // namespace WgcSession::~WgcSession() { @@ -135,8 +157,8 @@ bool WgcSession::createCaptureItem(HWND window) { item_ = item; const auto size = item_.Size(); - width_ = static_cast(size.Width); - height_ = static_cast(size.Height); + width_ = roundUpToEven(static_cast(size.Width)); + height_ = roundUpToEven(static_cast(size.Height)); return width_ > 0 && height_ > 0; } @@ -199,7 +221,7 @@ bool WgcSession::initialize(HMONITOR monitor, int fps, bool captureCursor) { winrtDevice_, wgdx::DirectXPixelFormat::B8G8R8A8UIntNormalized, 2, - item_.Size()); + winrt::Windows::Graphics::SizeInt32{width_, height_}); session_ = framePool_.CreateCaptureSession(item_); if (!applySessionOptions(captureCursor)) { @@ -223,7 +245,7 @@ bool WgcSession::initialize(HWND window, int fps, bool captureCursor) { winrtDevice_, wgdx::DirectXPixelFormat::B8G8R8A8UIntNormalized, 2, - item_.Size()); + winrt::Windows::Graphics::SizeInt32{width_, height_}); session_ = framePool_.CreateCaptureSession(item_); if (!applySessionOptions(captureCursor)) {