The P17 display slice: VA-API frames to a layer surface¶
Status: recorded observations from milestone M27, reference profile, 2026-09-29
Milestone M27 is P17 Scaena's first slice (ADR-0003). Decoded video reaches
the compositor as a DMA-BUF, never as copied pixels (D75): the decoder exports
each surface as one fd, sends it over an AF_UNIX socket pair as SCM_RIGHTS
beside one line-delimited JSON-RPC 2.0 descriptor (D77), and P17 checks what
it received and attaches it to a zwlr_layer_shell_v1 surface through
zwp_linux_dmabuf_v1. The half that needs no GPU runs inside
make verify-all; the half that needs the reference profile runs here:
A pass is development evidence for the client half only (D79). The
compositor it runs against is not P04: on the reference profile it is the host
session's KDE KWin, or a nested KWin as in the subtitle run below, and the
label names which from the compositor's own command line. Nothing here is
evidence that P04 serves zwlr_layer_shell_v1 or zwp_linux_dmabuf_v1: P04
serves no Wayland protocol, and no milestone yet plans one that does. A pass
closes no hardware, accessibility or release gate.
What is tracked¶
| Path | Role |
|---|---|
crates/aegis-scaena/src/device.rs |
the decode node from the compositor's main device, through /sys/class/drm |
crates/aegis-scaena/src/decode.rs |
cros-libva: vendor check, MPEG-2 and JPEG decode, the ready poll, export_prime |
crates/aegis-scaena/src/jpeg.rs |
the baseline JPEG reader that fills the VA-API buffers |
crates/aegis-scaena/src/content.rs |
the index-row readback and CRC-32 over visible NV12 lines |
crates/aegis-scaena/src/reference.rs |
the reference MPEG-2 intra frame, third-party data |
crates/aegis-scaena/src/fixture.rs |
the committed fixture and its per-frame regression pins |
crates/aegis-scaena/fixtures/index-blocks-60.mjpeg |
the 60-frame Motion-JPEG fixture |
crates/aegis-scaena/src/present.rs |
the layer surface, the feedback table, import, commit, presentation |
crates/aegis-scaena/src/slice.rs |
the run and its cases; src/bin/aegis-scaena-display.rs starts it |
tools/verify_display.py |
the gate, run as make verify-display |
tools/test_display_slice.py |
the gate's half that runs inside make verify-all |
The transport, the descriptor, the attach checks, the surface state machine
and the Wayland waits (criteria 1 to 4, E27-1 and E27-3) are the same crate's;
crates/README.md lists its modules and tests, and
docs/roadmap/toolchain-admission.md records the toolchain the crate builds
with and the libva CI builds.
What the gate does¶
tools/verify_display.py reads the build toolchain back through
tools/display_toolchain.py, holds the committed fixture to its pinned
sha256, builds aegis-scaena-display with cargo build --locked, and starts
it twice. LIBVA_DRIVER_NAME is set in each child's environment only; the
gate's own environment is never changed.
aegis-scaena-display runwithLIBVA_DRIVER_NAME=iHDconnects to the compositorWAYLAND_DISPLAYnames, reads its registry under a deadline, creates a 256 x 64 layer surface on the overlay layer, bottom right, withkeyboard_interactivitynone, and waits for its first configure and itszwp_linux_dmabuf_feedback_v1. The main device comes from that feedback; the decode node is therenderDentry whose sysfsdevequals it, and its device file'sst_rdevmust equal it too. The VA display is opened on that node and its vendor string must name iHD before anything decodes.- The reference MPEG-2 frame decodes and is read back with
create_image. A planted zero-filled surface is read back through the same check. Then each fixture frame is decoded, polled ready, read back against its index row, exported withexport_prime, sent over the socket pair with its fd and received by P17, which checks it (DMA_BUF_MAGIC, the plane layout against the object size), admits its format and modifier against the surface's feedback, imports it withzwp_linux_buffer_params_v1.create, attaches and commits it with awp_presentationfeedback, and waits forpresented. After frame 30 a descriptor for NV12 withINTEL_4_TILED_DG2_RC_CCS, over a duplicate of the frame's fd, is sent the same way and must be refused before any request; frame 31 must present. aegis-scaena-display driver-probewithLIBVA_DRIVER_NAME=nvidia, the session's own value on the reference profile, resolves the same node and must be refused before anything is decoded or attached.
Without the reference profile's capabilities the gate prints SKIP: <reason>;
the display slice gate did not run. and exits 0, before it builds anything:
not Linux, no Wayland session socket, fewer than two DRM render nodes (negative
(b) needs a node that is not the compositor's main device), no iHD driver, no
cargo or rustc, or a session logind reports locked. Every case below must
report PASS. A case the binary reports with a SKIP line instead fails the
gate with the binary's reason; the one case it may report with a NOTE line
is the regression pin, as the fixture section says.
The gate prints rustc, libva, the VA vendor string, the compositor, the
decode node and the modifier the driver chose, and labels the run client half
only. It names the compositor from the Wayland socket's peer
(SO_PEERCRED), its executable from /proc and its package from
pacman -Qo; the compositor binary is never executed. On the reference
profile the socket's peer is kwin_wayland_wrapper, which holds the
listening socket for kwin_wayland; running it with --version prints the
version and then tries to take the session's socket lock, which is why the
gate reads the package database instead.
The cases¶
| Case | Criterion | What passes |
|---|---|---|
display/attach-before-ack-refused |
boundary | an attach before the first ack_configure is refused by the surface state machine |
display/other-render-node-refused |
negative (b) | every render node whose device number is not the main device is refused, before anything is opened on it |
display/driver-is-ihd |
5 | the VA vendor string names the iHD driver |
display/mpeg2-reference-crc |
5 | the reference frame's visible NV12 lines hash to CRC-32 0xa5713e52 |
display/planted-zero-surface-refused |
E27-2 negative | a zero-filled surface fails the index-row check |
display/fixture-frames-present |
5, boundary | 60 frames read back, 60 created, 0 failed, a presented per commit, frame 1 and frame 60 included |
display/received-fd-is-the-exported-one |
5 | every received fd has the exported one's (st_dev, st_ino) |
display/descriptor-carries-the-exported-modifier |
6 | every descriptor carries export_prime's modifier unchanged |
display/unadvertised-pair-refused |
negative (c) | NV12 with INTEL_4_TILED_DG2_RC_CCS is refused client-side, and the next frame presents |
display/frame-crc-regression-pin |
5 | each frame's CRC-32 equals its recorded pin under the recorded driver; under another, a NOTE reports it, not held |
display/presented-deadline |
boundary | a presentation feedback on a surface never committed fails at its 250 ms deadline, naming the event |
display/driver-not-ihd-refused |
negative (a) | under LIBVA_DRIVER_NAME=nvidia the node's VA display is refused on its vendor string |
The subtitles child presents the 90-frame Motion-JPEG subtitle fixture clip
at 30 frames per second with software pacing and evaluates presentation
feedback timestamps against the SubRip cues. The subtitle text is not
rasterised: a subtitle's presentation time is the wp_presentation timestamp
of the frame its start timecode falls on, measured from the timestamp of the
clip's first frame:
subtitle/cue-1-within-100ms: the presentation timestamp offset of the first frame of cue 1 falls within 100 ms of the cue's start timecode.subtitle/cue-2-within-100ms: the presentation timestamp offset of the first frame of cue 2 falls within 100 ms of the cue's start timecode.subtitle/cue-3-within-100ms: the presentation timestamp offset of the first frame of cue 3 falls within 100 ms of the cue's start timecode.subtitle/planted-150ms-late-refused: a planted artificial 150 ms delay exceeds the 100 ms tolerance window and is refused with late status.
Every Wayland wait -- the registry roundtrip, the first configure, the
surface feedback, each created or failed and each presented or
discarded -- polls the connection fd under its own deadline, and a missed
deadline fails the case and names the event (HISS-02). cros-libva's sync
wraps vaSyncSurface, which takes no timeout, so every decode is polled with
Surface::query_status until VASurfaceReady, at most 4,000 polls within
2 s, before sync or create_image is called; a missed deadline names the
surface.
The Motion-JPEG fixture¶
crates/aegis-scaena/fixtures/index-blocks-60.mjpeg is 48,477 bytes, sha256
fd45d15a2837113fc2d581f04a9b82ca7992a6d2b8940f20ab9cc95fe6767be5, pinned in
tools/verify_display.py and tools/test_display_slice.py. It is 60 baseline
JPEG frames of 256 x 64, 4:2:0, and each draws its frame number, 1 to 60, as a
row of 16 x 16 luma blocks on the MCU grid at (16, 16): one white guard block,
six bit blocks, most significant first, and one black guard block. The check
reads each block's mean luma from the decoded surface with create_image,
before the surface is exported, and thresholds it at 128, so a blank frame, a
frame out of order and a zero-filled surface all fail; the expected number
comes from the generator, never from an earlier run of the decoder.
It was generated for Aegis once, with ffmpeg n9.0.2, by the command below,
and carries the project licence through REUSE.toml. The gate never runs
ffmpeg; the command is its provenance, and running it again on 2026-09-29
reproduced the committed bytes exactly.
bit() { # the drawbox for bit $1, set in frame n when bit $1 of n+1 is 1
x=$((32 + 16 * (5 - $1)))
printf ',drawbox=x=%d:y=16:w=16:h=16:c=white:t=fill' "$x"
printf ":enable='eq(mod(floor((n+1)/%d),2),1)'" "$((1 << $1))"
}
filter="color=c=0x808080:s=256x64:r=30,format=yuvj420p"
filter="$filter,drawbox=x=16:y=16:w=16:h=16:c=white:t=fill"
filter="$filter,drawbox=x=32:y=16:w=112:h=16:c=black:t=fill"
for b in 5 4 3 2 1 0; do filter="$filter$(bit "$b")"; done
ffmpeg -nostdin -hide_banner -loglevel error -f lavfi -i "$filter" \
-frames:v 60 -c:v mjpeg -q:v 2 -huffman default \
-fflags +bitexact -flags +bitexact -f mjpeg index-blocks-60.mjpeg
FIXTURE_COMMAND in tools/verify_display.py holds the same arguments as
one vector, and tools/test_display_slice.py checks that it draws two guards
and six bits.
The per-frame CRC-32 of the decoded visible NV12 lines is a regression pin
only (RECORDED_FRAME_CRCS in src/fixture.rs), recorded with the driver
that produced it, the iHD driver 26.2.4. A run under the same driver must
match every pin, and one that differs fails the case. A run under another
prints a NOTE line with how many differ and every measured value, and holds
none of them: the gate accepts that line as reported, not held, and names the
case so in its summary, so a driver update alone does not turn the gate red.
Re-recording means a new measurement under the driver that produced it (the
D73 pattern). The content check is the index row, never the pin.
The subtitle fixture clip¶
The milestone M32 fixture provides three synchronized sibling streams across a common three-second timeline at 30 frames per second:
crates/aegis-scaena/fixtures/subtitle-clip-90.mjpeg: 279,517 bytes, sha256499248fe9619c9b9457687cc7abce67e00eb6af23a6285d9532befb3239c493c. It is 90 baseline JPEG frames of 256 x 64, 4:2:0.crates/aegis-scaena/fixtures/subtitle-clip-90.wav: 288,044 bytes, sha256c45da1e5be8e950fcf01483646b4d8b5e58041986625f86b3cf2a23b366f3597. It is a 48,000 Hz, 16-bit mono PCM tone containing 144,000 samples.crates/aegis-scaena/fixtures/subtitle-clip-90.srt: 157 bytes, sha25620a58bef6628d6628ee523dbcf656d5af8c02aab757cbdc8bcf12143b0917e71. It is hand-written for Aegis, with LF endings, no byte order mark, and three cues whose timecodes are multiples of 100 ms, so each falls on a frame boundary at 30 frames per second.
The files carry the project licence through the REUSE.toml ** table. The
gate never runs ffmpeg; the commands below document their provenance with
ffmpeg n9.0.2 and reproduced identical digests on 2026-10-17:
ffmpeg -nostdin -hide_banner -loglevel error \
-f lavfi -i "testsrc=s=256x64:r=30,format=yuvj420p" \
-frames:v 90 -c:v mjpeg -q:v 2 -huffman default \
-fflags +bitexact -flags +bitexact -f mjpeg subtitle-clip-90.mjpeg
ffmpeg -nostdin -hide_banner -loglevel error \
-f lavfi -i "sine=frequency=1000:sample_rate=48000" \
-t 3 -c:a pcm_s16le \
-fflags +bitexact -flags +bitexact -f wav subtitle-clip-90.wav
tools/subtitle_fixture.py pins all three digests, holds the two commands as
argument vectors, and tools/test_subtitle_fixture.py runs inside
make verify-all.
Audio synchronization is by construction only: the audio and video streams share a common duration and timeline, without a demuxed audio-sync measurement. Revisit this assumption at the first container fixture or demuxer admission.
The reference MPEG-2 frame¶
src/reference.rs carries the one intra frame of cros-libva's
libva_utils_mpeg2vldemo test at the revision D80 pins, and the VA-API
parameters that decode it: third-party data, BSD-3-Clause through cros-libva
(© 2022 The ChromiumOS Authors), adapted there from libva-utils
decode/mpeg2vldemo.cpp (MIT, © 2007-2008 Intel Corporation). The file
carries both as file-level SPDX headers, not as a REUSE.toml table, because
verify_licensing() holds every table to the project's two identifiers;
LICENSES/BSD-3-Clause.txt is the canonical text, pinned by digest, and
reuse lint passes. The expected CRC-32, 0xa5713e52, is upstream's
(lib/src/lib.rs:265) and is not re-recorded here: a driver that yields
another fails the case and names the driver, and adopting a new value is a
recorded decision.
The recorded run¶
Run r20260929T151938-0d35 of make verify-display, on 2026-09-29, on the
reference profile; both children's output is kept under
~/.cache/aegis-display/r20260929T151938-0d35. The gate printed:
- rustc: rustc 1.98.1 (48a229cea 2026-09-01)
- libva: 2.24.1 (VA-API 1.24.0), read back with pkg-config
- VA vendor string: Intel iHD driver for Intel(R) Gen Graphics - 26.2.4 ()
- compositor: /usr/bin/kwin_wayland_wrapper (package kwin 6.7.5-1.1); client half only (D79)
- compositor main device 226:129, decode node /dev/dri/renderD129 (226:129, kernel driver i915)
- modifier the driver chose: 0x0100000000000009 (I915_FORMAT_MOD_4_TILED)
Every case passed:
display/attach-before-ack-refused: refused before the first ack_configure: a buffer may not be attached before the first ack_configure.display/other-render-node-refused: refused (nvidia): render node renderD128 is 226:128, not the compositor's main device 226:129; refused (amdgpu): render node renderD130 is 226:130, not the compositor's main device 226:129.display/driver-is-ihd: the vendor string above.display/mpeg2-reference-crc: CRC-320xa5713e52over the visible NV12 lines.display/planted-zero-surface-refused: refused before export: the white guard block's mean luma is 0, below 128.display/fixture-frames-present: 60 frames decoded and read back against their index rows, frame 1 and frame 60 included; 60 created, 0 failed, 60 committed, 60 presented, 0 discarded, 59 released; slowest decode reached VASurfaceReady after 3 polls.display/received-fd-is-the-exported-one: 60 of 60 received fds have the exported (st_dev, st_ino).display/descriptor-carries-the-exported-modifier: 60 of 60 descriptors carry export_prime's modifier unchanged.display/unadvertised-pair-refused: refused client-side after frame 30: the compositor did not advertise fourcc NV12 with modifier 0x010000000000000a; frame 31 then presented.display/frame-crc-regression-pin: frame 1 CRC-32 0x895e99b6, frame 60 0x8fa24d23, under the recorded driver.display/presented-deadline: failed at its 250 ms deadline: wp_presentation_feedback presented or discarded did not arrive before its deadline.display/driver-not-ihd-refused, underLIBVA_DRIVER_NAME=nvidia: the vendor string was "VA-API NVDEC driver [direct backend]", refused before any decode.
Three later runs passed every case of both children again against the same
compositor process, with the same modifier and all 60 regression pins equal:
r20260929T163147-73a8 at 18:31 +02:00 on the same revision and
r20260929T163503-1020 at 18:35 +02:00, the slowest decode reaching
VASurfaceReady after 2 polls in each, and r20260929T170629-d8c5 at 19:06
+02:00 on the final revision, after 3. Between the first two the gate came to
take an absolute WAYLAND_DISPLAY by Path.is_absolute() and to split
LIBVA_DRIVERS_PATH on os.pathsep, as libva splits it (va/va.c,
ENV_VAR_SEPARATOR), so that its tests hold on the Windows leg of the
portability matrix (HISS-21); both read the same on Linux. Before the last,
the planted zero-filled surface came to be polled ready under the same
deadline before create_image and again before sync, as every decode is;
the earlier runs skipped that poll for that one surface, so the poll before
every sync and create_image rests on r20260929T170629-d8c5. The gate came
at the same time to accept the regression pin's NOTE under another driver,
to fail a case the binary skips with the binary's reason and to skip a host
with one render node; on the reference profile the case lines read as before.
When logind reports the session locked, make verify-display prints
SKIP: logind reports session <id> locked; its compositor presents no client
surface while the lock screen is up; the display slice gate did not run. and
exits 0. The binary run directly against such a compositor passes the first
five cases and fails the first frame's presented wait at its 1 s deadline,
naming the event: a locked session's compositor presents no client surface.
The recorded subtitle run¶
Run r20261017T194814-f961 of make verify-display, on 2026-10-17, on the
reference profile, is the evidence for M32; all three children's output is kept
under ~/.cache/aegis-display/r20261017T194814-f961.
By then the host session's KWin had the NVIDIA card as its main device (renderD128), and the gate skipped on it, as run r20261017T194646-392c recorded: "the compositor's main device renderD128 is driven by nvidia, not i915 or xe". The run therefore went to a second KWin, 6.8.0-1.1, started for it under the user session on the virtual backend and bound to the Intel card, and the gate was pointed at that compositor's socket. The bind was set in the compositor's environment only; no host setting changed:
KWIN_RENDER_NODES=/dev/dri/renderD130 kwin_wayland --virtual --no-lockscreen \
--socket aegis-m32 --width 1280 --height 720 &
WAYLAND_DISPLAY=aegis-m32 make verify-display
The gate printed:
- rustc: rustc 1.99.1 (58cf8f9c3 2026-10-13)
- libva: 2.24.1 (VA-API 1.24.0), read back with pkg-config
- VA vendor string: Intel iHD driver for Intel(R) Gen Graphics - 26.3.5 ()
- compositor: executable unreadable (Permission denied)
- compositor main device 226:130, decode node /dev/dri/renderD130 (226:130, kernel driver xe)
- modifier the driver chose: 0x0100000000000009 (I915_FORMAT_MOD_4_TILED)
The run and driver-probe children passed every M27 case again, with all 60
regression pins equal under the newer driver. The subtitles child presented
the 90 frames of the subtitle fixture clip, paced to 30 frames per second with
a largest per-frame slip of 1 ms, and passed:
display/other-render-node-refusedanddisplay/driver-is-ihd, as for M27.subtitle/cue-1-within-100ms: timecode 500 ms, presented offset 483 ms (difference -16 ms).subtitle/cue-2-within-100ms: timecode 1200 ms, presented offset 1183 ms (difference -16 ms).subtitle/cue-3-within-100ms: timecode 2000 ms, presented offset 1983 ms (difference -16 ms).subtitle/planted-150ms-late-refused: cue 2's offset moved 150 ms later is refused as late.
Each offset is the wp_presentation timestamp of the frame the cue's start
timecode falls on, measured from the timestamp of frame 0. On the virtual
backend that timestamp comes from KWin's own output clock, not from a scanout;
the measurement is the client's, as REQ-P17-08 asks. This run predates the
label reading the compositor's command line, so its label still named the host
session's compositor although the second KWin served it; the gate now reports
a compositor started with --virtual, --wayland-display or --x11-display
as a nested instance (compositor_origin in tools/verify_display.py). The
100 ms boundary itself, 100 ms passing and 101 ms failing, is held by
crates/aegis-scaena/tests/subtitle_timing.rs, not by the run.
What is not claimed¶
- HISS-03 on the frame path. P17's own frame loop, the receive, the
checks, the admission, the import, the commit and the waits in
slice.rs,transport.rs,attach.rsandpresent.rs, allocates nothing in Aegis code per frame. cros-libva heap-allocates for every decoded picture (Picture::newboxes its state, and each JPEG and MPEG-2 parameter buffer is aBox), the deviation D83 limits to that binding. wayland-client also allocates for every protocol object it creates, and each frame creates azwp_linux_buffer_params_v1, thewl_bufferitscreatedevent announces and awp_presentation_feedback, which the criterion's per-framecreatedandpresentedevents require. That allocation is third-party and outside D83's scope as recorded; it is disclosed with M27's evidence, not waived. No count is produced, because a counting allocator needs aGlobalAllocimplementation the crate's forbid lint does not admit. - A stuck decode on the hardware. The deadline on the ready poll is
exercised by
tests/decode_rules.rson a status source that never leavesVASurfaceRendering; the Arc A380 finished every decode within its first polls, and no run provoked a surface that stays busy. - The compositor's
invalid_format. The unadvertised pair is refused client-side, which is what the criterion asks; whether KWin would raiseinvalid_formatfor it is not tested, because a client test does not rest on one compositor's conformance. - Mapping. Aegis code never maps a file: the received DMA-BUF is handed
to the compositor, the compositor's format table is read with
pread, and the crate's hygiene test finds no mapping call and rustix'smmfeature is not admitted. The decoder's own images, read back withcreate_imagebefore export, are mapped inside libva. - CI. The Verification gate's runner has no GPU and no Wayland session,
so
make verify-displayis not part ofmake verify-all; its hardware-free half is.
Host safety¶
The run attaches one 256 x 64 layer surface without keyboard focus to the host
session for about sixty frames, a second or two, and destroys it with every
buffer on every path the binary can catch; the gate kills the binary at its
120 s deadline. Nothing is installed, no driver is bound, no device is
changed, and LIBVA_DRIVER_NAME exists only in the two children's
environments. Each run prints a run id and keeps both children's output in
AEGIS_DISPLAY_DIR, default ${XDG_CACHE_HOME:-$HOME/.cache}/aegis-display,
under that id; nothing is written into the repository but cargo's target
directory.