readPixels method
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
deviceTransienttile 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.
readbackRegionOfrefuses it forreadbackin words — "has no pixels to copy until a pass resolves it" — GL cannotreadPixelsa multisampled read framebuffer, and here it is refused twice over: a multisampled target is allocated withoutTEXTURE_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.
readbackFormatsleaves 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);
}
}