11// Copyright (c) Six Labors.
22// Licensed under the Six Labors Split License.
33
4+ using System . Diagnostics ;
45using System . Runtime . CompilerServices ;
56using System . Runtime . InteropServices ;
67using System . Text ;
@@ -388,25 +389,30 @@ public static WebGPUEnvironmentError ProbeComputePipelineSupport()
388389 }
389390
390391 /// <summary>
391- /// Executes one isolated compute-pipeline creation probe for <see cref="WebGPUEnvironment.ProbeComputePipelineSupport()"/>.
392+ /// Executes one isolated compute-pipeline creation and submission-readback probe for
393+ /// <see cref="WebGPUEnvironment.ProbeComputePipelineSupport()"/>.
392394 /// </summary>
393395 /// <returns>
394- /// <c>0</c> when compute-pipeline creation succeeded; <c>1</c> when the probe completed and reported failure.
396+ /// <c>0</c> when pipeline creation and an indexed submission with buffer readback succeeded;
397+ /// <c>1</c> when the probe completed and reported failure.
395398 /// Any other value means the isolated probe process terminated before the probe could return normally.
396399 /// </returns>
397400 public static int RunComputePipelineSupportProbe ( )
398401 {
399402 try
400403 {
401- if ( ! TryGetOrCreateDevice ( out WebGPUDeviceHandle ? deviceHandle , out _ , out _ )
402- || deviceHandle is null )
404+ if ( ! TryGetOrCreateDevice ( out WebGPUDeviceHandle ? deviceHandle , out WebGPUQueueHandle ? queueHandle , out _ )
405+ || deviceHandle is null
406+ || queueHandle is null )
403407 {
404408 return 1 ;
405409 }
406410
407411 WebGPU api = GetApi ( ) ;
408412 using WebGPUHandle . HandleReference deviceReference = deviceHandle . AcquireReference ( ) ;
413+ using WebGPUHandle . HandleReference queueReference = queueHandle . AcquireReference ( ) ;
409414 WGPUDeviceImpl * device = ( WGPUDeviceImpl * ) deviceReference . Handle ;
415+ WGPUQueueImpl * queue = ( WGPUQueueImpl * ) queueReference . Handle ;
410416
411417 ReadOnlySpan < byte > probeShader = "@compute @workgroup_size(1) fn main() {}\0 "u8 ;
412418
@@ -467,7 +473,12 @@ public static int RunComputePipelineSupportProbe()
467473 }
468474
469475 api . ComputePipelineRelease ( pipeline ) ;
470- return 0 ;
476+
477+ // 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 ;
471482 }
472483 finally
473484 {
@@ -487,6 +498,177 @@ public static int RunComputePipelineSupportProbe()
487498 }
488499 }
489500
501+ /// <summary>
502+ /// Exercises one indexed queue submission and buffer readback against the probe device.
503+ /// </summary>
504+ /// <param name="api">The WebGPU API loader.</param>
505+ /// <param name="device">The probe device.</param>
506+ /// <param name="queue">The probe device's queue.</param>
507+ /// <returns><see langword="true"/> when the submission completed and the readback buffer mapped.</returns>
508+ private static bool ProbeSubmissionReadback ( WebGPU api , WGPUDeviceImpl * device , WGPUQueueImpl * queue )
509+ {
510+ // Buffer-to-buffer copies require four-byte multiples; one readback row-alignment unit
511+ // keeps the probe within every adapter's minimum resource limits.
512+ const ulong probeByteCount = 256 ;
513+
514+ WGPUBufferImpl * sourceBuffer = null ;
515+ WGPUBufferImpl * readbackBuffer = null ;
516+ WGPUCommandEncoderImpl * commandEncoder = null ;
517+ WGPUCommandBufferImpl * commandBuffer = null ;
518+ try
519+ {
520+ WGPUBufferDescriptor sourceDescriptor = new ( )
521+ {
522+ usage = ( ulong ) BufferUsage . CopySrc ,
523+ size = probeByteCount ,
524+ mappedAtCreation = 0U ,
525+ } ;
526+
527+ WGPUBufferDescriptor readbackDescriptor = new ( )
528+ {
529+ usage = ( ulong ) ( BufferUsage . CopyDst | BufferUsage . MapRead ) ,
530+ size = probeByteCount ,
531+ mappedAtCreation = 0U ,
532+ } ;
533+
534+ sourceBuffer = api . DeviceCreateBuffer ( device , in sourceDescriptor ) ;
535+ readbackBuffer = api . DeviceCreateBuffer ( device , in readbackDescriptor ) ;
536+ if ( sourceBuffer is null || readbackBuffer is null )
537+ {
538+ return false ;
539+ }
540+
541+ WGPUCommandEncoderDescriptor encoderDescriptor = default ;
542+ commandEncoder = api . DeviceCreateCommandEncoder ( device , in encoderDescriptor ) ;
543+ if ( commandEncoder is null )
544+ {
545+ return false ;
546+ }
547+
548+ // The copy is the smallest command whose completion is observable from the host
549+ // through a mappable buffer, making the submission's progress provable.
550+ api . CommandEncoderCopyBufferToBuffer ( commandEncoder , sourceBuffer , 0 , readbackBuffer , 0 , probeByteCount ) ;
551+
552+ WGPUCommandBufferDescriptor commandBufferDescriptor = default ;
553+ commandBuffer = api . CommandEncoderFinish ( commandEncoder , in commandBufferDescriptor ) ;
554+ if ( commandBuffer is null )
555+ {
556+ return false ;
557+ }
558+
559+ ulong submissionIndex = api . QueueSubmitForIndex ( queue , 1 , ref commandBuffer ) ;
560+
561+ WGPUMapAsyncStatus mapStatus = default ;
562+ using ManualResetEventSlim mapReady = new ( false ) ;
563+ void Callback ( WGPUMapAsyncStatus status , void * userData )
564+ {
565+ _ = userData ;
566+ mapStatus = status ;
567+ mapReady . Set ( ) ;
568+ }
569+
570+ using WebGPUBufferMapCallback callback = WebGPUBufferMapCallback . From ( Callback ) ;
571+ api . BufferMapAsync ( readbackBuffer , MapMode . Read , 0 , ( nuint ) probeByteCount , callback , null ) ;
572+
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 )
578+ {
579+ _ = api . DevicePoll ( device , false , & submissionIndex ) ;
580+ if ( ! mapReady . IsSet )
581+ {
582+ Thread . Yield ( ) ;
583+ }
584+ }
585+
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 )
589+ {
590+ return false ;
591+ }
592+
593+ void * mapped = api . BufferGetConstMappedRange ( readbackBuffer , 0 , ( nuint ) probeByteCount ) ;
594+ if ( mapped is null )
595+ {
596+ return false ;
597+ }
598+
599+ api . BufferUnmap ( readbackBuffer ) ;
600+ return true ;
601+ }
602+ finally
603+ {
604+ if ( commandBuffer is not null )
605+ {
606+ api . CommandBufferRelease ( commandBuffer ) ;
607+ }
608+
609+ if ( commandEncoder is not null )
610+ {
611+ api . CommandEncoderRelease ( commandEncoder ) ;
612+ }
613+
614+ if ( readbackBuffer is not null )
615+ {
616+ api . BufferRelease ( readbackBuffer ) ;
617+ }
618+
619+ if ( sourceBuffer is not null )
620+ {
621+ api . BufferRelease ( sourceBuffer ) ;
622+ }
623+ }
624+ }
625+
626+ /// <summary>
627+ /// Renders one small retained scene containing a layer boundary and reads the result back.
628+ /// </summary>
629+ /// <returns><see langword="true"/> when the render completed and the target read back.</returns>
630+ /// <remarks>
631+ /// Layered retained scenes drive intermediate targets through the full submission and
632+ /// readback pipeline, which is the workload software adapters fail on while passing
633+ /// simpler probes.
634+ /// </remarks>
635+ private static bool ProbeSceneRenderReadback ( )
636+ {
637+ try
638+ {
639+ DrawingBackendScene ? scene = null ;
640+ try
641+ {
642+ using ( WebGPURenderTarget sceneTarget = new ( 16 , 16 ) )
643+ using ( DrawingCanvas sceneCanvas = sceneTarget . CreateCanvas ( ) )
644+ {
645+ sceneCanvas . Fill ( Brushes . Solid ( Color . Red ) , new RectanglePolygon ( 2 , 2 , 12 , 12 ) ) ;
646+ sceneCanvas . SaveLayer ( new GraphicsOptions { BlendPercentage = 0.65F } , new Rectangle ( 4 , 4 , 8 , 8 ) ) ;
647+ sceneCanvas . Fill ( Brushes . Solid ( Color . Blue ) , new RectanglePolygon ( 5 , 5 , 8 , 8 ) ) ;
648+ sceneCanvas . Restore ( ) ;
649+ scene = sceneCanvas . CreateScene ( ) ;
650+ }
651+
652+ using WebGPURenderTarget renderTarget = new ( 16 , 16 ) ;
653+ using ( DrawingCanvas canvas = renderTarget . CreateCanvas ( ) )
654+ {
655+ canvas . RenderScene ( scene ) ;
656+ }
657+
658+ using Image < PixelFormats . Rgba32 > image = renderTarget . ReadbackImage < PixelFormats . Rgba32 > ( ) ;
659+ return image . Width == 16 ;
660+ }
661+ finally
662+ {
663+ scene ? . Dispose ( ) ;
664+ }
665+ }
666+ catch
667+ {
668+ return false ;
669+ }
670+ }
671+
490672 /// <summary>
491673 /// Releases all runtime-owned GPU state and cached probe results. The process-wide loader
492674 /// wrappers remain rooted until operating-system process teardown.
@@ -735,6 +917,7 @@ void ReleaseAbandonedResult(WGPURequestAdapterStatus status, WGPUAdapterImpl* ad
735917 WGPURequestAdapterOptions options = new ( )
736918 {
737919 compatibleSurface = compatibleSurface ,
920+ forceFallbackAdapter = environmentOptions . ForceFallbackAdapter ? 1u : 0u ,
738921 powerPreference = environmentOptions . PowerPreference switch
739922 {
740923 WebGPUPowerPreference . Default => WGPUPowerPreference . Undefined ,
0 commit comments