Merge pull request 'daemon: AV1 Sequence Header OBU synthesiser + unit test' (#22) from noether/daemon-av1-obu-synth into main
Reviewed-on: #22
This commit was merged in pull request #22.
This commit is contained in:
@@ -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()
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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 */
|
||||
@@ -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