D672 - The audio drain was measurable without a device

Status: decided Date: 2026-09-10

What was holding the port calls back

orbistoun-audio served Init and Close and nothing else. The reason is in its own header and it is a good one: "guests frequently block on audio-buffer completion, so a stub that never signals a drained buffer hangs the title with no audio symptom to point at it" (D171). There is no audio backend, so a port that opened could never drain, and a title waiting on one would sit there forever with silence as the only clue.

So Close answered 0x8026_0003 to every handle, because no handle could name a port.

The measurement that changed it

obSCEne measured the whole contract on a title leg, and the load-bearing part is that output blocks and for how long:

check what a console did
090-audio/open-shapes 48 kHz accepted at chunk 0x100, 0x200, 0x400, 0x800; 44.1 kHz refused at all four with 0x8026_0008
090-audio/format-selector port state is 16 bytes; selector 0 → 81 00 **01** c7 ff ff 05 00, selectors 1 and 2 → the same with 02
090-audio/blocking eight 512-frame buffers took 0xdedc µs - 57 ms
090-audio/volume-flag flags 1, 2 and 3 each answered 0x0

The drain does not need a device. It is arithmetic on the sample rate, and the timing check is what says so rather than a header.

What is transcribed and what is decided

The format split is on the frequency alone. Chunk size decided no measured case - all four were accepted at 48 kHz and all four refused at 44.1 - so the shim decides on nothing else. A version that also refused an unmeasured chunk size would be inventing a rule the data does not contain.

The port state is bytes, not a struct, for the reason D671 gives for the pad: offset two is the channel count because obSCEne reads it as one and the three selectors differ there and nowhere else. What 0x81, 0xc7, 0xffff and 0x05 mean is not claimed.

Output waits the full duration, with no queue. The console returned in 57 ms against 85 ms of audio, which is a queue a few buffers deep absorbing the first calls. How deep is not measured. Picking a number would be inventing the single thing this could get wrong, so every call waits and the total lands above the check's 40 ms floor rather than near it.

The return is assumed. obSCEne discards it, so nothing has seen what a console answers. The frame count is the conventional shape and a guest testing rc < 0 reads it as success either way.

The clock had to agree

First run with all of it in place: 090-audio/blocking still reported 1 µs and read as not blocking - while sceAudioOutOutput was returning 0x200, so the shim was plainly running and plainly sleeping.

The guest's clock is logical by default: it advances a microsecond per read so a run repeats, because a measurement that cannot be repeated is not one (D181, D238, D582). Eight buffers that really slept 85 ms read as 8 µs to the guest.

clocks::advance exists for exactly this and says so in its own doc - "A sleep is time passing, and the clock has to agree" - added when usleep was found sleeping without it. The same wiring hazard, the same fix, one subsystem over: every call that makes time pass has to say so.

Worth recording as a rule rather than as an incident, because it will happen again the next time something blocks: a real sleep is invisible to the guest unless the clock is told.

What it bought

Every audio check passes - eight of eight, including two that had never run. 090-audio/blocking reports 0x14d51, 85,329 µs, which is eight 512-frame buffers at 48 kHz to the microsecond.

Against the matching hardware leg, the checks where hardware passes and orbistoun does not went from ten to five, and orbistoun now passes 174 against hardware's 167. What is left is 080-video/visual-flip and 111-modlink/walk (both skipped for capability orbistoun lacks), 101-input-ext/mouse-read and 130-layout/system-software-version (partial), and 900-surface/control (the one outright failure).