Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion Directory.Build.props
Original file line number Diff line number Diff line change
Expand Up @@ -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.
-->
<VersionPrefix>1.0.0</VersionPrefix>
<VersionPrefix>1.0.1</VersionPrefix>
<LangVersion>latest</LangVersion>
<Nullable>enable</Nullable>
<ImplicitUsings>enable</ImplicitUsings>
Expand Down
2 changes: 1 addition & 1 deletion TODO.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.
<https://whiteboard.sqlbi.com> 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
Expand Down
131 changes: 105 additions & 26 deletions src/SQLBI.Whiteboard/LiveView/LiveViewCaptureSession.cs
Original file line number Diff line number Diff line change
@@ -1,12 +1,14 @@
using System.Diagnostics;
using System.Runtime.InteropServices;
using System.Windows.Threading;
using SharpGen.Runtime;
using Vortice.Direct3D11;
using Vortice.DXGI;
using Windows.Foundation.Metadata;
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;

Expand All @@ -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;
Expand All @@ -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<SizeInt32>? ContentSizeChanged;
Expand Down Expand Up @@ -94,38 +103,41 @@ public bool CaptureCursor
public void AttachDevice(ID3D11Device1 device)
{
ArgumentNullException.ThrowIfNull(device);
VerifyDispatcherAccess();
using IDXGIDevice dxgiDevice = device.QueryInterface<IDXGIDevice>();
WinRtDirect3DDevice winRtDevice = CreateWinRtDevice(dxgiDevice);

lock (_gate)
{
ThrowIfDisposed();
StopCaptureCore();
_winRtDevice?.Dispose();
DisposeWinRt(_winRtDevice);
_winRtDevice = winRtDevice;
StartCaptureCore();
}
}

public void DetachDevice()
{
VerifyDispatcherAccess();
lock (_gate)
{
StopCaptureCore();
_winRtDevice?.Dispose();
DisposeWinRt(_winRtDevice);
_winRtDevice = null;
}
}

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);
Expand All @@ -138,6 +150,7 @@ public void SetTarget(GraphicsCaptureItem captureItem)

public void Freeze()
{
VerifyDispatcherAccess();
lock (_gate)
{
if (_captureItem is null || _isFrozen)
Expand All @@ -152,6 +165,7 @@ public void Freeze()

public void Resume()
{
VerifyDispatcherAccess();
lock (_gate)
{
if (_captureItem is null || !_isFrozen)
Expand All @@ -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)
{
Expand All @@ -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)
Expand All @@ -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)
Expand All @@ -227,9 +255,8 @@ public void Dispose()

_disposed = true;
StopCaptureCore();
UnsubscribeFromCurrentItem();
_captureItem = null;
_winRtDevice?.Dispose();
ReleaseCaptureItem();
DisposeWinRt(_winRtDevice);
_winRtDevice = null;
}
}
Expand Down Expand Up @@ -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)
Expand All @@ -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;
}
}
Expand All @@ -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;
}

Expand All @@ -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)
{
Expand All @@ -366,8 +412,7 @@ private void CaptureItem_Closed(GraphicsCaptureItem sender, object args)
}

StopCaptureCore();
UnsubscribeFromCurrentItem();
_captureItem = null;
ReleaseCaptureItem();
_isFrozen = true;
}

Expand All @@ -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
Expand Down Expand Up @@ -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,
Expand Down
3 changes: 2 additions & 1 deletion src/SQLBI.Whiteboard/LiveView/LiveViewPresenter.cs
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ namespace SQLBI.Whiteboard.LiveView;
/// </summary>
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;
Expand All @@ -26,6 +26,7 @@ public LiveViewPresenter(Guid objectId, Dispatcher dispatcher)
{
ObjectId = objectId;
_dispatcher = dispatcher;
_capture = new LiveViewCaptureSession(dispatcher);
Surface = new OnDemandDrawingSurface
{
Width = _contentSize.Width,
Expand Down