Scarlet exposes video decode hardware as a character device such as
/dev/video0. The API described here is the Scarlet video decode device
contract used by userspace. It is not tied to a particular transport: VirtIO
video and Apple AVD backends both plug into the same frontend.
The current API is intentionally small:
write()/read()provide a compatibility path for stateful backends that accept one coded access unit at a time.mmap()pluscontrol()commands provide the preferred zero-copy-ish path used byvideo_player.- Session-aware commands allow several independent task groups to own separate streams on the same device.
- Stateless H.264 and VP9 submit use a Scarlet request split: userspace parses
codec headers and manages codec reference state, while the kernel validates
copied parameters and lowers them to the selected backend.
user/lib/scarlet-codecscontains the userspace request builder used byvideo_player; future stateless codecs should follow the same split.
This interface is not a stable userspace ABI yet. The structures below document the current state so that callers and future driver changes have a shared reference.
Video decode devices are exposed as videoN, starting at video0. Userspace
talks to the device through normal file operations, mmap(), and control().
The PCI VirtIO video backend negotiates VIRTIO_F_VERSION_1 and the VirtIO
video RESOURCE_GUEST_PAGES feature. The host backend added with this work is
tools/vhost-video-videotoolbox, which decodes through Apple's VideoToolbox
when the QEMU vhost-user-video path is enabled. Apple AVD uses the same Scarlet
frontend with an Apple platform-device backend.
The kernel side accepts these coded stream formats:
| Name | Value |
|---|---|
| H.264 | 4098 |
| HEVC/H.265 | 4099 |
| VP9 | 4102 |
| AV1 | 4103 |
The convenience write() path is backend-specific compatibility behavior. The
mapped control path carries the coded format in each stateful submit request.
Apple AVD advertises stateless H.264 and VP9 rather than stateful codec
parsing, so callers must use SCARLET_VIDEO_SUBMIT_H264_STATELESS or
SCARLET_VIDEO_SUBMIT_VP9_STATELESS there.
Decoded frames are returned as an SVF1 frame:
| Offset | Size | Field |
|---|---|---|
| 0 | 4 | ASCII magic SVF1 |
| 4 | 4 | width, little-endian u32 |
| 8 | 4 | height, little-endian u32 |
| 12 | 4 | pixel format, little-endian u32 |
| 16 | 4 | payload length, little-endian u32 |
| 20 | N | frame payload |
The current player expects NV12 video-range payloads. That pixel format is
reported as 0x3432_3076 (v024).
The stream path is useful as a simple smoke test on stateful backends:
- Open
/dev/video0. - Write one H.264 Annex B access unit.
- Read 20 bytes of
SVF1header. - Read
payload_lenbytes of NV12 payload.
Only one pending decode is allowed per stream. If the backend has not produced a frame yet, reads may return status text or zero bytes rather than a complete frame. New users should prefer the mapped control path. Stateless backends such as Apple AVD do not parse the Annex B stream in the kernel.
Each video session owns one shared mapping:
| Region | Offset | Size |
|---|---|---|
| input bitstream | input_offset |
input_len |
| output frames | output_offset |
output_len |
After a successful dequeue, ScarletVideoDequeuedFrame.payload_offset and
payload_len identify the decoded payload inside the full mapping. Backends may
use output_len as a pool of frame slots, so callers must not assume the payload
always starts at output_offset + 20. Both mmap offset and length must be
page-aligned. The reported lengths come from the selected backend capabilities.
All commands use raw #[repr(C)] structures copied between Scarlet userspace
and the kernel. All current Scarlet targets are little-endian; do not treat
these layouts as a portable cross-OS ABI.
| Command | Value | Argument | Return |
|---|---|---|---|
SCARLET_VIDEO_GET_BUFFER |
0x5600 |
*mut ScarletVideoBufferInfo |
0 |
SCARLET_VIDEO_SUBMIT |
0x5601 |
*const ScarletVideoSubmit |
0 |
SCARLET_VIDEO_DEQUEUE |
0x5602 |
*mut ScarletVideoDequeuedFrame |
1 if ready, 0 if empty |
SCARLET_VIDEO_CREATE_SESSION |
0x5603 |
*mut ScarletVideoSessionInfo |
0 |
SCARLET_VIDEO_SUBMIT_SESSION |
0x5604 |
*const ScarletVideoSessionSubmit |
0 |
SCARLET_VIDEO_DEQUEUE_SESSION |
0x5605 |
*mut ScarletVideoSessionDequeuedFrame |
1 if ready, 0 if empty |
SCARLET_VIDEO_DESTROY_SESSION |
0x5606 |
*const ScarletVideoSessionInfo |
0 |
SCARLET_VIDEO_GET_CAPS |
0x5607 |
*mut ScarletVideoCapabilities |
0 |
SCARLET_VIDEO_SUBMIT_H264_STATELESS |
0x5608 |
*const ScarletVideoH264StatelessSubmit |
0 |
SCARLET_VIDEO_SUBMIT_VP9_STATELESS |
0x5609 |
*const ScarletVideoVp9StatelessSubmit |
0 |
SCARLET_VIDEO_GET_BUFFER uses the default stream, stream id 1. New code
should use SCARLET_VIDEO_CREATE_SESSION first; passing stream_id = 0
allocates an available session, while passing a nonzero stream_id claims or
queries that session.
For SCARLET_VIDEO_CREATE_SESSION, ScarletVideoSessionInfo.padding is treated
as an optional input coded format. 0 preserves the historical default of
H.264. Callers creating a VP9 stateless session pass SCARLET_VIDEO_FORMAT_VP9
there; the kernel clears the field before copying the structure back.
#[repr(C)]
struct ScarletVideoBufferInfo {
mmap_offset: u64,
mmap_len: u64,
input_offset: u64,
input_len: u32,
output_offset: u64,
output_len: u32,
}
#[repr(C)]
struct ScarletVideoSubmit {
input_len: u32,
coded_format: u32,
timestamp: u64,
}
#[repr(C)]
struct ScarletVideoDequeuedFrame {
width: u32,
height: u32,
pixel_format: u32,
payload_offset: u64,
payload_len: u32,
flags: u32,
timestamp: u64,
}
#[repr(C)]
struct ScarletVideoSessionInfo {
stream_id: u32,
padding: u32,
buffer: ScarletVideoBufferInfo,
}
#[repr(C)]
struct ScarletVideoSessionSubmit {
stream_id: u32,
input_len: u32,
coded_format: u32,
padding: u32,
timestamp: u64,
}
#[repr(C)]
struct ScarletVideoSessionDequeuedFrame {
stream_id: u32,
padding: u32,
frame: ScarletVideoDequeuedFrame,
}If timestamp is zero on submit, the driver assigns a monotonically increasing
per-session timestamp. flags is currently always zero.
SCARLET_VIDEO_SUBMIT_VP9_STATELESS takes mapped input bytes plus userspace
pointers to:
ScarletVideoVp9FrameParams: uncompressed-header derived frame syntax, render size, tile log2 dimensions, refresh flags, and last/golden/alternate reference timestamps.ScarletVideoVp9Probabilities: the 0x774-byte packed probability state after userspace parses the VP9 compressed header.ScarletVideoVp9Tiles: byte ranges for every tile payload inside the mapped input frame.
The kernel does not parse VP9 bitstream headers or maintain the VP9 frame
context as codec state. Userspace demuxes frames, parses the uncompressed and
compressed headers, updates probability state, constructs the tile table, and
tracks reference timestamps. The backend validates the AVD-supported subset and
lowers the request to hardware commands. Apple AVD's VP9-specific direct submit
sequence is tracked in apple-avd-vp9.md.
For Apple AVD VP9 bring-up, video_player can dump the userspace stateless
request before ioctl submission:
video_player --hwdc --dump-vp9-stateless root/vp9-dump root/example.webmThe dump contains Scarlet's generic VP9 ABI structures, not Apple/macOS
frame_params. Use the trace workflow in
apple-avd-vp9-re.md to compare those structures against
m1n1 captures and eiln/avd.
- Open
/dev/video0. - Call
SCARLET_VIDEO_CREATE_SESSIONwithstream_id = 0. - Map
buffer.mmap_lenbytes atbuffer.mmap_offsetwith read/write shared permissions. - Copy one coded access unit into
input_offset. - Call
SCARLET_VIDEO_SUBMIT_SESSIONwith the returned stream id, input length, coded format, and optional timestamp. - Poll
SCARLET_VIDEO_DEQUEUE_SESSIONuntil it returns1. - Read metadata from
ScarletVideoDequeuedFrame. - Use
payload_offsetandpayload_lento read the decoded NV12 payload from the mapping. - On teardown, unmap the buffer and call
SCARLET_VIDEO_DESTROY_SESSION.
The default-session commands follow the same model but omit the explicit
stream_id fields.
Sessions are owned by the current task's thread-group id. A task may only dequeue, mmap, or destroy sessions it owns. Closing the device releases sessions owned by that task group, and the driver also cleans up sessions whose owner task has exited.
The driver allows only one pending decode per session. A second submit before the
previous frame is completed returns an error. select/poll readiness is still
coarse in this prototype, so callers should treat a dequeue return value of 0
as "not ready yet" and retry with their own timeout.
- The ABI is duplicated in the kernel driver and
video_player; it should move to a shared userspace-visible header or crate before being treated as stable. - The mapped path has fixed buffer sizes and a fixed session limit.
- The output path currently assumes a single-buffer NV12 frame.
- Error reporting is mostly string-based through kernel
Result<&'static str>and status reads. - Backend support depends on the registered kernel backend. VirtIO depends on the host vhost-user-video process and VideoToolbox capabilities; Apple AVD depends on the platform DTB exposing AVD and DART resources.