cpal's ALSA loop exits on a `POLLERR` out of `alsa::poll()`, which reaches us as a
`BackendSpecific` error — classified non-fatal there, because an xrun must not tear the
stream down. So `device_died` was never set, nothing rebuilt, the ring filled, the render
thread parked on a full ring and kira's renderer stopped advancing every sound in the mix.
In grom's 2026-10-18 log that was four minutes of silence out of one `soggyplace-zone5.mp3`,
cleared only by a worldport replacing the slot; the `3D sound(s) never played — the
spatial-voice arena was full` count climbing to 267 in the same session is the proof that
nothing at all was completing, music included.
`StallWatch` reads the cycle count instead of the error string: a running stream that
reports no cycle for 2 s is dead however it died, including with nothing raised at all, and
`service` closes and reopens it exactly as it does a lost device. Reclassifying the error
could not have worked — an xrun arrives in the same variant and the string is all we get.
A window with no cycles is not a wall clock with no cycles. `service` runs once a frame on
the main thread and a 4 s load hitch is one window the callback ran right through, so the
span is taken between two `Instant`s and any window that saw a cycle resets it, however
long that window was. That is the same trap as `510dda3d3`, one layer down.
Measured while verifying it, and the reason for the other two pieces here: the rebuild held
the main thread 4.5 s with the audio server down. Attributed with `WOW_SYSTEM_TIMING=1
WOW_STALL_MS=400` to `sound::poll_mix_health`, then split — close 5 ms, finding the default
2 ms, `Stream::open` 4521 ms. Opening a device is a blocking call into the host's audio
stack. `SLOW_OPEN` now says so in the log, and the reopen wait backs off to 8 s while opens
are slow or refused, so an outage costs one blocked frame per attempt instead of one a
second. It did not happen in grom's own failure, where the server stayed up and the reopen
would be instant. Taking the open off the main thread needs a device thread owning every
open and close; not done here.
Verified: four offline tests — a stream calling back at 3 ms frames, the 4.1 s login hitch
arriving as one window of 380 cycles, a real stop named once on the limit, and the backoff's
escalation — plus two live runs on Linux/PipeWire, restarting pipewire under a running client
at the glue screen. Both read the same: position frozen, two genuine starve warns, `output
stream dropped — the stream stopped calling back; reopening`, a reopen in the same frame,
music advancing again. Before this, that was permanent.
Built and run on Linux only; the device layer's own files are untouched apart from the
comment that pointed the wrong way.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SgstFWtn1RxEJ5kVrpgYwn