@@ -381,7 +381,11 @@ public static WebGPUEnvironmentError ProbeComputePipelineSupport()
381381 computePipelineProbeResult = exitCode switch
382382 {
383383 0 => WebGPUEnvironmentError . Success ,
384- 1 => WebGPUEnvironmentError . ComputePipelineCreationFailed ,
384+
385+ // Codes one through nine are reserved for probe stages that completed and
386+ // reported the adapter unsupported; everything else means the isolated
387+ // process died mid-probe.
388+ >= 1 and <= 9 => WebGPUEnvironmentError . ComputePipelineCreationFailed ,
385389 _ => WebGPUEnvironmentError . ComputePipelineProbeProcessFailed ,
386390 } ;
387391 return computePipelineProbeResult . Value ;
@@ -475,10 +479,20 @@ public static int RunComputePipelineSupportProbe()
475479 api . ComputePipelineRelease ( pipeline ) ;
476480
477481 // Pipeline creation and even trivial submissions succeed on adapters
478- // whose render path later fails natively, so the probe must also prove
479- // an indexed submission with buffer map and one layered scene render
480- // with readback before reporting support.
481- return ProbeSubmissionReadback ( api , device , queue ) && ProbeSceneRenderReadback ( ) ? 0 : 1 ;
482+ // whose execution or render path later fails natively, so the probe
483+ // proves each deeper capability in turn. The distinct exit codes name
484+ // the first stage that failed.
485+ if ( ! ProbeSubmissionReadback ( api , device , queue ) )
486+ {
487+ return 2 ;
488+ }
489+
490+ if ( ! ProbeComputeDispatch ( api , device , queue ) )
491+ {
492+ return 3 ;
493+ }
494+
495+ return ProbeSceneRenderReadback ( ) ? 0 : 4 ;
482496 }
483497 finally
484498 {
@@ -557,47 +571,244 @@ private static bool ProbeSubmissionReadback(WebGPU api, WGPUDeviceImpl* device,
557571 }
558572
559573 ulong submissionIndex = api . QueueSubmitForIndex ( queue , 1 , ref commandBuffer ) ;
574+ return TryMapProbeBuffer ( api , device , readbackBuffer , ( nuint ) probeByteCount , submissionIndex ) ;
575+ }
576+ finally
577+ {
578+ if ( commandBuffer is not null )
579+ {
580+ api . CommandBufferRelease ( commandBuffer ) ;
581+ }
582+
583+ if ( commandEncoder is not null )
584+ {
585+ api . CommandEncoderRelease ( commandEncoder ) ;
586+ }
587+
588+ if ( readbackBuffer is not null )
589+ {
590+ api . BufferRelease ( readbackBuffer ) ;
591+ }
592+
593+ if ( sourceBuffer is not null )
594+ {
595+ api . BufferRelease ( sourceBuffer ) ;
596+ }
597+ }
598+ }
599+
600+ /// <summary>
601+ /// Maps a probe readback buffer after its submission and verifies readable bytes.
602+ /// </summary>
603+ /// <param name="api">The WebGPU API loader.</param>
604+ /// <param name="device">The probe device.</param>
605+ /// <param name="readbackBuffer">The mappable buffer written by the probe submission.</param>
606+ /// <param name="byteCount">The buffer length in bytes.</param>
607+ /// <param name="submissionIndex">The submission the map depends on.</param>
608+ /// <returns><see langword="true"/> when the buffer mapped with readable data.</returns>
609+ private static bool TryMapProbeBuffer (
610+ WebGPU api ,
611+ WGPUDeviceImpl * device ,
612+ WGPUBufferImpl * readbackBuffer ,
613+ nuint byteCount ,
614+ ulong submissionIndex )
615+ {
616+ WGPUMapAsyncStatus mapStatus = default ;
617+ using ManualResetEventSlim mapReady = new ( false ) ;
618+ void Callback ( WGPUMapAsyncStatus status , void * userData )
619+ {
620+ _ = userData ;
621+ mapStatus = status ;
622+ mapReady . Set ( ) ;
623+ }
560624
561- WGPUMapAsyncStatus mapStatus = default ;
562- using ManualResetEventSlim mapReady = new ( false ) ;
563- void Callback ( WGPUMapAsyncStatus status , void * userData )
625+ using WebGPUBufferMapCallback callback = WebGPUBufferMapCallback . From ( Callback ) ;
626+ api . BufferMapAsync ( readbackBuffer , MapMode . Read , 0 , byteCount , callback , null ) ;
627+
628+ // Non-blocking polls scoped to the submission index mirror the renderer's readback
629+ // wait, and the managed timeout turns an adapter that never completes the submission
630+ // into a probe failure instead of a hang.
631+ Stopwatch stopwatch = Stopwatch . StartNew ( ) ;
632+ while ( ! mapReady . IsSet && stopwatch . ElapsedMilliseconds < CallbackTimeoutMilliseconds )
633+ {
634+ _ = api . DevicePoll ( device , false , & submissionIndex ) ;
635+ if ( ! mapReady . IsSet )
564636 {
565- _ = userData ;
566- mapStatus = status ;
567- mapReady . Set ( ) ;
637+ Thread . Yield ( ) ;
568638 }
639+ }
640+
641+ // The status keeps its default value when the callback never fired, so timeouts and
642+ // explicit non-success map results fail through the same check.
643+ if ( ! mapReady . IsSet || mapStatus != WGPUMapAsyncStatus . Success )
644+ {
645+ return false ;
646+ }
647+
648+ void * mapped = api . BufferGetConstMappedRange ( readbackBuffer , 0 , byteCount ) ;
649+ if ( mapped is null )
650+ {
651+ return false ;
652+ }
653+
654+ api . BufferUnmap ( readbackBuffer ) ;
655+ return true ;
656+ }
657+
658+ /// <summary>
659+ /// Executes the chunk-reset production shader once and proves its submission completes.
660+ /// </summary>
661+ /// <param name="api">The WebGPU API loader.</param>
662+ /// <param name="device">The probe device.</param>
663+ /// <param name="queue">The probe device's queue.</param>
664+ /// <returns><see langword="true"/> when the dispatch completed and the readback mapped.</returns>
665+ /// <remarks>
666+ /// The chunk-reset stage is the smallest production shader: one workgroup, one thread, one
667+ /// storage binding. Executing it proves the adapter can run the full shader compilation
668+ /// chain, which pipeline creation alone does not.
669+ /// </remarks>
670+ private static bool ProbeComputeDispatch ( WebGPU api , WGPUDeviceImpl * device , WGPUQueueImpl * queue )
671+ {
672+ nuint statusByteCount = ( nuint ) sizeof ( GpuSceneBumpAllocators ) ;
673+
674+ WGPUBufferImpl * storageBuffer = null ;
675+ WGPUBufferImpl * readbackBuffer = null ;
676+ WGPUShaderModuleImpl * shaderModule = null ;
677+ WGPUBindGroupLayoutImpl * bindGroupLayout = null ;
678+ WGPUPipelineLayoutImpl * pipelineLayout = null ;
679+ WGPUComputePipelineImpl * pipeline = null ;
680+ WGPUBindGroupImpl * bindGroup = null ;
681+ WGPUCommandEncoderImpl * commandEncoder = null ;
682+ WGPUCommandBufferImpl * commandBuffer = null ;
683+ try
684+ {
685+ WGPUBufferDescriptor storageDescriptor = new ( )
686+ {
687+ usage = ( ulong ) ( BufferUsage . Storage | BufferUsage . CopySrc ) ,
688+ size = statusByteCount ,
689+ mappedAtCreation = 0U ,
690+ } ;
569691
570- using WebGPUBufferMapCallback callback = WebGPUBufferMapCallback . From ( Callback ) ;
571- api . BufferMapAsync ( readbackBuffer , MapMode . Read , 0 , ( nuint ) probeByteCount , callback , null ) ;
692+ WGPUBufferDescriptor readbackDescriptor = new ( )
693+ {
694+ usage = ( ulong ) ( BufferUsage . CopyDst | BufferUsage . MapRead ) ,
695+ size = statusByteCount ,
696+ mappedAtCreation = 0U ,
697+ } ;
698+
699+ storageBuffer = api . DeviceCreateBuffer ( device , in storageDescriptor ) ;
700+ readbackBuffer = api . DeviceCreateBuffer ( device , in readbackDescriptor ) ;
701+ if ( storageBuffer is null || readbackBuffer is null )
702+ {
703+ return false ;
704+ }
572705
573- // Non-blocking polls scoped to the submission index mirror the renderer's readback
574- // wait, and the managed timeout turns an adapter that never completes the submission
575- // into a probe failure instead of a hang.
576- Stopwatch stopwatch = Stopwatch . StartNew ( ) ;
577- while ( ! mapReady . IsSet && stopwatch . ElapsedMilliseconds < CallbackTimeoutMilliseconds )
706+ fixed ( byte * shaderCodePtr = ChunkResetComputeShader . ShaderCode )
578707 {
579- _ = api . DevicePoll ( device , false , & submissionIndex ) ;
580- if ( ! mapReady . IsSet )
708+ fixed ( byte * entryPointPtr = ChunkResetComputeShader . EntryPoint )
581709 {
582- Thread . Yield ( ) ;
710+ WGPUShaderSourceWGSL wgslDescriptor = new ( )
711+ {
712+ chain = new WGPUChainedStruct { sType = WGPUSType . ShaderSourceWGSL } ,
713+ code = shaderCodePtr
714+ } ;
715+
716+ WGPUShaderModuleDescriptor shaderDescriptor = new ( )
717+ {
718+ nextInChain = ( WGPUChainedStruct * ) & wgslDescriptor
719+ } ;
720+
721+ shaderModule = api . DeviceCreateShaderModule ( device , in shaderDescriptor ) ;
722+ if ( shaderModule is null
723+ || ! ChunkResetComputeShader . TryCreateBindGroupLayout ( api , device , out bindGroupLayout , out _ ) )
724+ {
725+ return false ;
726+ }
727+
728+ WGPUBindGroupLayoutImpl * * layouts = stackalloc WGPUBindGroupLayoutImpl * [ 1 ] { bindGroupLayout } ;
729+ WGPUPipelineLayoutDescriptor layoutDescriptor = new ( )
730+ {
731+ bindGroupLayoutCount = 1 ,
732+ bindGroupLayouts = layouts
733+ } ;
734+
735+ pipelineLayout = api . DeviceCreatePipelineLayout ( device , in layoutDescriptor ) ;
736+ if ( pipelineLayout is null )
737+ {
738+ return false ;
739+ }
740+
741+ WGPUComputePipelineDescriptor pipelineDescriptor = new ( )
742+ {
743+ layout = pipelineLayout ,
744+ compute = new WGPUComputeState
745+ {
746+ module = shaderModule ,
747+ entryPoint = entryPointPtr
748+ }
749+ } ;
750+
751+ pipeline = api . DeviceCreateComputePipeline ( device , in pipelineDescriptor ) ;
752+ if ( pipeline is null )
753+ {
754+ return false ;
755+ }
583756 }
584757 }
585758
586- // The status keeps its default value when the callback never fired, so timeouts and
587- // explicit non-success map results fail through the same check.
588- if ( ! mapReady . IsSet || mapStatus != WGPUMapAsyncStatus . Success )
759+ WGPUBindGroupEntry * bindEntries = stackalloc WGPUBindGroupEntry [ 1 ] ;
760+ bindEntries [ 0 ] = new WGPUBindGroupEntry
761+ {
762+ binding = 0 ,
763+ buffer = storageBuffer ,
764+ offset = 0 ,
765+ size = statusByteCount
766+ } ;
767+
768+ WGPUBindGroupDescriptor bindGroupDescriptor = new ( )
769+ {
770+ layout = bindGroupLayout ,
771+ entryCount = 1 ,
772+ entries = bindEntries
773+ } ;
774+
775+ bindGroup = api . DeviceCreateBindGroup ( device , in bindGroupDescriptor ) ;
776+ if ( bindGroup is null )
777+ {
778+ return false ;
779+ }
780+
781+ WGPUCommandEncoderDescriptor encoderDescriptor = default ;
782+ commandEncoder = api . DeviceCreateCommandEncoder ( device , in encoderDescriptor ) ;
783+ if ( commandEncoder is null )
589784 {
590785 return false ;
591786 }
592787
593- void * mapped = api . BufferGetConstMappedRange ( readbackBuffer , 0 , ( nuint ) probeByteCount ) ;
594- if ( mapped is null )
788+ WGPUComputePassDescriptor passDescriptor = default ;
789+ WGPUComputePassEncoderImpl * pass = api . CommandEncoderBeginComputePass ( commandEncoder , in passDescriptor ) ;
790+ if ( pass is null )
595791 {
596792 return false ;
597793 }
598794
599- api . BufferUnmap ( readbackBuffer ) ;
600- return true ;
795+ api . ComputePassEncoderSetPipeline ( pass , pipeline ) ;
796+ api . ComputePassEncoderSetBindGroup ( pass , 0 , bindGroup , 0 , null ) ;
797+ api . ComputePassEncoderDispatchWorkgroups ( pass , ChunkResetComputeShader . GetDispatchX ( ) , 1 , 1 ) ;
798+ api . ComputePassEncoderEnd ( pass ) ;
799+ api . ComputePassEncoderRelease ( pass ) ;
800+
801+ api . CommandEncoderCopyBufferToBuffer ( commandEncoder , storageBuffer , 0 , readbackBuffer , 0 , statusByteCount ) ;
802+
803+ WGPUCommandBufferDescriptor commandBufferDescriptor = default ;
804+ commandBuffer = api . CommandEncoderFinish ( commandEncoder , in commandBufferDescriptor ) ;
805+ if ( commandBuffer is null )
806+ {
807+ return false ;
808+ }
809+
810+ ulong submissionIndex = api . QueueSubmitForIndex ( queue , 1 , ref commandBuffer ) ;
811+ return TryMapProbeBuffer ( api , device , readbackBuffer , statusByteCount , submissionIndex ) ;
601812 }
602813 finally
603814 {
@@ -611,14 +822,39 @@ void Callback(WGPUMapAsyncStatus status, void* userData)
611822 api . CommandEncoderRelease ( commandEncoder ) ;
612823 }
613824
825+ if ( bindGroup is not null )
826+ {
827+ api . BindGroupRelease ( bindGroup ) ;
828+ }
829+
830+ if ( pipeline is not null )
831+ {
832+ api . ComputePipelineRelease ( pipeline ) ;
833+ }
834+
835+ if ( pipelineLayout is not null )
836+ {
837+ api . PipelineLayoutRelease ( pipelineLayout ) ;
838+ }
839+
840+ if ( bindGroupLayout is not null )
841+ {
842+ api . BindGroupLayoutRelease ( bindGroupLayout ) ;
843+ }
844+
845+ if ( shaderModule is not null )
846+ {
847+ api . ShaderModuleRelease ( shaderModule ) ;
848+ }
849+
614850 if ( readbackBuffer is not null )
615851 {
616852 api . BufferRelease ( readbackBuffer ) ;
617853 }
618854
619- if ( sourceBuffer is not null )
855+ if ( storageBuffer is not null )
620856 {
621- api . BufferRelease ( sourceBuffer ) ;
857+ api . BufferRelease ( storageBuffer ) ;
622858 }
623859 }
624860 }
0 commit comments