22// Licensed under the Six Labors Split License.
33
44using System . Collections . Concurrent ;
5+ using System . Runtime . InteropServices ;
56using Silk . NET . WebGPU ;
67using WgpuBuffer = Silk . NET . WebGPU . Buffer ;
78
@@ -10,6 +11,7 @@ namespace SixLabors.ImageSharp.Drawing.Processing.Backends;
1011internal static unsafe partial class WebGPURuntime
1112{
1213 private static readonly ConcurrentDictionary < nint , DeviceSharedState > DeviceStateCache = new ( ) ;
14+ private static readonly object DeviceStateCacheSync = new ( ) ;
1315
1416 /// <summary>
1517 /// Gets or creates process-scoped shared resources for the specified device.
@@ -20,32 +22,34 @@ internal static unsafe partial class WebGPURuntime
2022 internal static DeviceSharedState GetOrCreateDeviceState ( WebGPU api , WebGPUDeviceHandle deviceHandle )
2123 {
2224 nint cacheKey = deviceHandle . DangerousGetHandle ( ) ;
23- if ( DeviceStateCache . TryGetValue ( cacheKey , out DeviceSharedState ? existing ) )
24- {
25- return existing ;
26- }
2725
28- DeviceSharedState created = new ( api , deviceHandle ) ;
29- DeviceSharedState winner = DeviceStateCache . GetOrAdd ( cacheKey , created ) ;
30- if ( ! ReferenceEquals ( winner , created ) )
26+ lock ( DeviceStateCacheSync )
3127 {
32- created . Dispose ( ) ;
33- }
28+ if ( DeviceStateCache . TryGetValue ( cacheKey , out DeviceSharedState ? existing ) )
29+ {
30+ return existing ;
31+ }
3432
35- return winner ;
33+ DeviceSharedState created = new ( api , deviceHandle ) ;
34+ DeviceStateCache [ cacheKey ] = created ;
35+ return created ;
36+ }
3637 }
3738
3839 /// <summary>
3940 /// Disposes all cached device-scoped shared state.
4041 /// </summary>
4142 private static void ClearDeviceStateCache ( )
4243 {
43- foreach ( DeviceSharedState state in DeviceStateCache . Values )
44+ lock ( DeviceStateCacheSync )
4445 {
45- state . Dispose ( ) ;
46- }
46+ foreach ( DeviceSharedState state in DeviceStateCache . Values )
47+ {
48+ state . Dispose ( ) ;
49+ }
4750
48- DeviceStateCache . Clear ( ) ;
51+ DeviceStateCache . Clear ( ) ;
52+ }
4953 }
5054
5155 /// <summary>
@@ -59,6 +63,7 @@ internal sealed class DeviceSharedState : IDisposable
5963 private readonly ConcurrentDictionary < string , SharedBufferInfrastructure > sharedBuffers = new ( StringComparer . Ordinal ) ;
6064 private readonly HashSet < FeatureName > deviceFeatures ;
6165 private WebGPUHandle . HandleReference deviceReference ;
66+ private PfnErrorCallback uncapturedErrorCallback ;
6267 private bool disposed ;
6368
6469 internal DeviceSharedState ( WebGPU api , WebGPUDeviceHandle deviceHandle )
@@ -68,12 +73,15 @@ internal DeviceSharedState(WebGPU api, WebGPUDeviceHandle deviceHandle)
6873
6974 try
7075 {
76+ this . uncapturedErrorCallback = PfnErrorCallback . From ( HandleUncapturedError ) ;
7177 this . Device = ( Device * ) this . deviceReference . Handle ;
78+ this . Api . DeviceSetUncapturedErrorCallback ( this . Device , this . uncapturedErrorCallback , null ) ;
7279 this . deviceFeatures = EnumerateDeviceFeatures ( api , this . Device ) ;
7380 this . MaxStorageBufferBindingSize = QueryMaxStorageBufferBindingSize ( api , this . Device ) ;
7481 }
7582 catch
7683 {
84+ this . uncapturedErrorCallback . Dispose ( ) ;
7785 this . deviceReference . Dispose ( ) ;
7886 throw ;
7987 }
@@ -121,6 +129,34 @@ internal DeviceSharedState(WebGPU api, WebGPUDeviceHandle deviceHandle)
121129 public bool HasFeature ( FeatureName feature )
122130 => this . deviceFeatures . Contains ( feature ) ;
123131
132+ /// <summary>
133+ /// Forwards uncaptured native WebGPU errors through the public environment callback.
134+ /// </summary>
135+ private static void HandleUncapturedError ( ErrorType errorType , byte * message , void * userData )
136+ {
137+ _ = userData ;
138+
139+ string errorMessage = message is null
140+ ? string . Empty
141+ : Marshal . PtrToStringUTF8 ( ( nint ) message ) ?? string . Empty ;
142+
143+ WebGPUEnvironment . ReportUncapturedError ( ToPublicErrorType ( errorType ) , errorMessage ) ;
144+ }
145+
146+ /// <summary>
147+ /// Maps Silk's native error enum to the public API enum without exposing Silk types.
148+ /// </summary>
149+ private static WebGPUErrorType ToPublicErrorType ( ErrorType errorType )
150+ => errorType switch
151+ {
152+ ErrorType . NoError => WebGPUErrorType . NoError ,
153+ ErrorType . Validation => WebGPUErrorType . Validation ,
154+ ErrorType . OutOfMemory => WebGPUErrorType . OutOfMemory ,
155+ ErrorType . Internal => WebGPUErrorType . Internal ,
156+ ErrorType . DeviceLost => WebGPUErrorType . DeviceLost ,
157+ _ => WebGPUErrorType . Unknown
158+ } ;
159+
124160 /// <summary>
125161 /// Snapshots the feature set currently reported by the native device.
126162 /// </summary>
@@ -190,23 +226,7 @@ public bool TryGetOrCreateCompositePipeline(
190226 bindGroupLayout = null ;
191227 pipeline = null ;
192228
193- if ( this . disposed )
194- {
195- error = "WebGPU device state is disposed." ;
196- return false ;
197- }
198-
199- if ( string . IsNullOrWhiteSpace ( pipelineKey ) )
200- {
201- error = "Composite pipeline key cannot be empty." ;
202- return false ;
203- }
204-
205- if ( shaderCode . IsEmpty )
206- {
207- error = $ "Composite shader code is missing for pipeline '{ pipelineKey } '.";
208- return false ;
209- }
229+ ObjectDisposedException . ThrowIf ( this . disposed , this ) ;
210230
211231 CompositePipelineInfrastructure infrastructure = this . compositePipelines . GetOrAdd (
212232 pipelineKey ,
@@ -276,29 +296,7 @@ public bool TryGetOrCreateCompositeComputePipeline(
276296 bindGroupLayout = null ;
277297 pipeline = null ;
278298
279- if ( this . disposed )
280- {
281- error = "WebGPU device state is disposed." ;
282- return false ;
283- }
284-
285- if ( string . IsNullOrWhiteSpace ( pipelineKey ) )
286- {
287- error = "Composite compute pipeline key cannot be empty." ;
288- return false ;
289- }
290-
291- if ( shaderCode . IsEmpty )
292- {
293- error = $ "Composite compute shader code is missing for pipeline '{ pipelineKey } '.";
294- return false ;
295- }
296-
297- if ( entryPoint . IsEmpty )
298- {
299- error = $ "Composite compute entry point is missing for pipeline '{ pipelineKey } '.";
300- return false ;
301- }
299+ ObjectDisposedException . ThrowIf ( this . disposed , this ) ;
302300
303301 CompositeComputePipelineInfrastructure infrastructure = this . compositeComputePipelines . GetOrAdd (
304302 pipelineKey ,
@@ -365,23 +363,7 @@ public bool TryGetOrCreateSharedBuffer(
365363 buffer = null ;
366364 capacity = 0 ;
367365
368- if ( this . disposed )
369- {
370- error = "WebGPU device state is disposed." ;
371- return false ;
372- }
373-
374- if ( string . IsNullOrWhiteSpace ( bufferKey ) )
375- {
376- error = "Shared buffer key cannot be empty." ;
377- return false ;
378- }
379-
380- if ( requiredSize == 0 )
381- {
382- error = $ "Shared buffer '{ bufferKey } ' requires a non-zero size.";
383- return false ;
384- }
366+ ObjectDisposedException . ThrowIf ( this . disposed , this ) ;
385367
386368 SharedBufferInfrastructure infrastructure = this . sharedBuffers . GetOrAdd (
387369 bufferKey ,
@@ -467,6 +449,10 @@ public void Dispose()
467449
468450 this . sharedBuffers . Clear ( ) ;
469451
452+ // Clear the native callback slot before freeing Silk's delegate thunk.
453+ this . Api . DeviceSetUncapturedErrorCallback ( this . Device , default , null ) ;
454+ this . uncapturedErrorCallback . Dispose ( ) ;
455+
470456 this . deviceReference . Dispose ( ) ;
471457 this . disposed = true ;
472458 }
0 commit comments