readPixels method

  1. @override
Future<ByteData?> readPixels(
  1. TextureHandle texture
)

The texture's pixels, premultiplied RGBA8, rows from the top.

A copy for the eight-bit layouts and a conversion pass for a float one, because WebGPU's copy has no opinion about format. glReadPixels is asked for RGBA and UNSIGNED_BYTE and the driver converts on the way out; copyTextureToBuffer hands over the bytes as they are stored, so a rgba16float target copied straight out is half-floats wearing the name of a picture. The contract names this method as the way to read a float target — GraphicsDevice.readback refuses one above every backend and says so — and a backend that answered null here left the contract with no answer at all on this API.

So a float target is drawn into an r8g8b8a8UNormInt target of the same size by _toEightBit and the copy is made from that. The price is a full-screen pass and a second allocation per call, both of which are why the eight-bit path is still a plain copy: every golden this repository records reads back the frame, and the frame is already eight-bit. A value outside [0, 1] is clamped rather than wrapped — by the rgba8unorm target rather than by the shader, see _conversionModule — which is what the software rasteriser's own float-to-byte does and what a caller comparing against a PNG is asking for.

Null where the texture has nothing to read:

  • an attachment-only allocation — this backend's translation of deviceTransient tile memory, which holds nothing after the pass;
  • a multisampled target, which is a refusal shared with every other backend rather than one of this backend's own. readbackRegionOf refuses it for readback in words — "has no pixels to copy until a pass resolves it" — GL cannot readPixels a multisampled read framebuffer, and here it is refused twice over: a multisampled target is allocated without TEXTURE_BINDING, so the conversion pass could not sample one either. Read the resolve target;
  • a cube or any other non-2D texture, which is a readback of six pictures where the interface names one;
  • a block-compressed texture, which has no eight-bit bytes to hand back and cannot be sampled into a target through a pass that assumes one texel is one texel;
  • an sRGB layout, and this one is a decision rather than a gap. readbackFormats leaves the sRGB twins out because the backends disagree about whether a readback decodes: one hands back the stored bytes and another the linear values they stand for. A conversion pass here would sample the texture, which decodes, and so would invent a third answer. The caller's move is the one that message already names — read the same texture through its non-sRGB layout.

Implementation

@override
Future<ByteData?> readPixels(TextureHandle texture) async {
  final backend = texture.backend;
  if (backend is! WebGpuTexture || !backend.sampleable) return null;
  if (texture.sampleCount != 1) return null;
  if (texture.type != TextureType.texture2D) return null;
  if (readbackFormats.contains(texture.format)) {
    return _copyBack(backend, ScreenRect.of(texture));
  }
  if (!_convertibleFormats.contains(texture.format)) return null;
  final converted = _toEightBit(texture, backend);
  try {
    return await _copyBack(
      converted.backend as WebGpuTexture,
      ScreenRect.of(converted),
    );
  } finally {
    releaseTexture(converted);
  }
}