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(