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,