spice-server patches on a shaped link¶
This page records what was found, and patched, in spice-server itself while measuring SPICE on Kerbside's shaped-link rig: which video codec the streams on the earlier pages really used, why every released spice-server crashes when certain clients connect to a VM that is already streaming, and which of four small spice-server changes made a measurable difference. It uses the rig of Proxy backpressure on a shaped link and the streaming configurations of Streaming and the qemu damage series on a shaped link. Written on 2026-10-30.
The patches that earned their place form a series against spice-server
master, ready to send upstream but not yet sent. It lives in
kerbside-patches
under upstream/spice/: the series in series/v1/, the build script
in rig/build-spice.sh, the generated tables in
results/patch-matrix.md, and the two dropped patches in dropped/.
Summary¶
| Question | Answer |
|---|---|
Which codec did streaming-video=all use? |
MJPEG on every page since the re-baseline. Ryll changed the order it asks for codecs; the proxy relayed the request correctly. |
| Does the connect crash survive in current spice-server? | Yes. 0.15.2, 0.16.0 and master (adeb620a04, 2026-10-29) each crashed in 20 of 20 connects. A 16-line patch takes master to 0 of 20. |
| Which clients trigger it? | A client that sends its codec preference as the display channel comes up, as Ryll does. remote-viewer does not. |
| Hardware encoding on any GPU | Kept. vah264enc on this AMD GPU cut qemu's CPU by about 15% and, on one loaded link, p50 latency by 40% and p95 by nearly 80%, for 56 to 64 MiB of qemu memory paid only when a GStreamer encoder is built. |
| Trace ring 8 to 64 | Dropped: no measurable effect. |
| Frame-rate ceiling 30 to 60 | Dropped: no measurable effect. Rate control, not the ceiling, sets the frame rate on these links. |
| Upgrading 0.15.2 to master | Helps in 9 of 15 cells, chiefly qemu CPU and, with streaming-video=all, latency and bandwidth. It does not fix the crash. |
Which codec the streams used¶
The re-baseline reported every streaming-video=all stream as H.264,
and the pages after it repeated that. Only the re-baseline was right.
The raw runs behind the ACK-window, transcoding-measurement and
transcoding-spike pages recorded MJPEG (codec 1) stream creations, 252
in one set of runs and 2887 in the other, and no H.264 one. Those pages
have been corrected.
qemu 10.0.13 has no option to choose a codec, so spice-server starts
from its default order, spice:mjpeg first, and reorders it only when
the client sends SPICE_MSGC_DISPLAY_PREFERRED_VIDEO_CODEC_TYPE. What
changed was the client's request:
| Ryll commit | Date | Codecs it asks for |
|---|---|---|
| 50a9cc0, used by the re-baseline | 2026-09-24 | H.264, then MJPEG |
| 24a80c0 | 2026-10-11 | stops offering H.264 |
| 15cd776, in the 0d140b3 every later page used | 2026-10-16 | MJPEG first |
spice-server's debug log shows the result directly. With Ryll 0d140b3's
defaults it logs Preferred video-codecs: spice:mjpeg gstreamer:mjpeg
gstreamer:h264 gstreamer:vp8 gstreamer:vp9; with
--experimental-h264 it logs gstreamer:h264 first and builds x264enc
pipelines, through the proxy. kerbside-proxy relays the message
whenever the client's capabilities carry PREF_VIDEO_CODEC_TYPE, and
did so here.
So the MJPEG pages measured streams that Ryll decodes, and their stream rate control ran on reports from a client that drew the stream. Ryll still does not decode spice-server's H.264 (ryll#398). The H.264 rows below therefore measure latency, CPU and whether the hardware encoder engaged, not the rendered video.
The connect crash¶
What crashes¶
With streaming-video=all and the guest already streaming when Ryll
connected, each of 0.15.2 (Debian 13's package), 0.16.0 and master
adeb620a04 crashed qemu in 20 of 20 connects. With the patch, master
crashed in 0 of 20, and the assembled series in 0 of 5. 0.16.0, from
2025-05-05, is the latest release, so no released spice-server has
the fix.
Why¶
A new display client joins the channel's client list in dcc_new(),
but is given the streams that already exist only at the end of
dcc_start(). In between, spice-server reads the client's messages
synchronously while it waits for SPICE_MSGC_DISPLAY_INIT
(display_channel_client_wait_for_init()). A codec preference handled
there calls video_stream_detach_and_stop(), which queues a
STREAM_CLIP and a STREAM_DESTROY for every stream to every client,
the new one included. The new client's stream agents still have a NULL
display-client pointer, which only stream creation sets. When the first
STREAM_CLIP has been sent and released, its destructor dereferences
that pointer, and qemu dies.
The patch records in each stream agent whether the client has been given the stream, as spice-server already does for surfaces. Clip and destroy messages, and the stream's visible region, are only produced for a client that has the stream, and the agent's display-client pointer is set when the client is created rather than when a stream is.
Which clients trigger it¶
Only the new client's own codec preference can do it, and only if it
arrives before SPICE_MSGC_DISPLAY_INIT or in the same read:
- Ryll does. It sends its preference at link-up, and spice-server's log shows the preference handled before the client's display encoders are created, in every connect.
- spice-gtk does not, so remote-viewer does not. spice-gtk's display
channel sends only
SPICE_MSGC_DISPLAY_INIT, and optionally a preferred image compression, when it comes up (spice_display_channel_up()insrc/channel-display.c). It sends a codec preference only when the application callsspice_display_channel_change_preferred_video_codec_types(). remote-viewer (virt-viewer) never calls it; spice-gtk's test client, spicy, calls it from a menu, after connect. - Nothing else stops streams in that window. spice-server's other
callers of
video_stream_detach_and_stop()(a codec change from the host application, migration) and its stream timeout all run on the display worker thread, which is blocked in the wait forSPICE_MSGC_DISPLAY_INITuntil it ends. The client's preferred image compression only sets a field.
A preferred image compression from spice-gtk
(--spice-preferred-compression) is therefore harmless, and stock
remote-viewer users will not see this crash. Any client that states its
codec preference at connect will, on every connect to a streaming VM.
The patches¶
Each was measured in one run of the rig with the builds interleaved inside every pass, against master plus the crash fix. A difference counts only when every repeat of one build sits on the same side of every repeat of the other and the gap exceeds both the wider per-repeat range and a floor (2 ms, 0.02 cores or 8 MiB). A null check, between two builds expected to behave identically, flagged 1 of 15 cells, so a lone single-measure flag is within what the rig produces by chance.
Stock qemu cannot exercise the frame-rate or hardware encoder patches.
Its non-GL display pushes damage on a fixed 30 ms timer, so no stream
exceeds about 33 fps, and it cuts damage into 32-pixel-wide columns,
narrower than this host's vah264enc accepts (at least 128x128). Those
two were measured on a qemu with the damage series from the re-baseline,
which sends each damaged region whole, here as one 640x480 stream, and
paces by max-refresh-rate.
Hardware encoding on any GPU: kept¶
spice-server looked for hardware encoding plugins only when udev
reported an Intel GPU, although GStreamer's va plugin works on any
VA-API GPU. The series probes for them whenever the host has a DRM
render node, and falls back to the software encoder when a hardware
one cannot be built, started or encode, or cannot take the frame size.
It is two patches in the series: the fallback, then the broader probe.
On an AMD Radeon 780M (radeonsi, Mesa 25.0.7, GStreamer 1.26), with Ryll asking for H.264, at 30 frames per second:
| Link | Activity | p50 ms, x264enc / vah264enc | p95 ms | qemu cores | qemu peak RSS MiB |
|---|---|---|---|---|---|
| 20 ms / 50 Mbit | busy | 28 / 31 | 46 / 46 | 0.15 / 0.13 | 222 / 278 |
| 20 ms / 50 Mbit | heavy | 33 / 34 | 54 / 50 | 0.16 / 0.13 | 240 / 297 |
| 80 ms / 10 Mbit | busy | 91 / 91 | 109 / 106 | 0.15 / 0.13 | 219 / 283 |
| 80 ms / 10 Mbit | heavy | 186 / 107 | 1262 / 275 | 0.15 / 0.13 | 239 / 295 |
- vah264enc engaged on every H.264 stream, read from spice-server's log, with no fallbacks.
- The CPU saving was measurable in three of the four cells.
- At 80 ms / 10 Mbit heavy, vah264enc's smaller output (5.5 against 7.3 Mbit/s) also cut latency. Elsewhere latency did not change.
- The memory is the VA driver loaded into qemu. It is paid only once a GStreamer encoder is built: with MJPEG streams the series made no difference to qemu's RSS.
That cost is why the patch is kept only conditionally: a deployment
pays it only on a host with a usable GPU, and only when a client asks
for a GStreamer codec. With /dev/dri hidden from qemu, or holding a
render node that cannot be opened, streams were encoded by x264enc, as
were streams 32 pixels wide.
Trace ring from 8 to 64: dropped¶
When a streamable drawable leaves the tree without a stream,
spice-server remembers it in an 8-entry ring, so that a successor of
the same shape carries on its frame count towards starting a stream.
Stock qemu sends up to 19 columns per update, more than the ring holds.
Raising it to 64 changed nothing measurable: 14 of 15 cells under stock
qemu with MJPEG showed no difference, and every build created the same
streams in every cell (21 per repeat with all, 19 with filter). The
two flags were single measures at the noise rate (peak RSS -8 MiB,
spread 6; an idle p50 +5 ms, spread 4, where nothing streams). The ring
is consulted only when many drawables leave the tree at once, and in
these runs that never delayed a stream.
Frame-rate ceiling from 30 to 60: dropped¶
The patch worked as written: with qemu at max-refresh-rate=60 and a
60 fps guest activity, streams were created at 60 fps instead of 30.
But latency, qemu CPU, bandwidth and display messages per second were
unchanged in all 12 cells, with H.264 and with MJPEG, and nothing
regressed on the slow links. The client received 4 to 12 display
messages a second for every build: spice-server's rate control, not
the ceiling, sets the delivered frame rate on these links.
What upgrading 0.15.2 to master gains¶
Against Debian's 0.15.2 under stock qemu with MJPEG, master (with the crash fix, which these runs never needed) helped in 9 of 15 cells, showed nothing in 4 and regressed in 2. Plain master would measure the same: the activity started after the connect, so the crash never fired.
- qemu CPU fell 0.03 to 0.04 cores, about 20%, in five cells (both
activities at 20 ms / 50 Mbit with
all, busy withfilterthere, and busy at 80 ms withall). - With
streaming-video=all, p50 fell 12 to 18 ms and p95 21 to 264 ms at 80 ms / 10 Mbit heavy, 80 ms / 5 Mbit and 150 ms / 2 Mbit busy. Display bandwidth fell to a third or less, for example 34.4 to 9.8 Mbit/s at 20 ms / 50 Mbit heavy, with a third or fewer of the display messages. Image quality was not measured, so whether that is leaner encoding or fewer frames is open. - The regressions were p50 +19 ms (spread 18) at 80 ms / 10 Mbit busy
with
filter, and p95 +83 ms (spread 71) at 150 ms / 2 Mbit heavy withall, a saturated cell whose p50 is 7.3 s. - Idle showed nothing.
So upgrading is worth having for its own sake, but it does not make
streaming-video=all safe with clients that state a codec preference
at connect. No released spice-server has the fix.
Caveats¶
- One host and one GPU. An AMD Ryzen 7 8845HS with a Radeon 780M, Debian 13, under a desktop session. Intel's msdk and vaapi paths are unchanged by the series and were not measured.
- H.264 was not rendered. Ryll does not decode spice-server's H.264
(ryll#398), so in the H.264 rows the key-box latency is valid (the
key box is a
DRAW_COPY), but the stream reports that drive rate control describe streams the client failed to decode, the same for every build. - Small samples. Three repeats of 40 presses per cell (two in one
MJPEG run). At 150 ms / 2 Mbit heavy with
filtertoo few repeats completed to judge. - The crash fix has no automated test. Nothing in spice-server's test suite can link a display channel as a client; the evidence is the reproducer.
What comes next¶
Nothing here has been sent or filed.
- Send the series upstream. It applies to spice-server master as of
2026-10-29 and passes
meson testafter each patch. Until it lands and reaches distributions,streaming-video=allcrashes qemu for clients that send their codec preference at connect. - H.265. spice-server names it but rejects it, and Ryll cannot decode it, so it was not measured.
- Rate control. Delivered stream frames held at 4 to 12 a second whatever the frame-rate ceiling. That is the lead for any work on how quickly spice-server's bitrate ramps up.
- Decode H.264 in Ryll (ryll#398), so the H.264 rows can be re-measured with a client that draws them.
- Have Ryll send its codec preference after
SPICE_MSGC_DISPLAY_INIT. The crash needs the preference to arrive before spice-server has handled the init message, so a Ryll that waits until then would not trigger it on any released spice-server.