From 19869aeba43348451d807fa98ed1e5de509f9aad Mon Sep 17 00:00:00 2001 From: EtienneLescot Date: Fri, 6 Nov 2026 21:35:35 +0100 Subject: [PATCH] fix(capture): give WGC free buffers so a busy GPU stops halving the frame rate The pool had two buffers and the writer held one until the next tick, so under GPU load WGC dropped every other frame. Pool of three, drain the queue to the newest frame, hand the buffer back right after the copy, and log unique frames per second in [pacing]. Refs #510 --- .../native/wgc-capture/src/capture_session.h | 7 +++++ electron/native/wgc-capture/src/main.cpp | 19 +++++++++---- .../native/wgc-capture/src/wgc_session.cpp | 27 ++++++++++++------- electron/native/wgc-capture/src/wgc_session.h | 14 +++++++--- 4 files changed, 49 insertions(+), 18 deletions(-) diff --git a/electron/native/wgc-capture/src/capture_session.h b/electron/native/wgc-capture/src/capture_session.h index 467a3b3783..449bd64d56 100644 --- a/electron/native/wgc-capture/src/capture_session.h +++ b/electron/native/wgc-capture/src/capture_session.h @@ -88,6 +88,13 @@ class CaptureSession { } } + // DXGI already releases its frame inside tryGetNextFrame(). + void releaseFrame() { + if (backend_ == Backend::Wgc) { + wgc_.releaseFrame(); + } + } + void setFrameCallback(FrameCallback callback) { if (backend_ == Backend::Wgc) { wgc_.setFrameCallback(std::move(callback)); diff --git a/electron/native/wgc-capture/src/main.cpp b/electron/native/wgc-capture/src/main.cpp index ff369d9cd7..56987ac0f4 100644 --- a/electron/native/wgc-capture/src/main.cpp +++ b/electron/native/wgc-capture/src/main.cpp @@ -1079,6 +1079,10 @@ int wmain(int argc, wchar_t* argv[]) { const auto frameDuration = std::chrono::duration_cast( std::chrono::duration(1.0 / config.fps)); uint64_t frameIndex = 0; + // Ticks that pulled a new screen frame. frameIndex counts every tick, + // repeats included, so it cannot show a capture that only delivers + // every other frame (#510). + uint64_t uniqueFrames = 0; int64_t lastEncodedVideoTimestampHns = -1; int64_t lastWebcamTimestampHns = -1; // Both streams are written on a real-time cadence, one FrameSlotClock @@ -1191,6 +1195,8 @@ int wmain(int argc, wchar_t* argv[]) { // separate WGC callback thread left for it to take a // lock down with it. session.context()->CopyResource(latestFrameTexture.Get(), wgcTexture); + session.releaseFrame(); + uniqueFrames += 1; firstFrameWritten = true; } else if (!latestFrameTexture) { // No frame captured yet at all: nothing to encode @@ -1353,11 +1359,14 @@ int wmain(int argc, wchar_t* argv[]) { Hns(videoClock.nextDueHns() - nowHns)), frameDuration)); } - std::cerr << "[pacing] frames=" << frameIndex << " elapsed_ms=" - << std::chrono::duration_cast( - std::chrono::steady_clock::now() - control.recordingStartedAt) - .count() - << " last_sample_ms=" << lastEncodedVideoTimestampHns / 10'000 << std::endl; + const auto pacingElapsedMs = std::chrono::duration_cast( + std::chrono::steady_clock::now() - control.recordingStartedAt) + .count(); + std::cerr << "[pacing] frames=" << frameIndex << " elapsed_ms=" << pacingElapsedMs + << " last_sample_ms=" << lastEncodedVideoTimestampHns / 10'000 + << " unique_frames=" << uniqueFrames << " unique_fps=" + << (pacingElapsedMs > 0 ? uniqueFrames * 1000.0 / pacingElapsedMs : 0.0) + << std::endl; }; std::thread videoWriterThread; diff --git a/electron/native/wgc-capture/src/wgc_session.cpp b/electron/native/wgc-capture/src/wgc_session.cpp index 726d7effd1..0ceb576377 100644 --- a/electron/native/wgc-capture/src/wgc_session.cpp +++ b/electron/native/wgc-capture/src/wgc_session.cpp @@ -250,7 +250,7 @@ bool WgcSession::initialize(HMONITOR monitor, int fps, bool captureCursor) { framePool_ = wgcap::Direct3D11CaptureFramePool::CreateFreeThreaded( winrtDevice_, wgdx::DirectXPixelFormat::B8G8R8A8UIntNormalized, - 2, + kFramePoolBuffers, winrt::Windows::Graphics::SizeInt32{width_, height_}); session_ = framePool_.CreateCaptureSession(item_); @@ -274,7 +274,7 @@ bool WgcSession::initialize(HWND window, int fps, bool captureCursor, bool inclu framePool_ = wgcap::Direct3D11CaptureFramePool::CreateFreeThreaded( winrtDevice_, wgdx::DirectXPixelFormat::B8G8R8A8UIntNormalized, - 2, + kFramePoolBuffers, winrt::Windows::Graphics::SizeInt32{width_, height_}); session_ = framePool_.CreateCaptureSession(item_); @@ -308,10 +308,17 @@ bool WgcSession::tryGetNextFrame(ID3D11Texture2D** outTexture, int64_t* outTimes // does with *outTexture) is the caller's own doing on the caller's own // thread -- this class has no thread of its own left to hang on their // behalf. + // Drain the queue and keep only the newest frame: taking the oldest one + // per tick fell behind on a burst, and every frame left queued is a pool + // buffer WGC cannot write the next one into (#510). auto frame = framePool_.TryGetNextFrame(); if (!frame) { return false; } + while (auto newer = framePool_.TryGetNextFrame()) { + frame.Close(); + frame = newer; + } auto surface = frame.Surface(); auto access = surface.as<::Windows::Graphics::DirectX::Direct3D11::IDirect3DDxgiInterfaceAccess>(); @@ -321,13 +328,8 @@ bool WgcSession::tryGetNextFrame(ID3D11Texture2D** outTexture, int64_t* outTimes return false; } - // Closing the previous frame here (rather than right after this class - // copied out of it) returns it to the pool only once the caller has had a - // full interval to read the one before that -- the pool has 2 buffers, so - // closing eagerly would let WGC recycle a buffer the caller might still - // be mid-CopyResource on across the two-call boundary. currentFrame_ - // holds the reference that keeps *outTexture valid until this class's - // next call or stop() closes it. + // currentFrame_ keeps *outTexture valid until the caller has queued its + // copy and calls releaseFrame() (or the next call / stop() replaces it). currentFrame_ = frame; *outTexture = texture.Get(); @@ -335,6 +337,13 @@ bool WgcSession::tryGetNextFrame(ID3D11Texture2D** outTexture, int64_t* outTimes return true; } +void WgcSession::releaseFrame() { + if (currentFrame_) { + currentFrame_.Close(); + currentFrame_ = nullptr; + } +} + void WgcSession::setFrameCallback(FrameCallback callback) { if (!legacyCallbackRegistered_ && framePool_) { frameArrivedToken_ = framePool_.FrameArrived({this, &WgcSession::onFrameArrived}); diff --git a/electron/native/wgc-capture/src/wgc_session.h b/electron/native/wgc-capture/src/wgc_session.h index 4dbb33e43c..a401adcec0 100644 --- a/electron/native/wgc-capture/src/wgc_session.h +++ b/electron/native/wgc-capture/src/wgc_session.h @@ -65,6 +65,11 @@ class WgcSession { // out of it (e.g. via CopyResource) before either. Do not mix with // setFrameCallback() on the same session. bool tryGetNextFrame(ID3D11Texture2D** outTexture, int64_t* outTimestampHns); + // Hands the frame from the last tryGetNextFrame() back to the pool. Call it + // as soon as the copy out of it is queued: holding it until the next tick + // left WGC one free buffer, and under GPU load it dropped every other + // frame for lack of a second one (#510). + void releaseFrame(); // Legacy push-based path (OPENSCREEN_WGC_LEGACY_FRAME_CALLBACK=1 only). // callback runs on a WGC-owned thread inside FrameArrived and may be @@ -100,11 +105,12 @@ class WgcSession { winrt::Windows::Graphics::Capture::GraphicsCaptureItem item_{nullptr}; winrt::Windows::Graphics::Capture::Direct3D11CaptureFramePool framePool_{nullptr}; winrt::Windows::Graphics::Capture::GraphicsCaptureSession session_{nullptr}; + // Three buffers, not two: one the caller is copying from, one WGC is + // writing, one spare for a frame that lands before the copy has run on a + // busy GPU (#510). + static constexpr int32_t kFramePoolBuffers = 3; // Keeps the most recent frame's WinRT wrapper (and therefore its - // pool-owned texture) alive between tryGetNextFrame() calls: the pool is - // created with 2 buffers, so holding this reference is what keeps the - // texture valid for the caller to read from until the next call reclaims - // it. + // pool-owned texture) alive from tryGetNextFrame() until releaseFrame(). winrt::Windows::Graphics::Capture::Direct3D11CaptureFrame currentFrame_{nullptr}; // Legacy push-based path state; unused unless setFrameCallback() is called. winrt::event_token frameArrivedToken_{};