daemon: AV1 Sequence Header OBU synthesiser + unit test

V4L2 stateless AV1 passes the sequence header information as a
structured control (V4L2_CID_STATELESS_AV1_SEQUENCE) and ships
only tile-group bytes in the OUTPUT buffer.  libavcodec's AV1
decoder is full-bitstream, so the daemon needs to reconstruct
the OBU bytes the consumer parsed out before feeding the
assembled stream to libavcodec.

This commit lands the Sequence Header OBU half of that
reconstruction — av1_synth_sequence_header_obu().  Frame
Header / Frame OBU synthesisers + the integration that wires
the assembled OBUs into the decode hot path are separate
follow-on modules.

Module shape mirrors the H.264 NAL synthesiser (PR #1):

  - Public API: single function returning byte count or 0
    on overflow/invalid input.
  - Wire encoder uses the existing bitstream_writer (bsw_put_u
    is AV1's f(n); bsw_put_ue is bit-identical to AV1's uvlc;
    bsw_align_rbsp matches AV1's trailing_bits()).
  - AV1-specific helpers (leb128 size, min_bits_for, subsampling
    resolution per §5.5.2) are file-local statics.
  - No emulation prevention — AV1 uses leb128-sized OBUs for
    bitstream boundaries, not byte-pattern escapes.

Synthesis decisions for fields V4L2 doesn't carry are documented
verbatim in the file header (reduced_still_picture_header = 0;
single operating point at seq_level_idx = 13 / level 5.1;
color_description_present_flag = 0; chroma_sample_position = 0;
seq_choose_screen_detection_tools = 1; seq_choose_integer_mv = 1).

Rejection cases:
  - seq_profile > 2
  - bit_depth not in {8, 10, 12}
  - seq_profile = 1 + monochrome (4:4:4 forced colour)
  - seq_profile = 1 + bit_depth = 12 (only profile 2 allows it)
  - max_frame_{width,height}_minus_1 requiring > 16 length bits
  - out_cap too small to hold header + leb128 + payload

Each returns 0 to surface the mismatch loudly rather than emit
nonsense the libavcodec parser would reject downstream.

Unit test (test_av1_obu_synth.c, opt-in via DAEDALUS_BUILD_TESTS=ON)
exercises four cases bit-by-bit against a hand-computed reference:

  1. profile 0, 1080p, 8-bit, 4:2:0, order_hint on (7 bits),
     CDEF+restoration on — the common Pi 5 path.
  2. profile 0, 720p, 10-bit, monochrome — exercises high_bitdepth
     and the monochrome short-form color_config.
  3. profile 1 + bit_depth 12 → expects 0 (rejected).
  4. tiny out_cap → expects 0 (overflow).

All four green on hertz (aarch64 Arch, gcc Wall+Wextra+Wpedantic
clean).

This commit does not change daemon behaviour — av1_obu_synth.c is
built into the daemon binary so the symbols are reachable, but
no call site is wired yet.  Integration goes in the follow-on
DAEMON-AV1 patches that also synthesise the Frame Header OBU
and bracket the assembled OBUs with a Temporal Delimiter.

Refs reauktion/daedalus-v4l2#11 daemon-half; closes daedalus
backlog task #144.
This commit is contained in:
2026-05-23 15:41:07 +02:00
parent 872eec505e
commit 1e9619afe8
4 changed files with 726 additions and 0 deletions
+23
View File
@@ -52,6 +52,7 @@ add_executable(daedalus_v4l2_daemon
src/dmabuf_capture.c
src/bitstream_writer.c
src/h264_nal_synth.c
src/av1_obu_synth.c
)
target_include_directories(daedalus_v4l2_daemon
@@ -80,3 +81,25 @@ target_link_libraries(daedalus_v4l2_daemon
install(TARGETS daedalus_v4l2_daemon
RUNTIME DESTINATION /usr/local/bin)
# --- Unit tests (opt-in) -------------------------------------------------
#
# DAEDALUS_BUILD_TESTS=ON enables standalone test executables that run on
# the build host (no V4L2 / FFmpeg / Vulkan dependency). Used by CI to
# gate bitstream synthesis modules against regressions.
option(DAEDALUS_BUILD_TESTS "Build daemon unit tests" OFF)
if (DAEDALUS_BUILD_TESTS)
add_executable(test_av1_obu_synth
src/test_av1_obu_synth.c
src/av1_obu_synth.c
src/bitstream_writer.c
)
target_include_directories(test_av1_obu_synth PRIVATE src)
# Test binary does not link FFmpeg / Vulkan / dl — it exercises
# pure-C encoders against in-memory inputs.
enable_testing()
add_test(NAME av1_obu_synth COMMAND test_av1_obu_synth)
endif()
+327
View File
@@ -0,0 +1,327 @@
/* SPDX-License-Identifier: BSD-2-Clause */
/*
* av1_obu_synth.c — encode AV1 OBU bytes from V4L2 stateless controls.
*
* Spec references throughout are to the AOM AV1 Bitstream and Decoding
* Process Specification rev 1.0.0 with errata. See §5.3.2 OBU header
* syntax, §5.5.1 sequence_header_obu syntax, §5.9.2 trailing_bits().
*
* Synthesis defaults — fields the V4L2 control surface doesn't carry
* are set to values that match the "common-case profile-0 4:2:0 8-bit"
* path the V4L2 stateless AV1 contract is overwhelmingly used for.
* Specifically:
*
* - reduced_still_picture_header = 0 (full sequence-header form)
* - timing_info_present_flag = 0
* - decoder_model_info_present_flag = 0
* - initial_display_delay_present_flag = 0
* - operating_points_cnt_minus_1 = 0 (single operating point)
* - operating_point_idc[0] = 0 (all temporal/spatial layers)
* - seq_level_idx[0] = 13 (level 5.1 — supports up to 4K, well past
* anything libva-v4l2-request is likely to drive; libavcodec is
* lenient on level mismatches that don't constrain the frame size)
* - seq_tier[0] = 0
* - frame_id_numbers_present_flag = 0
* - seq_choose_screen_detection_tools = 1 (SELECT) so
* seq_force_screen_content_tools = 2 (SELECT)
* - seq_choose_integer_mv = 1 (SELECT) so seq_force_integer_mv = 2
* - color_description_present_flag = 0 (V4L2 ctrl doesn't carry CICP)
* - chroma_sample_position = 0 (CSP_UNKNOWN)
*
* If a V4L2 sequence control arrives with bit_depth / seq_profile /
* subsampling combinations the AV1 spec doesn't allow (e.g. profile 1
* with bit_depth 12), we return 0 to surface the mismatch loudly rather
* than silently encoding nonsense the libavcodec parser would reject.
*/
#include "av1_obu_synth.h"
#include "bitstream_writer.h"
#include <string.h>
#define OBU_SEQUENCE_HEADER 1
/* Default operating-point level: 5.1 — supports any frame size up to
* 4K@60fps. Well past anything the V4L2 path is realistically driven
* with on Pi 5; libavcodec doesn't enforce level against actual frame
* dims at decode time, it just uses the field to size some bitstream
* limits (max tile cols, etc.) that aren't load-bearing for stream
* conformance. */
#define DEFAULT_SEQ_LEVEL_IDX 13
/*
* leb128 (§4.10.5) — unsigned variable-length encoding, 7 value bits per
* byte, MSB of each byte set when another byte follows. Writes to
* @out[w..] at byte alignment. Returns number of bytes written, or 0
* on overflow. AV1 caps leb128 at 8 bytes (Leb128Bytes constraint).
*/
static size_t leb128_put(uint32_t v, uint8_t *out, size_t cap)
{
size_t w = 0;
do {
uint8_t byte = (uint8_t) (v & 0x7fu);
v >>= 7;
if (v != 0)
byte |= 0x80u;
if (w >= cap)
return 0;
out[w++] = byte;
} while (v != 0);
return w;
}
/*
* Smallest n such that (1 << n) > x; i.e. ceil(log2(x + 1)).
* Used to compute frame_width_bits_minus_1 / frame_height_bits_minus_1
* from max_frame_width_minus_1 / max_frame_height_minus_1. Spec wants
* n_bits ≥ ceil(log2(max+1)), with n_bits encoded as (n_bits - 1) in
* f(4) — so the value must fit in [1, 16]. We clamp to 16 (which
* accommodates a 65536-pixel frame, comfortably absurd).
*/
static int min_bits_for(uint32_t x)
{
int n = 0;
while (x) {
n++;
x >>= 1;
}
return n == 0 ? 1 : n;
}
/*
* Resolve subsampling per §5.5.2. V4L2 carries SUBSAMPLING_X and
* SUBSAMPLING_Y as flags but the AV1 spec forces them based on
* seq_profile + bit_depth in some branches. Returns 1 on success and
* 0 on illegal combination (e.g. profile 1 + bit_depth 12, which the
* spec doesn't allow). Output via the two int pointers.
*
* Note: we intentionally don't honour the V4L2 flags in the forced
* branches. Producers that set them inconsistently with seq_profile
* are bug; we trust the profile.
*/
static int resolve_subsampling(uint8_t seq_profile, uint8_t bit_depth,
uint32_t flags, bool monochrome,
int *out_x, int *out_y)
{
if (monochrome) {
*out_x = 1;
*out_y = 1;
return 1;
}
switch (seq_profile) {
case 0: /* 4:2:0 8/10-bit */
if (bit_depth != 8 && bit_depth != 10)
return 0;
*out_x = 1;
*out_y = 1;
return 1;
case 1: /* 4:4:4 8/10-bit */
if (bit_depth != 8 && bit_depth != 10)
return 0;
*out_x = 0;
*out_y = 0;
return 1;
case 2: /* 4:2:2 or 4:2:0/4:4:4 12-bit */
if (bit_depth == 12) {
*out_x = (flags & V4L2_AV1_SEQUENCE_FLAG_SUBSAMPLING_X) ? 1 : 0;
if (*out_x)
*out_y = (flags & V4L2_AV1_SEQUENCE_FLAG_SUBSAMPLING_Y) ? 1 : 0;
else
*out_y = 0;
} else if (bit_depth == 8 || bit_depth == 10) {
*out_x = 1; /* forced 4:2:2 */
*out_y = 0;
} else {
return 0;
}
return 1;
default:
return 0;
}
}
size_t av1_synth_sequence_header_obu(const struct v4l2_ctrl_av1_sequence *seq,
uint8_t *out, size_t out_cap)
{
uint8_t rbsp[64];
struct bs_writer bs;
uint32_t flags;
uint8_t bit_depth;
uint8_t seq_profile;
bool still_picture, monochrome, enable_order_hint;
int high_bitdepth, twelve_bit;
int subsampling_x, subsampling_y;
int width_bits, height_bits;
size_t payload_len;
size_t w;
if (!seq || !out || out_cap < 8)
return 0;
flags = seq->flags;
bit_depth = seq->bit_depth;
seq_profile = seq->seq_profile;
still_picture = !!(flags & V4L2_AV1_SEQUENCE_FLAG_STILL_PICTURE);
monochrome = !!(flags & V4L2_AV1_SEQUENCE_FLAG_MONO_CHROME);
enable_order_hint = !!(flags & V4L2_AV1_SEQUENCE_FLAG_ENABLE_ORDER_HINT);
/* Sanity checks against the spec's allowed combinations. */
if (seq_profile > 2)
return 0;
if (bit_depth != 8 && bit_depth != 10 && bit_depth != 12)
return 0;
if (seq_profile == 1 && monochrome)
return 0; /* profile 1 must be 4:4:4 colour */
high_bitdepth = (bit_depth > 8) ? 1 : 0;
twelve_bit = (seq_profile == 2 && bit_depth == 12) ? 1 : 0;
if (!resolve_subsampling(seq_profile, bit_depth, flags, monochrome,
&subsampling_x, &subsampling_y))
return 0;
width_bits = min_bits_for((uint32_t) seq->max_frame_width_minus_1);
height_bits = min_bits_for((uint32_t) seq->max_frame_height_minus_1);
if (width_bits > 16 || height_bits > 16)
return 0; /* spec encodes (n - 1) in f(4): n in [1, 16] */
bsw_init(&bs, rbsp, sizeof(rbsp));
/* --- sequence_header_obu --- §5.5.1 --- */
bsw_put_u(&bs, seq_profile, 3);
bsw_put_u(&bs, still_picture ? 1u : 0u, 1);
bsw_put_u(&bs, 0u, 1); /* reduced_still_picture_header */
/* full-form path (reduced_still_picture_header == 0) */
bsw_put_u(&bs, 0u, 1); /* timing_info_present_flag */
bsw_put_u(&bs, 0u, 1); /* initial_display_delay_present_flag */
bsw_put_u(&bs, 0u, 5); /* operating_points_cnt_minus_1 */
bsw_put_u(&bs, 0u, 12); /* operating_point_idc[0] */
bsw_put_u(&bs, DEFAULT_SEQ_LEVEL_IDX, 5); /* seq_level_idx[0] */
if (DEFAULT_SEQ_LEVEL_IDX > 7)
bsw_put_u(&bs, 0u, 1); /* seq_tier[0] */
bsw_put_u(&bs, (uint32_t)(width_bits - 1), 4); /* frame_width_bits_minus_1 */
bsw_put_u(&bs, (uint32_t)(height_bits - 1), 4); /* frame_height_bits_minus_1 */
bsw_put_u(&bs, (uint32_t) seq->max_frame_width_minus_1, width_bits);
bsw_put_u(&bs, (uint32_t) seq->max_frame_height_minus_1, height_bits);
bsw_put_u(&bs, 0u, 1); /* frame_id_numbers_present_flag */
bsw_put_u(&bs, (flags & V4L2_AV1_SEQUENCE_FLAG_USE_128X128_SUPERBLOCK) ? 1u : 0u, 1);
bsw_put_u(&bs, (flags & V4L2_AV1_SEQUENCE_FLAG_ENABLE_FILTER_INTRA) ? 1u : 0u, 1);
bsw_put_u(&bs, (flags & V4L2_AV1_SEQUENCE_FLAG_ENABLE_INTRA_EDGE_FILTER) ? 1u : 0u, 1);
/* non-still-picture block — V4L2 controls fill these in */
bsw_put_u(&bs, (flags & V4L2_AV1_SEQUENCE_FLAG_ENABLE_INTERINTRA_COMPOUND) ? 1u : 0u, 1);
bsw_put_u(&bs, (flags & V4L2_AV1_SEQUENCE_FLAG_ENABLE_MASKED_COMPOUND) ? 1u : 0u, 1);
bsw_put_u(&bs, (flags & V4L2_AV1_SEQUENCE_FLAG_ENABLE_WARPED_MOTION) ? 1u : 0u, 1);
bsw_put_u(&bs, (flags & V4L2_AV1_SEQUENCE_FLAG_ENABLE_DUAL_FILTER) ? 1u : 0u, 1);
bsw_put_u(&bs, enable_order_hint ? 1u : 0u, 1);
if (enable_order_hint) {
bsw_put_u(&bs, (flags & V4L2_AV1_SEQUENCE_FLAG_ENABLE_JNT_COMP) ? 1u : 0u, 1);
bsw_put_u(&bs, (flags & V4L2_AV1_SEQUENCE_FLAG_ENABLE_REF_FRAME_MVS) ? 1u : 0u, 1);
}
bsw_put_u(&bs, 1u, 1); /* seq_choose_screen_detection_tools */
/* seq_force_screen_content_tools = SELECT (2), so no further bits */
/* seq_choose_integer_mv path:
* seq_force_screen_content_tools > 0, so we emit seq_choose_integer_mv = 1
* (SELECT) — which leaves seq_force_integer_mv = SELECT (2) without
* further bits. */
bsw_put_u(&bs, 1u, 1); /* seq_choose_integer_mv */
if (enable_order_hint) {
uint8_t ohb = seq->order_hint_bits;
if (ohb < 1) ohb = 1;
if (ohb > 8) ohb = 8;
bsw_put_u(&bs, (uint32_t)(ohb - 1), 3); /* order_hint_bits_minus_1 */
}
bsw_put_u(&bs, (flags & V4L2_AV1_SEQUENCE_FLAG_ENABLE_SUPERRES) ? 1u : 0u, 1);
bsw_put_u(&bs, (flags & V4L2_AV1_SEQUENCE_FLAG_ENABLE_CDEF) ? 1u : 0u, 1);
bsw_put_u(&bs, (flags & V4L2_AV1_SEQUENCE_FLAG_ENABLE_RESTORATION) ? 1u : 0u, 1);
/* --- color_config() --- §5.5.2 --- */
bsw_put_u(&bs, high_bitdepth ? 1u : 0u, 1);
if (seq_profile == 2 && high_bitdepth)
bsw_put_u(&bs, twelve_bit ? 1u : 0u, 1);
if (seq_profile != 1)
bsw_put_u(&bs, monochrome ? 1u : 0u, 1);
bsw_put_u(&bs, 0u, 1); /* color_description_present_flag */
if (monochrome) {
bsw_put_u(&bs,
(flags & V4L2_AV1_SEQUENCE_FLAG_COLOR_RANGE) ? 1u : 0u, 1);
/* monochrome path: subsampling/chroma_sample_position/separate_uv_delta_q
* are forced by the spec — no further bits emitted. */
} else {
bsw_put_u(&bs,
(flags & V4L2_AV1_SEQUENCE_FLAG_COLOR_RANGE) ? 1u : 0u, 1);
/* subsampling encoding depends on seq_profile */
if (seq_profile == 2 && bit_depth == 12) {
bsw_put_u(&bs, subsampling_x ? 1u : 0u, 1);
if (subsampling_x)
bsw_put_u(&bs, subsampling_y ? 1u : 0u, 1);
}
/* profile 0 / profile 1 / profile 2 non-12-bit: subsampling is
* forced by the spec, no bits emitted. */
if (subsampling_x && subsampling_y) {
/* chroma_sample_position f(2) — V4L2 ctrl doesn't carry
* this; default CSP_UNKNOWN (0). */
bsw_put_u(&bs, 0u, 2);
}
bsw_put_u(&bs,
(flags & V4L2_AV1_SEQUENCE_FLAG_SEPARATE_UV_DELTA_Q) ? 1u : 0u, 1);
}
/* film_grain_params_present + trailing_bits */
bsw_put_u(&bs,
(flags & V4L2_AV1_SEQUENCE_FLAG_FILM_GRAIN_PARAMS_PRESENT) ? 1u : 0u, 1);
bsw_align_rbsp(&bs);
if (bsw_overflowed(&bs))
return 0;
payload_len = bsw_bytes(&bs);
/* --- assemble OBU: header byte | leb128(payload_len) | payload --- */
/* OBU header byte (§5.3.2):
* obu_forbidden_bit = 0 [bit 7]
* obu_type = 1 [bits 6..3] (OBU_SEQUENCE_HEADER)
* obu_extension_flag = 0 [bit 2]
* obu_has_size_field = 1 [bit 1]
* obu_reserved_1bit = 0 [bit 0]
* → 0_0001_0_1_0 = 0x0A
*/
w = 0;
if (w >= out_cap)
return 0;
out[w++] = (uint8_t)
((0u << 7) |
((OBU_SEQUENCE_HEADER & 0xfu) << 3) |
(0u << 2) |
(1u << 1) |
(0u << 0));
{
size_t leb_n = leb128_put((uint32_t) payload_len,
out + w, out_cap - w);
if (leb_n == 0)
return 0;
w += leb_n;
}
if (out_cap - w < payload_len)
return 0;
memcpy(out + w, rbsp, payload_len);
w += payload_len;
return w;
}
+50
View File
@@ -0,0 +1,50 @@
/* SPDX-License-Identifier: BSD-2-Clause */
/*
* av1_obu_synth.h — synthesise AV1 OBU bytes from the V4L2 stateless
* AV1 controls.
*
* V4L2 stateless AV1 (per drivers/media/v4l2-core/v4l2-h264.c-style
* contract) passes the OUTPUT buffer as bare tile-group bitstream and
* the sequence / frame-header information as structured controls
* (V4L2_CID_STATELESS_AV1_SEQUENCE, V4L2_CID_STATELESS_AV1_FRAME, ...).
* libavcodec's AV1 decoder is full-bitstream, so the daemon has to
* reconstruct the OBUs that the consumer parsed out and prepend them
* to the tile-group bytes before handing the assembled stream to
* libavcodec.
*
* This header covers the Sequence Header OBU (AV1 spec §5.5.1).
* The Frame Header / Frame OBU synthesisers are separate modules
* (follow-on tasks); they all share the same wire conventions:
* - No emulation prevention (AV1 uses leb128 sized fields instead).
* - obu_has_size_field = 1 in the OBU header byte.
* - obu_extension_flag = 0 (no temporal_id / spatial_id encoding).
* - trailing_bits() finalises the payload to a byte boundary the same
* way H.264's rbsp_trailing_bits does — bsw_align_rbsp covers it.
*
* Synthesis decisions for fields V4L2 doesn't carry are documented in
* the .c file (search for "synthesis default").
*/
#ifndef DAEDALUS_AV1_OBU_SYNTH_H
#define DAEDALUS_AV1_OBU_SYNTH_H
#include <stddef.h>
#include <stdint.h>
#include <linux/v4l2-controls.h>
/*
* Encode an AV1 Sequence Header OBU (header byte + leb128 size + RBSP)
* into @out. Returns total bytes written, or 0 on overflow / malformed
* input (e.g. inconsistent bit_depth vs seq_profile). @out_cap must
* be at least 32 bytes for any reasonable sequence header; 64 bytes
* is a generous upper bound.
*
* The caller is expected to bracket the resulting bytes with a
* Temporal Delimiter OBU (1 byte: 0x12 0x00) before any Frame OBU so
* that libavcodec's AV1 parser sees a well-formed access unit; the
* temporal-delimiter byte is trivial and not produced here.
*/
size_t av1_synth_sequence_header_obu(const struct v4l2_ctrl_av1_sequence *seq,
uint8_t *out, size_t out_cap);
#endif /* DAEDALUS_AV1_OBU_SYNTH_H */
+326
View File
@@ -0,0 +1,326 @@
/* SPDX-License-Identifier: BSD-2-Clause */
/*
* test_av1_obu_synth — standalone unit test for av1_synth_sequence_header_obu.
*
* Builds as an opt-in executable target (test_av1_obu_synth) gated on
* -DDAEDALUS_BUILD_TESTS=ON. Runs by default in the CI build matrix
* to gate the OBU encoder against regressions.
*
* Each test case sets up a struct v4l2_ctrl_av1_sequence with known
* field values, calls the synthesiser, then walks the output bit by
* bit against a hand-computed expected encoding. The bit-walker uses
* the same reader semantics as bitstream_writer: MSB-first within each
* byte, with the OBU header byte / leb128 size at byte-aligned
* positions and the RBSP payload starting at the byte right after.
*/
#include "av1_obu_synth.h"
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <stdint.h>
#include <linux/v4l2-controls.h>
/* MSB-first bit reader over a byte stream. */
struct br {
const uint8_t *buf;
size_t bytes;
size_t pos_bytes;
int pos_bit;
int overflow;
};
static void br_init(struct br *b, const uint8_t *buf, size_t bytes)
{
b->buf = buf;
b->bytes = bytes;
b->pos_bytes = 0;
b->pos_bit = 0;
b->overflow = 0;
}
static uint32_t br_get(struct br *b, int n)
{
uint32_t v = 0;
int i;
for (i = 0; i < n; i++) {
uint8_t bit;
if (b->pos_bytes >= b->bytes) {
b->overflow = 1;
return 0;
}
bit = (b->buf[b->pos_bytes] >> (7 - b->pos_bit)) & 1u;
v = (v << 1) | bit;
b->pos_bit++;
if (b->pos_bit == 8) {
b->pos_bit = 0;
b->pos_bytes++;
}
}
return v;
}
/* Round up to next byte; returns bytes consumed for boundary. */
static void br_byte_align(struct br *b)
{
if (b->pos_bit != 0) {
b->pos_bit = 0;
b->pos_bytes++;
}
}
#define CHECK(cond, ...) do { \
if (!(cond)) { \
fprintf(stderr, "FAIL %s:%d: ", \
__func__, __LINE__); \
fprintf(stderr, __VA_ARGS__); \
fputc('\n', stderr); \
return 1; \
} \
} while (0)
#define CHECK_EQ(actual, expected, name) do { \
uint32_t _a = (uint32_t)(actual); \
uint32_t _e = (uint32_t)(expected); \
if (_a != _e) { \
fprintf(stderr, "FAIL %s:%d %s: " \
"got %u, expected %u\n", \
__func__, __LINE__, (name), \
_a, _e); \
return 1; \
} \
} while (0)
/*
* Case 1: 1080p, profile 0 (4:2:0), 8-bit, color_range studio,
* order_hint enabled with 7 bits, CDEF + restoration on, no film grain.
* Covers the most common decode path libva-v4l2-request drives on
* the daedalus daemon.
*/
static int test_profile0_1080p(void)
{
struct v4l2_ctrl_av1_sequence seq;
uint8_t out[64];
size_t n;
struct br br;
uint32_t bit;
memset(&seq, 0, sizeof(seq));
seq.seq_profile = 0;
seq.order_hint_bits = 7;
seq.bit_depth = 8;
seq.max_frame_width_minus_1 = 1919; /* 1920 */
seq.max_frame_height_minus_1 = 1079; /* 1080 */
seq.flags =
V4L2_AV1_SEQUENCE_FLAG_USE_128X128_SUPERBLOCK |
V4L2_AV1_SEQUENCE_FLAG_ENABLE_ORDER_HINT |
V4L2_AV1_SEQUENCE_FLAG_ENABLE_CDEF |
V4L2_AV1_SEQUENCE_FLAG_ENABLE_RESTORATION;
/* COLOR_RANGE flag unset = studio swing (limited range, =0 in spec) */
n = av1_synth_sequence_header_obu(&seq, out, sizeof(out));
CHECK(n > 0 && n <= sizeof(out), "synthesiser returned %zu bytes", n);
/* OBU header byte: 0x0A (obu_type=1, has_size_field=1). */
CHECK_EQ(out[0], 0x0A, "OBU header byte");
/* leb128 size — payload fits in 1 byte for sub-128-byte payloads. */
CHECK(n >= 2, "OBU has size field byte");
CHECK((out[1] & 0x80) == 0, "leb128 single-byte form (no continuation)");
{
size_t payload_len = out[1] & 0x7fu;
CHECK_EQ(n, 2 + payload_len, "total length matches header+leb+payload");
}
/* Walk payload bits. */
br_init(&br, out + 2, n - 2);
bit = br_get(&br, 3); CHECK_EQ(bit, 0, "seq_profile");
bit = br_get(&br, 1); CHECK_EQ(bit, 0, "still_picture");
bit = br_get(&br, 1); CHECK_EQ(bit, 0, "reduced_still_picture_header");
bit = br_get(&br, 1); CHECK_EQ(bit, 0, "timing_info_present_flag");
bit = br_get(&br, 1); CHECK_EQ(bit, 0, "initial_display_delay_present_flag");
bit = br_get(&br, 5); CHECK_EQ(bit, 0, "operating_points_cnt_minus_1");
bit = br_get(&br, 12); CHECK_EQ(bit, 0, "operating_point_idc[0]");
bit = br_get(&br, 5); CHECK_EQ(bit, 13, "seq_level_idx[0]");
bit = br_get(&br, 1); CHECK_EQ(bit, 0, "seq_tier[0]");
/* min_bits_for(1919) = 11; encoded value = 11 - 1 = 10 */
bit = br_get(&br, 4); CHECK_EQ(bit, 10, "frame_width_bits_minus_1");
/* min_bits_for(1079) = 11; same value */
bit = br_get(&br, 4); CHECK_EQ(bit, 10, "frame_height_bits_minus_1");
bit = br_get(&br, 11); CHECK_EQ(bit, 1919, "max_frame_width_minus_1");
bit = br_get(&br, 11); CHECK_EQ(bit, 1079, "max_frame_height_minus_1");
bit = br_get(&br, 1); CHECK_EQ(bit, 0, "frame_id_numbers_present_flag");
bit = br_get(&br, 1); CHECK_EQ(bit, 1, "use_128x128_superblock");
bit = br_get(&br, 1); CHECK_EQ(bit, 0, "enable_filter_intra");
bit = br_get(&br, 1); CHECK_EQ(bit, 0, "enable_intra_edge_filter");
bit = br_get(&br, 1); CHECK_EQ(bit, 0, "enable_interintra_compound");
bit = br_get(&br, 1); CHECK_EQ(bit, 0, "enable_masked_compound");
bit = br_get(&br, 1); CHECK_EQ(bit, 0, "enable_warped_motion");
bit = br_get(&br, 1); CHECK_EQ(bit, 0, "enable_dual_filter");
bit = br_get(&br, 1); CHECK_EQ(bit, 1, "enable_order_hint");
bit = br_get(&br, 1); CHECK_EQ(bit, 0, "enable_jnt_comp");
bit = br_get(&br, 1); CHECK_EQ(bit, 0, "enable_ref_frame_mvs");
bit = br_get(&br, 1); CHECK_EQ(bit, 1, "seq_choose_screen_detection_tools");
bit = br_get(&br, 1); CHECK_EQ(bit, 1, "seq_choose_integer_mv");
/* order_hint_bits=7 → order_hint_bits_minus_1 = 6 */
bit = br_get(&br, 3); CHECK_EQ(bit, 6, "order_hint_bits_minus_1");
bit = br_get(&br, 1); CHECK_EQ(bit, 0, "enable_superres");
bit = br_get(&br, 1); CHECK_EQ(bit, 1, "enable_cdef");
bit = br_get(&br, 1); CHECK_EQ(bit, 1, "enable_restoration");
/* color_config: high_bitdepth=0 (8-bit), monochrome=0,
* color_description_present=0, color_range=0, subsampling forced (no bits),
* chroma_sample_position=0 (2 bits when subsampling_x && subsampling_y),
* separate_uv_delta_q=0. */
bit = br_get(&br, 1); CHECK_EQ(bit, 0, "high_bitdepth");
bit = br_get(&br, 1); CHECK_EQ(bit, 0, "monochrome");
bit = br_get(&br, 1); CHECK_EQ(bit, 0, "color_description_present_flag");
bit = br_get(&br, 1); CHECK_EQ(bit, 0, "color_range");
bit = br_get(&br, 2); CHECK_EQ(bit, 0, "chroma_sample_position");
bit = br_get(&br, 1); CHECK_EQ(bit, 0, "separate_uv_delta_q");
bit = br_get(&br, 1); CHECK_EQ(bit, 0, "film_grain_params_present");
/* trailing_bits — single '1' then zero-fill */
bit = br_get(&br, 1); CHECK_EQ(bit, 1, "trailing_bits stop_one");
br_byte_align(&br);
CHECK(!br.overflow, "no bit-reader overflow");
CHECK_EQ(br.pos_bytes, n - 2, "consumed exactly the payload");
printf(" profile0 1080p 8-bit: OK (%zu bytes)\n", n);
return 0;
}
/* Case 2: profile 0, 10-bit, 4:2:0, monochrome.
* Exercises the high_bitdepth + monochrome short-form color_config path. */
static int test_profile0_monochrome_10bit(void)
{
struct v4l2_ctrl_av1_sequence seq;
uint8_t out[64];
size_t n;
struct br br;
uint32_t bit;
memset(&seq, 0, sizeof(seq));
seq.seq_profile = 0;
seq.order_hint_bits = 0;
seq.bit_depth = 10;
seq.max_frame_width_minus_1 = 1279;
seq.max_frame_height_minus_1 = 719;
seq.flags = V4L2_AV1_SEQUENCE_FLAG_MONO_CHROME;
n = av1_synth_sequence_header_obu(&seq, out, sizeof(out));
CHECK(n > 0, "synthesiser returned %zu bytes", n);
CHECK_EQ(out[0], 0x0A, "OBU header byte");
br_init(&br, out + 2, n - 2);
bit = br_get(&br, 3); CHECK_EQ(bit, 0, "seq_profile");
bit = br_get(&br, 1); CHECK_EQ(bit, 0, "still_picture");
bit = br_get(&br, 1); CHECK_EQ(bit, 0, "reduced_still_picture_header");
bit = br_get(&br, 1); CHECK_EQ(bit, 0, "timing_info_present_flag");
bit = br_get(&br, 1); CHECK_EQ(bit, 0, "initial_display_delay_present_flag");
bit = br_get(&br, 5); CHECK_EQ(bit, 0, "operating_points_cnt_minus_1");
bit = br_get(&br, 12); CHECK_EQ(bit, 0, "operating_point_idc[0]");
bit = br_get(&br, 5); CHECK_EQ(bit, 13, "seq_level_idx[0]");
bit = br_get(&br, 1); CHECK_EQ(bit, 0, "seq_tier[0]");
/* 1279 fits in 11 bits → width_bits_minus_1 = 10 */
bit = br_get(&br, 4); CHECK_EQ(bit, 10, "frame_width_bits_minus_1");
/* 719 fits in 10 bits → height_bits_minus_1 = 9 */
bit = br_get(&br, 4); CHECK_EQ(bit, 9, "frame_height_bits_minus_1");
bit = br_get(&br, 11); CHECK_EQ(bit, 1279, "max_frame_width_minus_1");
bit = br_get(&br, 10); CHECK_EQ(bit, 719, "max_frame_height_minus_1");
bit = br_get(&br, 1); CHECK_EQ(bit, 0, "frame_id_numbers_present_flag");
bit = br_get(&br, 1); CHECK_EQ(bit, 0, "use_128x128_superblock");
bit = br_get(&br, 1); CHECK_EQ(bit, 0, "enable_filter_intra");
bit = br_get(&br, 1); CHECK_EQ(bit, 0, "enable_intra_edge_filter");
bit = br_get(&br, 1); CHECK_EQ(bit, 0, "enable_interintra_compound");
bit = br_get(&br, 1); CHECK_EQ(bit, 0, "enable_masked_compound");
bit = br_get(&br, 1); CHECK_EQ(bit, 0, "enable_warped_motion");
bit = br_get(&br, 1); CHECK_EQ(bit, 0, "enable_dual_filter");
bit = br_get(&br, 1); CHECK_EQ(bit, 0, "enable_order_hint");
/* enable_order_hint=0 → no jnt_comp / ref_frame_mvs / order_hint_bits */
bit = br_get(&br, 1); CHECK_EQ(bit, 1, "seq_choose_screen_detection_tools");
bit = br_get(&br, 1); CHECK_EQ(bit, 1, "seq_choose_integer_mv");
bit = br_get(&br, 1); CHECK_EQ(bit, 0, "enable_superres");
bit = br_get(&br, 1); CHECK_EQ(bit, 0, "enable_cdef");
bit = br_get(&br, 1); CHECK_EQ(bit, 0, "enable_restoration");
/* color_config: high_bitdepth=1 (10-bit), seq_profile==0 so no twelve_bit,
* monochrome=1, color_description_present=0, color_range=0.
* Monochrome short-form: no subsampling/chroma_sample_position/uv_delta_q bits. */
bit = br_get(&br, 1); CHECK_EQ(bit, 1, "high_bitdepth");
bit = br_get(&br, 1); CHECK_EQ(bit, 1, "monochrome");
bit = br_get(&br, 1); CHECK_EQ(bit, 0, "color_description_present_flag");
bit = br_get(&br, 1); CHECK_EQ(bit, 0, "color_range");
bit = br_get(&br, 1); CHECK_EQ(bit, 0, "film_grain_params_present");
bit = br_get(&br, 1); CHECK_EQ(bit, 1, "trailing_bits stop_one");
CHECK(!br.overflow, "no overflow");
printf(" profile0 monochrome 10-bit: OK (%zu bytes)\n", n);
return 0;
}
/* Case 3: reject illegal seq_profile + bit_depth combination. */
static int test_reject_invalid_profile_bitdepth(void)
{
struct v4l2_ctrl_av1_sequence seq;
uint8_t out[64];
size_t n;
memset(&seq, 0, sizeof(seq));
seq.seq_profile = 1; /* 4:4:4 only */
seq.bit_depth = 12; /* but profile 1 doesn't allow 12-bit */
seq.max_frame_width_minus_1 = 1919;
seq.max_frame_height_minus_1 = 1079;
n = av1_synth_sequence_header_obu(&seq, out, sizeof(out));
CHECK_EQ(n, 0, "expected 0 (rejected) for profile1+12bit");
printf(" reject profile1+12bit: OK\n");
return 0;
}
/* Case 4: small out_cap → overflow path. */
static int test_overflow(void)
{
struct v4l2_ctrl_av1_sequence seq;
uint8_t out[4]; /* deliberately too small */
size_t n;
memset(&seq, 0, sizeof(seq));
seq.seq_profile = 0;
seq.bit_depth = 8;
seq.max_frame_width_minus_1 = 1919;
seq.max_frame_height_minus_1 = 1079;
n = av1_synth_sequence_header_obu(&seq, out, sizeof(out));
CHECK_EQ(n, 0, "expected 0 (overflow) for tiny out buffer");
printf(" out_cap overflow: OK\n");
return 0;
}
int main(void)
{
int fail = 0;
printf("=== av1_synth_sequence_header_obu ===\n");
fail |= test_profile0_1080p();
fail |= test_profile0_monochrome_10bit();
fail |= test_reject_invalid_profile_bitdepth();
fail |= test_overflow();
if (fail) {
fprintf(stderr, "AV1 OBU synth tests FAILED\n");
return 1;
}
printf("AV1 OBU synth tests PASSED\n");
return 0;
}