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
+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;
}