Skip to content

Commit b87370f

Browse files
Probe render support before reporting WebGPU availability
The compute-pipeline probe passed on adapters whose submission or render path later fails natively, letting GPU-less CI hosts crash the test process inside the queue submit. Extend the isolated probe to prove an indexed submission with buffer readback and a layered retained-scene render, and add a ForceFallbackAdapter environment option so software adapters such as WARP can be selected deliberately for diagnosis.
1 parent 332e360 commit b87370f

2 files changed

Lines changed: 195 additions & 5 deletions

File tree

src/ImageSharp.Drawing.WebGPU/WebGPUEnvironmentOptions.cs

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,4 +21,11 @@ public sealed class WebGPUEnvironmentOptions
2121
/// Gets or sets the adapter power preference requested for the library-managed device.
2222
/// </summary>
2323
public WebGPUPowerPreference PowerPreference { get; set; } = WebGPUPowerPreference.HighPerformance;
24+
25+
/// <summary>
26+
/// Gets or sets a value indicating whether adapter requests are restricted to the platform's
27+
/// software fallback adapter, such as WARP on Windows. Software adapters match the rendering
28+
/// environment of GPU-less continuous-integration machines.
29+
/// </summary>
30+
public bool ForceFallbackAdapter { get; set; }
2431
}

src/ImageSharp.Drawing.WebGPU/WebGPURuntime.cs

Lines changed: 188 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
// Copyright (c) Six Labors.
22
// Licensed under the Six Labors Split License.
33

4+
using System.Diagnostics;
45
using System.Runtime.CompilerServices;
56
using System.Runtime.InteropServices;
67
using 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

Comments
 (0)