Skip to content

Latest commit

 

History

18 Commits

Folders and files

NameName
Last commit message
Last commit date
 
 
 
 
 
 
 
 
 
 

Repository files navigation

virtio-gpu-presentation-probe

shellcheck License: MIT

Answers one question about a QEMU/KVM guest with accelerated graphics: is the guest actually presenting frames to the host, or does it only think it is?

Problem

On guests using QEMU's GL-accelerated virtio graphics (virtio-gpu-gl or virtio-vga-gl, commonly with blob=on, which Venus/Vulkan and OpenGL 4.6 require per the QEMU virtio-gpu documentation), none of the usual inspection methods work.

virsh screenshot and QMP screendump fail with no surface. This was A/B tested on the origin guest with blob=on and blob=off (2026-07-12): both fail, so the breakage follows the GL scanout path itself, where frames travel as dmabufs and no main-memory display surface is populated for screendump to read. Anything built on screendump breaks with it.

Screenshots taken inside the guest (xwd, import, compositor tools) are worse than useless for this question. A guest can render every frame correctly to its own memory and never present one, in which case the in-guest screenshot shows a perfect desktop while the host shows black and a mouse cursor.

A human looking at the viewer window works, but not in CI, and not nine times in a row during a bisect.

The failure mode is real, not hypothetical. This probe was written to bisect an X server regression (XLibre commit ad1d4a28) where kwin composited and rendered normally but no frame was ever flushed to the scanout (the modesetting driver presents composited output through glamor, and the commit broke glamor's GLX vendor initialization, taking the present path with it). Every screenshot mechanism reported a healthy desktop. The screen was black.

How it works

QEMU has a trace event, virtio_gpu_cmd_res_flush, that fires once per RESOURCE_FLUSH command, the guest telling the host "present this rectangle". The probe enables the event, runs glxgears in the guest session so the screen is not idle-static, and counts events over a 30-second window read from the libvirt QEMU log.

Measured numbers from the origin case, identical guest and configuration:

working session:  ~1800 events / 30 s
broken session:       0 events / 30 s

The count is taken on the host, at the virtio transport, past the point where the guest's own view of the world can lie to you.

Install

It's one file with no dependencies beyond bash, ssh, and virsh:

curl -O https://raw.githubusercontent.com/jdmanring/virtio-gpu-presentation-probe/main/virtio-gpu-presentation-probe.sh
chmod +x virtio-gpu-presentation-probe.sh

Usage

One-time host setup, the only step the script cannot do for itself:

sudo setfacl -m u:$USER:r /var/log/libvirt/qemu/<vm>.log

The ACL is per-inode and silently dies whenever virtlogd recreates the log file. Re-grant it when the probe exits 2.

Then run it (the script arms the QEMU trace event itself on every run):

./virtio-gpu-presentation-probe.sh <vm-name> root@<guest-ip> [label] [window-seconds]
# prints (after ~45-100 s; progress goes to stderr):
myvm FLUSHES=1800 RATE=60/s SESSION=up

The method assumes QEMU was built with the log trace backend (on by default in distro builds) and that the domain logs through virtlogd to the usual file. If a guest you can see working reads FLUSHES=0, check that virsh qemu-monitor-command --hmp <vm> "info trace-events" | grep res_flush shows state 1 and that the log file is growing. A missing trace backend would otherwise read as a false "not presenting".

Exit codes are part of the contract: 0 presenting, 1 not presenting, 2 setup or session failure. That makes it usable directly as a git bisect run oracle or a CI gate without parsing the output.

Everything guest- or host-specific is an environment override, documented in the script header: VIRSH_URI for non-system libvirt, QEMU_LOG for nonstandard log paths, SESSION_PROC for non-KDE guests, XAUTH_GLOB for non-SDDM display managers, and DAMAGE_CMD, which replaces the whole damage generator and is also the hook for Wayland guest sessions (an example is in the header). THRESHOLD sets the presenting cutoff, default 100 events per window against a measured signal of about 60/s versus 0. It is per-window, so scale it down for windows under 5 seconds. The default damage generator is pre-flighted (glxgears present, xauth found) because its silent failure on an idle desktop would be indistinguishable from a broken guest; if you override DAMAGE_CMD, that check is yours. Don't run two probes against the same VM at once, since the first to exit disarms the shared trace event mid-window for the other.

Requirements in the guest: reachable sshd with key-based auth, and glxgears (mesa-demos or mesa-utils) unless you replace DAMAGE_CMD.

What a bad verdict tells you: exit 1 with the session up means frames are being rendered guest-side but no present command crosses the virtio transport. Suspect the guest's presentation path (DDX/modesetting driver, compositor present path) rather than virglrenderer or QEMU display code, and attach the probe line plus the guest's X or compositor log to the bug. Exit 0 with a black viewer means the guest IS presenting, so suspect the host viewer or display channel instead.

Relation to existing tools

openQA verifies guest screens by matching screenshots against reference images it calls needles (openQA documentation, "Needles"). Avocado-VT captures guest screens with QEMU screendump, from a periodic screendump thread in its VM handling (virttest/env_process.py). Both therefore depend on QEMU's display capture. Screendump is verified broken on GL virtio domains (this repo's origin case, and the reason this tool exists); whether openQA's VNC-based capture survives blob scanouts was not tested here. Independent of that, both frameworks answer a different question, "does the screen match a reference", where this probe answers "is presentation traffic flowing at all".

QEMU has no screendump readback for blob/dmabuf scanouts: the series that added blob scanout support (Osipenko, "virtio-gpu: Support blob scanout using dmabuf fd") covers display, not capture. Upstream expected screendump to work with egl-headless, which copies rendered frames back to a memory surface (Lureau, qemu-discuss, "egl-headless display screenshot"). On the origin guest (virtio-vga-gl with egl-headless, QEMU via libvirt) that does not hold today: screendump returns "no surface" with blob=on and with blob=off, A/B tested 2026-07-12. Whether that is a regression against Lureau's description or was always true of the virgl scanout path is a question for QEMU upstream. A screendump fix in QEMU would be the right long-term answer. Even with one, a screenshot of a static-but-correct screen cannot distinguish "presenting" from "presented once and stopped", which an event rate can.

DRM CRC checks, the kernel's frame-verification mechanism used by igt-gpu-tools (kernel documentation, "Display CRC Support"), run on the guest side of the virtio boundary, where this failure is invisible by construction: the guest believes it presented. In-guest tracing of the virtio_gpu driver observes the same commands but from the same untrusted side. This probe sits on the host, at the transport, which is the first place the truth is observable.

I found no existing tool that measures this. If one exists, file an issue and I will link it here.

The need is documented by its absence. Black-screen-with-virgl threads recur for years and resolve, when they resolve at all, by guesswork: a GNOME Boxes bug identified only after exhausting configuration changes (Arch BBS 264920), a known NVIDIA-host incompatibility found via Reddit hearsay (Arch BBS 302370), a compositor conflict isolated by trial and error (Gentoo forums), and GL display modes that plainly do not work (UTM #4941). None of those threads had a way to answer the first question a display developer would ask: is anything being presented?

Test coverage

Tested environment: Linux host (the script needs setfacl, virtlogd's log layout, and virsh, so the host side is Linux-only), QEMU 11.0.2, libvirt 12.5.0, bash 5. The measured rates and the screendump A/B result are facts about this stack; if your QEMU behaves differently, especially around screendump, that is worth reporting here and upstream.

Every path was exercised end-to-end against a live libvirt domain (2026-07-12), not just the happy one:

path how it was produced result
exit 0, X11 session working X server (XLibre 25.2.0), Plasma X11 FLUSHES=1801 RATE=60/s SESSION=up
exit 0, Wayland session Plasma Wayland, SESSION_PROC=kwin_wayland, DAMAGE_CMD=eglgears_wayland FLUSHES=1801 RATE=60/s SESSION=up
exit 1, real failure X server built at the regressing commit (renders, never presents) FLUSHES=0 RATE=0/s SESSION=up
exit 2, unreadable log QEMU_LOG=/nonexistent error to stderr
exit 2, unknown domain wrong VM name trace-arm error to stderr
exit 2, no session greeter only, no session process FLUSHES=2250 SESSION=down

The last row is why the session gate exists. An SDDM greeter alone can present at up to ~75 events/s while animating (it measures 0 when static), so a flush count without the session check could call a dead login loop "good". The count is still printed; the verdict is withheld.

Example

examples/xserver-bisect-harness.sh is the origin use case: an unattended git bisect of an X server inside a guest, using the probe as the good/bad oracle (build, display-manager restart into an autologin session, probe, git bisect good|bad). It found the regressing commit in 9 steps with no one watching the screen. Paths and meson options in it are specific to that hunt; treat it as a template.

Contributing

Bug reports and portability fixes (other display managers, other session types) are welcome; CI runs shellcheck on both scripts, so run it on your patch first. Scope is deliberately frozen: virtio-gpu only. Passthrough has no virtio boundary for the host to observe, and QXL domains don't need a probe, since screendump works there. Feature requests that widen the scope will be declined with thanks.

License

MIT.

About

Tells you whether a GL-accelerated QEMU/KVM guest is actually presenting frames, by counting virtio-gpu flush trace events. For when screendump says 'no surface'.

Topics

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages