From 1aee1527ae901405b3617306498e49a3266dee1a Mon Sep 17 00:00:00 2001 From: Marco Russo Date: Tue, 25 Aug 2026 10:51:23 +0200 Subject: [PATCH] Fix Store LiveView crash on resize and bump to 1.0.1 Store-packaged Graphics Capture RCWs are apartment-bound. Leaving frame.Surface, the capture session, and GraphicsCaptureItem for the GC finalizer called Marshal.Release from GC.RunFinalizers, which is the AccessViolationException that closed the Store app a few seconds after resizing a LiveView. Dispose those WinRT objects on the dispatcher that created them, close pending frames before FramePool.Recreate, and marshal item.Closed back to the UI thread. VersionPrefix moves to 1.0.1 so the Store can take the fix. --- Directory.Build.props | 2 +- TODO.md | 2 +- .../LiveView/LiveViewCaptureSession.cs | 131 ++++++++++++++---- .../LiveView/LiveViewPresenter.cs | 3 +- 4 files changed, 109 insertions(+), 29 deletions(-) diff --git a/Directory.Build.props b/Directory.Build.props index ae4a22d..c4b7b87 100644 --- a/Directory.Build.props +++ b/Directory.Build.props @@ -5,7 +5,7 @@ scripts/build-installer.ps1 both read it from here, so releasing is a reviewed change to this line rather than an edit in a pipeline variable group. --> - 1.0.0 + 1.0.1 latest enable enable diff --git a/TODO.md b/TODO.md index 981c639..7356a7f 100644 --- a/TODO.md +++ b/TODO.md @@ -16,7 +16,7 @@ The delivery chain works end to end: a merge to `main` builds, signs, and publis pre-release to GitHub Releases, and one approval promotes that same build to a release. reads its download links from the release manifest deployed beside it and needs no edit per release. The current product version is `VersionPrefix` in `Directory.Build.props` -(1.0.0). Identity version for the Store package is `VersionPrefix.0` (`1.0.0.0`). +(1.0.1). Identity version for the Store package is `VersionPrefix.0` (`1.0.1.0`). Declaring that number is decision 20 in [docs/decisions.md](docs/decisions.md). What 1.0 was waiting on shipped during 0.9.x: Preferences, `.wimport`, Explorer and VS Code diff --git a/src/SQLBI.Whiteboard/LiveView/LiveViewCaptureSession.cs b/src/SQLBI.Whiteboard/LiveView/LiveViewCaptureSession.cs index 37c3b8f..5184479 100644 --- a/src/SQLBI.Whiteboard/LiveView/LiveViewCaptureSession.cs +++ b/src/SQLBI.Whiteboard/LiveView/LiveViewCaptureSession.cs @@ -1,5 +1,6 @@ using System.Diagnostics; using System.Runtime.InteropServices; +using System.Windows.Threading; using SharpGen.Runtime; using Vortice.Direct3D11; using Vortice.DXGI; @@ -7,6 +8,7 @@ using Windows.Graphics; using Windows.Graphics.Capture; using Windows.Graphics.DirectX; +using Windows.Graphics.DirectX.Direct3D11; using WinRT; using WinRtDirect3DDevice = Windows.Graphics.DirectX.Direct3D11.IDirect3DDevice; @@ -19,6 +21,7 @@ namespace SQLBI.Whiteboard.LiveView; internal sealed class LiveViewCaptureSession : IDisposable { private readonly object _gate = new(); + private readonly Dispatcher _dispatcher; private WinRtDirect3DDevice? _winRtDevice; private GraphicsCaptureItem? _captureItem; private Direct3D11CaptureFramePool? _framePool; @@ -31,6 +34,12 @@ internal sealed class LiveViewCaptureSession : IDisposable private long _lastAcceptedTimestamp; private bool _disposed; + public LiveViewCaptureSession(Dispatcher dispatcher) + { + ArgumentNullException.ThrowIfNull(dispatcher); + _dispatcher = dispatcher; + } + public event Action? FrameAvailable; public event Action? ContentSizeChanged; @@ -94,6 +103,7 @@ public bool CaptureCursor public void AttachDevice(ID3D11Device1 device) { ArgumentNullException.ThrowIfNull(device); + VerifyDispatcherAccess(); using IDXGIDevice dxgiDevice = device.QueryInterface(); WinRtDirect3DDevice winRtDevice = CreateWinRtDevice(dxgiDevice); @@ -101,7 +111,7 @@ public void AttachDevice(ID3D11Device1 device) { ThrowIfDisposed(); StopCaptureCore(); - _winRtDevice?.Dispose(); + DisposeWinRt(_winRtDevice); _winRtDevice = winRtDevice; StartCaptureCore(); } @@ -109,10 +119,11 @@ public void AttachDevice(ID3D11Device1 device) public void DetachDevice() { + VerifyDispatcherAccess(); lock (_gate) { StopCaptureCore(); - _winRtDevice?.Dispose(); + DisposeWinRt(_winRtDevice); _winRtDevice = null; } } @@ -120,12 +131,13 @@ public void DetachDevice() public void SetTarget(GraphicsCaptureItem captureItem) { ArgumentNullException.ThrowIfNull(captureItem); + VerifyDispatcherAccess(); lock (_gate) { ThrowIfDisposed(); StopCaptureCore(); - UnsubscribeFromCurrentItem(); + ReleaseCaptureItem(); _captureItem = captureItem; _captureItem.Closed += CaptureItem_Closed; _contentSize = SanitizeSize(captureItem.Size); @@ -138,6 +150,7 @@ public void SetTarget(GraphicsCaptureItem captureItem) public void Freeze() { + VerifyDispatcherAccess(); lock (_gate) { if (_captureItem is null || _isFrozen) @@ -152,6 +165,7 @@ public void Freeze() public void Resume() { + VerifyDispatcherAccess(); lock (_gate) { if (_captureItem is null || !_isFrozen) @@ -166,17 +180,18 @@ public void Resume() public void ClearTarget() { + VerifyDispatcherAccess(); lock (_gate) { StopCaptureCore(); - UnsubscribeFromCurrentItem(); - _captureItem = null; + ReleaseCaptureItem(); _isFrozen = true; } } public bool TryPresent(ID3D11DeviceContext1 context, ID3D11Texture2D destinationTexture) { + VerifyDispatcherAccess(); Direct3D11CaptureFrame? frame; lock (_gate) { @@ -189,9 +204,11 @@ public bool TryPresent(ID3D11DeviceContext1 context, ID3D11Texture2D destination return false; } + IDirect3DSurface? surface = null; try { - using ID3D11Texture2D sourceTexture = GetTexture(frame.Surface); + surface = frame.Surface; + using ID3D11Texture2D sourceTexture = GetTexture(surface); Texture2DDescription source = sourceTexture.Description; Texture2DDescription destination = destinationTexture.Description; if (source.Width != destination.Width || source.Height != destination.Height) @@ -212,12 +229,23 @@ public bool TryPresent(ID3D11DeviceContext1 context, ID3D11Texture2D destination } finally { - frame.Dispose(); + // Close the surface RCW on this thread. Leaving it for the GC + // finalizer AVs in Store-packaged processes: those WinRT objects + // are apartment-bound and Marshal.Release from GC.RunFinalizers + // is the 0xC0000005 that closes the app a few seconds after resize. + DisposeWinRt(surface); + DisposeWinRt(frame); } } public void Dispose() { + if (!_dispatcher.CheckAccess()) + { + _dispatcher.Invoke(Dispose); + return; + } + lock (_gate) { if (_disposed) @@ -227,9 +255,8 @@ public void Dispose() _disposed = true; StopCaptureCore(); - UnsubscribeFromCurrentItem(); - _captureItem = null; - _winRtDevice?.Dispose(); + ReleaseCaptureItem(); + DisposeWinRt(_winRtDevice); _winRtDevice = null; } } @@ -270,18 +297,19 @@ private void StopCaptureCore() { Direct3D11CaptureFramePool? framePool = _framePool; GraphicsCaptureSession? captureSession = _captureSession; + Direct3D11CaptureFrame? pendingFrame = _pendingFrame; _framePool = null; _captureSession = null; + _pendingFrame = null; if (framePool is not null) { framePool.FrameArrived -= FramePool_FrameArrived; } - captureSession?.Dispose(); - framePool?.Dispose(); - _pendingFrame?.Dispose(); - _pendingFrame = null; + DisposeWinRt(captureSession); + DisposeWinRt(framePool); + DisposeWinRt(pendingFrame); } private void FramePool_FrameArrived(Direct3D11CaptureFramePool sender, object args) @@ -300,19 +328,24 @@ private void FramePool_FrameArrived(Direct3D11CaptureFramePool sender, object ar { if (!ReferenceEquals(sender, _framePool) || _isFrozen) { - frame.Dispose(); + DisposeWinRt(frame); return; } if (size.Width != _contentSize.Width || size.Height != _contentSize.Height) { + // Recreate discards native frames. Close the pending wrapper + // first so its finalizer cannot Release a pointer Recreate + // has already invalidated. + DisposeWinRt(_pendingFrame); + _pendingFrame = null; _contentSize = size; sender.Recreate( _winRtDevice!, DirectXPixelFormat.B8G8R8A8UIntNormalized, 2, size); - frame.Dispose(); + DisposeWinRt(frame); frame = null; } } @@ -328,7 +361,7 @@ private void FramePool_FrameArrived(Direct3D11CaptureFramePool sender, object ar long previous = Interlocked.Read(ref _lastAcceptedTimestamp); if (previous != 0 && now - previous < minimumTicks) { - frame.Dispose(); + DisposeWinRt(frame); return; } @@ -337,26 +370,39 @@ private void FramePool_FrameArrived(Direct3D11CaptureFramePool sender, object ar { if (!ReferenceEquals(sender, _framePool) || _isFrozen) { - frame.Dispose(); + DisposeWinRt(frame); return; } Direct3D11CaptureFrame? replaced = _pendingFrame; _pendingFrame = frame; frame = null; - replaced?.Dispose(); + DisposeWinRt(replaced); } FrameAvailable?.Invoke(); } catch (Exception exception) { - frame?.Dispose(); + DisposeWinRt(frame); CaptureFailed?.Invoke(exception); } } private void CaptureItem_Closed(GraphicsCaptureItem sender, object args) + { + // Closed can arrive on a capture worker. Session.Close must run on the + // dispatcher that created it; the finalizer path is what crashes Store. + if (_dispatcher.CheckAccess()) + { + HandleTargetClosed(sender); + return; + } + + _ = _dispatcher.BeginInvoke(() => HandleTargetClosed(sender)); + } + + private void HandleTargetClosed(GraphicsCaptureItem sender) { lock (_gate) { @@ -366,8 +412,7 @@ private void CaptureItem_Closed(GraphicsCaptureItem sender, object args) } StopCaptureCore(); - UnsubscribeFromCurrentItem(); - _captureItem = null; + ReleaseCaptureItem(); _isFrozen = true; } @@ -385,15 +430,21 @@ private void ApplyMinimumUpdateInterval(GraphicsCaptureSession? session) } } - private void UnsubscribeFromCurrentItem() + private void ReleaseCaptureItem() { - if (_captureItem is not null) + if (_captureItem is null) { - _captureItem.Closed -= CaptureItem_Closed; + return; } + + _captureItem.Closed -= CaptureItem_Closed; + // GraphicsCaptureItem is not IClosable. Drop the RCW's COM pointer here + // so IObjectReference.Finalize never Release's it from the GC thread. + DisposeWinRt(_captureItem); + _captureItem = null; } - private static ID3D11Texture2D GetTexture(Windows.Graphics.DirectX.Direct3D11.IDirect3DSurface surface) + private static ID3D11Texture2D GetTexture(IDirect3DSurface surface) { nint inspectable = ((IWinRTObject)surface).NativeObject.GetRef(); try @@ -427,6 +478,34 @@ private static WinRtDirect3DDevice CreateWinRtDevice(IDXGIDevice dxgiDevice) private void ThrowIfDisposed() => ObjectDisposedException.ThrowIf(_disposed, this); + private void VerifyDispatcherAccess() => _dispatcher.VerifyAccess(); + + private static void DisposeWinRt(object? value) + { + if (value is null) + { + return; + } + + try + { + if (value is IDisposable disposable) + { + disposable.Dispose(); + return; + } + + if (value is IWinRTObject winrt) + { + winrt.NativeObject.Dispose(); + } + } + catch (Exception exception) + { + Debug.WriteLine($"[LiveView] WinRT release failed: {exception}"); + } + } + [DllImport("d3d11.dll", ExactSpelling = true)] private static extern int CreateDirect3D11DeviceFromDXGIDevice( nint dxgiDevice, diff --git a/src/SQLBI.Whiteboard/LiveView/LiveViewPresenter.cs b/src/SQLBI.Whiteboard/LiveView/LiveViewPresenter.cs index c2c657e..0802883 100644 --- a/src/SQLBI.Whiteboard/LiveView/LiveViewPresenter.cs +++ b/src/SQLBI.Whiteboard/LiveView/LiveViewPresenter.cs @@ -15,7 +15,7 @@ namespace SQLBI.Whiteboard.LiveView; /// internal sealed class LiveViewPresenter : IDisposable { - private readonly LiveViewCaptureSession _capture = new(); + private readonly LiveViewCaptureSession _capture; private readonly Dispatcher _dispatcher; private SizeInt32 _contentSize = new(960, 540); private int _invalidateQueued; @@ -26,6 +26,7 @@ public LiveViewPresenter(Guid objectId, Dispatcher dispatcher) { ObjectId = objectId; _dispatcher = dispatcher; + _capture = new LiveViewCaptureSession(dispatcher); Surface = new OnDemandDrawingSurface { Width = _contentSize.Width,