fangorn/sunshine-qemu
public
ref:main
EPIC Epic: Stream QEMU VMs to Moonlight via a Sunshine QEMU D-Bus backend #1
open
Opened by cole.christensen@gmail.com
EPIC Children (1 of 10 closed)
1
of 10
closed
10%
Open (9)
-
Phase 3: upstream to LizardByte Sunshine, QEMU, libvirt
fangorn/sunshine-qemu#8 -
Deferred: emulated USB gamepad device in QEMU for Windows guests
fangorn/sunshine-qemu#11 -
Phase 1c: keyboard, mouse, touch input to QEMU
fangorn/sunshine-qemu#5 -
Phase 1d: multi-VM deployment (ports, mDNS, systemd, libvirt)
fangorn/sunshine-qemu#6 -
Phase 1a: qemu capture source (shm + DMABUF zero-copy, mode changes, cursor)
fangorn/sunshine-qemu#3 -
Phase 1b: guest audio out
fangorn/sunshine-qemu#4 -
Deferred: macOS host support (QEMU+HVF)
fangorn/sunshine-qemu#10 -
Phase 2: gamepads for Linux guests
fangorn/sunshine-qemu#7 -
CI: Linux build + headless QEMU integration job on Anvil
fangorn/sunshine-qemu#9
Closed (1)
-
Phase 0: dev environment, E2E harness, QemuSession core, capture spike
fangorn/sunshine-qemu#2
Linked PRs (1)
Pull requests
Links
Sub-tasks
Epic: Stream QEMU VMs to Moonlight via a Sunshine QEMU D-Bus backend
Goal
Any QEMU VM started with -display dbus -audiodev dbus can be paired and streamed from an unmodified Moonlight client, with no agent in the guest. It works from firmware through OS boot, on any guest OS. Hardware-encoded where possible, and zero-copy when QEMU hands us DMABUFs.
Non-goals (for now)
- macOS or Windows hosts. Linux/KVM hosts only (see #10, deferred).
- HDR (virtio-gpu can’t produce HDR scanouts today).
- Clipboard, USB redirection, file transfer.
- Any change to the Moonlight client.
START HERE (for the implementing agent)
- Host: a Linux x86_64 or aarch64 machine. KVM (
/dev/kvm) is strongly preferred. A VAAPI (Intel/AMD) or NVENC GPU is needed for the DMABUF/hwenc requirements; everything else runs with software encoding. - Clone:
anvil repo clone fangorn/sunshine-qemu && cd sunshine-qemu && git submodule update --init --recursive - Read upstream’s
AGENTS.mdfirst. These rules are enforced upstream and we must follow them so the work can be sent upstream (#8):- gtest; the test binary is
tests/test_sunshineinside the build dir. - Build dirs are prefixed
cmake-build-. - Everything must have doxygen docs or the build fails. Use
/** @brief ... */blocks and///<for inline comments. - Follow
.clang-format. - Add tests for all new or changed code; target 100% coverage of changed code.
- Never create issues or PRs in the LizardByte GitHub org. All our issues and PRs go on Anvil
fangorn/sunshine-qemu.
- gtest; the test binary is
- Workflow (fangorn rules): TDD (red → green → refactor). One branch and one Anvil PR per issue, with
Closes #N.mainis protected. Before calling anything done, runanvil requirement status --repo fangorn/sunshine-qemu. - Requirement traceability: annotate every gtest with a comment directly above it:
// @tag requirements: [REQ-CAP-002]. Then link it:anvil requirement link REQ-CAP-002 --test "QemuDbusCapture.ShmScanoutDeliversFrame" --test-file tests/unit/platform/linux/qemu/test_capture.cpp --test-line 42 --repo fangorn/sunshine-qemu - Order: #2 → #3 → (#4, #5 in parallel) → #9 → #6 → #7 → #8. #10 and #11 are deferred.
- Keep
JOURNAL.mdnotes at stopping points on your branch (not upstreamable; keep it out of the upstream PR in #8).
Architecture
QEMU (-display dbus[,addr=...] -audiodev dbus,id=snd0)
│ D-Bus: /org/qemu/Display1/VM, /Console_N, /Audio
│ per-listener p2p socketpair (QEMU is the auth *server* on it)
▼
Sunshine process (one per VM)
src/platform/linux/qemu/ <- NEW
session.{h,cpp} QemuSession: one GDBus connection, console discovery,
GMainContext thread, reconnect/VM-gone handling
generated/ gdbus-codegen output from vendored XML
capture.cpp display_t impl: Listener object (Scanout/Update,
Unix.Map, ScanoutDMABUF, ScanoutDMABUF2, cursor)
audio.cpp audio_control_t / mic_t from AudioOutListener
input.cpp Moonlight input -> Keyboard/Mouse/MultiTouch
keymap.{h,cpp} Windows VK -> Linux KEY_* -> QEMU qnum
src/platform/linux/misc.cpp: source::QEMU in source_e, verify_qemu(),
display()/display_names() dispatch
src/platform/virtualhid_input.cpp + src/audio.cpp: route to qemu impls
when capture == "qemu"
▼
existing encode pipeline (FFmpeg: VAAPI / CUDA-NVENC / Vulkan / software)
▼
Moonlight client
Design decisions (already made; don’t revisit without cause)
- Out-of-process Sunshine module, not code inside QEMU. QEMU is GPL-2.0-only and Sunshine is GPL-3.0, and QEMU upstream moved remote-display servers out of process by design.
- C++ and GDBus (GLib/gio), bindings generated with
gdbus-codegen, the same way QEMU does it. We don’t use the Rustqemu-displaycrate. - Vendor the preprocessed XML. QEMU’s
ui/dbus-display1.xmlhas<?if $(env.HOST_OS) ...?>processing instructions thatgdbus-codegencan’t parse. QEMU runsscripts/xml-preprocess.pywithHOST_OS=linuxfirst. Run the same step once and commit the output tothird-party/qemu-dbus-display/dbus-display1.xml, recording the QEMU commit and license in a README. Regenerate the C bindings at build time with the same flags QEMU uses:--interface-prefix org.qemu. --c-namespace QemuDBus --glib-min-required 2.64. - Connection modes: support bus mode first. QEMU owns
org.qemuon the session bus or onaddr=; tests use a privatedbus-daemon. Add p2p mode (-display dbus,p2p=yesplus QMPadd_client protocol=@dbus-display) in #6 if libvirt needs it. - Capability negotiation is client-advertised. QEMU only uses
Listener.Unix.MapandListener.Unix.ScanoutDMABUF2if our/org/qemu/Display1/Listenerobject lists them in itsInterfacesproperty (see QEMUui/dbus-listener.c,dbus_display_listener_implements). Forgetting this silently falls back to copying full pixel data over the socket. - Build flag:
SUNSHINE_ENABLE_QEMUincmake/prep/options.cmake, alongsideSUNSHINE_ENABLE_KWIN/PORTAL. Default ON on Linux when gio ≥ 2.64 is found. Whencaptureisn’tqemu, Sunshine must behave exactly as before (REQ-CMP-001). - One Sunshine process per VM. Multi-VM is handled by deployment (#6), not by making Sunshine multi-host.
Verified facts (checked against source on 2026-09-12)
- Sunshine
mainat upstreamdd7a1f7:platf::display_tis insrc/platform/common.h:680;capture()at :714;make_avcodec_encode_device()at :737.mic_t(:803) andaudio_control_t(:819) are host audio capture. Sunshine has no client→host microphone path (REQ-AUD-002 is deprecated for that reason).- Linux source selection is
source_eplusconfig::video.capture("kms"|"wlr"|"x11"|"portal"|"kwin"|"nvfbc") insrc/platform/linux/misc.cpp:1109, 1200-1380. - Input on Linux and macOS goes through libvirtualhid via free functions in
src/platform/virtualhid_input.cpp:1026-1100;platf::input()is called fromsrc/input.cpp:2277. - Audio controller is created by
platf::audio_control(), called fromsrc/audio.cpp:320. - Keyboard modcodes from Moonlight are Windows virtual-key codes.
- Config keys:
capture,port(base port; others offset vianet::map_port),sunshine_name,file_state,credentials_file,pkey,cert,file_apps,min_log_level. - Headless pairing:
POST /api/pin {"pairing_id","pin","name"}on the config web server.
- QEMU 11.1.x
ui/dbus-display1.xml:VM(Name, UUID, ConsoleIDs);Console(RegisterListener(h), SetUIInfo, Width/Height/Label).Keyboard(Press/Release with a QEMU qnum keycode, i.e. xtkbd with the high bit re-encoded; QEMU has a Linux→qcode table inui/input-keymap.c).Mouse(Press/Release with buttons Left=0 Middle=1 Right=2 WheelUp=3 WheelDown=4 Side=5 Extra=6; SetAbsPosition only when IsAbsolute; RelMotion only when not).MultiTouch.SendEvent(kind, slot, x, y).Listener(Scanout/Update with pixman format and data; ScanoutDMABUF/UpdateDMABUF; Disable; MouseSet; CursorDefine as ARGB32);Listener.Unix.Map(ScanoutMap(fd, offset, w, h, stride, fmt)/UpdateMap);Listener.Unix.ScanoutDMABUF2(multi-plane).Audio.RegisterOutListener(h);AudioOutListener(Init with PCM format, Write(id, ay), SetEnabled, SetVolume, Fini);NSamplesdefaults to 480, i.e. 10 ms at 48 kHz.- No gamepad interface.
- No existing Sunshine-on-QEMU-D-Bus project was found in a web search on 2026-09-12.
Children
- #2 Phase 0: dev environment, E2E harness, QemuSession core, capture spike
- #3 Phase 1a: capture source (shm + DMABUF zero-copy)
- #4 Phase 1b: guest audio out
- #5 Phase 1c: keyboard, mouse, touch input
- #6 Phase 1d: multi-VM deployment (ports, mDNS, systemd, libvirt)
- #7 Phase 2: gamepads (Linux guests)
- #8 Phase 3: upstream to LizardByte, QEMU, libvirt
- #9 CI: Linux build + headless QEMU integration job on Anvil
- #10 Deferred: macOS host support
- #11 Deferred: emulated USB gamepad device in QEMU for Windows guests
Requirements
REQ-E2E-001, REQ-CAP-001…006, REQ-AUD-001, REQ-INP-001…004, REQ-DEP-001/002, REQ-CMP-001, REQ-NFR-001 (draft). REQ-AUD-002 is deprecated.