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:
@@ -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;
|
||||
}
|
||||
Reference in New Issue
Block a user