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
6 changes: 3 additions & 3 deletions electron/native/bin/win32-x64/helpers-manifest.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
Binary file modified electron/native/bin/win32-x64/wgc-capture.exe
Binary file not shown.
21 changes: 20 additions & 1 deletion electron/native/wgc-capture/src/main.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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();
Expand All @@ -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;
}
Comment thread
coderabbitai[bot] marked this conversation as resolved.

if (audioActive) {
writeCompanionAudioTimingMetadata(
config.audioOutputPath,
Expand Down
62 changes: 61 additions & 1 deletion electron/native/wgc-capture/src/mf_encoder.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
#include <mfapi.h>
#include <mferror.h>
#include <codecapi.h>
#include <algorithm>
#include <iostream>
#include <cstring>

Expand Down Expand Up @@ -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;
Expand All @@ -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<UINT>(width_) &&
sourceDesc.Height == static_cast<UINT>(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<UINT>(width_));
sourceBox.bottom = (std::min)(sourceDesc.Height, static_cast<UINT>(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);
Expand Down Expand Up @@ -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();
Expand Down
2 changes: 2 additions & 0 deletions electron/native/wgc-capture/src/mf_encoder.h
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,8 @@ class MFEncoder {
ID3D11Device* device_ = nullptr;
ID3D11DeviceContext* context_ = nullptr;
ComPtr<ID3D11Texture2D> stagingTexture_;
ComPtr<ID3D11Texture2D> resizeCompositeTexture_;
ComPtr<ID3D11RenderTargetView> resizeCompositeView_;
std::vector<uint8_t> nv12Buffer_;
std::vector<uint8_t> lastFrameBuffer_;
DWORD streamIndex_ = 0;
Expand Down
51 changes: 50 additions & 1 deletion electron/native/wgc-capture/src/wgc_session.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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() {
Expand Down Expand Up @@ -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_,
Expand All @@ -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;
}
Comment thread
coderabbitai[bot] marked this conversation as resolved.

return true;
Expand Down Expand Up @@ -174,6 +217,7 @@ bool WgcSession::startCapture() {
if (!session_ || !framePool_) return false;

capturing_ = true;
fatalError_ = false;
lastFrameTimeHns_ = 0;

frameArrivedRevoker_ = framePool_.FrameArrived(
Expand Down Expand Up @@ -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<std::chrono::duration<int64_t, std::ratio<1, 10000000>>>(timestamp).count();
Expand Down
6 changes: 6 additions & 0 deletions electron/native/wgc-capture/src/wgc_session.h
Original file line number Diff line number Diff line change
Expand Up @@ -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_; }
Expand All @@ -45,9 +46,12 @@ class WgcSession {

FrameCallback frameCallback_;
std::atomic<bool> capturing_{false};
std::atomic<bool> 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;

Expand All @@ -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);
Expand Down