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.1</VersionPrefix>
<VersionPrefix>1.0.2</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.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
Expand Down
2 changes: 2 additions & 0 deletions src/SQLBI.Whiteboard/App.xaml.cs
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
using System.IO;
using System.Windows;
using SQLBI.Whiteboard.LiveView;

namespace SQLBI.Whiteboard;

Expand All @@ -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();
Expand Down
83 changes: 34 additions & 49 deletions src/SQLBI.Whiteboard/LiveView/LiveViewCaptureSession.cs
Original file line number Diff line number Diff line change
Expand Up @@ -111,7 +111,7 @@ public void AttachDevice(ID3D11Device1 device)
{
ThrowIfDisposed();
StopCaptureCore();
DisposeWinRt(_winRtDevice);
WinRtThreading.Release(_winRtDevice);
_winRtDevice = winRtDevice;
StartCaptureCore();
}
Expand All @@ -123,7 +123,7 @@ public void DetachDevice()
lock (_gate)
{
StopCaptureCore();
DisposeWinRt(_winRtDevice);
WinRtThreading.Release(_winRtDevice);
_winRtDevice = null;
}
}
Expand Down Expand Up @@ -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);
}
}

Expand All @@ -256,7 +252,7 @@ public void Dispose()
_disposed = true;
StopCaptureCore();
ReleaseCaptureItem();
DisposeWinRt(_winRtDevice);
WinRtThreading.Release(_winRtDevice);
_winRtDevice = null;
}
}
Expand All @@ -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,
Expand Down Expand Up @@ -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
{
Expand All @@ -328,7 +334,7 @@ private void FramePool_FrameArrived(Direct3D11CaptureFramePool sender, object ar
{
if (!ReferenceEquals(sender, _framePool) || _isFrozen)
{
DisposeWinRt(frame);
WinRtThreading.Release(frame);
return;
}

Expand All @@ -337,15 +343,15 @@ 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(
_winRtDevice!,
DirectXPixelFormat.B8G8R8A8UIntNormalized,
2,
size);
DisposeWinRt(frame);
WinRtThreading.Release(frame);
frame = null;
}
}
Expand All @@ -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;
}

Expand All @@ -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)
Expand Down Expand Up @@ -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;
}

Expand Down Expand Up @@ -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,
Expand Down
88 changes: 88 additions & 0 deletions src/SQLBI.Whiteboard/LiveView/WinRtThreading.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,88 @@
using System.Diagnostics;
using System.Runtime.InteropServices;
using Windows.System;
using WinRT;

namespace SQLBI.Whiteboard.LiveView;

/// <summary>
/// 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
/// <see cref="Windows.Graphics.Capture.Direct3D11CaptureFramePool.Create"/>
/// delivers frames there, and Release every RCW on that same thread.
/// </summary>
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<DispatcherQueueOptions>(),
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;
}

/// <summary>
/// Close IClosable WinRT objects, then drop the CsWinRT COM pointer on
/// this thread so <c>IObjectReference.Finalize</c> has nothing to Release.
/// </summary>
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);
}
14 changes: 11 additions & 3 deletions src/SQLBI.Whiteboard/MainWindow.xaml.cs
Original file line number Diff line number Diff line change
Expand Up @@ -3541,10 +3541,18 @@ private async Task ReconnectLiveViewAsync(LiveViewBoardObject liveView)

private async Task<GraphicsCaptureItem?> 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(
Expand Down