fangorn/sunshine-qemu
public
ref:main
Phase 0: dev environment, E2E harness, QemuSession core, capture spike #2
closed
Opened by cole.christensen@gmail.com
Pull requests
Closed by (1)
Links
Phase 0: dev environment, E2E harness, QemuSession core, capture spike
Parent: #1. Read the epic’s START HERE and Architecture sections first.
Primary journey first: this issue builds the end-to-end test that every later phase has to keep passing, plus the minimum code needed to make it pass on the simplest path: shared memory, software encoding, bus mode.
Requirements
- REQ-E2E-001: a Moonlight client streams a QEMU VM with no guest agent
- REQ-CAP-001: connect to the QEMU D-Bus display and discover consoles
- REQ-CMP-001: unchanged behavior when
capture != qemu - REQ-NFR-001 (draft): measure latency and propose a final target
Steps
0. Environment and baseline (no code changes)
- Install Sunshine’s Linux build dependencies. The most reliable list is
scripts/linux_build.shor.github/workflows/ci-linux.yml; also installlibglib2.0-dev(gio ≥ 2.64),dbus-daemon,qemu-system-x86(≥ 9.0; 11.x preferred),ovmf. -
cmake -B cmake-build-debug -G Ninja -DCMAKE_BUILD_TYPE=Debug -DBUILD_TESTS=ON -DBUILD_DOCS=OFF && ninja -C cmake-build-debug - Record the baseline:
cmake-build-debug/tests/test_sunshinepasses unmodified. If anything fails on a clean upstream tree, write it in JOURNAL.md so it isn’t blamed on us. - Check that QEMU was built with D-Bus display:
qemu-system-x86_64 -display help | grep dbusand-audiodev help | grep dbus. If not, build QEMU from source with--enable-dbus-displayand note the version.
1. Guest test image
- Choose a tiny deterministic guest whose screen shows known content with no agent. Recommended: a small Alpine or buildroot image, or even just the OVMF/SeaBIOS boot screen for the first test.
- Best option: a guest that draws a known pattern on the framebuffer, e.g. colored quadrants plus a frame counter, so pixel assertions are exact. Keep generation scripted under
tests/e2e/qemu/guest/and never commit large binaries; build or download them into a cache dir in the test script.
- Best option: a guest that draws a known pattern on the framebuffer, e.g. colored quadrants plus a frame counter, so pixel assertions are exact. Keep generation scripted under
- Standard invocation (put it in a script,
tests/e2e/qemu/run_vm.sh):
Usedbus-daemon --session --print-address --fork --address=unix:path=$T/bus.sockqemu-system-x86_64 -accel kvm -accel tcg -m 512 -name e2e-guest \-device virtio-vga -display dbus,addr=unix:path=$T/bus.sock \-audiodev dbus,id=snd0 -device intel-hda -device hda-output,audiodev=snd0 \-device virtio-tablet-pci -device virtio-keyboard-pci ...virtio-vgawithout GL here (the shared-memory path). The DMABUF path (virtio-vga-gl/-display dbus,gl=on) is for #3.
2. E2E harness (write first; it must fail red)
- Headless Moonlight test client in
tests/e2e/moonlight_client/:- Stream through
third-party/moonlight-common-c(already a submodule):LiStartConnectionwith a decoder callback that decodes H.264/HEVC with FFmpeg (already a dependency) and keeps the last decoded frame. - Pairing and launch need GameStream HTTP (serverinfo, pair, applist, launch). moonlight-common-c doesn’t include this; moonlight-embedded’s
libgamestream(GPL-3) does. Vendor or port a minimal version. Don’t use a GUI Moonlight. - Automate the PIN: the client generates it, and the harness sends it to Sunshine with
POST https://localhost:<port+1>/api/pin(creds created viasunshine --creds user passon a temp config).
- Stream through
- Harness script
tests/e2e/qemu/e2e_stream.sh(or a gtest-driven fixture):- start the dbus-daemon and QEMU guest
- start Sunshine with a temp config:
capture = qemu,qemu_dbus_address = unix:path=$T/bus.sock,encoder = software, a uniqueport, tempfile_state/credentials_file/pkey/cert - pair, launch the default “Desktop” app, and receive ≥ N frames
- assert that the decoded frame matches the guest’s known pattern within a tolerance (PSNR or per-quadrant mean color)
- tear everything down, including on failure
- Annotate with
// @tag requirements: [REQ-E2E-001]and link viaanvil requirement link.
3. Vendored XML and generated bindings
-
third-party/qemu-dbus-display/: preprocesseddbus-display1.xml(produced with QEMUscripts/xml-preprocess.py,HOST_OS=linux), plus a README noting the QEMU commit, the exact command, and the license. - CMake:
SUNSHINE_ENABLE_QEMUoption;find_package(PkgConfig)+pkg_check_modules(GIO gio-2.0>=2.64 gio-unix-2.0);add_custom_commandrunninggdbus-codegen --interface-prefix org.qemu. --c-namespace QemuDBus --glib-min-required 2.64 --generate-c-code dbus-display1. Compile the generated C as C, defineSUNSHINE_BUILD_QEMU.
4. QemuSession core (TDD)
src/platform/linux/qemu/session.{h,cpp}:
- Owns a
GMainContextand a dedicated thread running aGMainLoop. All GDBus calls go through this context, and the public API is thread-safe. -
connect(address): bus connection, proxy for/org/qemu/Display1/VMon nameorg.qemu, readName,UUID,ConsoleIDs. For each console, readLabel,Type,Width,Height,Interfaces. -
register_listener(console_id, callbacks):socketpair(AF_UNIX, SOCK_STREAM), hand one end toConsole.RegisterListener(h)with aGUnixFDList, and on our end rung_dbus_connection_new_sync(..., G_DBUS_CONNECTION_FLAGS_AUTHENTICATION_CLIENT)(QEMU is the auth server; see QEMUui/dbus-console.c~L312). Export aQemuDBusDisplay1Listenerskeleton at/org/qemu/Display1/Listener. TheInterfacesproperty must listorg.qemu.Display1.Listener.Unix.Map(and...ScanoutDMABUF2once #3 supports it). - Detect VM disappearance (connection closed / name vanished) and report it so capture can return
capture_e::reinitorerror. - Unit tests: run a fake QEMU in-process. Export
VM/Consoleskeletons on a privateGTestDBusor adbus-daemonstarted by the fixture, then drive Scanout/Update calls into our listener and assert what’s received. This gives fast, deterministic tests without real QEMU. Tag REQ-CAP-001.
5. Minimal capture spike (enough for REQ-E2E-001)
-
source::QEMUinsrc/platform/linux/misc.cpp:verify_qemu()returns true only whenconfig::video.capture == "qemu"and a connection succeeds. Never auto-detect (auto-detect would break REQ-CMP-001). Add dispatch indisplay()anddisplay_names(); display names are console IDs/labels. - New config keys in
src/config.{h,cpp}with doxygen, plus en-only localization entries and web UI if the config-consistency integration test (tests/integration/test_config_consistency.cpp) requires them:qemu_dbus_address(string) andqemu_console(default the first graphical console). -
display_timpl, shared-memory only: handleScanout(full frame),Update(damage rect, blit into the retained frame),ScanoutMap/UpdateMap(mmap the fd; handleoffset), and pixman formatsx8r8g8b8/a8r8g8b8at minimum. Hand frames out asmem_type_e::systemimages throughpull_free_image_cb/push_captured_image_cb, paced to the client framerate by repeating the last frame when there’s no damage. - Unit tests with the fake QEMU from step 4.
- REQ-CMP-001 test: with
captureunset orkms,verify_qemu()is false and no D-Bus connection is attempted.
6. Latency measurement (REQ-NFR-001)
- Add debug-level timing from listener callback receipt to encoder packet out, using the existing
logging::time_delta_periodic_loggerstyle. - Record p50/p95 at 1080p60 for shm+software, shm+VAAPI/NVENC if available, and later DMABUF (#3). Update REQ-NFR-001 with a measured target and set it
active.
Done when
- E2E test green locally: pair → stream → pixel assertion on a real QEMU guest
- Unit tests for session and capture green; upstream tests still green
- doxygen and clang-format clean; build passes with
SUNSHINE_ENABLE_QEMU=ONandOFF - Requirements linked;
anvil requirement statuspasses - PR on Anvil with
Closes #2
Risks / notes
- If GameStream HTTP pairing in the test client turns into a rabbit hole, stop and escalate. Don’t mock the Sunshine side of the stream; the point of this test is the real pipeline.
- TCG without KVM is fine for correctness but useless for latency numbers.