diff --git a/electron/native/wgc-capture/src/capture_session.h b/electron/native/wgc-capture/src/capture_session.h index 467a3b378..449bd64d5 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 ff369d9cd..56987ac0f 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 726d7effd..0ceb57637 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 4dbb33e43..a401adcec 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_{};