Skip to content

Commit 6354ce8

Browse files
committed
Fix native wait lifetime during asynchronous fence completion
1 parent 40dbe40 commit 6354ce8

5 files changed

Lines changed: 370 additions & 32 deletions

File tree

‎src/ComputeSharp.Win32.D3D12/Windows/um/WinBase/Windows.cs‎

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -9,12 +9,15 @@ namespace ComputeSharp.Win32;
99

1010
internal static unsafe partial class Windows
1111
{
12-
[DllImport("kernel32", ExactSpelling = true)]
13-
public static extern BOOL RegisterWaitForSingleObject([NativeTypeName("PHANDLE")] HANDLE* phNewWaitObject, HANDLE hObject, [NativeTypeName("WAITORTIMERCALLBACK")] delegate* unmanaged<void*, byte, void> Callback, [NativeTypeName("PVOID")] void* Context, [NativeTypeName("ULONG")] uint dwMilliseconds, [NativeTypeName("ULONG")] uint dwFlags);
12+
[LibraryImport("kernel32.dll", SetLastError = true)]
13+
public static partial BOOL RegisterWaitForSingleObject([NativeTypeName("PHANDLE")] HANDLE* phNewWaitObject, HANDLE hObject, [NativeTypeName("WAITORTIMERCALLBACK")] delegate* unmanaged<void*, byte, void> Callback, [NativeTypeName("PVOID")] void* Context, [NativeTypeName("ULONG")] uint dwMilliseconds, [NativeTypeName("ULONG")] uint dwFlags);
14+
15+
[LibraryImport("kernel32.dll", SetLastError = true)]
16+
public static partial BOOL UnregisterWaitEx(HANDLE WaitHandle, HANDLE CompletionEvent);
1417

1518
[DllImport("kernel32", ExactSpelling = true)]
1619
public static extern BOOL UnregisterWait(HANDLE WaitHandle);
1720

1821
[NativeTypeName("#define INFINITE 0xFFFFFFFF")]
1922
public const uint INFINITE = 0xFFFFFFFF;
20-
}
23+
}

‎src/ComputeSharp.Win32.D3D12/Windows/um/synchapi/Windows.cs‎

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -9,10 +9,10 @@ namespace ComputeSharp.Win32;
99

1010
internal static unsafe partial class Windows
1111
{
12-
[DllImport("kernel32", ExactSpelling = true)]
13-
public static extern HANDLE CreateEventW([NativeTypeName("LPSECURITY_ATTRIBUTES")] SECURITY_ATTRIBUTES* lpEventAttributes, BOOL bManualReset, BOOL bInitialState, [NativeTypeName("LPCWSTR")] ushort* lpName);
12+
[LibraryImport("kernel32.dll", SetLastError = true)]
13+
public static partial HANDLE CreateEventW([NativeTypeName("LPSECURITY_ATTRIBUTES")] SECURITY_ATTRIBUTES* lpEventAttributes, BOOL bManualReset, BOOL bInitialState, [NativeTypeName("LPCWSTR")] ushort* lpName);
1414

1515
[DllImport("kernel32", ExactSpelling = true)]
1616
[return: NativeTypeName("DWORD")]
1717
public static extern uint WaitForSingleObjectEx(HANDLE hHandle, [NativeTypeName("DWORD")] uint dwMilliseconds, BOOL bAlertable);
18-
}
18+
}
Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
using System;
2+
using System.Threading;
3+
4+
namespace ComputeSharp.Graphics.Commands;
5+
6+
// The callback may run before RegisterWaitForSingleObject returns its handle.
7+
// Exactly one of the publisher/callback owns cleanup, after both have arrived.
8+
internal struct FenceWaitPublication
9+
{
10+
private int state;
11+
12+
public bool OnRegistered()
13+
{
14+
return Arrive(1, 2);
15+
}
16+
17+
public bool OnSignaled()
18+
{
19+
return Arrive(2, 1);
20+
}
21+
22+
private bool Arrive(int own, int other)
23+
{
24+
int previous = Interlocked.CompareExchange(ref this.state, own, 0);
25+
if (previous == 0)
26+
{
27+
return false;
28+
}
29+
30+
if (previous == other && Interlocked.CompareExchange(ref this.state, 3, other) == other)
31+
{
32+
return true;
33+
}
34+
35+
throw new InvalidOperationException("Fence wait completion/publication occurred more than once.");
36+
}
37+
38+
public static bool UnregisterAccepted(bool success, int error, bool insideCallback)
39+
{
40+
return success || (insideCallback && error == 997); // ERROR_IO_PENDING: this callback is still executing.
41+
}
42+
}

‎src/ComputeSharp/Graphics/GraphicsDevice.Execute.cs‎

Lines changed: 114 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -134,6 +134,10 @@ private struct CallbackContext
134134
/// The event handle set when the target fence value is reached.
135135
/// </summary>
136136
public HANDLE EventHandle;
137+
138+
public HANDLE WaitHandle;
139+
140+
public FenceWaitPublication Publication;
137141
}
138142

139143
/// <summary>
@@ -151,39 +155,93 @@ private static WaitForFenceValueTaskSource WaitForFenceAsync(
151155
ID3D12GraphicsCommandList* d3D12GraphicsCommandList,
152156
ID3D12CommandAllocator* d3D12CommandAllocator)
153157
{
154-
HANDLE eventHandle = Windows.CreateEventW(null, Windows.FALSE, Windows.FALSE, null);
155-
156-
device.d3D12ComputeFence.Get()->SetEventOnCompletion(d3D12FenceValue, eventHandle).Assert();
157-
158-
WaitForFenceValueTaskSource waitForFenceValueTaskSource = WaitForFenceValueTaskSource.Rent();
159-
CallbackContext* callbackContext = (CallbackContext*)NativeMemory.Alloc((nuint)sizeof(CallbackContext));
160-
161-
callbackContext->WaitForFenceValueTaskSourceHandle = GCHandle.Alloc(waitForFenceValueTaskSource);
162-
callbackContext->GraphicsDeviceHandle = GCHandle.Alloc(device);
163-
callbackContext->D3D12GraphicsCommandList = d3D12GraphicsCommandList;
164-
callbackContext->D3D12CommandAllocator = d3D12CommandAllocator;
165-
callbackContext->EventHandle = eventHandle;
158+
WaitForFenceValueTaskSource? waitForFenceValueTaskSource = null;
159+
CallbackContext* callbackContext = null;
160+
bool referenceHeld = false;
161+
try
162+
{
163+
waitForFenceValueTaskSource = WaitForFenceValueTaskSource.Rent();
164+
callbackContext = (CallbackContext*)NativeMemory.AllocZeroed((nuint)sizeof(CallbackContext));
165+
if (callbackContext is null)
166+
{
167+
throw new OutOfMemoryException();
168+
}
166169

167-
HANDLE waitHandle;
170+
callbackContext->WaitForFenceValueTaskSourceHandle = GCHandle.Alloc(waitForFenceValueTaskSource);
171+
callbackContext->GraphicsDeviceHandle = GCHandle.Alloc(device);
172+
callbackContext->D3D12GraphicsCommandList = d3D12GraphicsCommandList;
173+
callbackContext->D3D12CommandAllocator = d3D12CommandAllocator;
174+
device.GetReferenceTracker().DangerousAddRef();
175+
referenceHeld = true;
168176

169-
device.GetReferenceTracker().DangerousAddRef();
177+
callbackContext->EventHandle = Windows.CreateEventW(null, Windows.FALSE, Windows.FALSE, null);
178+
if (callbackContext->EventHandle == default)
179+
{
180+
throw new Win32Exception(Marshal.GetLastPInvokeError());
181+
}
170182

171-
int result = Windows.RegisterWaitForSingleObject(
172-
phNewWaitObject: &waitHandle,
173-
hObject: eventHandle,
174-
Callback: &WaitForSingleObjectCallbackForWaitForFenceAsync,
175-
Context: callbackContext,
176-
dwMilliseconds: Windows.INFINITE,
177-
dwFlags: 0);
183+
device.d3D12ComputeFence.Get()->SetEventOnCompletion(d3D12FenceValue, callbackContext->EventHandle).Assert();
184+
185+
HANDLE waitHandle;
186+
int result = Windows.RegisterWaitForSingleObject(
187+
phNewWaitObject: &waitHandle,
188+
hObject: callbackContext->EventHandle,
189+
Callback: &WaitForSingleObjectCallbackForWaitForFenceAsync,
190+
Context: callbackContext,
191+
dwMilliseconds: Windows.INFINITE,
192+
dwFlags: 0x00000008); // WT_EXECUTEONLYONCE
193+
if (result == 0)
194+
{
195+
throw new Win32Exception(Marshal.GetLastPInvokeError());
196+
}
178197

179-
// The register is successful if the return value is nonzero
180-
if (result == 0)
198+
// Publish the handle before allowing cleanup. A callback arriving
199+
// early records completion and returns without touching it again.
200+
callbackContext->WaitHandle = waitHandle;
201+
}
202+
catch
181203
{
182-
device.GetReferenceTracker().DangerousRelease();
204+
// The command list was already submitted/detached. Drain it before
205+
// closing an event registered with the fence or recycling its allocator.
206+
try
207+
{
208+
device.d3D12ComputeFence.Get()->SetEventOnCompletion(d3D12FenceValue, default).Assert();
209+
device.computeCommandListPool.Return(d3D12GraphicsCommandList, d3D12CommandAllocator);
210+
}
211+
finally
212+
{
213+
if (callbackContext is not null)
214+
{
215+
if (callbackContext->WaitForFenceValueTaskSourceHandle.IsAllocated)
216+
{
217+
callbackContext->WaitForFenceValueTaskSourceHandle.Free();
218+
}
219+
220+
if (callbackContext->GraphicsDeviceHandle.IsAllocated)
221+
{
222+
callbackContext->GraphicsDeviceHandle.Free();
223+
}
224+
225+
if (callbackContext->EventHandle != default)
226+
{
227+
_ = Windows.CloseHandle(callbackContext->EventHandle);
228+
}
229+
230+
NativeMemory.Free(callbackContext);
231+
}
232+
233+
if (referenceHeld)
234+
{
235+
device.GetReferenceTracker().DangerousRelease();
236+
}
237+
}
183238

184-
NativeMemory.Free(callbackContext);
239+
throw;
240+
}
185241

186-
default(Win32Exception).Throw(E.E_FAIL);
242+
if (callbackContext->Publication.OnRegistered())
243+
{
244+
CompleteFenceWait(callbackContext, insideCallback: false);
187245
}
188246

189247
return waitForFenceValueTaskSource;
@@ -199,7 +257,32 @@ private static void WaitForSingleObjectCallbackForWaitForFenceAsync(void* pConte
199257
{
200258
CallbackContext* callbackContext = (CallbackContext*)pContext;
201259

260+
if (callbackContext->Publication.OnSignaled())
261+
{
262+
CompleteFenceWait(callbackContext, insideCallback: true);
263+
}
264+
}
265+
266+
private static void CompleteFenceWait(CallbackContext* callbackContext, bool insideCallback)
267+
{
268+
// Never block on our own callback. The early-callback/publisher path
269+
// runs outside the callback and joins it before freeing its context.
270+
int unregistered = Windows.UnregisterWaitEx(callbackContext->WaitHandle,
271+
insideCallback ? default : (HANDLE)(-1));
272+
int error = unregistered == 0 ? Marshal.GetLastPInvokeError() : 0;
273+
Exception? failure = FenceWaitPublication.UnregisterAccepted(unregistered != 0, error, insideCallback)
274+
? null : new Win32Exception(error);
275+
202276
WaitForFenceValueTaskSource waitForFenceValueTaskSource = Unsafe.As<WaitForFenceValueTaskSource>(callbackContext->WaitForFenceValueTaskSourceHandle.Target)!;
277+
if (failure is not null)
278+
{
279+
// Cancellation did not establish that the OS released the wait.
280+
// Quarantine its resources and fault the caller rather than free
281+
// a context/event whose native ownership is now uncertain.
282+
waitForFenceValueTaskSource.Fail(failure);
283+
return;
284+
}
285+
203286
GraphicsDevice device = Unsafe.As<GraphicsDevice>(callbackContext->GraphicsDeviceHandle.Target)!;
204287
ID3D12GraphicsCommandList* d3D12GraphicsCommandList = callbackContext->D3D12GraphicsCommandList;
205288
ID3D12CommandAllocator* d3D12CommandAllocator = callbackContext->D3D12CommandAllocator;
@@ -277,6 +360,11 @@ public void Complete()
277360
this.manualResetValueTaskSource.SetResult(null);
278361
}
279362

363+
public void Fail(Exception error)
364+
{
365+
this.manualResetValueTaskSource.SetException(error);
366+
}
367+
280368
/// <inheritdoc/>
281369
public void GetResult(short token)
282370
{

0 commit comments

Comments
 (0)