Procedure to replicate the working Apollo-on-Linux virtual display on a Strix Halo machine (Ubuntu 26.04, kernel 7.1.2, dracut initramfs, GDM Wayland session, SSH admin, Moonlight client).
Result: a true virtual monitor at a custom resolution (1920x1280@60) that Sunshine streams to Moonlight with keyboard/mouse control, independent of any physical monitor — equivalent to Apollo's virtual display feature on Windows.
A custom EDID is forced onto a spare (disconnected) DisplayPort output via two
kernel parameters. The DRM stack then believes a monitor is attached there, the
GNOME desktop session renders onto it, and Sunshine (capture=kms) captures it:
drm.edid_firmware=<port>:edid/virtual-display.bin— serve our EDID on that portvideo=<port>:e— force the connector on at boot (preserve mode)
Do not plug a real monitor into the chosen port afterwards — it inherits the virtual EDID.
- root (sudo) access
- Kernel with
drm.edid_firmwaresupport (4.14+; verified on 7.1.2) - A spare disconnected DP/HDMI output on the GPU (Strix Halo: DP-1..DP-8, HDMI-A-1)
python3; optionallyedid-decodefor a sanity check of the generated EDID- The GPU must drive the target pixel clock on that port (1920x1280@60 ≈ 162 MHz is trivial for DP/HDMI)
tools/gen_virtual_edid.py— generates the EDID (correct Samsung-style DTD byte layout)tools/virtual-display-setup.sh— installs everything, patches GRUB + initramfs + Sunshine configplan.md— design notes and the lessons learned (EDID DTD layout bug, dracutinstall_items, etc.)
cd /home/me/Apollo
sudo ./tools/virtual-display-setup.shDefaults: auto-detected spare port, 1920x1280@60. Customize with flags:
sudo ./tools/virtual-display-setup.sh --port DP-2 --width 1920 --height 1280 --refresh 60What it does:
- Auto-detects the first disconnected DP/HDMI port (override with
--port). - Generates and installs
/lib/firmware/edid/virtual-display.bin(shows anedid-decodeDTD check if available). - Writes the initramfs hook:
- dracut:
/etc/dracut.conf.d/99-virtual-display.conf(install_items, notadd_firmware— unsupported in dracut 110) - initramfs-tools:
/etc/initramfs-tools/hooks/virtual-displaythen rebuilds the initramfs.
- dracut:
- Appends
drm.edid_firmware=<port>:edid/virtual-display.bin video=<port>:eto the lastGRUB_CMDLINE_LINUX_DEFAULTline in/etc/default/grub(only the last line takes effect — duplicate lines exist) and runsupdate-grub. A backup is kept alongside. - Sets
capture=kmsin~/.config/sunshine/sunshine.conf.
It ends by printing the reboot command.
sudo systemctl reboot -i-i is required: the block inhibitors (gnome-session, gsd-power, unattended-upgrades)
otherwise abort shutdown.
sudo ./tools/virtual-display-setup.sh --verifyChecks that the params are active in /proc/cmdline, the connector is connected,
the target resolution is in its mode list, the sysfs EDID matches the firmware, and
pins output_name=<N> in sunshine.conf from the Sunshine monitor list
("Monitor N is ").
The sysfs EDID comparison needs root (the sysfs file is root-owned).
- Desktop session renders the virtual port (it becomes a GNOME logical monitor).
- If the desktop comes up at the wrong resolution, apply a GNOME monitor config for
the port at 1920x1280:
(get the current
gdbus call --session --dest org.gnome.Mutter.DisplayConfig \ --object-path /org/gnome/Mutter/DisplayConfig \ --method org.gnome.Mutter.DisplayConfig.ApplyMonitorsConfig \ <serial> 0 [<connector-id-array>] [<logical-monitor-specs>] [{}]serial, connector IDs, and monitor specs fromorg.gnome.Mutter.DisplayConfig.GetCurrentStatefirst). - Moonlight should show the desktop at 1920x1280 with working keyboard/mouse.
Input needs
/dev/uinputwritable — Sunshine's inputtino devices ("Mouse passthrough", "Keyboard passthrough") are created at startup.
- Admin over SSH; do not RDP into the box while the auto-login session is active (it kills the session).
- If a real monitor is ever plugged into the chosen port, it will show the virtual
modes; remove the two kernel params + drop-in and
update-grub+ rebuild the initramfs to revert.
| Symptom | Cause / fix |
|---|---|
| Virtual port stuck at 640x480 | EDID DTDs rejected — regenerate with tools/gen_virtual_edid.py (old generators used a wrong DTD byte layout) and reinstall |
update-initramfs doesn't embed the EDID |
dracut: use install_items, not add_firmware; verify with sudo lsinitramfs /boot/initrd.img-$(uname -r) (non-sudo silently fails on root-owned initrds) |
Kernel params don't appear in /proc/cmdline |
GRUB edit went to a dead duplicate line — only the last GRUB_CMDLINE_LINUX_DEFAULT is used |
| Reboot aborts with "inhibitors exist" | sudo systemctl reboot -i |
| "No keyboard or mouse" in Moonlight | Usually a tiny/black stream — fix resolution first; input devices exist as long as /dev/uinput is writable |
modetest unavailable |
libdrm-tools isn't installable on Ubuntu 26.04 — use the --verify checks instead |
- Not a kernel-level virtual output like
amdgpu.virtual_display(which disables physical outputs, so it was rejected) or VKMS — the EDID-on-spare-port trick is per-machine setup, not a plug-and-play feature. - The repo's
SUNSHINE_ENABLE_VIRTUAL(capture=virtual) path is a separate, fragile mechanism (steals a connected-but-idle physical connector) and is not what this setup uses.