create static method
- required int width,
- required int height,
- required WebGpuSectionStages stages,
Opens a device over a canvas of width by height, or answers null where
this browser has no WebGPU.
Asynchronous, and it is the one place the fourth backend costs anything
outside itself. requestAdapter and requestDevice are both promises,
so a WebGPU device cannot be built by a constructor the way the other
three are. Nothing in flutter3d_hardware says how a device is made — the
contract starts once one exists — so this costs the contract nothing and
costs whatever chooses a backend an await.
Null rather than a throw for a browser with no WebGPU: that is the
ordinary case and not a failure, and a caller's move is to pick another
backend. openWebGpu is where the null becomes the one sentence worth
putting on a screen.
The features are asked for rather than assumed, and only the ones this
adapter admits to having. A WebGPU device gets exactly what it requested:
sampling a BC7 texture on a device that did not ask for
texture-compression-bc is a validation error, not a slow path. The other
half of that rule is the trap — requestDevice asked for a feature the
adapter does not carry rejects the promise rather than handing back a
device without it, so a list of wants written as a constant is a game that
does not start on the first machine that lacks one of them. The adapter is
asked, the intersection is requested, and supportsTextureFormat then
answers from gpuDevice.features — what was granted — rather than from
this list, because the two are not the same thing.
Five are worth asking for: filtering of 32-bit float textures, the full-precision depth-stencil format, and the three block-compression families, which are what a KTX2 asset arrives in.
Implementation
static Future<WebGpuDevice?> create({
required int width,
required int height,
required WebGpuSectionStages stages,
}) async {
final gpu = gpuNavigator.gpu;
if (gpu == null) return null;
final adapter = await gpu
.requestAdapter(
GPURequestAdapterOptions(powerPreference: 'high-performance'),
)
.toDart;
if (adapter == null) return null;
final wanted = <String>[
for (final feature in const <String>[
GpuFeature.float32Filterable,
GpuFeature.depth32FloatStencil8,
GpuFeature.textureCompressionBc,
GpuFeature.textureCompressionEtc2,
GpuFeature.textureCompressionAstc,
])
if (adapter.features.has(feature)) feature,
];
final gpuDevice = await adapter
.requestDevice(
GPUDeviceDescriptor(
label: 'flutter3d',
requiredFeatures: gpuStrings(wanted),
),
)
.toDart;
final canvas = _document.createElement('canvas') as _Canvas
..width = width
..height = height;
final context = GpuCanvas(canvas).getContext('webgpu');
if (context == null) {
gpuDevice.destroy();
return null;
}
context.configure(
GPUCanvasConfiguration(
device: gpuDevice,
// **Not `getPreferredCanvasFormat`**, and that is a decision rather
// than an oversight. Presenting here is a texture-to-texture copy, and
// a copy demands the two formats match; the engine's frame is
// [defaultColorFormat], so a canvas configured as the machine's
// preferred `bgra8unorm` would need a whole blit pass to reach. The
// browser converts on composite instead, once, off the frame's path.
format: gpuTextureFormat(TextureFormat.r8g8b8a8UNormInt)!,
// `COPY_DST` and not `RENDER_ATTACHMENT`, for the same reason.
usage: GpuTextureUsage.copyDst,
alphaMode: 'opaque',
),
);
return WebGpuDevice._(gpuDevice, canvas, context, stages);
}