create static method

Future<WebGpuDevice?> create({
  1. required int width,
  2. required int height,
  3. 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);
}