open static method
Opens a device over a canvas of width by height, with the engine's
own stages unless others are given — WGSL translated from
flutter3d_shaders, beside the reflection a GPUShaderModule cannot be
asked for.
Without stages, the project's own bundle comes first: the webgpu
section of EngineBundleStamp.projectAsset, which flutter3d_build's
hook writes with the stages the project needs, read through loadAsset
(the application's asset bundle when null). Where the project has no
such bundle, or its bundle has no webgpu section (a build on a machine
without glslang and naga), the table tool/generate_shaders.dart wrote
is loaded instead, as a deferred part of the program a web build fetches
only then. A bundle that is there and cannot be read is a
ShaderBundleException naming the asset.
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.
Throws a DeviceUnavailableException for a browser with no
navigator.gpu, for one that hands back no adapter — a machine whose GPU
is blocklisted, which is the common case on an old driver and looks
identical from Dart — and for a canvas that will not give a webgpu
context. It answered null before 1.0, and openWebGpu turned the null
into this sentence; a caller's move is still to pick another backend,
which DeviceRegistry.open does with it.
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 textureFormatSupport then
answers from _gpuDevice.features — what was granted — rather than from
this list, because the two are not the same thing.
The stage-binding limits are the adapter's own (M3): a device asked
for none keeps WebGPU's minimums, 16 sampled textures and 16 samplers a
stage, and the engine's Pbr stage alone binds most of them, so a lit model's
own textures ran into a pipeline-layout limit the adapter did not have.
webgpuRequiredLimits asks for what the adapter reports, which it
always grants; what a bundle may count on everywhere is the minimum
(MaterialStageBudget), which the build holds a material to. Since
1.0.0-rc.1+1 that includes the uniform blocks with a dynamic offset in
one pipeline layout, and the bind groups, storage buffers and storage
textures (webgpuRequestedLimits): every uniform block here has a
dynamic offset, and a pair of 9 was refused at WebGPU's 8 on an
adapter that had 10. A pair over what the adapter really has is refused
once, by name and limit, on pipelineRefusals (see bindingsFor).
GpuFeature.requested is the list: filtering of 32-bit float textures,
the full-precision depth-stencil format, the three block-compression
families a KTX2 asset arrives in, timestamps, and since 1.0 everything
webgpuDeviceFeatures reports behind an adapter feature — unclipped
depth, indirect first instance, dual-source and float32 blending,
rg11b10ufloat targets, f16, subgroups, clip distances and
multi-draw-indirect.
Implementation
static Future<WebGpuDevice> open({
required int width,
required int height,
WebGpuSectionStages? stages,
Future<ByteData?> Function(String key)? loadAsset,
}) async {
final engineStages = stages ?? await _engineStages(loadAsset ?? _loadAsset);
const unavailable = DeviceUnavailableException(
'WebGPU is not available in this browser. The engine needs '
'navigator.gpu and an adapter it will hand out — a browser without '
'WebGPU, and a machine whose GPU the browser has blocklisted, fail '
'here the same way.',
backend: _backend,
);
final gpu = gpuNavigator.gpu;
if (gpu == null) throw unavailable;
final adapter = await gpu
.requestAdapter(
GPURequestAdapterOptions(powerPreference: 'high-performance'),
)
.toDart;
if (adapter == null) throw unavailable;
final wanted = <String>[
for (final feature in GpuFeature.requested)
if (adapter.features.has(feature)) feature,
];
final created = await adapter
.requestDevice(
GPUDeviceDescriptor(
label: 'flutter3d',
requiredFeatures: gpuStrings(wanted),
requiredLimits: gpuRequiredLimits(
webgpuRequiredLimits(adapter.limits),
),
),
)
.toDart;
final canvas = _document.createElement('canvas') as _Canvas
..width = width
..height = height;
final context = GpuCanvas(canvas).getContext('webgpu');
if (context == null) {
created.destroy();
throw const DeviceUnavailableException(
'WebGPU gave no `webgpu` context for the canvas.',
backend: _backend,
);
}
context.configure(
GPUCanvasConfiguration(
device: created,
// **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._(created, canvas, context, engineStages);
}