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,