ref:e431a566a6298fd6df43a5b8fda80eabfdf2be69

docs: document streaming several QEMU VMs with systemd and libvirt

docs/qemu.md covers the three ways to reach a VM (bus, qmp: and libvirt:), raw QEMU command lines, ports/names/state for several instances, the systemd units and their start order (Sunshine checks the display once at startup, so the units wait for the VM), libvirt domain XML, permissions and the hook, virt-manager steps, troubleshooting, and what hasn't been tested yet. configuration.md and the web UI describe the new qemu_dbus_address forms; tests/e2e/README.md the multi-instance, p2p and packaging tests. The system unit no longer sets SupplementaryGroups (a missing group fails the unit; sysusers adds the memberships) or a Documentation URL that doesn't exist yet. Refs #6 Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
SHA: e431a566a6298fd6df43a5b8fda80eabfdf2be69
Author: Cole Christensen <cole.christensen@gmail.com>
Date: 2026-09-13 01:24
Parents: e6a2878
7 files changed +379 -8
Type
docs/configuration.md +12 −2
@@ -2262,8 +2262,15 @@
<tr>
<td>Description</td>
<td colspan="2">
D-Bus address of the bus that a QEMU VM's display is on. Only used when [capture](#capture) is `qemu`.
Use the same address given to QEMU with `-display dbus,addr=`.
Where the QEMU VM's display is. Only used when [capture](#capture) is `qemu`. One of:
<ul>
<li>a D-Bus address where QEMU owns `org.qemu`: the address given to QEMU with `-display dbus,addr=`</li>
<li>`qmp:<socket>`: a QMP socket of a QEMU started with `-display dbus,p2p=on`; Sunshine adds itself
as a display client with `add_client`</li>
<li>`libvirt:<domain>` or `libvirt:<domain>?uri=<uri>`: a libvirt domain with `<graphics type='dbus'>`;
with `p2p='yes'` Sunshine connects through `virDomainOpenGraphicsFD`, otherwise to the domain's bus</li>
</ul>
See [QEMU virtual machines](qemu.md) for setups with several VMs, systemd and libvirt.
@note{Applies to Linux only.}
</td>
</tr>
@@ -2275,6 +2282,9 @@
<td>Example</td>
<td colspan="2">@code{}
qemu_dbus_address = unix:path=/run/sunshine-qemu/vm0/bus.sock
@endcode
@code{}
qemu_dbus_address = libvirt:win11
@endcode</td>
</tr>
</table>
docs/Doxyfile +1 −0
@@ -49,6 +49,7 @@
awesome_sunshine.md \
guides.md \
performance_tuning.md \
qemu.md \
api.md \
troubleshooting.md \
building.md \
docs/qemu.md +313 −0
@@ -1,0 +1,313 @@
# QEMU virtual machines
Sunshine can stream a QEMU virtual machine with no software in the guest. With
[capture](configuration.md#capture) set to `qemu`, the picture, the guest's sound and keyboard, mouse and touch
input go through QEMU's D-Bus display (`-display dbus`). Gamepads still use virtual host devices.
Each VM gets its own Sunshine process with its own configuration file, ports, pairing and name. The files for that
are in `packaging/linux/qemu` in the source tree:
| File | Purpose |
|-------------------------------|--------------------------------------------------------------------------------------|
| `sunshine-qemu@.service` | systemd system unit, one instance per VM, reads `/etc/sunshine-qemu/<vm>.conf` |
| `user/sunshine-qemu@.service` | systemd user unit for rootless setups, reads `~/.config/sunshine-qemu/<vm>.conf` |
| `sunshine-qemu.sysusers` | creates the `sunshine-qemu` user of the system unit |
| `sunshine-qemu-wait-vm` | the units' `ExecStartPre`: waits until the VM's display is reachable |
| `sunshine-qemu-port` | picks non-overlapping base ports for VMs and checks existing configs |
| `libvirt-hook-qemu` | libvirt hook that starts and stops the unit with the domain |
| `example.conf` | a commented per-VM configuration |
## Requirements
- A Sunshine build with the QEMU backend (`-DSUNSHINE_ENABLE_QEMU=ON`, needs GLib/GIO 2.64 and `gdbus-codegen`).
- QEMU 7.0 or newer with the D-Bus display: `qemu-system-x86_64 -display help` lists `dbus`. QEMU 8.2 (Ubuntu 24.04)
and 11.1 were tested. QEMU 8.2 sends the picture as D-Bus messages, newer versions share memory with Sunshine.
- For libvirt: libvirt 8.4.0 or newer (`<graphics type='dbus'>`).
## How Sunshine reaches the VM
[qemu_dbus_address](configuration.md#qemu_dbus_address) takes one of these forms.
| Address | QEMU display | Who can connect |
|--------------------------------------|--------------------------------------|-----------------------------------------------------------------|
| `unix:path=/run/vm/bus.sock` | `-display dbus,addr=...` | Clients allowed by that bus's policy (by default only its owner) |
| `qmp:/run/sunshine-qemu/vm.qmp` | `-display dbus,p2p=on` and `-qmp` | Anyone who can open the QMP socket |
| `libvirt:<domain>` | `<graphics type='dbus' p2p='yes'/>` | Users with libvirt's `open_graphics` permission on the domain |
| `libvirt:<domain>?uri=qemu:///session` | `<graphics type='dbus'/>`, session libvirt | The user running the session libvirt |
- **Bus** (`unix:path=`): QEMU owns `org.qemu` on a D-Bus bus, usually a private `dbus-daemon`. Several clients can
share it.
- **QMP** (`qmp:`): QEMU runs with no bus. Sunshine connects to a QMP socket, hands QEMU one end of a socket pair with
`getfd` and `add_client protocol=@dbus-display`, and talks D-Bus directly on the other end. It reconnects the same
way when the VM restarts. QEMU serves one QMP client at a time, so give Sunshine its own `-qmp` socket.
- **libvirt** (`libvirt:`): Sunshine loads `libvirt.so.0` at run time. For a `p2p='yes'` display it calls
`virDomainOpenGraphicsFD`, which does the same `add_client` through libvirt; libvirt labels the socket for the
domain, so it works with SELinux and AppArmor confinement. For a display without `p2p`, Sunshine reads the bus
address from the running domain's XML and connects to that bus.
QEMU serves one peer-to-peer (`p2p`) control connection at a time. Another client that attaches the same way, for
example virt-viewer through libvirt, takes the VM's display objects away from Sunshine until Sunshine reconnects.
## Raw QEMU
Bus mode, with a private bus:
```bash
mkdir -p /run/sunshine-qemu/win11
dbus-daemon --session --fork --address=unix:path=/run/sunshine-qemu/win11/bus.sock
qemu-system-x86_64 -name win11 -accel kvm -m 8G \
-device virtio-vga \
-display dbus,addr=unix:path=/run/sunshine-qemu/win11/bus.sock,audiodev=snd0 \
-audiodev dbus,id=snd0 -device intel-hda -device hda-output,audiodev=snd0 \
-device virtio-tablet-pci -device virtio-keyboard-pci \
...
```
```
qemu_dbus_address = unix:path=/run/sunshine-qemu/win11/bus.sock
```
Peer to peer through QMP, with no bus:
```bash
qemu-system-x86_64 -name win11 -accel kvm -m 8G \
-device virtio-vga \
-display dbus,p2p=on,audiodev=snd0 \
-qmp unix:/run/sunshine-qemu/win11.qmp,server=on,wait=off \
-audiodev dbus,id=snd0 -device intel-hda -device hda-output,audiodev=snd0 \
-device virtio-tablet-pci -device virtio-keyboard-pci \
...
```
```
qemu_dbus_address = qmp:/run/sunshine-qemu/win11.qmp
```
Options that matter for streaming:
- `audiodev=` on `-display dbus` together with `-audiodev dbus`: without them QEMU exports no audio to Sunshine.
- An absolute pointer (`-device virtio-tablet-pci`, or a USB tablet) makes Moonlight's mouse follow exactly. With
only a PS/2 mouse, Moonlight's absolute positions work only while the guest shows a hardware cursor.
- `-device virtio-multitouch-pci` for native touch; see [native_pen_touch](configuration.md#native_pen_touch).
- `-device virtio-vga-gl -display dbus,gl=on,rendernode=/dev/dri/renderD128` sends GPU buffers (DMABUF) that VAAPI,
NVENC and Vulkan encode without a copy. QEMU must render on the encoder's GPU: set
[adapter_name](configuration.md#adapter_name) to the same render node for VAAPI.
## Several VMs on one host
Every instance needs its own ports, name and state. Nothing else is shared, so configuration files are enough:
- **Ports.** Every Sunshine port is an offset from [port](configuration.md#port), from `port - 5` to `port + 21`,
including the web UI (`port + 1`). Give each VM a base port 100 above the previous one: 48089, 48189, 48289, and
so on (47989 stays free for a desktop Sunshine). `sunshine-qemu-port --dir /etc/sunshine-qemu --write <vm>` adds
the next free base port to `<vm>.conf`, and `sunshine-qemu-port --dir /etc/sunshine-qemu --check` reports
overlapping configs. Open each instance's ports in the firewall.
- **Name.** [sunshine_name](configuration.md#sunshine_name) is what Moonlight shows and the name advertised over
mDNS, so each VM is listed on its own. Keep it unique on the host.
- **State.** [file_state](configuration.md#file_state), [credentials_file](configuration.md#credentials_file),
[pkey](configuration.md#pkey), [cert](configuration.md#cert), [file_apps](configuration.md#file_apps) and
[log_path](configuration.md#log_path) hold pairing, web UI credentials, certificates, apps and the log. Relative
paths are resolved in Sunshine's configuration directory, which the units set per VM with `XDG_CONFIG_HOME`, so
pairing a client with one VM doesn't pair it with another.
- **UPnP.** Set [upnp](configuration.md#upnp) to `disabled`: instances would ask the router for mappings on each
other's behalf.
- **Audio.** With `capture = qemu` Sunshine takes the sound from QEMU and creates no PulseAudio sinks.
## systemd
Install the files (paths as used by the units):
```bash
sudo install -Dm644 packaging/linux/qemu/sunshine-qemu@.service /etc/systemd/system/sunshine-qemu@.service
sudo install -Dm644 packaging/linux/qemu/sunshine-qemu.sysusers /usr/lib/sysusers.d/sunshine-qemu.conf
sudo install -Dm755 packaging/linux/qemu/sunshine-qemu-wait-vm /usr/lib/sunshine-qemu/sunshine-qemu-wait-vm
sudo install -Dm755 packaging/linux/qemu/sunshine-qemu-port /usr/lib/sunshine-qemu/sunshine-qemu-port
sudo systemd-sysusers
sudo install -Dm640 -g sunshine-qemu packaging/linux/qemu/example.conf /etc/sunshine-qemu/win11.conf
# edit /etc/sunshine-qemu/win11.conf, then:
sudo /usr/lib/sunshine-qemu/sunshine-qemu-port --dir /etc/sunshine-qemu --check
sudo -u sunshine-qemu XDG_CONFIG_HOME=/var/lib/sunshine-qemu/win11 sunshine /etc/sunshine-qemu/win11.conf --creds <user> <password>
sudo systemctl daemon-reload
sudo systemctl enable --now sunshine-qemu@win11.service
```
The `--creds` line needs `/var/lib/sunshine-qemu/win11`, which systemd creates on the unit's first start; run it
after starting the unit once, or create the directory owned by `sunshine-qemu` first.
The unit runs `sunshine /etc/sunshine-qemu/<vm>.conf` as `sunshine-qemu` with its state in
`/var/lib/sunshine-qemu/<vm>`, restarts it on failure, and has `After=` on libvirt and Avahi. It uses
`PrivateTmp=yes` and `ProtectHome=yes`, so keep sockets Sunshine connects to under `/run`, not `/tmp` or home
directories.
**Start order.** Sunshine checks QEMU's display once, when it starts. If the VM isn't reachable then, it logs
`Unable to initialize capture method` and keeps running without being able to stream, even after the VM comes up,
until it is restarted. So the unit's `ExecStartPre` runs `sunshine-qemu-wait-vm`, which waits (120 s by default)
until QEMU owns `org.qemu` on the configured bus, the QMP socket exists, or `virsh domstate` reports the libvirt
domain running. When it gives up, the unit fails and `Restart=on-failure` waits again. Start the unit after the VM,
or let the libvirt hook do it. Once Sunshine has found the VM, a guest reboot or a VM restart doesn't need a Sunshine
restart: it reconnects.
**Rootless.** For VMs you run as your own user (raw QEMU, or `qemu:///session`), use the user unit:
```bash
install -Dm644 packaging/linux/qemu/user/sunshine-qemu@.service ~/.config/systemd/user/sunshine-qemu@.service
install -Dm644 packaging/linux/qemu/example.conf ~/.config/sunshine-qemu/win11.conf
systemctl --user daemon-reload
systemctl --user enable --now sunshine-qemu@win11.service
```
It still expects `sunshine-qemu-wait-vm` in `/usr/lib/sunshine-qemu/`; edit the `ExecStartPre` path otherwise.
## libvirt
### Domain XML
For the system libvirt (`qemu:///system`), use a peer-to-peer display:
```xml
<domain type='kvm'>
<name>win11</name>
<devices>
<graphics type='dbus' p2p='yes'>
<audio id='1'/>
</graphics>
<audio id='1' type='dbus'/>
<sound model='ich9'>
<audio id='1'/>
</sound>
<video>
<model type='virtio'/>
</video>
<input type='tablet' bus='virtio'/>
<input type='keyboard' bus='virtio'/>
</devices>
</domain>
```
```
qemu_dbus_address = libvirt:win11
```
For DMABUF zero-copy add `<gl enable='yes' rendernode='/dev/dri/renderD128'/>` inside `<graphics>` and
`<acceleration accel3d='yes'/>` inside the video `<model>`. Without `rendernode`, libvirt picks the first render
node itself, which fails on hosts without `/dev/dri`; use `<gl enable='no'/>` there.
The system unit's user needs libvirt's `domain:open_graphics` permission. The simplest way is the `libvirt` group
(`usermod -aG libvirt sunshine-qemu`), but that group can manage every VM and is effectively root. With
`access_drivers = [ "polkit" ]` in `/etc/libvirt/libvirtd.conf` (or `virtqemud.conf`) a polkit rule can grant only
what Sunshine uses:
```js
// /etc/polkit-1/rules.d/50-sunshine-qemu.rules
polkit.addRule(function(action, subject) {
if (subject.user == "sunshine-qemu" &&
(action.id == "org.libvirt.unix.manage" ||
action.id == "org.libvirt.api.connect.getattr" ||
action.id == "org.libvirt.api.connect.search-domains" ||
action.id == "org.libvirt.api.domain.getattr" ||
action.id == "org.libvirt.api.domain.read" ||
action.id == "org.libvirt.api.domain.open-graphics")) {
return polkit.Result.YES;
}
});
```
Check the exact action names against your libvirt's ACL documentation; this rule hasn't been tested yet.
Why not the bus libvirt starts for a display without `p2p`? For every `<graphics type='dbus'>` without `p2p` or
`address`, libvirt starts a private `dbus-daemon` for the domain under `/run/libvirt/qemu/dbus/` whose path contains
the domain id (so it changes on every start). Its policy only admits the domain's own user (`libvirt-qemu` or `qemu`)
and, from libvirt 11.2, root; being in the socket's group isn't enough. Use that mode only when Sunshine runs as the
same user, as with `qemu:///session`:
```
qemu_dbus_address = libvirt:win11?uri=qemu:///session
```
Sunshine then reads the bus address from the running domain's XML each time it connects.
libvirt 11.2 and newer may start its own display helpers (`qemu-rdp` for `<graphics type='rdp'>`, and from 12.5
`qemu-vnc`) on a bus-mode display; they share the VM with Sunshine.
### Hook
`libvirt-hook-qemu` starts `sunshine-qemu@<domain>.service` when a domain starts (and when libvirt reconnects to a
running domain) and stops it when the domain stops. It only acts on domains that have `/etc/sunshine-qemu/<domain>.conf`,
never waits for Sunshine and never fails the domain operation.
```bash
# libvirt 6.5 and newer: hook directory
sudo install -Dm755 packaging/linux/qemu/libvirt-hook-qemu /etc/libvirt/hooks/qemu.d/sunshine-qemu
# older libvirt, or no other qemu hook: sudo install -Dm755 packaging/linux/qemu/libvirt-hook-qemu /etc/libvirt/hooks/qemu
sudo systemctl restart virtqemud # or libvirtd; libvirt only notices a new hook on restart
```
Don't enable the unit when the hook manages it. A hook must not call libvirt (libvirt holds the domain while hooks
run), so the hook starts the unit asynchronously and Sunshine connects once the domain is up.
### virt-manager
virt-manager has no editor for D-Bus displays, so change the XML:
1. **Edit → Preferences → General**: enable **Enable XML editing**.
2. Shut the VM down and open its details (**Open**, then the light bulb icon).
3. Select **Overview**, open the **XML** tab and, inside `<devices>`, replace the `<graphics type='spice'>` or
`<graphics type='vnc'>` element with `<graphics type='dbus' p2p='yes'><audio id='1'/></graphics>`. Add
`<audio id='1' type='dbus'/>` and make the sound device use it (`<sound model='ich9'><audio id='1'/></sound>`).
Remove SPICE-only devices virt-manager added (`<channel type='spicevmc'>`, `<redirdev type='spicevmc'>`,
`<audio type='spice'>`) if libvirt refuses the XML. Click **Apply**.
4. Under **Add Hardware → Input** add an **EvTouch USB Graphics Tablet** or a virtio tablet if the VM has only a PS/2
mouse.
5. Write `/etc/sunshine-qemu/<domain>.conf` with `qemu_dbus_address = libvirt:<domain>` and install the hook.
6. Start the VM from virt-manager: `systemctl status sunshine-qemu@<domain>` should show Sunshine running, and
Moonlight lists the VM under its `sunshine_name`.
virt-manager's own console can't show a D-Bus display; use Moonlight (or `virt-viewer`, which then takes the
display away from Sunshine, see above).
## Troubleshooting
### "Unable to initialize capture method" or "Unable to find display or encoder during startup"
The VM's display wasn't reachable when Sunshine started. Sunshine doesn't try again by itself: fix the cause and
restart it (the systemd units wait for the VM first). The `qemu:` log line above names the reason:
- `no QEMU owns [org.qemu]`: QEMU isn't running or uses another bus. Start the VM first; the systemd units wait for it.
- `couldn't connect to D-Bus address`: check the path, and that Sunshine's user may connect. A bus started by libvirt
or by another user refuses other users: use `p2p='yes'` or `qmp:`.
- `no QMP greeting`: another client holds the QMP socket; give Sunshine its own `-qmp` socket.
- `QMP add_client failed ... p2p connections not accepted in bus mode`: QEMU runs `-display dbus` without `p2p=on`.
- `couldn't load libvirt.so.0`: install libvirt's client library (`libvirt0` or `libvirt-libs`).
- `virDomainOpenGraphicsFD ... failed` or an access error: Sunshine's user lacks the libvirt permission above, or the
domain's display has no `p2p='yes'`.
- `domain [...] has no <graphics type='dbus'> device`: the domain XML still has a SPICE or VNC display.
### Permission denied on the render node, or "QEMU renders on ..." errors
With `gl=on`, QEMU and the encoder must use the same GPU. Set `rendernode=` in QEMU (or `<gl rendernode=>` in libvirt)
and [adapter_name](configuration.md#adapter_name) to the same `/dev/dri/renderD*` for VAAPI; NVENC needs QEMU on the
NVIDIA GPU's render node. Sunshine's user needs the `render` (and on some systems `video`) group.
### The mouse is offset or doesn't move
Sunshine sends absolute positions when QEMU reports an absolute pointer (`IsAbsolute`). A VM with only a PS/2 mouse is
relative: Moonlight positions then only work while the guest shows a hardware cursor. Add a virtio or USB tablet. If
touches do nothing, the VM lacks `virtio-multitouch-pci`; disable [native_pen_touch](configuration.md#native_pen_touch).
### No sound
QEMU exports audio only with `-audiodev dbus,id=...` plus `-display dbus,audiodev=...` (libvirt:
`<audio type='dbus'>` referenced from `<graphics>`), and the guest's sound card must use that audiodev.
### Two VMs show up as one, or a port is in use
Give every instance its own `port` (100 apart) and `sunshine_name`, and run `sunshine-qemu-port --check`. An
`Address already in use` in the log names the port that collides.
## What was tested
On Ubuntu 24.04 under WSL2 with QEMU 8.2 and 11.1: bus and QMP peer-to-peer streaming with picture, audio, input and
guest reboots, two VMs streamed at the same time by two instances (distinct mDNS names, ports and pairing), the units
with `systemd-analyze verify`, and the hook and helpers with fake `systemctl`, `gdbus` and `virsh`. The libvirt path
is tested against a stand-in for `libvirt.so.0` only. Not tested yet: a real libvirt domain (virt-manager), SELinux or
AppArmor confinement, the polkit rule, and DMABUF on a host with a GPU render node.
packaging/linux/qemu/sunshine-qemu@.service +1 −3
@@ -5,7 +5,6 @@
[Unit]
Description=Sunshine game stream host for VM %I
Documentation=https://github.com/LizardByte/Sunshine/blob/master/docs/qemu.md
After=network-online.target avahi-daemon.service virtqemud.service libvirtd.service
Wants=network-online.target
ConditionPathExists=/etc/sunshine-qemu/%I.conf
@@ -17,10 +16,9 @@
# A dedicated user. For qemu_dbus_address = libvirt:<domain> with p2p='yes' it needs the libvirt
# open_graphics permission (the libvirt group, or a polkit rule); for a bus address it must be allowed
# to connect to that bus (see docs/qemu.md).
# sunshine-qemu.sysusers also puts it in the video and render groups for GPU encoders.
User=sunshine-qemu
Group=sunshine-qemu
# GPU encoders (VAAPI render nodes, NVIDIA devices)
SupplementaryGroups=video render
# Per-VM state: relative paths in the config (file_state, credentials_file, pkey, cert, file_apps,
# log_path) and everything Sunshine keeps in its own directory land in /var/lib/sunshine-qemu/<vm>.
StateDirectory=sunshine-qemu/%i
packaging/linux/qemu/user/sunshine-qemu@.service +0 −1
@@ -4,7 +4,6 @@
[Unit]
Description=Sunshine game stream host for VM %I
Documentation=https://github.com/LizardByte/Sunshine/blob/master/docs/qemu.md
ConditionPathExists=%E/sunshine-qemu/%I.conf
StartLimitIntervalSec=300
StartLimitBurst=10
src_assets/common/assets/web/public/assets/locale/en.json +1 −1
@@ -377,7 +377,7 @@
"port_warning": "Exposing the Web UI to the internet is a security risk! Proceed at your own risk!",
"port_web_ui": "Web UI",
"qemu_dbus_address": "QEMU D-Bus Address",
"qemu_dbus_address_desc": "D-Bus address of the bus the VM's display is on, as given to QEMU with -display dbus,addr=. Leave blank to use the session bus.",
"qemu_dbus_address_desc": "Where the VM's display is: the D-Bus address given to QEMU with -display dbus,addr=, qmp:<socket> for a QEMU with -display dbus,p2p=on, or libvirt:<domain> for a libvirt domain with a D-Bus display. Leave blank to use the session bus.",
"qp": "Quantization Parameter",
"qp_desc": "Some devices may not support Constant Bit Rate. For those devices, QP is used instead. Higher value means more compression, but less quality.",
"qsv_coder": "QuickSync Coder (H264)",
tests/e2e/README.md +51 −1
@@ -11,8 +11,10 @@
| `qemu/guest/pattern.S` | 512-byte boot sector: one second of text mode, then VGA mode 13h with red, green, blue, white quadrants and a blinking square |
| `qemu/guest/build_guest.sh` | Builds the guest disk image with GNU `as`/`ld` (no binaries are committed) |
| `qemu/guest/linux/` | Linux test guest (Alpine kernel + initramfs built from pinned downloads): plays a sine tone, logs input events; see its README |
| `qemu/run_vm.sh` | Starts a private `dbus-daemon` and QEMU with `-display dbus,addr=...,audiodev=snd0` and `-audiodev dbus`; `VM_INPUT` picks virtio, PS/2 or multi-touch input devices |
| `qemu/run_vm.sh` | Starts a private `dbus-daemon` and QEMU with `-display dbus,addr=...,audiodev=snd0` and `-audiodev dbus`; `VM_INPUT` picks virtio, PS/2 or multi-touch input devices; `VM_DBUS=qmp` starts QEMU in peer-to-peer mode with a QMP socket for Sunshine instead |
| `qemu/e2e_stream.sh` | The end-to-end test (REQ-E2E-001), also used for latency numbers (REQ-NFR-001), the guest reboot test (REQ-CAP-004), the guest audio test (REQ-AUD-001) and the guest input tests (REQ-INP-001..003) |
| `qemu/multi_instance.sh` | Two VMs, two Sunshine instances and two clients at the same time (REQ-DEP-001) |
| `qemu/avahi_sandbox.sh` | Private Avahi daemon (user/network namespace, private system bus) for the mDNS checks |
| `qemu/check_guest_input.py` | Checks the Linux guest's evdev log against the input the client sent |
| `qemu/qmp.py` | Sends one QMP command to the VM (used for `system_reset`) |
| `moonlight_client/` | Headless client: GameStream pairing/launch plus moonlight-common-c and FFmpeg decoding |
@@ -116,5 +118,53 @@
relative mouse (which need QEMU's `MouseSet` position) aren't covered end to end; the unit tests cover
them. With `virtio-multitouch-pci`, QEMU delivers mouse buttons to the touch device and drops wheel
events, which is why touch has its own mode.
## Peer-to-peer display through QMP
```bash
E2E_TRANSPORT=qmp tests/e2e/qemu/e2e_stream.sh
```
QEMU runs with `-display dbus,p2p=on` and no bus. Sunshine gets `qemu_dbus_address = qmp:<socket>`, adds itself as a
display client with QMP `getfd` and `add_client protocol=@dbus-display`, and connects again the same way after a
client disconnects or the guest reboots. Combines with `E2E_AUDIO`, `E2E_INPUT`, `E2E_SESSIONS` and
`E2E_RESET_AFTER_FRAMES`. This is the path libvirt's `virDomainOpenGraphicsFD` uses for `<graphics type='dbus' p2p='yes'>`
(REQ-DEP-002); libvirt itself isn't part of the E2E.
## Several VMs at once
```bash
tests/e2e/qemu/multi_instance.sh # both VMs on private buses
E2E_VM2_TRANSPORT=qmp tests/e2e/qemu/multi_instance.sh # the second VM peer to peer
```
Starts two pattern VMs and two Sunshine instances whose config files differ only in the VM address, `sunshine_name`
(`sq-vm-1`, `sq-vm-2`), state paths and the base port that `packaging/linux/qemu/sunshine-qemu-port` allocates
(48089, 48189). Both instances share one `HOME`. A client pairs with each instance and both stream at the same time.
The script then checks (REQ-DEP-001):
- mDNS: `avahi-browse` finds exactly two `_nvstream._tcp` services, named `sq-vm-1` and `sq-vm-2`, on each instance's
HTTP port, and no instance renamed its service after a collision
- ports: while both stream, every TCP listener and UDP socket of each instance is inside its own range
(port - 5 to port + 21), the expected GameStream, web UI, RTSP and stream ports are open, and no port is used twice
- pairing: each client's certificate is accepted by its own instance (`PairStatus` 1) and refused by the other, each
web UI lists one paired client, and the instances have different unique ids
- each instance streamed its own VM, no PulseAudio sinks were created, and nothing but the empty
`~/.config/sunshine` directory was written to the shared `HOME`
mDNS uses `qemu/avahi_sandbox.sh`: `avahi-daemon` from packages fetched with `apt-get download` (not installed) runs
in a user, network and mount namespace with only a dummy interface, registered on a private stand-in for the system
bus that the instances and `avahi-browse` reach through `DBUS_SYSTEM_BUS_ADDRESS`. It needs unprivileged user
namespaces; `E2E_MDNS=0` skips the mDNS checks where they aren't available.
## Packaging tests
```bash
tests/packaging/linux/qemu/test_sunshine_qemu.sh
```
Not an E2E test, but it lives outside `test_sunshine` too: `systemd-analyze verify` on the units, the libvirt hook with a
fake `systemctl`, `sunshine-qemu-wait-vm` with fake `gdbus` and `virsh`, `sunshine-qemu-port`, and shellcheck
(REQ-DEP-001, REQ-DEP-002).
## GL display (DMABUF)