D011 - The display names its scanout buffers once, and they must be plain

Status: decided Date: 2026-09-21

The choice

agc_display_open_adopting takes the buffers a renderer wants scanned out, names them to VideoOut in the one registration the platform allows, and reports their flip indices. There is no call to add a buffer afterwards, and there will not be one, because the platform does not have it.

A renderer that wants its own target shown without a copy hands it over at open, and the target must carry no compression metadata.

Why registration happens exactly once

Measured, obSCEne REQ-20260921T1202Z-9a4c:

attempt on a handle that already has buffers result
re-register the same set at index 0 0x80290010
register a larger set at index 0 0x80290010
register a set at index 1 0x80290010
register one buffer at index 2 0x80290001
open a second handle on the same output refused, 0x80290001

0x80290010 is SCE_VIDEO_OUT_ERROR_SLOT_OCCUPIED. So a buffer set cannot be extended. Whatever is named at open is what this display shows until the set is released.

-9a4c also reported sceVideoOutUnregisterBuffers as not exported, but its by-name lookup returns 0 for sceVideoOutOpen as well, so it could not have found it. The import census lists it present (obscene/data/hardware/ps5-full.txt:706). The display binds it as (handle, set index) and releases set 0 at close (oops-sdk REQ-20261006T0750Z-6a4e). Whether a released output then takes a different set is the measurement that decides replacement.

That is why the entry point is an open variant rather than an adopt on a live display. An earlier version of this API was the latter - agc_display_adopt_buffer, added and removed the same day - and it could not have worked at any point in its life. The shape of the call now matches the shape of the constraint.

The practical consequence for a caller is worth stating plainly: the buffer must already exist before the display opens. A renderer whose target is allocated lazily, on its first frame, has to force that allocation first. oops-mesa does exactly this - it builds the drawable's colour image, then opens the display with it.

Why the buffer must be uncompressed

This display registers its buffers with dcc_control = 0 - delta colour compression off. The display controller therefore reads whatever it is given as raw pixels.

Hand it a compressed surface and it scans the compressed bytes as colour. The result is not a failure, a refusal or a black screen: it is a plausible-looking wrong picture. oops-mesa put one on the panel - geometry correctly placed, sized and oriented, with a sparse lattice of correct pixels inside it - and the frame hash was perfect throughout, because the hash was taken from the render and not from what the display read (oops-mesa worklog 070, obSCEne REQ-20260921T1349Z-8b52).

So an adopted buffer must be a plain surface, and a caller should check that rather than assume it. The check that works is the allocation size: a plain 64KB_R_X target is the height padded to a multiple of 128 and nothing more - 1920 * 1152 * 4 = 8847360 at 1080p. Mesa's default allocation was 49152 bytes larger, and that excess was its displayable DCC metadata.

What the layout has to be, and the good news about it

agc_display_scanout_layout reports what this display scans: tiling_mode = 0 is the GPU's 64KB_R_X render-target swizzle, tiling_mode = 1 is linear rows. sceVideoOutSetBufferAttribute2 writes that argument straight through into offset 0x04 of the attribute block without masking or validating it (-8b52).

A GPU-rendered 64KB_R_X target is byte-identical to what this display's own tiler produces. That is measured across a whole frame, not inferred from a block: -8b52 compared a Mesa-drawn 1920x1080 target (CB_COLOR0_ATTRIB3 = 0x0dc6c000, SW_MODE 27) against the display tiler's detile over all 135 64 KiB blocks - 2073600 pixels, zero mismatches.

So a renderer does not need to match some private layout this library invented. It renders into 64KB_R_X, which is what the hardware's colour block produces anyway, and the display can show it.

Linear is not an escape route at display size: REQ-20260920T0745Z-2d7f measured the RDNA2 colour block dropping pixels on a 1920x1080 target with a linear swizzle.

What this is worth

oops-mesa's present went from 37229 us to 3675 us by taking it - a tenfold cut, with the CPU no longer touching a single pixel of the frame. The same saving is available to oops-gl, which asks for it in REQ-20260919T1927Z-7e21 against its own linear scratch copy, and the API was written for both callers rather than for the one that needed it first.

What would reverse this

A measurement showing dcc_control can be configured to match a compressed surface's metadata. -8b52 names that as the untaken alternative to disabling compression, and it would be worth having: DCC exists to save bandwidth, and refusing it is a cost as well as a simplification. It would change the precondition on an adopted buffer, not the single-shot registration around it.