Skip to content

Commit ff5bfba

Browse files
authored
Keep Graphics Capture on the UI thread and bump to 1.0.2 (#70)
1.0.1 still crashed in the Store with `AccessViolationException` from `IObjectReference.Finalize` (`0xC0000005`). Disposing the session and frame fields was not enough. Two RCWs were still left for the GC, and both match create-LiveView-then-resize: - `GraphicsCapturePicker` is not `IDisposable`. The pick method allocated it and dropped it. Resize allocates, GC runs, the finalizer `Marshal.Release`s off the UI apartment. - `CreateFreeThreaded` built frames on a worker. Rate-limiting disposed most of those wrappers on that worker. In a packaged process they are apartment-bound. Create a WinRT `DispatcherQueue` on the WPF UI thread and hold the native controller for the process lifetime. Switch the frame pool to `Create` so `FrameArrived` stays on that thread. `Release` every capture RCW there, including the picker. `VersionPrefix` is 1.0.2 so this can go to the Store (identity `1.0.2.0`). Verified locally: `dotnet build Whiteboard.sln -c Release` is clean, Core smoke tests pass. Confirmation is still a packaged install on a machine that crashed on 1.0.1; the portable ZIP never hit this path.
1 parent 9506673 commit ff5bfba

6 files changed

Lines changed: 137 additions & 54 deletions

File tree

Directory.Build.props

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@
55
scripts/build-installer.ps1 both read it from here, so releasing is a reviewed change
66
to this line rather than an edit in a pipeline variable group.
77
-->
8-
<VersionPrefix>1.0.1</VersionPrefix>
8+
<VersionPrefix>1.0.2</VersionPrefix>
99
<LangVersion>latest</LangVersion>
1010
<Nullable>enable</Nullable>
1111
<ImplicitUsings>enable</ImplicitUsings>

TODO.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@ The delivery chain works end to end: a merge to `main` builds, signs, and publis
1616
pre-release to GitHub Releases, and one approval promotes that same build to a release.
1717
<https://whiteboard.sqlbi.com> reads its download links from the release manifest
1818
deployed beside it and needs no edit per release. The current product version is `VersionPrefix` in `Directory.Build.props`
19-
(1.0.1). Identity version for the Store package is `VersionPrefix.0` (`1.0.1.0`).
19+
(1.0.2). Identity version for the Store package is `VersionPrefix.0` (`1.0.2.0`).
2020

2121
Declaring that number is decision 20 in [docs/decisions.md](docs/decisions.md). What 1.0
2222
was waiting on shipped during 0.9.x: Preferences, `.wimport`, Explorer and VS Code

src/SQLBI.Whiteboard/App.xaml.cs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
using System.IO;
22
using System.Windows;
3+
using SQLBI.Whiteboard.LiveView;
34

45
namespace SQLBI.Whiteboard;
56

@@ -8,6 +9,7 @@ public partial class App : Application
89
protected override void OnStartup(StartupEventArgs e)
910
{
1011
base.OnStartup(e);
12+
WinRtThreading.EnsureDispatcherQueue();
1113
var window = new MainWindow(FindBoardPath(e.Args));
1214
MainWindow = window;
1315
window.Show();

src/SQLBI.Whiteboard/LiveView/LiveViewCaptureSession.cs

Lines changed: 34 additions & 49 deletions
Original file line numberDiff line numberDiff line change
@@ -111,7 +111,7 @@ public void AttachDevice(ID3D11Device1 device)
111111
{
112112
ThrowIfDisposed();
113113
StopCaptureCore();
114-
DisposeWinRt(_winRtDevice);
114+
WinRtThreading.Release(_winRtDevice);
115115
_winRtDevice = winRtDevice;
116116
StartCaptureCore();
117117
}
@@ -123,7 +123,7 @@ public void DetachDevice()
123123
lock (_gate)
124124
{
125125
StopCaptureCore();
126-
DisposeWinRt(_winRtDevice);
126+
WinRtThreading.Release(_winRtDevice);
127127
_winRtDevice = null;
128128
}
129129
}
@@ -229,12 +229,8 @@ public bool TryPresent(ID3D11DeviceContext1 context, ID3D11Texture2D destination
229229
}
230230
finally
231231
{
232-
// Close the surface RCW on this thread. Leaving it for the GC
233-
// finalizer AVs in Store-packaged processes: those WinRT objects
234-
// are apartment-bound and Marshal.Release from GC.RunFinalizers
235-
// is the 0xC0000005 that closes the app a few seconds after resize.
236-
DisposeWinRt(surface);
237-
DisposeWinRt(frame);
232+
WinRtThreading.Release(surface);
233+
WinRtThreading.Release(frame);
238234
}
239235
}
240236

@@ -256,7 +252,7 @@ public void Dispose()
256252
_disposed = true;
257253
StopCaptureCore();
258254
ReleaseCaptureItem();
259-
DisposeWinRt(_winRtDevice);
255+
WinRtThreading.Release(_winRtDevice);
260256
_winRtDevice = null;
261257
}
262258
}
@@ -268,8 +264,12 @@ private void StartCaptureCore()
268264
return;
269265
}
270266

267+
WinRtThreading.EnsureDispatcherQueue();
271268
_contentSize = SanitizeSize(_captureItem.Size);
272-
Direct3D11CaptureFramePool framePool = Direct3D11CaptureFramePool.CreateFreeThreaded(
269+
// Create, not CreateFreeThreaded: frames and their RCWs must be born
270+
// on this dispatcher. Free-threaded FrameArrived ran on a worker,
271+
// and those IObjectReferences finalized with 0xC0000005 in the Store.
272+
Direct3D11CaptureFramePool framePool = Direct3D11CaptureFramePool.Create(
273273
_winRtDevice,
274274
DirectXPixelFormat.B8G8R8A8UIntNormalized,
275275
2,
@@ -307,13 +307,19 @@ private void StopCaptureCore()
307307
framePool.FrameArrived -= FramePool_FrameArrived;
308308
}
309309

310-
DisposeWinRt(captureSession);
311-
DisposeWinRt(framePool);
312-
DisposeWinRt(pendingFrame);
310+
WinRtThreading.Release(captureSession);
311+
WinRtThreading.Release(framePool);
312+
WinRtThreading.Release(pendingFrame);
313313
}
314314

315315
private void FramePool_FrameArrived(Direct3D11CaptureFramePool sender, object args)
316316
{
317+
if (!_dispatcher.CheckAccess())
318+
{
319+
_ = _dispatcher.BeginInvoke(() => FramePool_FrameArrived(sender, args));
320+
return;
321+
}
322+
317323
Direct3D11CaptureFrame? frame = null;
318324
try
319325
{
@@ -328,7 +334,7 @@ private void FramePool_FrameArrived(Direct3D11CaptureFramePool sender, object ar
328334
{
329335
if (!ReferenceEquals(sender, _framePool) || _isFrozen)
330336
{
331-
DisposeWinRt(frame);
337+
WinRtThreading.Release(frame);
332338
return;
333339
}
334340

@@ -337,15 +343,15 @@ private void FramePool_FrameArrived(Direct3D11CaptureFramePool sender, object ar
337343
// Recreate discards native frames. Close the pending wrapper
338344
// first so its finalizer cannot Release a pointer Recreate
339345
// has already invalidated.
340-
DisposeWinRt(_pendingFrame);
346+
WinRtThreading.Release(_pendingFrame);
341347
_pendingFrame = null;
342348
_contentSize = size;
343349
sender.Recreate(
344350
_winRtDevice!,
345351
DirectXPixelFormat.B8G8R8A8UIntNormalized,
346352
2,
347353
size);
348-
DisposeWinRt(frame);
354+
WinRtThreading.Release(frame);
349355
frame = null;
350356
}
351357
}
@@ -361,7 +367,7 @@ private void FramePool_FrameArrived(Direct3D11CaptureFramePool sender, object ar
361367
long previous = Interlocked.Read(ref _lastAcceptedTimestamp);
362368
if (previous != 0 && now - previous < minimumTicks)
363369
{
364-
DisposeWinRt(frame);
370+
WinRtThreading.Release(frame);
365371
return;
366372
}
367373

@@ -370,23 +376,30 @@ private void FramePool_FrameArrived(Direct3D11CaptureFramePool sender, object ar
370376
{
371377
if (!ReferenceEquals(sender, _framePool) || _isFrozen)
372378
{
373-
DisposeWinRt(frame);
379+
WinRtThreading.Release(frame);
374380
return;
375381
}
376382

377383
Direct3D11CaptureFrame? replaced = _pendingFrame;
378384
_pendingFrame = frame;
379385
frame = null;
380-
DisposeWinRt(replaced);
386+
WinRtThreading.Release(replaced);
381387
}
382388

383389
FrameAvailable?.Invoke();
384390
}
385391
catch (Exception exception)
386392
{
387-
DisposeWinRt(frame);
393+
WinRtThreading.Release(frame);
388394
CaptureFailed?.Invoke(exception);
389395
}
396+
finally
397+
{
398+
if (args is IWinRTObject && !ReferenceEquals(args, sender))
399+
{
400+
WinRtThreading.Release(args);
401+
}
402+
}
390403
}
391404

392405
private void CaptureItem_Closed(GraphicsCaptureItem sender, object args)
@@ -438,9 +451,7 @@ private void ReleaseCaptureItem()
438451
}
439452

440453
_captureItem.Closed -= CaptureItem_Closed;
441-
// GraphicsCaptureItem is not IClosable. Drop the RCW's COM pointer here
442-
// so IObjectReference.Finalize never Release's it from the GC thread.
443-
DisposeWinRt(_captureItem);
454+
WinRtThreading.Release(_captureItem);
444455
_captureItem = null;
445456
}
446457

@@ -480,32 +491,6 @@ private static WinRtDirect3DDevice CreateWinRtDevice(IDXGIDevice dxgiDevice)
480491

481492
private void VerifyDispatcherAccess() => _dispatcher.VerifyAccess();
482493

483-
private static void DisposeWinRt(object? value)
484-
{
485-
if (value is null)
486-
{
487-
return;
488-
}
489-
490-
try
491-
{
492-
if (value is IDisposable disposable)
493-
{
494-
disposable.Dispose();
495-
return;
496-
}
497-
498-
if (value is IWinRTObject winrt)
499-
{
500-
winrt.NativeObject.Dispose();
501-
}
502-
}
503-
catch (Exception exception)
504-
{
505-
Debug.WriteLine($"[LiveView] WinRT release failed: {exception}");
506-
}
507-
}
508-
509494
[DllImport("d3d11.dll", ExactSpelling = true)]
510495
private static extern int CreateDirect3D11DeviceFromDXGIDevice(
511496
nint dxgiDevice,
Lines changed: 88 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,88 @@
1+
using System.Diagnostics;
2+
using System.Runtime.InteropServices;
3+
using Windows.System;
4+
using WinRT;
5+
6+
namespace SQLBI.Whiteboard.LiveView;
7+
8+
/// <summary>
9+
/// WinRT Graphics Capture RCWs are apartment-bound in a packaged (Store)
10+
/// process. Releasing them from the GC finalizer is the AccessViolation
11+
/// that closes the app. Keep a DispatcherQueue on the UI thread so
12+
/// <see cref="Windows.Graphics.Capture.Direct3D11CaptureFramePool.Create"/>
13+
/// delivers frames there, and Release every RCW on that same thread.
14+
/// </summary>
15+
internal static class WinRtThreading
16+
{
17+
// Native ref kept for the process lifetime. Wrapping it in a CsWinRT
18+
// RCW would only recreate the finalizer problem this exists to avoid.
19+
private static nint _dispatcherQueueController;
20+
21+
public static void EnsureDispatcherQueue()
22+
{
23+
if (DispatcherQueue.GetForCurrentThread() is not null)
24+
{
25+
return;
26+
}
27+
28+
DispatcherQueueOptions options = new()
29+
{
30+
dwSize = Marshal.SizeOf<DispatcherQueueOptions>(),
31+
threadType = 2, // DQTYPE_THREAD_CURRENT
32+
apartmentType = 0, // DQTAT_COM_NONE — WPF already initialized STA
33+
};
34+
35+
int hr = CreateDispatcherQueueController(options, out nint pointer);
36+
Marshal.ThrowExceptionForHR(hr);
37+
_dispatcherQueueController = pointer;
38+
}
39+
40+
/// <summary>
41+
/// Close IClosable WinRT objects, then drop the CsWinRT COM pointer on
42+
/// this thread so <c>IObjectReference.Finalize</c> has nothing to Release.
43+
/// </summary>
44+
public static void Release(object? value)
45+
{
46+
if (value is null)
47+
{
48+
return;
49+
}
50+
51+
if (value is IDisposable disposable)
52+
{
53+
try
54+
{
55+
disposable.Dispose();
56+
}
57+
catch (Exception exception)
58+
{
59+
Debug.WriteLine($"[LiveView] WinRT Close failed: {exception}");
60+
}
61+
}
62+
63+
if (value is IWinRTObject winrt)
64+
{
65+
try
66+
{
67+
winrt.NativeObject.Dispose();
68+
}
69+
catch (Exception exception)
70+
{
71+
Debug.WriteLine($"[LiveView] WinRT Release failed: {exception}");
72+
}
73+
}
74+
}
75+
76+
[StructLayout(LayoutKind.Sequential)]
77+
private struct DispatcherQueueOptions
78+
{
79+
public int dwSize;
80+
public int threadType;
81+
public int apartmentType;
82+
}
83+
84+
[DllImport("CoreMessaging.dll", ExactSpelling = true)]
85+
private static extern int CreateDispatcherQueueController(
86+
DispatcherQueueOptions options,
87+
out nint dispatcherQueueController);
88+
}

src/SQLBI.Whiteboard/MainWindow.xaml.cs

Lines changed: 11 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -3541,10 +3541,18 @@ private async Task ReconnectLiveViewAsync(LiveViewBoardObject liveView)
35413541

35423542
private async Task<GraphicsCaptureItem?> PickLiveViewTargetAsync()
35433543
{
3544+
WinRtThreading.EnsureDispatcherQueue();
35443545
GraphicsCapturePicker picker = new();
3545-
nint windowHandle = new WindowInteropHelper(this).Handle;
3546-
WinRT.Interop.InitializeWithWindow.Initialize(picker, windowHandle);
3547-
return await picker.PickSingleItemAsync();
3546+
try
3547+
{
3548+
nint windowHandle = new WindowInteropHelper(this).Handle;
3549+
WinRT.Interop.InitializeWithWindow.Initialize(picker, windowHandle);
3550+
return await picker.PickSingleItemAsync();
3551+
}
3552+
finally
3553+
{
3554+
WinRtThreading.Release(picker);
3555+
}
35483556
}
35493557

35503558
private void AttachLiveViewPresenter(

0 commit comments

Comments
 (0)