perf(codec): pass YUV through to the renderer and use slice threading

The receiver decoded H.264 to YUV420P, converted it to RGBA via a
CPU-intensive swscale pass, then uploaded 4 bytes/pixel to an SDL
texture — only for the GPU to convert back to RGB during rendering.
This eliminated the swscale pass entirely (40-60% of receiver CPU at
1080p) and cut the texture upload by 62%.

- DecodedFrame now carries three YUV420P planes with their strides
  instead of a packed RGBA buffer; the decoder copies the planes
  directly from the AVFrame (zero conversion for the common software
  path). Non-YUV420P decoder output (e.g. NV12 from v4l2m2m) is
  converted once to YUV420P.
- The SDL renderer uploads via SDL_UpdateYUVTexture with
  SDL_PIXELFORMAT_IYUV; the GPU does the YUV→RGB conversion during
  rendering.
- Decoder threading: slice-level with 4 threads (parallelizes within a
  frame, no added latency), not frame-level (which buffers multiple
  frames — the initial thread_count=0 broke the loopback test because
  the H.264 decoder introduced a multi-frame delay before producing
  output).
- The round-trip test converts decoded YUV back to RGBA for pixel
  comparison via a test-local swscale call (the pipeline itself never
  converts).

meson test 5/5 in both configurations, valgrind clean.
This commit is contained in:
2026-09-09 09:45:22 +02:00
parent b4d1411fd9
commit 6516b45b02
5 changed files with 122 additions and 35 deletions
+1 -1
View File
@@ -90,7 +90,7 @@ struct ReceiverSink {
for (const sc::DecodedFrame& decoded : sc::codec_value(decoded_result)) {
last_width = decoded.width;
last_height = decoded.height;
saw_frame = decoded.width > 0 && !decoded.rgba_pixels.empty();
saw_frame = decoded.width > 0 && !decoded.plane_y.empty();
decoded_frames.fetch_add(1);
}
}