77#include < winrt/base.h>
88
99#include < chrono>
10+ #include < exception>
1011#include < iostream>
1112#include < thread>
1213
@@ -31,6 +32,57 @@ bool succeeded(HRESULT hr, const char* label) {
3132 return false ;
3233}
3334
35+ // Turns a C++/WinRT throw into a logged `false`.
36+ //
37+ // The projected calls on the setup path -- get_activation_factory, .as<>,
38+ // item_.Size(), CreateFreeThreaded, CreateCaptureSession, FrameArrived,
39+ // StartCapture -- report failure by throwing, and none of them was caught.
40+ // initialize() therefore could not return false: an exception from any of them
41+ // unwound past it into std::terminate and the process ended with no message at
42+ // all, leaving Electron to report "the helper exited before recording started"
43+ // and nothing else. The HRESULT was in the exception the whole time.
44+ //
45+ // No such failure has actually been observed. This is written from reading the
46+ // calls, not from a reproduction -- see the PR for the crash that prompted it
47+ // and turned out to be an unrelated stack-buffer overrun, which is a fastfail
48+ // rather than an exception and is not catchable here or anywhere.
49+ //
50+ // The label is the diagnostic, so a region never spans two calls a reader would
51+ // want told apart: the frame pool and the capture session get one each. Where a
52+ // region does cover several calls it is because they are one step under one
53+ // name -- "GraphicsCaptureItem for a monitor" is the activation factory, the
54+ // interop cast and Size(), and knowing which of those three threw would not
55+ // change what you do next. `succeeded()` above stays as it is for the calls
56+ // that return an HRESULT rather than throwing; this is its counterpart, not its
57+ // replacement.
58+ template <typename Body>
59+ bool guardWinrt (const char * what, Body&& body) {
60+ try {
61+ return body ();
62+ } catch (winrt::hresult_error const & error) {
63+ std::cerr << " ERROR: " << what << " threw (hr=0x" << std::hex
64+ << static_cast <uint32_t >(error.code ()) << std::dec << " ): "
65+ << winrt::to_string (error.message ()) << std::endl;
66+ return false ;
67+ } catch (std::exception const & error) {
68+ std::cerr << " ERROR: " << what << " threw (" << error.what () << " )" << std::endl;
69+ return false ;
70+ } catch (...) {
71+ std::cerr << " ERROR: " << what << " threw a non-standard exception" << std::endl;
72+ return false ;
73+ }
74+ }
75+
76+ // Deliberately no GraphicsCaptureSession::IsSupported() pre-flight here, though
77+ // it would give a nicer message than an HRESULT on some later call. It is the
78+ // one thing that could *refuse* a recording that works today: a machine where
79+ // IsSupported() answers false but capture would have succeeded records fine now
80+ // and would stop doing so, and there is no evidence either way about whether
81+ // such a machine exists. That is the exact shape of the #336 regression -- a new
82+ // gate in front of a path that was working -- and a better error message is not
83+ // worth carrying it. Everything below only adds a branch that did not exist, so
84+ // at worst it never runs.
85+
3486int64_t timeSpanToHns (wf::TimeSpan const & value) {
3587 return value.count ();
3688}
@@ -132,43 +184,77 @@ bool WgcSession::createD3DDevice() {
132184}
133185
134186bool WgcSession::createCaptureItem (HMONITOR monitor) {
135- auto factory = winrt::get_activation_factory<wgcap::GraphicsCaptureItem>();
136- auto interop = factory.as <IGraphicsCaptureItemInterop>();
137-
138- wgcap::GraphicsCaptureItem item{nullptr };
139- HRESULT hr = interop->CreateForMonitor (
140- monitor,
141- winrt::guid_of<wgcap::GraphicsCaptureItem>(),
142- reinterpret_cast <void **>(winrt::put_abi (item)));
143- if (!succeeded (hr, " CreateForMonitor" )) {
144- return false ;
145- }
187+ return guardWinrt (" GraphicsCaptureItem for a monitor" , [&] {
188+ auto factory = winrt::get_activation_factory<wgcap::GraphicsCaptureItem>();
189+ auto interop = factory.as <IGraphicsCaptureItemInterop>();
190+
191+ wgcap::GraphicsCaptureItem item{nullptr };
192+ HRESULT hr = interop->CreateForMonitor (
193+ monitor,
194+ winrt::guid_of<wgcap::GraphicsCaptureItem>(),
195+ reinterpret_cast <void **>(winrt::put_abi (item)));
196+ if (!succeeded (hr, " CreateForMonitor" )) {
197+ return false ;
198+ }
146199
147- item_ = item;
148- const auto size = item_.Size ();
149- width_ = static_cast <int >(size.Width );
150- height_ = static_cast <int >(size.Height );
151- return width_ > 0 && height_ > 0 ;
200+ item_ = item;
201+ const auto size = item_.Size ();
202+ width_ = static_cast <int >(size.Width );
203+ height_ = static_cast <int >(size.Height );
204+ return width_ > 0 && height_ > 0 ;
205+ });
152206}
153207
154208bool WgcSession::createCaptureItem (HWND window) {
155- auto factory = winrt::get_activation_factory<wgcap::GraphicsCaptureItem>();
156- auto interop = factory.as <IGraphicsCaptureItemInterop>();
157-
158- wgcap::GraphicsCaptureItem item{nullptr };
159- HRESULT hr = interop->CreateForWindow (
160- window,
161- winrt::guid_of<wgcap::GraphicsCaptureItem>(),
162- reinterpret_cast <void **>(winrt::put_abi (item)));
163- if (!succeeded (hr, " CreateForWindow" )) {
209+ return guardWinrt (" GraphicsCaptureItem for a window" , [&] {
210+ auto factory = winrt::get_activation_factory<wgcap::GraphicsCaptureItem>();
211+ auto interop = factory.as <IGraphicsCaptureItemInterop>();
212+
213+ wgcap::GraphicsCaptureItem item{nullptr };
214+ HRESULT hr = interop->CreateForWindow (
215+ window,
216+ winrt::guid_of<wgcap::GraphicsCaptureItem>(),
217+ reinterpret_cast <void **>(winrt::put_abi (item)));
218+ if (!succeeded (hr, " CreateForWindow" )) {
219+ return false ;
220+ }
221+
222+ item_ = item;
223+ const auto size = item_.Size ();
224+ width_ = roundUpToEven (static_cast <int >(size.Width ));
225+ height_ = roundUpToEven (static_cast <int >(size.Height ));
226+ return width_ > 0 && height_ > 0 ;
227+ });
228+ }
229+
230+ // Two guards, not one around both: they are separate projected calls, and a
231+ // single region would have reported a CreateCaptureSession throw under the
232+ // CreateFreeThreaded label -- naming the wrong call, which is worse than naming
233+ // none.
234+ bool WgcSession::createFramePoolAndSession () {
235+ const bool pooled = guardWinrt (" Direct3D11CaptureFramePool::CreateFreeThreaded" , [&] {
236+ framePool_ = wgcap::Direct3D11CaptureFramePool::CreateFreeThreaded (
237+ winrtDevice_,
238+ wgdx::DirectXPixelFormat::B8G8R8A8UIntNormalized,
239+ 2 ,
240+ winrt::Windows::Graphics::SizeInt32{width_, height_});
241+ return true ;
242+ });
243+ if (!pooled) {
164244 return false ;
165245 }
166246
167- item_ = item;
168- const auto size = item_.Size ();
169- width_ = roundUpToEven (static_cast <int >(size.Width ));
170- height_ = roundUpToEven (static_cast <int >(size.Height ));
171- return width_ > 0 && height_ > 0 ;
247+ return guardWinrt (" Direct3D11CaptureFramePool::CreateCaptureSession" , [&] {
248+ session_ = framePool_.CreateCaptureSession (item_);
249+ return true ;
250+ });
251+ }
252+
253+ bool WgcSession::registerFrameArrived () {
254+ return guardWinrt (" Direct3D11CaptureFramePool::FrameArrived" , [&] {
255+ frameArrivedToken_ = framePool_.FrameArrived ({this , &WgcSession::onFrameArrived});
256+ return true ;
257+ });
172258}
173259
174260bool WgcSession::applySessionOptions (bool captureCursor) {
@@ -217,52 +303,26 @@ bool WgcSession::applySessionOptions(bool captureCursor) {
217303 return true ;
218304}
219305
306+ // Every step reports its own failure, so the two overloads are the step list and
307+ // nothing else. Each returns false rather than throwing past its caller, which
308+ // is what main.cpp's "Failed to initialize WGC display session" has always
309+ // assumed and, until now, was not true of any of them.
220310bool WgcSession::initialize (HMONITOR monitor, int fps, bool captureCursor) {
221311 fps_ = fps > 0 ? fps : 60 ;
222- if (!createD3DDevice ()) {
223- return false ;
224- }
225- if (!createCaptureItem (monitor)) {
226- return false ;
227- }
228-
229- framePool_ = wgcap::Direct3D11CaptureFramePool::CreateFreeThreaded (
230- winrtDevice_,
231- wgdx::DirectXPixelFormat::B8G8R8A8UIntNormalized,
232- 2 ,
233- winrt::Windows::Graphics::SizeInt32{width_, height_});
234- session_ = framePool_.CreateCaptureSession (item_);
235-
236- if (!applySessionOptions (captureCursor)) {
237- return false ;
238- }
239-
240- frameArrivedToken_ = framePool_.FrameArrived ({this , &WgcSession::onFrameArrived});
241- return true ;
312+ return createD3DDevice () &&
313+ createCaptureItem (monitor) &&
314+ createFramePoolAndSession () &&
315+ applySessionOptions (captureCursor) &&
316+ registerFrameArrived ();
242317}
243318
244319bool WgcSession::initialize (HWND window, int fps, bool captureCursor) {
245320 fps_ = fps > 0 ? fps : 60 ;
246- if (!createD3DDevice ()) {
247- return false ;
248- }
249- if (!createCaptureItem (window)) {
250- return false ;
251- }
252-
253- framePool_ = wgcap::Direct3D11CaptureFramePool::CreateFreeThreaded (
254- winrtDevice_,
255- wgdx::DirectXPixelFormat::B8G8R8A8UIntNormalized,
256- 2 ,
257- winrt::Windows::Graphics::SizeInt32{width_, height_});
258- session_ = framePool_.CreateCaptureSession (item_);
259-
260- if (!applySessionOptions (captureCursor)) {
261- return false ;
262- }
263-
264- frameArrivedToken_ = framePool_.FrameArrived ({this , &WgcSession::onFrameArrived});
265- return true ;
321+ return createD3DDevice () &&
322+ createCaptureItem (window) &&
323+ createFramePoolAndSession () &&
324+ applySessionOptions (captureCursor) &&
325+ registerFrameArrived ();
266326}
267327
268328void WgcSession::setFrameCallback (FrameCallback callback) {
@@ -277,7 +337,12 @@ bool WgcSession::start() {
277337 if (!applySessionOptions (captureCursor_)) {
278338 return false ;
279339 }
280- session_.StartCapture ();
340+ if (!guardWinrt (" GraphicsCaptureSession::StartCapture" , [&] {
341+ session_.StartCapture ();
342+ return true ;
343+ })) {
344+ return false ;
345+ }
281346 started_ = true ;
282347 return true ;
283348}
@@ -359,9 +424,36 @@ void WgcSession::stop() {
359424 d3dDevice_.Reset ();
360425}
361426
427+ // The same defect as the setup path, on the hot path. TryGetNextFrame,
428+ // Surface(), the interop cast and SystemRelativeTime() are all projections that
429+ // throw, and a throw leaving a WinRT delegate goes straight to std::terminate --
430+ // a recording that ends with the process disappearing mid-capture, no stderr,
431+ // and the partial file as the only evidence.
432+ //
433+ // Dropping the frame is the only useful response: one bad frame is not a reason
434+ // to end a recording, and a surface that has gone bad usually stays bad. So it
435+ // is logged once and not at frame rate, which at 60 fps is the difference
436+ // between a diagnostic and a denial of service on the log.
362437void WgcSession::onFrameArrived (
363438 wgcap::Direct3D11CaptureFramePool const & sender,
364439 wf::IInspectable const &) {
440+ try {
441+ deliverFrame (sender);
442+ } catch (winrt::hresult_error const & error) {
443+ if (!frameErrorLogged_.exchange (true )) {
444+ std::cerr << " WARNING: Dropped a WGC frame (hr=0x" << std::hex
445+ << static_cast <uint32_t >(error.code ()) << std::dec
446+ << " ). Further frame errors are not repeated." << std::endl;
447+ }
448+ } catch (...) {
449+ if (!frameErrorLogged_.exchange (true )) {
450+ std::cerr << " WARNING: Dropped a WGC frame. "
451+ << " Further frame errors are not repeated." << std::endl;
452+ }
453+ }
454+ }
455+
456+ void WgcSession::deliverFrame (wgcap::Direct3D11CaptureFramePool const & sender) {
365457 auto frame = sender.TryGetNextFrame ();
366458 if (!frame) {
367459 return ;
0 commit comments