Skip to content

Commit 36eef59

Browse files
Stage the WebGPU support probe and name each failing capability
Split the isolated availability probe into staged checks: pipeline creation, an indexed submission with buffer readback, one production compute dispatch, and a layered retained-scene render. Distinct exit codes name the first failing stage, and codes reserved for completed probes map to the unsupported result instead of a process failure. Adapters whose driver stack wedges after real render work, such as WARP, now classify as unsupported before any test or consumer can reach the native fault.
1 parent b87370f commit 36eef59

1 file changed

Lines changed: 266 additions & 30 deletions

File tree

src/ImageSharp.Drawing.WebGPU/WebGPURuntime.cs

Lines changed: 266 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -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

Comments
 (0)