ref:5876eb6ebabbd51fd82e2d85ba4513ee942e8ae9

test(e2e): select the encoder and record NVENC latency on the RTX 4090

Add E2E_ENCODER so latency runs can use a hardware encoder. Correct the Phase 0 notes: the host has an RTX 4090 exposed through WSL2; only the DRM render node (needed for VAAPI and QEMU's GL/DMABUF display) is missing. NVENC runs show capture pacing, not encoding, dominates the latency. Refs #2 Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01BPNw4PCgkEfhyCjQT19wsb
SHA: 5876eb6ebabbd51fd82e2d85ba4513ee942e8ae9
Author: Cole Christensen <cole.christensen@gmail.com>
Date: 2026-09-12 21:30
Parents: c209fee
3 files changed +43 -15
Type
JOURNAL.md +35 −13
@@ -6,8 +6,13 @@
### Environment
- Host: Ubuntu 24.04.4 on WSL2 (kernel 6.18), 20 cores, `/dev/kvm` present, no `/dev/dri`
(so no VAAPI/DMABUF here; the shm path and software encoding only).
- Host: Ubuntu 24.04.4 on WSL2 (kernel 6.18), 20 cores, `/dev/kvm` present, NVIDIA RTX 4090
(Windows driver 610.88, exposed through `/dev/dxg` and `/usr/lib/wsl/lib`, which include
`libcuda` and `libnvidia-encode`). CUDA toolkits 12.8 and 13.2 are in `/usr/local`.
- WSL2 has **no DRM render node** (`/dev/dri` is absent): no VAAPI, no GBM, and no DMABUF export.
QEMU refuses `-device virtio-vga-gl -display dbus,gl=on` with "egl: no drm render node
available". So the DMABUF zero-copy path (#3) can't run here; NVENC encoding of the shm path can.
An earlier version of these notes wrongly said this host has no GPU because `/dev/dri` was missing.
- Toolchain: upstream needs **gcc-14** on Ubuntu 24.04 (`scripts/linux_build.sh` picks it;
`src/platform/linux/kmsgrab.cpp` uses `std::ranges::to`). The default gcc 13 fails to build a
clean upstream tree. Configure with `CC=gcc-14 CXX=g++-14`.
@@ -65,18 +70,35 @@
### REQ-NFR-001 measurements (host processing latency: QEMU call receipt → encoded frame sent)
Release build, gcc-14, 20-core host under WSL2, KVM, `encoder = software` (libx264), guest
framebuffer 640x400 scaled by Sunshine, ~330 samples per run over 18 s:
Release build, gcc-14, 20-core host under WSL2, KVM, guest framebuffer 640x400 scaled by
Sunshine, ~330 samples per run over 18 s. NVENC runs use a CUDA build
(`-DCMAKE_CUDA_COMPILER=/usr/local/cuda-13.2/bin/nvcc`, `encoder = nvenc`, `h264_nvenc` with the
RAM→CUDA upload) on the RTX 4090.
| Transport (QEMU) | Stream | p50 | p95 |
|---------------------------------|-------------|---------|---------|
| Shared memory map (11.1.1) | 1920x1080@60 | 14.1 ms | 21.4 ms |
| D-Bus messages (8.2.2) | 1920x1080@60 | 14.1 ms | 21.1 ms |
| Shared memory map, Debug build | 1280x800@30 | 17.2 ms | 32.9 ms |
| D-Bus messages, Debug build | 1280x800@30 | 20.2 ms | 34.6 ms |
| Transport (QEMU) | Encoder | Stream | p50 | p95 |
|---------------------------------|-----------------|--------------|---------|---------|
| Shared memory map (11.1.1) | NVENC | 1920x1080@60 | 11.1 ms | 20.4 ms |
| D-Bus messages (8.2.2) | NVENC | 1920x1080@60 | 12.2 ms | 19.5 ms |
| Shared memory map (11.1.1) | software x264 | 1920x1080@60 | 14.1 ms | 21.4 ms |
| D-Bus messages (8.2.2) | software x264 | 1920x1080@60 | 14.1 ms | 21.1 ms |
| Shared memory map, Debug build | software x264 | 1280x800@30 | 17.2 ms | 32.9 ms |
| D-Bus messages, Debug build | software x264 | 1280x800@30 | 20.2 ms | 34.6 ms |
The transports tie because the guest framebuffer is small; software scaling and x264 dominate.
No VAAPI/NVENC or DMABUF on this host, so the draft "p95 ≤ 10 ms on the DMABUF path" can't be
confirmed here; it stays for #3 on a GPU host.
The transports tie because the guest framebuffer is small.
**Capture pacing dominates, not encoding.** The capture loop wakes on a fixed tick of one frame
interval (16.7 ms at 60 fps) and copies whatever damage arrived since the last tick. Damage lands
uniformly within the interval, so it waits 0–16.7 ms (p50 ≈ 8 ms, p95 ≈ 16 ms) before the copy.
Add ~3 ms for NVENC and that predicts p50 ≈ 11 ms and p95 ≈ 19–20 ms, which matches the
measurements. Hardware encoding only saves the 2–3 ms that x264 spends. Waking the capture thread on
damage (a condition variable with the next frame deadline as timeout) should remove most of the
wait. That belongs with the pacing work in #3.
The draft "p95 ≤ 10 ms on the DMABUF path" can't be measured here, because WSL2 has no DRM render
node for QEMU's GL display. It stays for #3 on native Linux.
Seen during encoder probing with the CUDA build, not caused by this backend: "cuda::cuda_t doesn't
support any format other than AV_PIX_FMT_NV12 and AV_PIX_FMT_YUV444P" (the RAM→CUDA upload rejects
10-bit probes, as it does for x11 capture). The 8-bit H.264 stream is unaffected.
### Observations for later phases
tests/e2e/qemu/e2e_stream.sh +4 −2
@@ -3,7 +3,7 @@
# End-to-end test (REQ-E2E-001): an unmodified Moonlight client streams a QEMU VM that has no guest agent.
#
# 1. start a private dbus-daemon and a QEMU guest that draws a known pattern (run_vm.sh)
# 2. start Sunshine with capture = qemu and software encoding on a unique port
# 2. start Sunshine with capture = qemu on a unique port (software encoding unless E2E_ENCODER is set)
# 3. pair a headless Moonlight client (PIN approved through Sunshine's web API), launch Desktop
# 4. decode the stream and check the quadrant colors of the guest pattern
# 5. tear everything down, also on failure
@@ -18,5 +18,6 @@
# E2E_TIMEOUT stream timeout in seconds (default: 90; raise it for TCG)
# E2E_WIDTH/E2E_HEIGHT/E2E_FPS stream mode (default: 1280x800 at 30 fps)
# E2E_FRAMES decoded frames to receive before checking (default: 30; raise for latency runs)
# E2E_ENCODER Sunshine encoder: software, nvenc, vaapi, vulkan (default: software)
# E2E_KEEP set to 1 to keep the work directory
set -euo pipefail
@@ -33,6 +34,7 @@
height="${E2E_HEIGHT:-800}"
fps="${E2E_FPS:-30}"
frames="${E2E_FRAMES:-30}"
encoder="${E2E_ENCODER:-software}"
for bin in "${sunshine_bin}" "${client_bin}"; do
if [[ ! -x "${bin}" ]]; then
@@ -88,7 +90,7 @@
cat > "${work}/sunshine.conf" <<EOF
capture = qemu
qemu_dbus_address = ${bus_address}
encoder = ${encoder}
encoder = software
port = ${port}
sunshine_name = sunshine-e2e-${port}
file_state = ${work}/sunshine_state.json
tests/e2e/README.md +4 −0
@@ -57,3 +57,7 @@
E2E_ARTIFACTS=/tmp/latency tests/e2e/qemu/e2e_stream.sh
```
Set `E2E_ENCODER=nvenc` (a Sunshine build with CUDA) or `E2E_ENCODER=vaapi` to measure a hardware
encoder instead of software x264. NVENC also works under WSL2; VAAPI and DMABUF need a DRM render
node, which WSL2 doesn't have.