fix(render): keep the whole SDL lifecycle on the render thread

The receiver window never appeared on Wayland. Two causes:

- The window was created on the main thread while event pumping and
  presenting ran on the render thread. SDL's Wayland backend requires a
  window's creation, event processing, drawing, and destruction to
  happen on one thread; a cross-thread surface simply never maps, with
  no error reported. The renderer now lives entirely on the render
  thread, with a condition-variable handshake so ReceiverPipeline::
  start() still reports renderer failures and timeouts.
- On Wayland a window is invisible until the first render commit, so
  even a healthy receiver showed nothing while waiting for a stream.
  The renderer presents one blank frame at init: the window is visible
  immediately, black until video arrives.

Also log 'stream started (WxH)' when the first frame decodes and the
first rendering failure, which is what made the remaining debugging
observable.

Validated headlessly end-to-end: a synthetic solid-color RTP feed drove
the real receiver over localhost UDP; the window mapped, reported
'stream started (320x240)', and a grim screenshot of the window region
showed the fed color (V=198, SATAVG=74 during the red feed). Phase 5
marked complete in docs/PHASES.md; current phase is now Phase 6.
This commit is contained in:
2026-09-07 11:17:35 +02:00
parent 3f8e92c6af
commit d2736b8a94
4 changed files with 86 additions and 15 deletions
+15
View File
@@ -31,6 +31,21 @@ loopback validation pending. See bottom for the run commands.
of bytes), microsecond encoder time_base (kills the 25fps pts
quantization that duplicated timestamps), and a best-effort 4MB
SO_RCVBUF on the receive socket (kernel clamps to rmem_max).
- **Display fixes after the receiver window never appeared**: (1) SDL must
own the window from one thread — creating it on main and pumping/presenting
from the render thread left the Wayland surface unmapped; the renderer now
creates/polls/presents/destroys entirely on the render thread (with a
start() init handshake). (2) Wayland windows are invisible until the first
render commit, so the renderer presents a blank frame at init (visible
black window while waiting). (3) The receiver logs `stream started (WxH)`
on the first decoded frame and any first present failure.
- **Phase 5 validated end-to-end** (headless, without the portal): a
synthetic RTP feed of solid red/green frames drove the real receiver over
localhost UDP; the window mapped, logged `stream started (320x240)`, and a
`grim` screenshot of the window region showed the fed color (V=198,
SAT=74 during the red feed) — decoded video visibly rendering. The user's
real sender run showed capture→encode→send working (1 IDR + 12 P-frames,
IDR capped at ~20KB by the VBV).
- **Automated validation**: `meson test` 4/4 — new `udp loopback` test
encodes synthetic frames, packetizes, sends over a real localhost UDP
socket, depacketizes, and decodes 10/10 frames with correct dimensions.