diff --git a/electron/native/wgc-capture/CMakeLists.txt b/electron/native/wgc-capture/CMakeLists.txt index 7f0cbf02d5..87eaf2af97 100644 --- a/electron/native/wgc-capture/CMakeLists.txt +++ b/electron/native/wgc-capture/CMakeLists.txt @@ -38,6 +38,7 @@ set(CMAKE_CXX_EXTENSIONS OFF) add_executable(wgc-capture src/audio_sample_utils.cpp src/audio_sample_utils.h + src/capture_session.h src/desktop_icon_cover.cpp src/desktop_icon_cover.h src/dpi_awareness.h @@ -47,6 +48,8 @@ add_executable(wgc-capture src/frame_visibility.h src/dshow_webcam_capture.cpp src/dshow_webcam_capture.h + src/dxgi_duplication_session.cpp + src/dxgi_duplication_session.h src/main.cpp src/mf_encoder.cpp src/mf_encoder.h @@ -78,6 +81,7 @@ target_link_libraries(wgc-capture PRIVATE avrt d3d11 dxgi + gdi32 mf mfplat mfreadwrite diff --git a/electron/native/wgc-capture/src/capture_session.h b/electron/native/wgc-capture/src/capture_session.h new file mode 100644 index 0000000000..467a3b3783 --- /dev/null +++ b/electron/native/wgc-capture/src/capture_session.h @@ -0,0 +1,166 @@ +#pragma once + +#include "dxgi_duplication_session.h" +#include "wgc_session.h" + +#include +#include +#include +#include +#include +#include + +// One frame source, two backends, chosen at initialize() time. +// +// main.cpp used to talk to WgcSession directly. It now talks to this instead, +// so the choice between WGC and DXGI Desktop Duplication lives in one place and +// every downstream stage -- the encoder, the MP4 muxer, audio, webcam, the +// cursor sampler, the JSON event protocol -- is unchanged and unaware. +// +// The fallback exists because CreateForMonitor can be refused with +// E_ACCESSDENIED on a machine that captures perfectly well otherwise; see +// dxgi_duplication_session.h for the measurements. WGC is still tried first +// everywhere, so a machine where it works keeps the backend it has always had. +class CaptureSession { +public: + using FrameCallback = WgcSession::FrameCallback; + + // OPENSCREEN_CAPTURE_BACKEND=dxgi force Desktop Duplication (diagnostics) + // =wgc force WGC, no fallback (diagnostics) + // unset try WGC, fall back to DXGI + static std::string requestedBackend() { + const char* raw = std::getenv("OPENSCREEN_CAPTURE_BACKEND"); + if (!raw) { + return {}; + } + std::string value(raw); + for (char& c : value) { + c = static_cast(std::tolower(static_cast(c))); + } + return value; + } + + bool initialize(HMONITOR monitor, int fps, bool captureCursor) { + const std::string requested = requestedBackend(); + if (requested == "dxgi") { + return startDxgi(monitor, fps, captureCursor, "requested"); + } + if (wgc_.initialize(monitor, fps, captureCursor)) { + return adoptWgc(); + } + wgcUnavailable_ = true; + if (requested == "wgc") { + return false; + } + wgc_.stop(); + return startDxgi(monitor, fps, captureCursor, "wgc-unavailable"); + } + + bool initialize(HWND window, int fps, bool captureCursor, bool includeSecondaryWindows = false) { + // Desktop Duplication duplicates an output, never a single window, so a + // window take has no fallback: it is WGC or it is a failure. + if (wgc_.initialize(window, fps, captureCursor, includeSecondaryWindows)) { + return adoptWgc(); + } + wgcUnavailable_ = true; + return false; + } + + bool start() { + switch (backend_) { + case Backend::Wgc: + return wgc_.start(); + case Backend::Dxgi: + return dxgi_.start(); + default: + return false; + } + } + + bool tryGetNextFrame(ID3D11Texture2D** outTexture, int64_t* outTimestampHns) { + switch (backend_) { + case Backend::Wgc: + return wgc_.tryGetNextFrame(outTexture, outTimestampHns); + case Backend::Dxgi: + return dxgi_.tryGetNextFrame(outTexture, outTimestampHns); + default: + return false; + } + } + + void setFrameCallback(FrameCallback callback) { + if (backend_ == Backend::Wgc) { + wgc_.setFrameCallback(std::move(callback)); + } + } + + bool quiesceLegacyCallback(int drainTimeoutMs = 5000) { + if (backend_ == Backend::Wgc) { + return wgc_.quiesceLegacyCallback(drainTimeoutMs); + } + return true; + } + + void stop() { + wgc_.stop(); + dxgi_.stop(); + backend_ = Backend::None; + } + + int captureWidth() const { + return backend_ == Backend::Dxgi ? dxgi_.captureWidth() : wgc_.captureWidth(); + } + + int captureHeight() const { + return backend_ == Backend::Dxgi ? dxgi_.captureHeight() : wgc_.captureHeight(); + } + + ID3D11Device* device() const { + return backend_ == Backend::Dxgi ? dxgi_.device() : wgc_.device(); + } + + ID3D11DeviceContext* context() const { + return backend_ == Backend::Dxgi ? dxgi_.context() : wgc_.context(); + } + + bool usingDxgi() const { return backend_ == Backend::Dxgi; } + bool wgcUnavailable() const { return wgcUnavailable_; } + + const char* backendName() const { + switch (backend_) { + case Backend::Wgc: + return "wgc"; + case Backend::Dxgi: + return "dxgi-duplication"; + default: + return "none"; + } + } + +private: + enum class Backend { None, Wgc, Dxgi }; + + bool adoptWgc() { + backend_ = Backend::Wgc; + std::cout << "{\"event\":\"capture-backend\",\"backend\":\"wgc\"}" << std::endl; + return true; + } + + bool startDxgi(HMONITOR monitor, int fps, bool captureCursor, const char* reason) { + if (!dxgi_.initialize(monitor, fps, captureCursor)) { + return false; + } + backend_ = Backend::Dxgi; + // Printed rather than only logged: which backend ran is a per-machine + // outcome the app reports next to encoder-selection, so a future report + // says which one produced the file without anyone having to guess. + std::cout << "{\"event\":\"capture-backend\",\"backend\":\"dxgi-duplication\",\"reason\":\"" + << reason << "\"}" << std::endl; + return true; + } + + Backend backend_ = Backend::None; + WgcSession wgc_; + DxgiDuplicationSession dxgi_; + bool wgcUnavailable_ = false; +}; diff --git a/electron/native/wgc-capture/src/dxgi_duplication_session.cpp b/electron/native/wgc-capture/src/dxgi_duplication_session.cpp new file mode 100644 index 0000000000..e4a4b8c07a --- /dev/null +++ b/electron/native/wgc-capture/src/dxgi_duplication_session.cpp @@ -0,0 +1,368 @@ +#include "dxgi_duplication_session.h" + +#include +#include + +namespace { + +// Same device shape WgcSession creates, but on the adapter that owns the output: +// DuplicateOutput refuses a device from any other adapter, which is the normal +// case for a display wired to the discrete GPU of a hybrid laptop. +bool createDevice(IDXGIAdapter1* adapter, Microsoft::WRL::ComPtr& device, + Microsoft::WRL::ComPtr& context) { + const D3D_FEATURE_LEVEL levels[] = { + D3D_FEATURE_LEVEL_11_1, + D3D_FEATURE_LEVEL_11_0, + D3D_FEATURE_LEVEL_10_1, + D3D_FEATURE_LEVEL_10_0, + }; + UINT flags = D3D11_CREATE_DEVICE_BGRA_SUPPORT; + D3D_FEATURE_LEVEL obtained = D3D_FEATURE_LEVEL_11_0; + HRESULT hr = D3D11CreateDevice(adapter, D3D_DRIVER_TYPE_UNKNOWN, nullptr, flags, levels, + ARRAYSIZE(levels), D3D11_SDK_VERSION, device.GetAddressOf(), + &obtained, context.GetAddressOf()); + if (FAILED(hr)) { + hr = D3D11CreateDevice(adapter, D3D_DRIVER_TYPE_UNKNOWN, nullptr, + flags & ~D3D11_CREATE_DEVICE_BGRA_SUPPORT, levels, + ARRAYSIZE(levels), D3D11_SDK_VERSION, device.GetAddressOf(), + &obtained, context.GetAddressOf()); + } + if (FAILED(hr)) { + std::cerr << "ERROR: DXGI capture could not create a D3D11 device (hr=0x" << std::hex << hr + << std::dec << ")" << std::endl; + return false; + } + return true; +} + +} // namespace + +DxgiDuplicationSession::~DxgiDuplicationSession() { + stop(); +} + +bool DxgiDuplicationSession::findOutput(HMONITOR monitor) { + output_.Reset(); + adapter_.Reset(); + Microsoft::WRL::ComPtr factory; + if (FAILED(CreateDXGIFactory1(IID_PPV_ARGS(&factory)))) { + std::cerr << "ERROR: DXGI capture could not create a DXGI factory" << std::endl; + return false; + } + + // The monitor handle the caller matched with EnumDisplayMonitors is the only + // thing tying the app's chosen source to an output, so match on it rather + // than on index or position: a display can be rearranged between the picker + // and the take. Every adapter is searched, not just the default one. + Microsoft::WRL::ComPtr adapter; + for (UINT a = 0; factory->EnumAdapters1(a, adapter.ReleaseAndGetAddressOf()) == S_OK; ++a) { + Microsoft::WRL::ComPtr candidate; + for (UINT i = 0; adapter->EnumOutputs(i, candidate.ReleaseAndGetAddressOf()) == S_OK; ++i) { + DXGI_OUTPUT_DESC desc{}; + if (FAILED(candidate->GetDesc(&desc)) || desc.Monitor != monitor) { + continue; + } + if (desc.Rotation != DXGI_MODE_ROTATION_IDENTITY && + desc.Rotation != DXGI_MODE_ROTATION_UNSPECIFIED) { + // Desktop Duplication hands over the unrotated scan-out image, so a + // portrait display would record sideways. Refuse rather than do that. + std::cerr << "ERROR: DXGI capture does not support rotated displays (rotation=" + << desc.Rotation << ")" << std::endl; + return false; + } + if (FAILED(candidate.As(&output_))) { + std::cerr << "ERROR: DXGI capture could not query IDXGIOutput1 for the chosen monitor" + << std::endl; + return false; + } + adapter_ = adapter; + desktopRect_ = desc.DesktopCoordinates; + return true; + } + } + std::cerr << "ERROR: DXGI capture could not find an output for the chosen monitor" + << std::endl; + return false; +} + +bool DxgiDuplicationSession::createD3DDevice() { + if (d3dDevice_) { + return true; + } + if (!createDevice(adapter_.Get(), d3dDevice_, d3dContext_)) { + return false; + } + QueryPerformanceFrequency(&qpcFrequency_); + return true; +} + +bool DxgiDuplicationSession::createDuplication() { + Microsoft::WRL::ComPtr duplication; + HRESULT hr = output_->DuplicateOutput(d3dDevice_.Get(), duplication.GetAddressOf()); + if (FAILED(hr)) { + // DXGI_ERROR_NOT_CURRENTLY_AVAILABLE is what a machine reports when the + // maximum number of duplications is already in use; anything else here + // is a driver refusal. + std::cerr << "ERROR: DXGI capture DuplicateOutput failed (hr=0x" << std::hex << hr << std::dec + << ")" << std::endl; + return false; + } + duplication_ = duplication; + + DXGI_OUTDUPL_DESC desc{}; + duplication_->GetDesc(&desc); + width_ = static_cast(desc.ModeDesc.Width); + height_ = static_cast(desc.ModeDesc.Height); + if (width_ <= 0 || height_ <= 0) { + std::cerr << "ERROR: DXGI capture reported an empty desktop (" << width_ << "x" << height_ + << ")" << std::endl; + return false; + } + return true; +} + +bool DxgiDuplicationSession::ensureCopyTexture() { + if (copyTexture_) { + D3D11_TEXTURE2D_DESC existing{}; + copyTexture_->GetDesc(&existing); + if (static_cast(existing.Width) == width_ && + static_cast(existing.Height) == height_) { + return true; + } + copyTexture_.Reset(); + } + + // A private destination for the acquired frame, so AcquireNextFrame can be + // released immediately and the pointer handed to the caller stays valid. + // Flags match what main.cpp already does when it copies a WGC frame out: + // no bind flags, no CPU access -- it is only ever a CopyResource source. + // With the system cursor it must also take a GDI DC, which needs a render + // target that is GDI-compatible (drawCursor). + D3D11_TEXTURE2D_DESC desc{}; + desc.Width = static_cast(width_); + desc.Height = static_cast(height_); + desc.MipLevels = 1; + desc.ArraySize = 1; + desc.Format = DXGI_FORMAT_B8G8R8A8_UNORM; + desc.SampleDesc.Count = 1; + desc.Usage = D3D11_USAGE_DEFAULT; + desc.BindFlags = captureCursor_ ? D3D11_BIND_RENDER_TARGET : 0; + desc.CPUAccessFlags = 0; + desc.MiscFlags = captureCursor_ ? D3D11_RESOURCE_MISC_GDI_COMPATIBLE : 0; + const HRESULT hr = d3dDevice_->CreateTexture2D(&desc, nullptr, copyTexture_.GetAddressOf()); + if (FAILED(hr)) { + std::cerr << "ERROR: DXGI capture could not create its copy texture (hr=0x" << std::hex << hr + << std::dec << ")" << std::endl; + return false; + } + return true; +} + +bool DxgiDuplicationSession::initialize(HMONITOR monitor, int fps, bool captureCursor) { + if (!monitor) { + return false; + } + captureCursor_ = captureCursor; + fps_ = std::clamp(fps, 1, 120); + monitor_ = monitor; + + if (!findOutput(monitor) || !createD3DDevice()) { + return false; + } + if (!createDuplication()) { + return false; + } + if (!ensureCopyTexture()) { + return false; + } + return true; +} + +bool DxgiDuplicationSession::initialize(HWND, int, bool, bool) { + // Desktop Duplication duplicates an output, never a single window. There is + // no DXGI equivalent of CreateForWindow, so a window take has to stay on + // WGC -- say so rather than pretending to be ready. + std::cerr << "WARNING: DXGI capture cannot capture a single window; only displays" << std::endl; + return false; +} + +bool DxgiDuplicationSession::start() { + if (!duplication_) { + return false; + } + started_ = true; + return true; +} + +int64_t DxgiDuplicationSession::ticksToHns(LONGLONG ticks) const { + if (qpcFrequency_.QuadPart <= 0) { + return ticks; + } + // 10'000'000 == one second in 100 ns units. + // Split so ticks * 10^7 never overflows (it would after ~25 h of uptime). + const LONGLONG f = qpcFrequency_.QuadPart; + return static_cast((ticks / f) * 10'000'000LL + ((ticks % f) * 10'000'000LL) / f); +} + +bool DxgiDuplicationSession::reacquire() { + // DXGI_ERROR_ACCESS_LOST is normal: a UAC prompt, a desktop switch, a + // resolution or refresh-rate change, or another process taking exclusive + // fullscreen all invalidate the duplication. Rebuild it and let the caller + // keep polling -- a frame is simply skipped, never a recording ended. While + // the secure desktop is up, DuplicateOutput keeps failing, so the caller + // retries about once a second until it comes back. + lastReacquireAttemptMs_ = GetTickCount64(); + duplication_.Reset(); + copyTexture_.Reset(); + if (!d3dDevice_ || !findOutput(monitor_) || !createDuplication() || !ensureCopyTexture()) { + duplication_.Reset(); + if (!warnedReacquireFailed_) { + warnedReacquireFailed_ = true; + std::cerr << "WARNING: DXGI capture lost the desktop and could not reacquire it yet; " + "retrying every second" + << std::endl; + } + return false; + } + warnedReacquireFailed_ = false; + std::cout << "{\"event\":\"capture-reacquired\",\"backend\":\"dxgi\"}" << std::endl; + return true; +} + +bool DxgiDuplicationSession::tryGetNextFrame(ID3D11Texture2D** outTexture, int64_t* outTimestampHns) { + if (!outTexture || !outTimestampHns || !started_) { + return false; + } + if (!duplication_) { + // A previous rebuild failed: try again, throttled, rather than give up. + if (GetTickCount64() - lastReacquireAttemptMs_ < 1000 || !reacquire()) { + return false; + } + } + *outTexture = nullptr; + + // Wait no longer than one frame interval. main.cpp polls this on its own + // cadence while it holds no lock, and a longer block here would simply make + // the writer answer `stop` late. + const UINT timeoutMs = static_cast(std::max(1, 1000 / std::max(1, fps_))); + DXGI_OUTDUPL_FRAME_INFO info{}; + Microsoft::WRL::ComPtr resource; + const HRESULT hr = duplication_->AcquireNextFrame(timeoutMs, &info, &resource); + + if (hr == DXGI_ERROR_WAIT_TIMEOUT) { + // A static desktop presents nothing. Not an error, and not a dropped frame. + return false; + } + if (hr == DXGI_ERROR_ACCESS_LOST) { + reacquire(); + return false; + } + if (FAILED(hr)) { + if (hr == DXGI_ERROR_INVALID_CALL) { + // Only reachable if a previous frame was not released; recover rather + // than wedge the writer on a permanently failing acquire. + duplication_->ReleaseFrame(); + return false; + } + std::cerr << "WARNING: DXGI capture AcquireNextFrame failed (hr=0x" << std::hex << hr + << std::dec << ")" << std::endl; + return false; + } + + Microsoft::WRL::ComPtr acquired; + bool copied = false; + if (SUCCEEDED(resource.As(&acquired)) && acquired) { + if (ensureCopyTexture()) { + d3dContext_->CopyResource(copyTexture_.Get(), acquired.Get()); + copied = true; + } + } + + // The frame must be released before the next acquisition, whatever happened + // above -- holding it would make every later AcquireNextFrame fail. + duplication_->ReleaseFrame(); + + if (!copied) { + return false; + } + + if (captureCursor_) { + drawCursor(); + } + + // LastPresentTime shares the QPC clock. A zero means the frame carried no + // timestamp, which falls back to now -- the value is only ever used as a + // delta from the first frame, so monotonic is what matters. + LONGLONG ticks = info.LastPresentTime.QuadPart; + if (ticks == 0) { + LARGE_INTEGER now{}; + QueryPerformanceCounter(&now); + ticks = now.QuadPart; + } + *outTimestampHns = ticksToHns(ticks); + *outTexture = copyTexture_.Get(); + return true; +} + +void DxgiDuplicationSession::drawCursor() { + CURSORINFO ci{}; + ci.cbSize = sizeof(ci); + if (!GetCursorInfo(&ci) || !(ci.flags & CURSOR_SHOWING) || !ci.hCursor) { + return; + } + ICONINFO icon{}; + if (!GetIconInfo(ci.hCursor, &icon)) { + return; + } + // GetIconInfo hands over copies of the bitmaps, which the caller owns. + if (icon.hbmMask) { + DeleteObject(icon.hbmMask); + } + if (icon.hbmColor) { + DeleteObject(icon.hbmColor); + } + + Microsoft::WRL::ComPtr surface; + HDC dc = nullptr; + if (FAILED(copyTexture_.As(&surface)) || FAILED(surface->GetDC(FALSE, &dc))) { + return; + } + // The helper is per-monitor DPI aware, so the cursor position and the + // output's desktop rectangle are both in physical pixels. DrawIconEx with + // DI_NORMAL applies the AND/XOR masks, so monochrome and inverting cursors + // look the way they do on screen. + DrawIconEx(dc, ci.ptScreenPos.x - desktopRect_.left - static_cast(icon.xHotspot), + ci.ptScreenPos.y - desktopRect_.top - static_cast(icon.yHotspot), ci.hCursor, 0, + 0, 0, nullptr, DI_NORMAL | DI_DEFAULTSIZE); + surface->ReleaseDC(nullptr); +} + +void DxgiDuplicationSession::setFrameCallback(FrameCallback callback) { + (void)callback; + // Deliberately inert: Desktop Duplication is pull-only, and rebuilding the + // WGC callback-thread model here would reintroduce the frame-lock hazard + // that pull-based delivery exists to avoid (see wgc_session.h). main.cpp + // keeps the pull path when this backend is active. +} + +bool DxgiDuplicationSession::quiesceLegacyCallback(int) { + // Nothing was ever registered -- report drained so the caller never waits. + return true; +} + +void DxgiDuplicationSession::stop() { + started_ = false; + if (duplication_) { + // An acquired-but-unreleased frame would keep the output duplicated for + // the rest of the process, so release unconditionally before dropping it. + duplication_->ReleaseFrame(); + } + copyTexture_.Reset(); + duplication_.Reset(); + output_.Reset(); + adapter_.Reset(); + d3dContext_.Reset(); + d3dDevice_.Reset(); + width_ = 0; + height_ = 0; +} diff --git a/electron/native/wgc-capture/src/dxgi_duplication_session.h b/electron/native/wgc-capture/src/dxgi_duplication_session.h new file mode 100644 index 0000000000..911a0b6f60 --- /dev/null +++ b/electron/native/wgc-capture/src/dxgi_duplication_session.h @@ -0,0 +1,97 @@ +#pragma once + +#include +#include +#include +#include + +#include +#include + +// DXGI Desktop Duplication capture, as an alternative frame source to +// WgcSession for the same pipeline. +// +// Why it exists: IGraphicsCaptureItemInterop::CreateForMonitor can be refused +// with E_ACCESSDENIED on a machine where the OS reports WGC as supported +// (GraphicsCaptureSession::IsSupported() == 1) and where every other capture +// route works -- measured on Windows 10 22H2 (19045.6466), where the same +// process that gets 0x80070005 from CreateForMonitor acquires 2560x1440 BGRA +// frames from IDXGIOutputDuplication for the same monitor, in the same run. +// Window capture has no equivalent here: Desktop Duplication only ever +// duplicates an output, so initialize(HWND, ...) refuses. +// +// The public surface mirrors WgcSession deliberately. main.cpp drives both +// through the CaptureSession wrapper, so nothing downstream -- encoder, MP4 +// muxer, audio, webcam, cursor sampler, the JSON event protocol -- knows which +// backend produced a frame. +class DxgiDuplicationSession { +public: + using FrameCallback = std::function; + + DxgiDuplicationSession() = default; + ~DxgiDuplicationSession(); + + DxgiDuplicationSession(const DxgiDuplicationSession&) = delete; + DxgiDuplicationSession& operator=(const DxgiDuplicationSession&) = delete; + + bool initialize(HMONITOR monitor, int fps, bool captureCursor); + // Unsupported by design -- see the class comment. Returns false and says so + // once, so a window take falls back to the WGC failure path rather than + // silently recording nothing. + bool initialize(HWND window, int fps, bool captureCursor, bool includeSecondaryWindows = false); + + bool start(); + // Returns the most recent frame's texture, or false when no new frame has + // been presented since the last call. Unlike WgcSession, the texture is a + // private copy owned by this session rather than pool-owned: AcquireNextFrame + // requires the frame to be released before the next acquisition, so the + // desktop image is copied out immediately and the returned pointer stays + // valid until the next call or stop(). + bool tryGetNextFrame(ID3D11Texture2D** outTexture, int64_t* outTimestampHns); + + // Pull-only backend. WgcSession's FrameArrived path exists as a rollback + // lever for a driver bug specific to WGC's callback thread (see wgc_session.h); + // Desktop Duplication has no equivalent push mode, so these are inert and + // main.cpp keeps the pull path when this backend is the active one. + void setFrameCallback(FrameCallback callback); + bool quiesceLegacyCallback(int drainTimeoutMs = 5000); + void stop(); + + int captureWidth() const { return width_; } + int captureHeight() const { return height_; } + ID3D11Device* device() const { return d3dDevice_.Get(); } + ID3D11DeviceContext* context() const { return d3dContext_.Get(); } + +private: + bool findOutput(HMONITOR monitor); + bool createD3DDevice(); + bool createDuplication(); + bool ensureCopyTexture(); + bool reacquire(); + // WGC draws the pointer itself; Desktop Duplication never does, so with the + // system cursor mode it is drawn here, over the private copy, through GDI. + void drawCursor(); + // LastPresentTime and QueryPerformanceCounter share the QPC clock, which is + // the same clock WGC's SystemRelativeTime is derived from -- and main.cpp + // only ever uses the value as a delta from the first frame, so a monotonic + // 100 ns count is all this has to be. + int64_t ticksToHns(LONGLONG ticks) const; + + Microsoft::WRL::ComPtr d3dDevice_; + Microsoft::WRL::ComPtr d3dContext_; + Microsoft::WRL::ComPtr adapter_; + Microsoft::WRL::ComPtr output_; + Microsoft::WRL::ComPtr duplication_; + Microsoft::WRL::ComPtr copyTexture_; + + HMONITOR monitor_ = nullptr; + LARGE_INTEGER qpcFrequency_{}; + int width_ = 0; + int height_ = 0; + int fps_ = 60; + bool captureCursor_ = false; + bool started_ = false; + RECT desktopRect_{}; + bool warnedReacquireFailed_ = false; + ULONGLONG lastReacquireAttemptMs_ = 0; +}; diff --git a/electron/native/wgc-capture/src/main.cpp b/electron/native/wgc-capture/src/main.cpp index bd28c5eb1b..ff369d9cd7 100644 --- a/electron/native/wgc-capture/src/main.cpp +++ b/electron/native/wgc-capture/src/main.cpp @@ -10,7 +10,7 @@ #include "wasapi_render_keepalive.h" #include "frame_visibility.h" #include "webcam_capture.h" -#include "wgc_session.h" +#include "capture_session.h" #include @@ -704,7 +704,7 @@ int wmain(int argc, wchar_t* argv[]) { // Declared before the session so it outlives it: the icons come back only // once nothing is recording any more. DesktopIconCover desktopIconCover; - WgcSession session; + CaptureSession session; HMONITOR capturedMonitor = nullptr; if (config.sourceType == "display") { HMONITOR monitor = findMonitorForCapture( @@ -716,7 +716,9 @@ int wmain(int argc, wchar_t* argv[]) { } capturedMonitor = monitor; if (!session.initialize(monitor, config.fps, config.captureCursor)) { - std::cerr << "ERROR: Failed to initialize WGC display session" << std::endl; + std::cerr << "ERROR: Failed to initialize display capture: Windows Graphics Capture " + "was refused and DXGI Desktop Duplication could not start either" + << std::endl; return 1; } // Before `recording-started`, so the first frame is already clean. A cover @@ -741,7 +743,7 @@ int wmain(int argc, wchar_t* argv[]) { const bool includeSecondaryWindows = readEnvInt("OPENSCREEN_WGC_DISABLE_SECONDARY_WINDOWS", 0) != 1; if (!session.initialize(window, config.fps, config.captureCursor, includeSecondaryWindows)) { - std::cerr << "ERROR: Failed to initialize WGC window session" << std::endl; + std::cerr << "ERROR: Failed to initialize window capture (Windows Graphics Capture)" << std::endl; return 1; } } else { @@ -1011,7 +1013,9 @@ int wmain(int argc, wchar_t* argv[]) { // OPENSCREEN_WGC_LEGACY_FRAME_CALLBACK=1 reverts to the previously // shipped push-based design (frameMutex/frameCv guard the handoff from // WGC's own callback thread) as a rollback lever -- see wgc_session.h. - const bool legacyFrameCallback = useLegacyFrameCallback(); + // The DXGI fallback is pull-only (see capture_session.h), so the legacy + // FrameArrived rollback lever only means anything on the WGC backend. + const bool legacyFrameCallback = useLegacyFrameCallback() && !session.usingDxgi(); CaptureControl control; std::atomic firstFrameWritten = false; std::atomic encodeFailed = false; @@ -1162,6 +1166,23 @@ int wmain(int argc, wchar_t* argv[]) { break; } } + // A display mode change mid-take (resolution, scale) + // hands over frames of a new size. The encoder was set + // up for the first one, and CopyResource between + // mismatched textures is undefined, so end the take + // with a clear error instead of recording garbage. + D3D11_TEXTURE2D_DESC frameDesc{}; + D3D11_TEXTURE2D_DESC latestDesc{}; + wgcTexture->GetDesc(&frameDesc); + latestFrameTexture->GetDesc(&latestDesc); + if (frameDesc.Width != latestDesc.Width || frameDesc.Height != latestDesc.Height) { + std::cerr << "ERROR: Capture size changed mid-recording (" << latestDesc.Width + << "x" << latestDesc.Height << " -> " << frameDesc.Width << "x" + << frameDesc.Height << "); ending the take" << std::endl; + encodeFailed = true; + control.requestStop(); + break; + } // The wedge risk this class exists to avoid: this call // can block inside the display driver and never return // (#252, still true of PR #305's DXGI path on some @@ -1477,7 +1498,8 @@ int wmain(int argc, wchar_t* argv[]) { if (audioMixer) { audioMixer->stop(); } - std::cerr << "ERROR: Failed to start WGC session" << std::endl; + std::cerr << "ERROR: Failed to start native capture session (" << session.backendName() << ")" + << std::endl; return 1; }