diff --git a/electron/native/bin/win32-x64/helpers-manifest.json b/electron/native/bin/win32-x64/helpers-manifest.json index be08a2b3d..bbcef557a 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": "88226bd499c820081a27264b40f37875d0949d9f921920738e05b7eaeb1a9e7d", + "binarySha256": "4f8873abbff58add37672114184c154dee838939ee8f2b7df45d658d078af28c", "sourceDir": "electron/native/wgc-capture", - "sourceFingerprint": "7190e148ede1fb1c573cc1866405d6345b0c35378a7e32499580bffacf1cc6d2", - "updatedAt": "2026-05-07T15:21:44.354Z" + "sourceFingerprint": "6f725fad6eb515d81b2d553ec45ddbf7c681d2725bba48f0c0722ce00166d967", + "updatedAt": "2026-05-09T00:49:28.306Z" }, "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 e55df5063..37de4c9c7 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 2c433b3bc..0f007826b 100644 --- a/electron/native/wgc-capture/src/main.cpp +++ b/electron/native/wgc-capture/src/main.cpp @@ -397,7 +397,7 @@ int main(int argc, char* argv[]) { } // Wait for stop signal while pausing/resuming audio tracks in lockstep. - while (!g_stopRequested) { + while (!g_stopRequested && !session.hasFatalError()) { if (g_pauseRequested) { if (audioActive) loopback.pause(); if (micActive) micCapture.pause(); @@ -416,6 +416,25 @@ int main(int argc, char* argv[]) { if (audioActive) loopback.stop(); if (micActive) micCapture.stop(); + if (session.hasFatalError()) { + std::cerr << "ERROR: WGC capture session failed during recording" << std::endl; + encoder.finalize(); + DeleteFileW(outputPathW.c_str()); + if (!config.audioOutputPath.empty()) { + const std::wstring audioPathW = utf8ToWide(config.audioOutputPath); + const std::wstring audioMetadataPathW = utf8ToWide(config.audioOutputPath + ".json"); + DeleteFileW(audioPathW.c_str()); + DeleteFileW(audioMetadataPathW.c_str()); + } + if (!config.micOutputPath.empty()) { + const std::wstring micPathW = utf8ToWide(config.micOutputPath); + const std::wstring micMetadataPathW = utf8ToWide(config.micOutputPath + ".json"); + DeleteFileW(micPathW.c_str()); + DeleteFileW(micMetadataPathW.c_str()); + } + return 1; + } + if (audioActive) { writeCompanionAudioTimingMetadata( config.audioOutputPath, diff --git a/electron/native/wgc-capture/src/mf_encoder.cpp b/electron/native/wgc-capture/src/mf_encoder.cpp index 30969e394..1a89b21e7 100644 --- a/electron/native/wgc-capture/src/mf_encoder.cpp +++ b/electron/native/wgc-capture/src/mf_encoder.cpp @@ -2,6 +2,7 @@ #include #include #include +#include #include #include @@ -116,6 +117,31 @@ bool MFEncoder::initialize(const std::wstring& outputPath, int width, int height return false; } + // WGC window captures can change frame size while recording. Keep the muxer + // output dimensions stable by compositing resized frames into this fixed + // BGRA surface before CPU readback. + D3D11_TEXTURE2D_DESC compositeDesc = {}; + compositeDesc.Width = width_; + compositeDesc.Height = height_; + compositeDesc.MipLevels = 1; + compositeDesc.ArraySize = 1; + compositeDesc.Format = DXGI_FORMAT_B8G8R8A8_UNORM; + compositeDesc.SampleDesc.Count = 1; + compositeDesc.Usage = D3D11_USAGE_DEFAULT; + compositeDesc.BindFlags = D3D11_BIND_RENDER_TARGET; + + hr = device_->CreateTexture2D(&compositeDesc, nullptr, &resizeCompositeTexture_); + if (FAILED(hr)) { + std::cerr << "ERROR: Failed to create resize composite texture: 0x" << std::hex << hr << std::endl; + return false; + } + + hr = device_->CreateRenderTargetView(resizeCompositeTexture_.Get(), nullptr, &resizeCompositeView_); + if (FAILED(hr)) { + std::cerr << "ERROR: Failed to create resize composite view: 0x" << std::hex << hr << std::endl; + return false; + } + // Pre-allocate NV12 buffer const int ySize = width_ * height_; const int uvSize = (width_ / 2) * (height_ / 2) * 2; @@ -132,7 +158,39 @@ bool MFEncoder::writeFrame(ID3D11Texture2D* texture, int64_t timestampHns) { if (!initialized_ || !sinkWriter_) return false; - context_->CopyResource(stagingTexture_.Get(), texture); + D3D11_TEXTURE2D_DESC sourceDesc = {}; + texture->GetDesc(&sourceDesc); + + if (sourceDesc.Width == static_cast(width_) && + sourceDesc.Height == static_cast(height_)) { + context_->CopyResource(stagingTexture_.Get(), texture); + } else { + if (!resizeCompositeTexture_ || !resizeCompositeView_) return false; + + const FLOAT clearColor[4] = {0.0f, 0.0f, 0.0f, 1.0f}; + context_->ClearRenderTargetView(resizeCompositeView_.Get(), clearColor); + + D3D11_BOX sourceBox = {}; + sourceBox.left = 0; + sourceBox.top = 0; + sourceBox.front = 0; + sourceBox.right = (std::min)(sourceDesc.Width, static_cast(width_)); + sourceBox.bottom = (std::min)(sourceDesc.Height, static_cast(height_)); + sourceBox.back = 1; + + if (sourceBox.right == 0 || sourceBox.bottom == 0) return false; + + context_->CopySubresourceRegion( + resizeCompositeTexture_.Get(), + 0, + 0, + 0, + 0, + texture, + 0, + &sourceBox); + context_->CopyResource(stagingTexture_.Get(), resizeCompositeTexture_.Get()); + } D3D11_MAPPED_SUBRESOURCE mapped; HRESULT hr = context_->Map(stagingTexture_.Get(), 0, D3D11_MAP_READ, 0, &mapped); @@ -242,6 +300,8 @@ bool MFEncoder::finalize() { initialized_ = false; sinkWriter_.Reset(); stagingTexture_.Reset(); + resizeCompositeView_.Reset(); + resizeCompositeTexture_.Reset(); nv12Buffer_.clear(); lastFrameBuffer_.clear(); nv12Buffer_.shrink_to_fit(); diff --git a/electron/native/wgc-capture/src/mf_encoder.h b/electron/native/wgc-capture/src/mf_encoder.h index 21f490320..48fdeaadb 100644 --- a/electron/native/wgc-capture/src/mf_encoder.h +++ b/electron/native/wgc-capture/src/mf_encoder.h @@ -30,6 +30,8 @@ class MFEncoder { ID3D11Device* device_ = nullptr; ID3D11DeviceContext* context_ = nullptr; ComPtr stagingTexture_; + ComPtr resizeCompositeTexture_; + ComPtr resizeCompositeView_; std::vector nv12Buffer_; std::vector lastFrameBuffer_; DWORD streamIndex_ = 0; diff --git a/electron/native/wgc-capture/src/wgc_session.cpp b/electron/native/wgc-capture/src/wgc_session.cpp index 987c649b5..898a9fdc4 100644 --- a/electron/native/wgc-capture/src/wgc_session.cpp +++ b/electron/native/wgc-capture/src/wgc_session.cpp @@ -24,6 +24,12 @@ extern "C" { IInspectable** graphicsDevice); } +static int normalizeFramePoolExtent(int value) { + int normalized = value < 2 ? 2 : value; + if ((normalized % 2) != 0) ++normalized; + return normalized; +} + WgcSession::WgcSession() {} WgcSession::~WgcSession() { @@ -114,6 +120,8 @@ bool WgcSession::initializeWithItem(int fps) { auto size = captureItem_.Size(); captureWidth_ = size.Width; captureHeight_ = size.Height; + framePoolWidth_ = size.Width; + framePoolHeight_ = size.Height; framePool_ = winrt::Windows::Graphics::Capture::Direct3D11CaptureFramePool::CreateFreeThreaded( winrtDevice_, @@ -128,7 +136,42 @@ bool WgcSession::initializeWithItem(int fps) { // IsBorderRequired is only available on Windows 11+ (build 22000). propagating an hresult_error results in Native Windows capture failure try { session_.IsBorderRequired(false); + } catch (winrt::hresult_error const&) { + } + + return true; +} + +bool WgcSession::recreateFramePoolIfNeeded( + winrt::Windows::Graphics::SizeInt32 const& contentSize) { + if (!framePool_) return false; + + const int normalizedWidth = normalizeFramePoolExtent(contentSize.Width); + const int normalizedHeight = normalizeFramePoolExtent(contentSize.Height); + if (normalizedWidth == framePoolWidth_ && normalizedHeight == framePoolHeight_) { + return false; + } + + winrt::Windows::Graphics::SizeInt32 normalizedSize{ + normalizedWidth, + normalizedHeight, + }; + + try { + framePool_.Recreate( + winrtDevice_, + winrt::Windows::Graphics::DirectX::DirectXPixelFormat::B8G8R8A8UIntNormalized, + 2, + normalizedSize); + framePoolWidth_ = normalizedWidth; + framePoolHeight_ = normalizedHeight; + std::cerr << "INFO: Recreated WGC frame pool for resized content " + << framePoolWidth_ << "x" << framePoolHeight_ << std::endl; } catch (winrt::hresult_error const& e) { + fatalError_ = true; + capturing_ = false; + std::cerr << "ERROR: Failed to recreate WGC frame pool after resize: 0x" + << std::hex << e.code() << std::dec << std::endl; } return true; @@ -174,6 +217,7 @@ bool WgcSession::startCapture() { if (!session_ || !framePool_) return false; capturing_ = true; + fatalError_ = false; lastFrameTimeHns_ = 0; frameArrivedRevoker_ = framePool_.FrameArrived( @@ -205,10 +249,15 @@ void WgcSession::onFrameArrived( winrt::Windows::Graphics::Capture::Direct3D11CaptureFramePool const& sender, winrt::Windows::Foundation::IInspectable const&) { - if (!capturing_) return; + if (!capturing_ || fatalError_) return; auto frame = sender.TryGetNextFrame(); if (!frame) return; + auto contentSize = frame.ContentSize(); + if (recreateFramePoolIfNeeded(contentSize)) { + frame.Close(); + return; + } auto timestamp = frame.SystemRelativeTime(); int64_t frameTimeHns = std::chrono::duration_cast>>(timestamp).count(); diff --git a/electron/native/wgc-capture/src/wgc_session.h b/electron/native/wgc-capture/src/wgc_session.h index 5ca6be344..1755f4b16 100644 --- a/electron/native/wgc-capture/src/wgc_session.h +++ b/electron/native/wgc-capture/src/wgc_session.h @@ -28,6 +28,7 @@ class WgcSession { void setFrameCallback(FrameCallback callback); bool startCapture(); void stopCapture(); + bool hasFatalError() const { return fatalError_.load(); } int captureWidth() const { return captureWidth_; } int captureHeight() const { return captureHeight_; } @@ -45,9 +46,12 @@ class WgcSession { FrameCallback frameCallback_; std::atomic capturing_{false}; + std::atomic fatalError_{false}; int fps_ = 60; int captureWidth_ = 0; int captureHeight_ = 0; + int framePoolWidth_ = 0; + int framePoolHeight_ = 0; int64_t frameIntervalHns_ = 0; int64_t lastFrameTimeHns_ = 0; @@ -56,6 +60,8 @@ class WgcSession { winrt::Windows::Graphics::Capture::GraphicsCaptureItem createCaptureItemForMonitor(HMONITOR monitor); winrt::Windows::Graphics::Capture::GraphicsCaptureItem createCaptureItemForWindow(HWND hwnd); bool initializeWithItem(int fps); + bool recreateFramePoolIfNeeded( + winrt::Windows::Graphics::SizeInt32 const& contentSize); void onFrameArrived( winrt::Windows::Graphics::Capture::Direct3D11CaptureFramePool const& sender, winrt::Windows::Foundation::IInspectable const& args);