diff --git a/daemon/CMakeLists.txt b/daemon/CMakeLists.txt index 8c5b237..2051019 100644 --- a/daemon/CMakeLists.txt +++ b/daemon/CMakeLists.txt @@ -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() diff --git a/daemon/src/av1_obu_synth.c b/daemon/src/av1_obu_synth.c new file mode 100644 index 0000000..7152ed8 --- /dev/null +++ b/daemon/src/av1_obu_synth.c @@ -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 + +#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; +} diff --git a/daemon/src/av1_obu_synth.h b/daemon/src/av1_obu_synth.h new file mode 100644 index 0000000..88cc14a --- /dev/null +++ b/daemon/src/av1_obu_synth.h @@ -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 +#include + +#include + +/* + * 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 */ diff --git a/daemon/src/test_av1_obu_synth.c b/daemon/src/test_av1_obu_synth.c new file mode 100644 index 0000000..be46e16 --- /dev/null +++ b/daemon/src/test_av1_obu_synth.c @@ -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 +#include +#include +#include + +#include + +/* 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; +}