diff --git a/Directory.Build.props b/Directory.Build.props index c4b7b87..f06e988 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.1 + 1.0.2 latest enable enable diff --git a/TODO.md b/TODO.md index 7356a7f..73f6a34 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.1). Identity version for the Store package is `VersionPrefix.0` (`1.0.1.0`). +(1.0.2). Identity version for the Store package is `VersionPrefix.0` (`1.0.2.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/App.xaml.cs b/src/SQLBI.Whiteboard/App.xaml.cs index ada1346..fbc9cdd 100644 --- a/src/SQLBI.Whiteboard/App.xaml.cs +++ b/src/SQLBI.Whiteboard/App.xaml.cs @@ -1,5 +1,6 @@ using System.IO; using System.Windows; +using SQLBI.Whiteboard.LiveView; namespace SQLBI.Whiteboard; @@ -8,6 +9,7 @@ public partial class App : Application protected override void OnStartup(StartupEventArgs e) { base.OnStartup(e); + WinRtThreading.EnsureDispatcherQueue(); var window = new MainWindow(FindBoardPath(e.Args)); MainWindow = window; window.Show(); diff --git a/src/SQLBI.Whiteboard/LiveView/LiveViewCaptureSession.cs b/src/SQLBI.Whiteboard/LiveView/LiveViewCaptureSession.cs index 5184479..e609f7b 100644 --- a/src/SQLBI.Whiteboard/LiveView/LiveViewCaptureSession.cs +++ b/src/SQLBI.Whiteboard/LiveView/LiveViewCaptureSession.cs @@ -111,7 +111,7 @@ public void AttachDevice(ID3D11Device1 device) { ThrowIfDisposed(); StopCaptureCore(); - DisposeWinRt(_winRtDevice); + WinRtThreading.Release(_winRtDevice); _winRtDevice = winRtDevice; StartCaptureCore(); } @@ -123,7 +123,7 @@ public void DetachDevice() lock (_gate) { StopCaptureCore(); - DisposeWinRt(_winRtDevice); + WinRtThreading.Release(_winRtDevice); _winRtDevice = null; } } @@ -229,12 +229,8 @@ public bool TryPresent(ID3D11DeviceContext1 context, ID3D11Texture2D destination } finally { - // 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); + WinRtThreading.Release(surface); + WinRtThreading.Release(frame); } } @@ -256,7 +252,7 @@ public void Dispose() _disposed = true; StopCaptureCore(); ReleaseCaptureItem(); - DisposeWinRt(_winRtDevice); + WinRtThreading.Release(_winRtDevice); _winRtDevice = null; } } @@ -268,8 +264,12 @@ private void StartCaptureCore() return; } + WinRtThreading.EnsureDispatcherQueue(); _contentSize = SanitizeSize(_captureItem.Size); - Direct3D11CaptureFramePool framePool = Direct3D11CaptureFramePool.CreateFreeThreaded( + // Create, not CreateFreeThreaded: frames and their RCWs must be born + // on this dispatcher. Free-threaded FrameArrived ran on a worker, + // and those IObjectReferences finalized with 0xC0000005 in the Store. + Direct3D11CaptureFramePool framePool = Direct3D11CaptureFramePool.Create( _winRtDevice, DirectXPixelFormat.B8G8R8A8UIntNormalized, 2, @@ -307,13 +307,19 @@ private void StopCaptureCore() framePool.FrameArrived -= FramePool_FrameArrived; } - DisposeWinRt(captureSession); - DisposeWinRt(framePool); - DisposeWinRt(pendingFrame); + WinRtThreading.Release(captureSession); + WinRtThreading.Release(framePool); + WinRtThreading.Release(pendingFrame); } private void FramePool_FrameArrived(Direct3D11CaptureFramePool sender, object args) { + if (!_dispatcher.CheckAccess()) + { + _ = _dispatcher.BeginInvoke(() => FramePool_FrameArrived(sender, args)); + return; + } + Direct3D11CaptureFrame? frame = null; try { @@ -328,7 +334,7 @@ private void FramePool_FrameArrived(Direct3D11CaptureFramePool sender, object ar { if (!ReferenceEquals(sender, _framePool) || _isFrozen) { - DisposeWinRt(frame); + WinRtThreading.Release(frame); return; } @@ -337,7 +343,7 @@ private void FramePool_FrameArrived(Direct3D11CaptureFramePool sender, object ar // Recreate discards native frames. Close the pending wrapper // first so its finalizer cannot Release a pointer Recreate // has already invalidated. - DisposeWinRt(_pendingFrame); + WinRtThreading.Release(_pendingFrame); _pendingFrame = null; _contentSize = size; sender.Recreate( @@ -345,7 +351,7 @@ private void FramePool_FrameArrived(Direct3D11CaptureFramePool sender, object ar DirectXPixelFormat.B8G8R8A8UIntNormalized, 2, size); - DisposeWinRt(frame); + WinRtThreading.Release(frame); frame = null; } } @@ -361,7 +367,7 @@ private void FramePool_FrameArrived(Direct3D11CaptureFramePool sender, object ar long previous = Interlocked.Read(ref _lastAcceptedTimestamp); if (previous != 0 && now - previous < minimumTicks) { - DisposeWinRt(frame); + WinRtThreading.Release(frame); return; } @@ -370,23 +376,30 @@ private void FramePool_FrameArrived(Direct3D11CaptureFramePool sender, object ar { if (!ReferenceEquals(sender, _framePool) || _isFrozen) { - DisposeWinRt(frame); + WinRtThreading.Release(frame); return; } Direct3D11CaptureFrame? replaced = _pendingFrame; _pendingFrame = frame; frame = null; - DisposeWinRt(replaced); + WinRtThreading.Release(replaced); } FrameAvailable?.Invoke(); } catch (Exception exception) { - DisposeWinRt(frame); + WinRtThreading.Release(frame); CaptureFailed?.Invoke(exception); } + finally + { + if (args is IWinRTObject && !ReferenceEquals(args, sender)) + { + WinRtThreading.Release(args); + } + } } private void CaptureItem_Closed(GraphicsCaptureItem sender, object args) @@ -438,9 +451,7 @@ private void ReleaseCaptureItem() } _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); + WinRtThreading.Release(_captureItem); _captureItem = null; } @@ -480,32 +491,6 @@ private static WinRtDirect3DDevice CreateWinRtDevice(IDXGIDevice dxgiDevice) 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/WinRtThreading.cs b/src/SQLBI.Whiteboard/LiveView/WinRtThreading.cs new file mode 100644 index 0000000..f6d6eda --- /dev/null +++ b/src/SQLBI.Whiteboard/LiveView/WinRtThreading.cs @@ -0,0 +1,88 @@ +using System.Diagnostics; +using System.Runtime.InteropServices; +using Windows.System; +using WinRT; + +namespace SQLBI.Whiteboard.LiveView; + +/// +/// WinRT Graphics Capture RCWs are apartment-bound in a packaged (Store) +/// process. Releasing them from the GC finalizer is the AccessViolation +/// that closes the app. Keep a DispatcherQueue on the UI thread so +/// +/// delivers frames there, and Release every RCW on that same thread. +/// +internal static class WinRtThreading +{ + // Native ref kept for the process lifetime. Wrapping it in a CsWinRT + // RCW would only recreate the finalizer problem this exists to avoid. + private static nint _dispatcherQueueController; + + public static void EnsureDispatcherQueue() + { + if (DispatcherQueue.GetForCurrentThread() is not null) + { + return; + } + + DispatcherQueueOptions options = new() + { + dwSize = Marshal.SizeOf(), + threadType = 2, // DQTYPE_THREAD_CURRENT + apartmentType = 0, // DQTAT_COM_NONE — WPF already initialized STA + }; + + int hr = CreateDispatcherQueueController(options, out nint pointer); + Marshal.ThrowExceptionForHR(hr); + _dispatcherQueueController = pointer; + } + + /// + /// Close IClosable WinRT objects, then drop the CsWinRT COM pointer on + /// this thread so IObjectReference.Finalize has nothing to Release. + /// + public static void Release(object? value) + { + if (value is null) + { + return; + } + + if (value is IDisposable disposable) + { + try + { + disposable.Dispose(); + } + catch (Exception exception) + { + Debug.WriteLine($"[LiveView] WinRT Close failed: {exception}"); + } + } + + if (value is IWinRTObject winrt) + { + try + { + winrt.NativeObject.Dispose(); + } + catch (Exception exception) + { + Debug.WriteLine($"[LiveView] WinRT Release failed: {exception}"); + } + } + } + + [StructLayout(LayoutKind.Sequential)] + private struct DispatcherQueueOptions + { + public int dwSize; + public int threadType; + public int apartmentType; + } + + [DllImport("CoreMessaging.dll", ExactSpelling = true)] + private static extern int CreateDispatcherQueueController( + DispatcherQueueOptions options, + out nint dispatcherQueueController); +} diff --git a/src/SQLBI.Whiteboard/MainWindow.xaml.cs b/src/SQLBI.Whiteboard/MainWindow.xaml.cs index 9718395..bbc0454 100644 --- a/src/SQLBI.Whiteboard/MainWindow.xaml.cs +++ b/src/SQLBI.Whiteboard/MainWindow.xaml.cs @@ -3541,10 +3541,18 @@ private async Task ReconnectLiveViewAsync(LiveViewBoardObject liveView) private async Task PickLiveViewTargetAsync() { + WinRtThreading.EnsureDispatcherQueue(); GraphicsCapturePicker picker = new(); - nint windowHandle = new WindowInteropHelper(this).Handle; - WinRT.Interop.InitializeWithWindow.Initialize(picker, windowHandle); - return await picker.PickSingleItemAsync(); + try + { + nint windowHandle = new WindowInteropHelper(this).Handle; + WinRT.Interop.InitializeWithWindow.Initialize(picker, windowHandle); + return await picker.PickSingleItemAsync(); + } + finally + { + WinRtThreading.Release(picker); + } } private void AttachLiveViewPresenter(