Compare commits
10 Commits
872eec505e
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
| 6b1d90816d | |||
| dbf01eddb8 | |||
| 5d1ff51178 | |||
| 9797a0daa6 | |||
| 3a8f5405d4 | |||
| 4cfe0b470f | |||
| b958ef8166 | |||
| 94be8c3d03 | |||
| 1e9619afe8 | |||
| a43296c1ed |
+36
-2
@@ -40,6 +40,11 @@ pkg_check_modules(FFMPEG REQUIRED IMPORTED_TARGET
|
|||||||
# libdaedalus_core.a must precede -lvulkan because the static archive
|
# libdaedalus_core.a must precede -lvulkan because the static archive
|
||||||
# references vulkan symbols and the linker resolves left-to-right.
|
# references vulkan symbols and the linker resolves left-to-right.
|
||||||
pkg_check_modules(DAEDALUS_FOURIER REQUIRED daedalus-fourier)
|
pkg_check_modules(DAEDALUS_FOURIER REQUIRED daedalus-fourier)
|
||||||
|
# daedalus-decoder — frame-major UMA H.264 decoder. Linked into the
|
||||||
|
# shadow-mode path (env DAEDALUS_SHADOW_MODE=1) and inert otherwise.
|
||||||
|
# Linked unconditionally to keep CMake configurations symmetrical
|
||||||
|
# between production and shadow-mode runs.
|
||||||
|
pkg_check_modules(DAEDALUS_DECODER REQUIRED daedalus-decoder)
|
||||||
find_package(Vulkan REQUIRED)
|
find_package(Vulkan REQUIRED)
|
||||||
|
|
||||||
add_executable(daedalus_v4l2_daemon
|
add_executable(daedalus_v4l2_daemon
|
||||||
@@ -48,10 +53,12 @@ add_executable(daedalus_v4l2_daemon
|
|||||||
src/log.c
|
src/log.c
|
||||||
src/parser.c
|
src/parser.c
|
||||||
src/decoder.c
|
src/decoder.c
|
||||||
|
src/shadow_decoder.c
|
||||||
src/chardev_client.c
|
src/chardev_client.c
|
||||||
src/dmabuf_capture.c
|
src/dmabuf_capture.c
|
||||||
src/bitstream_writer.c
|
src/bitstream_writer.c
|
||||||
src/h264_nal_synth.c
|
src/h264_nal_synth.c
|
||||||
|
src/av1_obu_synth.c
|
||||||
)
|
)
|
||||||
|
|
||||||
target_include_directories(daedalus_v4l2_daemon
|
target_include_directories(daedalus_v4l2_daemon
|
||||||
@@ -60,23 +67,50 @@ target_include_directories(daedalus_v4l2_daemon
|
|||||||
${CMAKE_CURRENT_SOURCE_DIR}/../include
|
${CMAKE_CURRENT_SOURCE_DIR}/../include
|
||||||
${FFMPEG_INCLUDE_DIRS}
|
${FFMPEG_INCLUDE_DIRS}
|
||||||
${DAEDALUS_FOURIER_INCLUDE_DIRS}
|
${DAEDALUS_FOURIER_INCLUDE_DIRS}
|
||||||
|
${DAEDALUS_DECODER_INCLUDE_DIRS}
|
||||||
)
|
)
|
||||||
|
|
||||||
# dl for dlopen, pthread for future threading work.
|
# dl for dlopen, pthread for future threading work.
|
||||||
target_link_directories(daedalus_v4l2_daemon
|
target_link_directories(daedalus_v4l2_daemon
|
||||||
PRIVATE
|
PRIVATE
|
||||||
${DAEDALUS_FOURIER_LIBRARY_DIRS}
|
${DAEDALUS_FOURIER_LIBRARY_DIRS}
|
||||||
|
${DAEDALUS_DECODER_LIBRARY_DIRS}
|
||||||
)
|
)
|
||||||
|
|
||||||
target_link_libraries(daedalus_v4l2_daemon
|
target_link_libraries(daedalus_v4l2_daemon
|
||||||
PRIVATE
|
PRIVATE
|
||||||
dl
|
dl
|
||||||
pthread
|
pthread
|
||||||
# Order matters: libdaedalus_core.a first (so its undefined
|
# Order matters for left-to-right linker resolution of
|
||||||
# vulkan symbols register), then -lvulkan to satisfy them.
|
# static archives. daedalus-decoder references symbols
|
||||||
|
# from daedalus-fourier; daedalus-fourier references
|
||||||
|
# vulkan symbols. So: decoder, fourier, vulkan.
|
||||||
|
${DAEDALUS_DECODER_LIBRARIES}
|
||||||
${DAEDALUS_FOURIER_LIBRARIES}
|
${DAEDALUS_FOURIER_LIBRARIES}
|
||||||
Vulkan::Vulkan
|
Vulkan::Vulkan
|
||||||
)
|
)
|
||||||
|
|
||||||
install(TARGETS daedalus_v4l2_daemon
|
install(TARGETS daedalus_v4l2_daemon
|
||||||
RUNTIME DESTINATION /usr/local/bin)
|
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,897 @@
|
|||||||
|
/* 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
|
||||||
|
#define OBU_TEMPORAL_DELIMITER 2
|
||||||
|
#define OBU_FRAME_HEADER 3
|
||||||
|
#define OBU_TILE_GROUP 4
|
||||||
|
#define OBU_FRAME 6
|
||||||
|
|
||||||
|
/* AV1 §3 ref-frame symbolic constants — values per the spec table.
|
||||||
|
* INTRA_FRAME is index 0 (used for intra-only); LAST_FRAME..ALTREF_FRAME
|
||||||
|
* are 1..7. TOTAL_REFS_PER_FRAME = 8 (V4L2 mirrors this). */
|
||||||
|
#define AV1_INTRA_FRAME 0
|
||||||
|
#define AV1_LAST_FRAME 1
|
||||||
|
#define AV1_NUM_REF_FRAMES 8 /* the DPB size */
|
||||||
|
#define AV1_REFS_PER_FRAME 7 /* refs available to an inter frame */
|
||||||
|
#define AV1_PRIMARY_REF_NONE 7
|
||||||
|
|
||||||
|
/* 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;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* -------------------------------------------------------------------
|
||||||
|
* Shared OBU wrap helper (header byte + leb128 size + payload). Used
|
||||||
|
* by frame_header_obu and the temporal_delimiter helper; the sequence
|
||||||
|
* header above predates this factor-out and keeps its inline
|
||||||
|
* assembly so its memory footprint stays predictable.
|
||||||
|
* ----------------------------------------------------------------- */
|
||||||
|
static size_t wrap_obu(uint8_t obu_type, const uint8_t *payload,
|
||||||
|
size_t payload_len, uint8_t *out, size_t out_cap)
|
||||||
|
{
|
||||||
|
size_t w = 0;
|
||||||
|
|
||||||
|
if (out_cap < 2)
|
||||||
|
return 0;
|
||||||
|
out[w++] = (uint8_t)(
|
||||||
|
(0u << 7) |
|
||||||
|
((obu_type & 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;
|
||||||
|
if (payload_len)
|
||||||
|
memcpy(out + w, payload, payload_len);
|
||||||
|
w += payload_len;
|
||||||
|
return w;
|
||||||
|
}
|
||||||
|
|
||||||
|
size_t av1_synth_temporal_delimiter_obu(uint8_t *out, size_t out_cap)
|
||||||
|
{
|
||||||
|
return wrap_obu(OBU_TEMPORAL_DELIMITER, NULL, 0, out, out_cap);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* -------------------------------------------------------------------
|
||||||
|
* Frame Header OBU — §5.9
|
||||||
|
*
|
||||||
|
* The encoder is sectioned to mirror the spec. Each subsection
|
||||||
|
* helper writes into the shared bs_writer and signals "out of
|
||||||
|
* scope" by setting a sticky `*unsupported` flag that the top-level
|
||||||
|
* checks before returning. This keeps the spec-mirror linear and
|
||||||
|
* the failure modes diagnosable.
|
||||||
|
* ----------------------------------------------------------------- */
|
||||||
|
|
||||||
|
/* MiCols / MiRows per spec §3 — 4x4-unit count, rounded up to the
|
||||||
|
* 8x8 alignment the spec uses for tiling math. Returns AlignPow2
|
||||||
|
* of ((dim + 7) >> 3) at miSize=2 (8x8 mi-block). */
|
||||||
|
static uint32_t mi_cols_for(uint32_t frame_width)
|
||||||
|
{
|
||||||
|
uint32_t mi = (frame_width + 7u) >> 3;
|
||||||
|
return mi << 1; /* 4x4 mi units == miCols */
|
||||||
|
}
|
||||||
|
static uint32_t mi_rows_for(uint32_t frame_height)
|
||||||
|
{
|
||||||
|
uint32_t mi = (frame_height + 7u) >> 3;
|
||||||
|
return mi << 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* tile_log2(blkSize, target) per AV1 §5.9.15 — smallest k such that
|
||||||
|
* (blkSize << k) >= target. */
|
||||||
|
static int tile_log2_ge(int blk, int target)
|
||||||
|
{
|
||||||
|
int k = 0;
|
||||||
|
while ((blk << k) < target) k++;
|
||||||
|
return k;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* §5.9.12 quantization_params */
|
||||||
|
static void write_quantization_params(struct bs_writer *bs,
|
||||||
|
const struct v4l2_av1_quantization *q,
|
||||||
|
bool num_planes_gt_1,
|
||||||
|
bool separate_uv_delta_q)
|
||||||
|
{
|
||||||
|
bsw_put_u(bs, q->base_q_idx, 8);
|
||||||
|
|
||||||
|
/* read_delta_q: 1 bit "delta_coded" + (s(7)?) — we always emit
|
||||||
|
* the full delta if non-zero, zero-encoded as delta_coded=0
|
||||||
|
* (single bit). */
|
||||||
|
#define EMIT_DELTA_Q(val) do { \
|
||||||
|
int _v = (int8_t)(val); \
|
||||||
|
if (_v != 0) { \
|
||||||
|
bsw_put_u(bs, 1u, 1); \
|
||||||
|
/* su(1+6): sign + 6-bit magnitude */ \
|
||||||
|
if (_v < 0) { \
|
||||||
|
bsw_put_u(bs, (uint32_t)(_v + 128) & 0x7fu, 7); \
|
||||||
|
} else { \
|
||||||
|
bsw_put_u(bs, (uint32_t)_v & 0x7fu, 7); \
|
||||||
|
} \
|
||||||
|
} else { \
|
||||||
|
bsw_put_u(bs, 0u, 1); \
|
||||||
|
} \
|
||||||
|
} while (0)
|
||||||
|
|
||||||
|
EMIT_DELTA_Q(q->delta_q_y_dc);
|
||||||
|
|
||||||
|
if (num_planes_gt_1) {
|
||||||
|
if (separate_uv_delta_q)
|
||||||
|
bsw_put_u(bs,
|
||||||
|
(q->flags & V4L2_AV1_QUANTIZATION_FLAG_DIFF_UV_DELTA) ? 1u : 0u,
|
||||||
|
1);
|
||||||
|
EMIT_DELTA_Q(q->delta_q_u_dc);
|
||||||
|
EMIT_DELTA_Q(q->delta_q_u_ac);
|
||||||
|
if (separate_uv_delta_q &&
|
||||||
|
(q->flags & V4L2_AV1_QUANTIZATION_FLAG_DIFF_UV_DELTA)) {
|
||||||
|
EMIT_DELTA_Q(q->delta_q_v_dc);
|
||||||
|
EMIT_DELTA_Q(q->delta_q_v_ac);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
#undef EMIT_DELTA_Q
|
||||||
|
|
||||||
|
bsw_put_u(bs,
|
||||||
|
(q->flags & V4L2_AV1_QUANTIZATION_FLAG_USING_QMATRIX) ? 1u : 0u,
|
||||||
|
1);
|
||||||
|
if (q->flags & V4L2_AV1_QUANTIZATION_FLAG_USING_QMATRIX) {
|
||||||
|
bsw_put_u(bs, q->qm_y, 4);
|
||||||
|
bsw_put_u(bs, q->qm_u, 4);
|
||||||
|
if (num_planes_gt_1 && separate_uv_delta_q)
|
||||||
|
bsw_put_u(bs, q->qm_v, 4);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* §5.9.11 loop_filter_params */
|
||||||
|
static void write_loop_filter_params(struct bs_writer *bs,
|
||||||
|
const struct v4l2_av1_loop_filter *lf,
|
||||||
|
bool num_planes_gt_1,
|
||||||
|
bool coded_lossless_or_allow_intrabc)
|
||||||
|
{
|
||||||
|
if (coded_lossless_or_allow_intrabc) {
|
||||||
|
/* spec §6.8.10: when CodedLossless or allow_intrabc is set,
|
||||||
|
* loop filter levels are inferred and not coded. */
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
bsw_put_u(bs, lf->level[0], 6);
|
||||||
|
bsw_put_u(bs, lf->level[1], 6);
|
||||||
|
if (num_planes_gt_1) {
|
||||||
|
if (lf->level[0] || lf->level[1]) {
|
||||||
|
bsw_put_u(bs, lf->level[2], 6);
|
||||||
|
bsw_put_u(bs, lf->level[3], 6);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
bsw_put_u(bs, lf->sharpness, 3);
|
||||||
|
|
||||||
|
/* loop_filter_delta_enabled */
|
||||||
|
bool delta_en = !!(lf->flags & V4L2_AV1_LOOP_FILTER_FLAG_DELTA_ENABLED);
|
||||||
|
bsw_put_u(bs, delta_en ? 1u : 0u, 1);
|
||||||
|
if (delta_en) {
|
||||||
|
bool delta_upd = !!(lf->flags & V4L2_AV1_LOOP_FILTER_FLAG_DELTA_UPDATE);
|
||||||
|
bsw_put_u(bs, delta_upd ? 1u : 0u, 1);
|
||||||
|
if (delta_upd) {
|
||||||
|
int i;
|
||||||
|
for (i = 0; i < 8; i++) {
|
||||||
|
/* update_ref_delta: emit 0 (no update) — V4L2 carries the
|
||||||
|
* cumulative state; trying to differentially encode here
|
||||||
|
* would need previous-frame state we don't track. */
|
||||||
|
bsw_put_u(bs, 0u, 1);
|
||||||
|
}
|
||||||
|
for (i = 0; i < 2; i++)
|
||||||
|
bsw_put_u(bs, 0u, 1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* §5.9.19 cdef_params */
|
||||||
|
static void write_cdef_params(struct bs_writer *bs,
|
||||||
|
const struct v4l2_av1_cdef *cdef,
|
||||||
|
bool num_planes_gt_1,
|
||||||
|
bool enable_cdef,
|
||||||
|
bool coded_lossless_or_intrabc)
|
||||||
|
{
|
||||||
|
int i, n;
|
||||||
|
if (!enable_cdef || coded_lossless_or_intrabc)
|
||||||
|
return;
|
||||||
|
bsw_put_u(bs, cdef->damping_minus_3, 2);
|
||||||
|
bsw_put_u(bs, cdef->bits, 2);
|
||||||
|
n = 1 << cdef->bits;
|
||||||
|
for (i = 0; i < n; i++) {
|
||||||
|
bsw_put_u(bs, cdef->y_pri_strength[i] & 0xfu, 4);
|
||||||
|
bsw_put_u(bs, cdef->y_sec_strength[i] & 0x3u, 2);
|
||||||
|
if (num_planes_gt_1) {
|
||||||
|
bsw_put_u(bs, cdef->uv_pri_strength[i] & 0xfu, 4);
|
||||||
|
bsw_put_u(bs, cdef->uv_sec_strength[i] & 0x3u, 2);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* §5.9.20 lr_params — only RESTORE_NONE supported here */
|
||||||
|
static int write_lr_params(struct bs_writer *bs,
|
||||||
|
const struct v4l2_av1_loop_restoration *lr,
|
||||||
|
int num_planes,
|
||||||
|
bool enable_restoration,
|
||||||
|
bool coded_lossless_or_intrabc)
|
||||||
|
{
|
||||||
|
int p;
|
||||||
|
if (!enable_restoration || coded_lossless_or_intrabc)
|
||||||
|
return 1;
|
||||||
|
|
||||||
|
/* Out-of-scope if ANY plane uses restoration */
|
||||||
|
if (lr->frame_restoration_type[0] != V4L2_AV1_FRAME_RESTORE_NONE)
|
||||||
|
return 0;
|
||||||
|
if (num_planes > 1) {
|
||||||
|
if (lr->frame_restoration_type[1] != V4L2_AV1_FRAME_RESTORE_NONE)
|
||||||
|
return 0;
|
||||||
|
if (lr->frame_restoration_type[2] != V4L2_AV1_FRAME_RESTORE_NONE)
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
/* Emit 2-bit RESTORE_NONE per plane */
|
||||||
|
for (p = 0; p < num_planes; p++)
|
||||||
|
bsw_put_u(bs, 0u, 2);
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* §5.9.15 tile_info — single-tile uniform-spacing path only */
|
||||||
|
static int write_tile_info_single_tile(struct bs_writer *bs,
|
||||||
|
uint32_t frame_width,
|
||||||
|
uint32_t frame_height,
|
||||||
|
bool use_128_sb)
|
||||||
|
{
|
||||||
|
uint32_t mi_cols = mi_cols_for(frame_width);
|
||||||
|
uint32_t mi_rows = mi_rows_for(frame_height);
|
||||||
|
int sb_log2 = use_128_sb ? 5 : 4; /* mi units */
|
||||||
|
uint32_t sb_cols = (mi_cols + ((1u << sb_log2) - 1u)) >> sb_log2;
|
||||||
|
uint32_t sb_rows = (mi_rows + ((1u << sb_log2) - 1u)) >> sb_log2;
|
||||||
|
int min_log2_cols = tile_log2_ge(use_128_sb ? 4096 : 4096 / 1,
|
||||||
|
(int)(sb_cols * (use_128_sb ? 128 : 64)));
|
||||||
|
(void) min_log2_cols;
|
||||||
|
|
||||||
|
/* uniform_tile_spacing_flag = 1, both increment loops = 0 →
|
||||||
|
* tile_cols_log2 = tile_rows_log2 = 0 (single tile). This
|
||||||
|
* matches "uniform spacing with no width/height halving" which
|
||||||
|
* is the simplest valid encoding. */
|
||||||
|
bsw_put_u(bs, 1u, 1); /* uniform_tile_spacing_flag */
|
||||||
|
|
||||||
|
/* increment_tile_cols_log2: 0 zeros + the next non-increment
|
||||||
|
* bit terminates the loop. In single-tile mode we encode the
|
||||||
|
* terminator immediately. */
|
||||||
|
(void) sb_cols;
|
||||||
|
(void) sb_rows;
|
||||||
|
/* The increment loops in the spec run while
|
||||||
|
* tile_cols_log2 < max_log2_tile_cols, reading bits until a 0
|
||||||
|
* appears. For our forced single-tile, we emit a single 0 bit
|
||||||
|
* to terminate the cols loop and another for the rows loop. */
|
||||||
|
bsw_put_u(bs, 0u, 1); /* terminate cols */
|
||||||
|
bsw_put_u(bs, 0u, 1); /* terminate rows */
|
||||||
|
|
||||||
|
/* tile_size_bytes_minus_1: 0 (1 byte) — only meaningful when
|
||||||
|
* NumTiles > 1, but spec emits it unconditionally when
|
||||||
|
* NumTiles > 1. Single tile → not emitted. We're single tile,
|
||||||
|
* skip. */
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
size_t av1_synth_frame_header_obu(const struct v4l2_ctrl_av1_sequence *seq,
|
||||||
|
const struct v4l2_ctrl_av1_frame *fr,
|
||||||
|
uint8_t *out, size_t out_cap)
|
||||||
|
{
|
||||||
|
uint8_t rbsp[256];
|
||||||
|
struct bs_writer bs;
|
||||||
|
uint32_t sf, ff;
|
||||||
|
bool show_existing_frame = false;
|
||||||
|
bool reduced_still_picture_header;
|
||||||
|
bool show_frame, showable_frame, error_resilient_mode;
|
||||||
|
bool disable_cdf_update, allow_screen_content_tools;
|
||||||
|
bool force_integer_mv, allow_intrabc, frame_size_override;
|
||||||
|
bool allow_high_precision_mv, is_motion_mode_switchable;
|
||||||
|
bool use_ref_frame_mvs, disable_frame_end_update_cdf;
|
||||||
|
bool reference_select, allow_warped_motion, reduced_tx_set;
|
||||||
|
bool skip_mode_present, monochrome;
|
||||||
|
uint8_t frame_type, primary_ref_frame;
|
||||||
|
uint32_t frame_width, frame_height;
|
||||||
|
int num_planes;
|
||||||
|
int width_bits, height_bits;
|
||||||
|
uint8_t order_hint_bits;
|
||||||
|
bool enable_order_hint, enable_ref_frame_mvs, enable_warped_motion_seq;
|
||||||
|
bool enable_cdef_seq, enable_restoration_seq;
|
||||||
|
int i;
|
||||||
|
|
||||||
|
if (!seq || !fr || !out || out_cap < 16)
|
||||||
|
return 0;
|
||||||
|
|
||||||
|
sf = seq->flags;
|
||||||
|
ff = fr->flags;
|
||||||
|
|
||||||
|
/* sanity */
|
||||||
|
monochrome = !!(sf & V4L2_AV1_SEQUENCE_FLAG_MONO_CHROME);
|
||||||
|
num_planes = monochrome ? 1 : 3;
|
||||||
|
enable_order_hint = !!(sf & V4L2_AV1_SEQUENCE_FLAG_ENABLE_ORDER_HINT);
|
||||||
|
enable_ref_frame_mvs = !!(sf & V4L2_AV1_SEQUENCE_FLAG_ENABLE_REF_FRAME_MVS);
|
||||||
|
enable_warped_motion_seq = !!(sf & V4L2_AV1_SEQUENCE_FLAG_ENABLE_WARPED_MOTION);
|
||||||
|
enable_cdef_seq = !!(sf & V4L2_AV1_SEQUENCE_FLAG_ENABLE_CDEF);
|
||||||
|
enable_restoration_seq = !!(sf & V4L2_AV1_SEQUENCE_FLAG_ENABLE_RESTORATION);
|
||||||
|
order_hint_bits = enable_order_hint ? seq->order_hint_bits : 0;
|
||||||
|
if (order_hint_bits > 8) order_hint_bits = 8;
|
||||||
|
reduced_still_picture_header = false; /* matches sequence-header default */
|
||||||
|
|
||||||
|
frame_type = fr->frame_type;
|
||||||
|
if (frame_type == V4L2_AV1_SWITCH_FRAME)
|
||||||
|
return 0; /* out of scope */
|
||||||
|
|
||||||
|
show_frame = !!(ff & V4L2_AV1_FRAME_FLAG_SHOW_FRAME);
|
||||||
|
showable_frame = !!(ff & V4L2_AV1_FRAME_FLAG_SHOWABLE_FRAME);
|
||||||
|
error_resilient_mode = !!(ff & V4L2_AV1_FRAME_FLAG_ERROR_RESILIENT_MODE);
|
||||||
|
disable_cdf_update = !!(ff & V4L2_AV1_FRAME_FLAG_DISABLE_CDF_UPDATE);
|
||||||
|
allow_screen_content_tools = !!(ff & V4L2_AV1_FRAME_FLAG_ALLOW_SCREEN_CONTENT_TOOLS);
|
||||||
|
force_integer_mv = !!(ff & V4L2_AV1_FRAME_FLAG_FORCE_INTEGER_MV);
|
||||||
|
allow_intrabc = !!(ff & V4L2_AV1_FRAME_FLAG_ALLOW_INTRABC);
|
||||||
|
frame_size_override = !!(ff & V4L2_AV1_FRAME_FLAG_FRAME_SIZE_OVERRIDE);
|
||||||
|
allow_high_precision_mv = !!(ff & V4L2_AV1_FRAME_FLAG_ALLOW_HIGH_PRECISION_MV);
|
||||||
|
is_motion_mode_switchable = !!(ff & V4L2_AV1_FRAME_FLAG_IS_MOTION_MODE_SWITCHABLE);
|
||||||
|
use_ref_frame_mvs = !!(ff & V4L2_AV1_FRAME_FLAG_USE_REF_FRAME_MVS);
|
||||||
|
disable_frame_end_update_cdf = !!(ff & V4L2_AV1_FRAME_FLAG_DISABLE_FRAME_END_UPDATE_CDF);
|
||||||
|
reference_select = !!(ff & V4L2_AV1_FRAME_FLAG_REFERENCE_SELECT);
|
||||||
|
allow_warped_motion = !!(ff & V4L2_AV1_FRAME_FLAG_ALLOW_WARPED_MOTION);
|
||||||
|
reduced_tx_set = !!(ff & V4L2_AV1_FRAME_FLAG_REDUCED_TX_SET);
|
||||||
|
skip_mode_present = !!(ff & V4L2_AV1_FRAME_FLAG_SKIP_MODE_PRESENT);
|
||||||
|
primary_ref_frame = fr->primary_ref_frame;
|
||||||
|
|
||||||
|
frame_width = fr->frame_width_minus_1 + 1;
|
||||||
|
frame_height = fr->frame_height_minus_1 + 1;
|
||||||
|
|
||||||
|
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);
|
||||||
|
|
||||||
|
bsw_init(&bs, rbsp, sizeof(rbsp));
|
||||||
|
|
||||||
|
/* show_existing_frame: 0 (V4L2 doesn't surface the show-only path
|
||||||
|
* — every fr ctrl describes a real decoded frame). */
|
||||||
|
bsw_put_u(&bs, show_existing_frame ? 1u : 0u, 1);
|
||||||
|
|
||||||
|
bsw_put_u(&bs, (uint32_t) frame_type, 2);
|
||||||
|
bsw_put_u(&bs, show_frame ? 1u : 0u, 1);
|
||||||
|
if (show_frame) {
|
||||||
|
/* No decoder_model_info_present_flag emitted in seq header,
|
||||||
|
* so no buffer-removal-time bits here either. */
|
||||||
|
} else {
|
||||||
|
bsw_put_u(&bs, showable_frame ? 1u : 0u, 1);
|
||||||
|
}
|
||||||
|
if (frame_type == V4L2_AV1_SWITCH_FRAME ||
|
||||||
|
(frame_type == V4L2_AV1_KEY_FRAME && show_frame)) {
|
||||||
|
/* error_resilient_mode = 1 inferred — not coded */
|
||||||
|
} else {
|
||||||
|
bsw_put_u(&bs, error_resilient_mode ? 1u : 0u, 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
bsw_put_u(&bs, disable_cdf_update ? 1u : 0u, 1);
|
||||||
|
/* allow_screen_content_tools coded as 1 bit when sequence
|
||||||
|
* forces NOT-SELECT; SELECT mode means we always emit a 1 bit
|
||||||
|
* for the SELECT_SCREEN_CONTENT_TOOLS path. Our sequence
|
||||||
|
* header always emits SELECT, so emit a single bit equal to
|
||||||
|
* the V4L2 flag. */
|
||||||
|
bsw_put_u(&bs, allow_screen_content_tools ? 1u : 0u, 1);
|
||||||
|
if (allow_screen_content_tools) {
|
||||||
|
/* seq_force_integer_mv = SELECT (2) so:
|
||||||
|
* force_integer_mv coded as 1 bit */
|
||||||
|
bsw_put_u(&bs, force_integer_mv ? 1u : 0u, 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* frame_id_numbers_present_flag = 0 in seq → no current_frame_id */
|
||||||
|
|
||||||
|
if (frame_type != V4L2_AV1_SWITCH_FRAME && !reduced_still_picture_header)
|
||||||
|
bsw_put_u(&bs, frame_size_override ? 1u : 0u, 1);
|
||||||
|
|
||||||
|
if (enable_order_hint)
|
||||||
|
bsw_put_u(&bs, fr->order_hint, order_hint_bits);
|
||||||
|
|
||||||
|
if (frame_type != V4L2_AV1_KEY_FRAME && frame_type != V4L2_AV1_INTRA_ONLY_FRAME &&
|
||||||
|
!error_resilient_mode)
|
||||||
|
bsw_put_u(&bs, primary_ref_frame, 3);
|
||||||
|
|
||||||
|
/* frame_size + render_size (§5.9.5, §5.9.6) */
|
||||||
|
if (frame_size_override) {
|
||||||
|
bsw_put_u(&bs, fr->frame_width_minus_1, width_bits);
|
||||||
|
bsw_put_u(&bs, fr->frame_height_minus_1, height_bits);
|
||||||
|
}
|
||||||
|
/* superres_params: §5.9.8 */
|
||||||
|
{
|
||||||
|
bool use_superres = !!(ff & V4L2_AV1_FRAME_FLAG_USE_SUPERRES);
|
||||||
|
if (sf & V4L2_AV1_SEQUENCE_FLAG_ENABLE_SUPERRES)
|
||||||
|
bsw_put_u(&bs, use_superres ? 1u : 0u, 1);
|
||||||
|
if (use_superres) {
|
||||||
|
/* coded_denom = superres_denom - SUPERRES_DENOM_MIN(9) */
|
||||||
|
int denom = fr->superres_denom;
|
||||||
|
if (denom < 9) denom = 9;
|
||||||
|
bsw_put_u(&bs, (uint32_t)(denom - 9) & 0x7u, 3);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
/* render_size present flag: 1 if render dims given */
|
||||||
|
{
|
||||||
|
bool render_and_frame_match =
|
||||||
|
(fr->render_width_minus_1 == fr->frame_width_minus_1) &&
|
||||||
|
(fr->render_height_minus_1 == fr->frame_height_minus_1);
|
||||||
|
bsw_put_u(&bs, render_and_frame_match ? 0u : 1u, 1);
|
||||||
|
if (!render_and_frame_match) {
|
||||||
|
bsw_put_u(&bs, fr->render_width_minus_1, 16);
|
||||||
|
bsw_put_u(&bs, fr->render_height_minus_1, 16);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (frame_type != V4L2_AV1_KEY_FRAME && frame_type != V4L2_AV1_INTRA_ONLY_FRAME) {
|
||||||
|
/* allow_intrabc only on key/intra-only — skip for inter */
|
||||||
|
(void) allow_intrabc;
|
||||||
|
if (!error_resilient_mode && enable_order_hint)
|
||||||
|
bsw_put_u(&bs, 0u, 1); /* frame_refs_short_signaling */
|
||||||
|
/* read ref_frame_idx for each of REFS_PER_FRAME */
|
||||||
|
for (i = 0; i < AV1_REFS_PER_FRAME; i++) {
|
||||||
|
int8_t idx = fr->ref_frame_idx[i];
|
||||||
|
if (idx < 0) idx = 0;
|
||||||
|
bsw_put_u(&bs, (uint32_t)(idx & 0x7), 3);
|
||||||
|
}
|
||||||
|
if (frame_size_override && !error_resilient_mode) {
|
||||||
|
/* found_ref loop — emit "no" for each, so frame_size
|
||||||
|
* fields above already populated. */
|
||||||
|
for (i = 0; i < AV1_REFS_PER_FRAME; i++)
|
||||||
|
bsw_put_u(&bs, 0u, 1);
|
||||||
|
}
|
||||||
|
bsw_put_u(&bs, allow_high_precision_mv ? 1u : 0u, 1);
|
||||||
|
/* read_interpolation_filter: is_filter_switchable + value */
|
||||||
|
{
|
||||||
|
int interp = fr->interpolation_filter;
|
||||||
|
bool switchable = (interp == V4L2_AV1_INTERPOLATION_FILTER_SWITCHABLE);
|
||||||
|
bsw_put_u(&bs, switchable ? 1u : 0u, 1);
|
||||||
|
if (!switchable)
|
||||||
|
bsw_put_u(&bs, (uint32_t)interp & 0x3u, 2);
|
||||||
|
}
|
||||||
|
bsw_put_u(&bs, is_motion_mode_switchable ? 1u : 0u, 1);
|
||||||
|
if (!error_resilient_mode && enable_ref_frame_mvs)
|
||||||
|
bsw_put_u(&bs, use_ref_frame_mvs ? 1u : 0u, 1);
|
||||||
|
} else {
|
||||||
|
if (frame_type == V4L2_AV1_INTRA_ONLY_FRAME && allow_screen_content_tools)
|
||||||
|
bsw_put_u(&bs, allow_intrabc ? 1u : 0u, 1);
|
||||||
|
else if (frame_type == V4L2_AV1_KEY_FRAME && allow_screen_content_tools)
|
||||||
|
bsw_put_u(&bs, allow_intrabc ? 1u : 0u, 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* disable_frame_end_update_cdf */
|
||||||
|
if (!disable_cdf_update)
|
||||||
|
bsw_put_u(&bs, disable_frame_end_update_cdf ? 1u : 0u, 1);
|
||||||
|
|
||||||
|
/* tile_info: single-tile path */
|
||||||
|
{
|
||||||
|
bool use_128 = !!(sf & V4L2_AV1_SEQUENCE_FLAG_USE_128X128_SUPERBLOCK);
|
||||||
|
if (!write_tile_info_single_tile(&bs, frame_width, frame_height,
|
||||||
|
use_128))
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* quantization_params */
|
||||||
|
write_quantization_params(&bs, &fr->quantization,
|
||||||
|
num_planes > 1,
|
||||||
|
!!(sf & V4L2_AV1_SEQUENCE_FLAG_SEPARATE_UV_DELTA_Q));
|
||||||
|
|
||||||
|
/* segmentation_params: only enabled=0 supported */
|
||||||
|
{
|
||||||
|
bool seg_en = !!(fr->segmentation.flags & V4L2_AV1_SEGMENTATION_FLAG_ENABLED);
|
||||||
|
if (seg_en)
|
||||||
|
return 0;
|
||||||
|
bsw_put_u(&bs, 0u, 1); /* segmentation_enabled */
|
||||||
|
}
|
||||||
|
|
||||||
|
/* delta_q_params + delta_lf_params */
|
||||||
|
{
|
||||||
|
bool delta_q_present = !!(fr->quantization.flags &
|
||||||
|
V4L2_AV1_QUANTIZATION_FLAG_DELTA_Q_PRESENT);
|
||||||
|
if (fr->quantization.base_q_idx > 0) {
|
||||||
|
bsw_put_u(&bs, delta_q_present ? 1u : 0u, 1);
|
||||||
|
if (delta_q_present)
|
||||||
|
bsw_put_u(&bs, fr->quantization.delta_q_res & 0x3u, 2);
|
||||||
|
}
|
||||||
|
if (delta_q_present && !allow_intrabc) {
|
||||||
|
bool delta_lf_present =
|
||||||
|
!!(fr->loop_filter.flags & V4L2_AV1_LOOP_FILTER_FLAG_DELTA_LF_PRESENT);
|
||||||
|
bsw_put_u(&bs, delta_lf_present ? 1u : 0u, 1);
|
||||||
|
if (delta_lf_present) {
|
||||||
|
bsw_put_u(&bs, fr->loop_filter.delta_lf_res & 0x3u, 2);
|
||||||
|
bsw_put_u(&bs,
|
||||||
|
(fr->loop_filter.flags & V4L2_AV1_LOOP_FILTER_FLAG_DELTA_LF_MULTI)
|
||||||
|
? 1u : 0u, 1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* coded_lossless heuristic: when base_q_idx==0 and all deltas==0
|
||||||
|
* and qm not in use, AV1 treats the frame as lossless. We
|
||||||
|
* approximate with the base_q_idx check; the lf/cdef writers
|
||||||
|
* gate on the same value. */
|
||||||
|
{
|
||||||
|
bool coded_lossless = (fr->quantization.base_q_idx == 0);
|
||||||
|
|
||||||
|
write_loop_filter_params(&bs, &fr->loop_filter,
|
||||||
|
num_planes > 1,
|
||||||
|
coded_lossless || allow_intrabc);
|
||||||
|
write_cdef_params(&bs, &fr->cdef, num_planes > 1,
|
||||||
|
enable_cdef_seq,
|
||||||
|
coded_lossless || allow_intrabc);
|
||||||
|
if (!write_lr_params(&bs, &fr->loop_restoration, num_planes,
|
||||||
|
enable_restoration_seq,
|
||||||
|
coded_lossless || allow_intrabc))
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* read_tx_mode (§5.9.21) */
|
||||||
|
{
|
||||||
|
bool coded_lossless = (fr->quantization.base_q_idx == 0);
|
||||||
|
if (coded_lossless) {
|
||||||
|
/* tx_mode = ONLY_4X4 (inferred) */
|
||||||
|
} else {
|
||||||
|
int tx_mode = fr->tx_mode;
|
||||||
|
bsw_put_u(&bs, (tx_mode == V4L2_AV1_TX_MODE_SELECT) ? 1u : 0u, 1);
|
||||||
|
if (tx_mode != V4L2_AV1_TX_MODE_SELECT)
|
||||||
|
bsw_put_u(&bs, (tx_mode == V4L2_AV1_TX_MODE_LARGEST) ? 1u : 0u, 1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* frame_reference_mode (§5.9.23) */
|
||||||
|
if (frame_type != V4L2_AV1_KEY_FRAME && frame_type != V4L2_AV1_INTRA_ONLY_FRAME)
|
||||||
|
bsw_put_u(&bs, reference_select ? 1u : 0u, 1);
|
||||||
|
|
||||||
|
/* skip_mode_params (§5.9.22) */
|
||||||
|
{
|
||||||
|
bool skip_allowed = !!(ff & V4L2_AV1_FRAME_FLAG_SKIP_MODE_ALLOWED);
|
||||||
|
if (skip_allowed)
|
||||||
|
bsw_put_u(&bs, skip_mode_present ? 1u : 0u, 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* reduced_tx_set */
|
||||||
|
bsw_put_u(&bs, reduced_tx_set ? 1u : 0u, 1);
|
||||||
|
|
||||||
|
/* global_motion_params: §5.9.24 — emit IDENTITY for each ref */
|
||||||
|
if (frame_type != V4L2_AV1_KEY_FRAME && frame_type != V4L2_AV1_INTRA_ONLY_FRAME) {
|
||||||
|
int r;
|
||||||
|
(void) enable_warped_motion_seq;
|
||||||
|
(void) allow_warped_motion;
|
||||||
|
for (r = 1; r < AV1_NUM_REF_FRAMES; r++) {
|
||||||
|
uint8_t wm_type = fr->global_motion.type[r];
|
||||||
|
if (wm_type != V4L2_AV1_WARP_MODEL_IDENTITY)
|
||||||
|
return 0; /* out of scope */
|
||||||
|
bsw_put_u(&bs, 0u, 1); /* is_global = 0 → identity */
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* film_grain_params: §6.8.20 — only "not present" path supported */
|
||||||
|
if (sf & V4L2_AV1_SEQUENCE_FLAG_FILM_GRAIN_PARAMS_PRESENT)
|
||||||
|
return 0; /* out of scope: film grain coding deferred */
|
||||||
|
|
||||||
|
bsw_align_rbsp(&bs);
|
||||||
|
if (bsw_overflowed(&bs))
|
||||||
|
return 0;
|
||||||
|
|
||||||
|
return wrap_obu(OBU_FRAME_HEADER, rbsp, bsw_bytes(&bs), out, out_cap);
|
||||||
|
}
|
||||||
@@ -0,0 +1,119 @@
|
|||||||
|
/* 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 Sequence Header (§5.5.1), Temporal Delimiter
|
||||||
|
* (§5.6), and Frame Header (§5.9) OBUs. 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);
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Encode an AV1 Temporal Delimiter OBU into @out. Always exactly 2
|
||||||
|
* bytes: 0x12 (obu_type=TEMPORAL_DELIMITER, has_size_field=1) followed
|
||||||
|
* by 0x00 (leb128(0) — zero-payload). Returns 2 on success, 0 if
|
||||||
|
* @out_cap < 2.
|
||||||
|
*
|
||||||
|
* Per AV1 spec §5.6 every temporal unit MUST start with a Temporal
|
||||||
|
* Delimiter OBU when temporal_delimiter_obus_present is implied — the
|
||||||
|
* libavcodec AV1 parser uses TD OBUs as access-unit boundaries when
|
||||||
|
* fed full-bitstream input.
|
||||||
|
*/
|
||||||
|
size_t av1_synth_temporal_delimiter_obu(uint8_t *out, size_t out_cap);
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Integration status (2026-05-23):
|
||||||
|
*
|
||||||
|
* The Sequence / Frame Header / Temporal Delimiter encoders below are
|
||||||
|
* standalone primitives. They are NOT yet called from decoder.c — the
|
||||||
|
* AV1 decode hot path still passes the OUTPUT buffer straight to
|
||||||
|
* libavcodec, which only works if the V4L2 consumer happens to be
|
||||||
|
* sending a fully-OBU'd access unit (i.e. is not strictly following
|
||||||
|
* the V4L2 stateless AV1 "tile-group bytes only" contract).
|
||||||
|
*
|
||||||
|
* Wiring these primitives in requires a separate kernel-side change:
|
||||||
|
*
|
||||||
|
* - extend daedalus_v4l2_proto.h with a `struct daedalus_av1_meta`
|
||||||
|
* mirroring v4l2_ctrl_av1_sequence + v4l2_ctrl_av1_frame
|
||||||
|
* - update kernel/daedalus_v4l2_main.c to capture
|
||||||
|
* V4L2_CID_STATELESS_AV1_{SEQUENCE,FRAME} at device_run time and
|
||||||
|
* ship the meta alongside the bitstream over the chardev
|
||||||
|
* - update daemon/src/chardev_client.c to receive the meta
|
||||||
|
* - update daemon/src/decoder.c to: synth TD + SH + FH OBUs, wrap
|
||||||
|
* the OUTPUT bytes as an OBU_TILE_GROUP, concat in that order,
|
||||||
|
* hand the assembled bitstream to libavcodec
|
||||||
|
*
|
||||||
|
* Tracked as a follow-on; see daedalus-v4l2 task notes.
|
||||||
|
*/
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Encode an AV1 Frame Header OBU from the V4L2 stateless frame control
|
||||||
|
* (and the matching sequence control, which provides fields the
|
||||||
|
* frame-header encoder branches on per §5.9.1).
|
||||||
|
*
|
||||||
|
* Scope (this revision — libva-v4l2-request common-case path):
|
||||||
|
* - Frame types KEY / INTER / INTRA_ONLY. SWITCH frames return 0
|
||||||
|
* (caller should fall back to libavcodec native parsing).
|
||||||
|
* - segmentation_params() emits the "segmentation disabled" path
|
||||||
|
* when V4L2_AV1_SEGMENTATION_FLAG_ENABLED is 0. Enabled
|
||||||
|
* segmentation returns 0.
|
||||||
|
* - loop_restoration_params(): only RESTORE_NONE on all planes
|
||||||
|
* supported. Other restoration types return 0.
|
||||||
|
* - global_motion: only IDENTITY warp model emitted. Non-IDENTITY
|
||||||
|
* entries return 0.
|
||||||
|
* - film_grain_params(): treated as "not present" — only valid when
|
||||||
|
* the sequence header has film_grain_params_present = 0. If the
|
||||||
|
* sequence claims film grain is present this revision returns 0
|
||||||
|
* (the per-frame film-grain coding is a separate follow-on).
|
||||||
|
*
|
||||||
|
* Out-of-scope branches return 0 so the caller can surface a coverage
|
||||||
|
* warning and fall back to direct libavcodec parsing of the original
|
||||||
|
* bitstream where possible.
|
||||||
|
*
|
||||||
|
* @out_cap must be at least 128 bytes for any reasonable frame header;
|
||||||
|
* 256 bytes is a safe upper bound for the supported subset.
|
||||||
|
*/
|
||||||
|
size_t av1_synth_frame_header_obu(const struct v4l2_ctrl_av1_sequence *seq,
|
||||||
|
const struct v4l2_ctrl_av1_frame *fr,
|
||||||
|
uint8_t *out, size_t out_cap);
|
||||||
|
|
||||||
|
#endif /* DAEDALUS_AV1_OBU_SYNTH_H */
|
||||||
+83
-2
@@ -6,6 +6,7 @@
|
|||||||
#include "ffmpeg_loader.h"
|
#include "ffmpeg_loader.h"
|
||||||
#include "h264_nal_synth.h"
|
#include "h264_nal_synth.h"
|
||||||
#include "log.h"
|
#include "log.h"
|
||||||
|
#include "shadow_decoder.h"
|
||||||
|
|
||||||
#include <errno.h>
|
#include <errno.h>
|
||||||
#include <stdlib.h>
|
#include <stdlib.h>
|
||||||
@@ -110,6 +111,13 @@ int daedalus_decoder_init(struct daedalus_decoder *dec,
|
|||||||
loader->av_packet_free(&dec->pkt);
|
loader->av_packet_free(&dec->pkt);
|
||||||
return -ENOMEM;
|
return -ENOMEM;
|
||||||
}
|
}
|
||||||
|
/*
|
||||||
|
* Returns NULL when DAEDALUS_SHADOW_MODE != "1" or the loaded
|
||||||
|
* libavcodec lacks the per-MB inspection callback. Both are
|
||||||
|
* the normal production state — the rest of decoder.c is
|
||||||
|
* shadow-aware via NULL-safe shadow_decoder_* entry points.
|
||||||
|
*/
|
||||||
|
dec->shadow = shadow_decoder_create(loader);
|
||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -117,6 +125,8 @@ void daedalus_decoder_cleanup(struct daedalus_decoder *dec)
|
|||||||
{
|
{
|
||||||
if (!dec || !dec->loader)
|
if (!dec || !dec->loader)
|
||||||
return;
|
return;
|
||||||
|
if (dec->shadow)
|
||||||
|
shadow_decoder_destroy(dec->shadow);
|
||||||
if (dec->ctx_vp9)
|
if (dec->ctx_vp9)
|
||||||
dec->loader->avcodec_free_context(&dec->ctx_vp9);
|
dec->loader->avcodec_free_context(&dec->ctx_vp9);
|
||||||
if (dec->ctx_av1)
|
if (dec->ctx_av1)
|
||||||
@@ -211,6 +221,16 @@ static int decoder_open_codec(struct daedalus_decoder *dec, uint32_t codec_id,
|
|||||||
*cache = ctx;
|
*cache = ctx;
|
||||||
*out = ctx;
|
*out = ctx;
|
||||||
log_info("decoder: opened %s context", codec->name);
|
log_info("decoder: opened %s context", codec->name);
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Shadow-mode hook on H.264 only: install the per-MB inspection
|
||||||
|
* callback once the AVCodecContext is open. NULL-safe — when
|
||||||
|
* shadow mode is disabled (the normal production case) this
|
||||||
|
* does nothing.
|
||||||
|
*/
|
||||||
|
if (codec_id == DAEDALUS_CODEC_H264)
|
||||||
|
shadow_decoder_install_cb(dec->shadow, ctx);
|
||||||
|
|
||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -273,6 +293,20 @@ static int pack_p010_to_plane(struct AVFrame *fr,
|
|||||||
if (!base)
|
if (!base)
|
||||||
return -EINVAL;
|
return -EINVAL;
|
||||||
|
|
||||||
|
/* Bounds-check (see pack_nv12_single comment). P010 stores 16
|
||||||
|
* bits per sample on both Y and CbCr planes; stride is in bytes
|
||||||
|
* and already accounts for the 2× expansion. */
|
||||||
|
{
|
||||||
|
size_t y_size_chk = (size_t) stride * (size_t) h;
|
||||||
|
size_t required = y_size_chk + (size_t) stride * (size_t) ch;
|
||||||
|
if (planes->size[0] < required) {
|
||||||
|
log_warn("pack_p010: frame %dx%d (stride=%u required=%zu) "
|
||||||
|
"exceeds CAPTURE plane[0] size %zu — skipping pack",
|
||||||
|
w, h, stride, required, planes->size[0]);
|
||||||
|
return -EOVERFLOW;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
dst_y = base;
|
dst_y = base;
|
||||||
y_size = (size_t) stride * (size_t) h;
|
y_size = (size_t) stride * (size_t) h;
|
||||||
dst_uv = base + y_size;
|
dst_uv = base + y_size;
|
||||||
@@ -320,7 +354,7 @@ static int pack_nv12_single_to_plane(struct AVFrame *fr,
|
|||||||
uint8_t *base;
|
uint8_t *base;
|
||||||
uint32_t stride;
|
uint32_t stride;
|
||||||
uint8_t *dst_y, *dst_uv;
|
uint8_t *dst_y, *dst_uv;
|
||||||
size_t y_size;
|
size_t y_size, required;
|
||||||
|
|
||||||
if (!desc || !planes || planes->nr < 1)
|
if (!desc || !planes || planes->nr < 1)
|
||||||
return -EINVAL;
|
return -EINVAL;
|
||||||
@@ -339,8 +373,27 @@ static int pack_nv12_single_to_plane(struct AVFrame *fr,
|
|||||||
if (!base)
|
if (!base)
|
||||||
return -EINVAL;
|
return -EINVAL;
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Bounds-check before any write — the V4L2 client's CAPTURE
|
||||||
|
* dmabuf may have been sized for a smaller frame than what
|
||||||
|
* libavcodec just decoded (e.g. YouTube DASH stepping
|
||||||
|
* resolution mid-stream — libva is supposed to handle the
|
||||||
|
* SOURCE_CHANGE event with STREAMOFF + S_FMT + REQBUFS but
|
||||||
|
* sometimes a stale request slips through carrying the old
|
||||||
|
* buffer size). Writing the chroma interleave loop into an
|
||||||
|
* undersized mapping faults the daemon with SIGSEGV mid-frame.
|
||||||
|
* Bail loudly with a warn instead.
|
||||||
|
*/
|
||||||
|
y_size = (size_t) stride * (size_t) h;
|
||||||
|
required = y_size + (size_t) stride * (size_t) ch;
|
||||||
|
if (planes->size[0] < required) {
|
||||||
|
log_warn("pack_nv12_single: frame %dx%d (stride=%u required=%zu) "
|
||||||
|
"exceeds CAPTURE plane[0] size %zu — skipping pack",
|
||||||
|
w, h, stride, required, planes->size[0]);
|
||||||
|
return -EOVERFLOW;
|
||||||
|
}
|
||||||
|
|
||||||
dst_y = base;
|
dst_y = base;
|
||||||
y_size = (size_t) stride * (size_t) h;
|
|
||||||
dst_uv = base + y_size;
|
dst_uv = base + y_size;
|
||||||
|
|
||||||
for (y = 0; y < h; y++)
|
for (y = 0; y < h; y++)
|
||||||
@@ -395,6 +448,24 @@ static int pack_nv12_to_planes(struct AVFrame *fr,
|
|||||||
if (!dst_y || !dst_uv)
|
if (!dst_y || !dst_uv)
|
||||||
return -EINVAL;
|
return -EINVAL;
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Bounds-check both planes against the mapped dmabuf size. See
|
||||||
|
* pack_nv12_single_to_plane comment for the resolution-change-
|
||||||
|
* mid-stream crash story this protects against.
|
||||||
|
*/
|
||||||
|
{
|
||||||
|
size_t y_required = (size_t) dst_y_stride * (size_t) h;
|
||||||
|
size_t uv_required = (size_t) dst_uv_stride * (size_t) ch;
|
||||||
|
if (planes->size[0] < y_required ||
|
||||||
|
planes->size[1] < uv_required) {
|
||||||
|
log_warn("pack_nv12_2plane: frame %dx%d "
|
||||||
|
"(y=%zu/%zu uv=%zu/%zu) exceeds CAPTURE — skipping pack",
|
||||||
|
w, h, y_required, planes->size[0],
|
||||||
|
uv_required, planes->size[1]);
|
||||||
|
return -EOVERFLOW;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/* Y plane copy — strip source stride padding. */
|
/* Y plane copy — strip source stride padding. */
|
||||||
for (y = 0; y < h; y++)
|
for (y = 0; y < h; y++)
|
||||||
memcpy(dst_y + (size_t) y * dst_y_stride,
|
memcpy(dst_y + (size_t) y * dst_y_stride,
|
||||||
@@ -544,6 +615,16 @@ int daedalus_decoder_run_request(struct daedalus_decoder *dec,
|
|||||||
goto out;
|
goto out;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Shadow-mode frame-boundary hook. H.264-only — the per-MB
|
||||||
|
* callback is only registered for H.264, so on VP9/AV1 frames
|
||||||
|
* shadow->mbs_this_frame stays zero anyway, but keeping the
|
||||||
|
* codec gate here makes the log lines easier to read.
|
||||||
|
* NULL-safe.
|
||||||
|
*/
|
||||||
|
if (req->codec_id == DAEDALUS_CODEC_H264)
|
||||||
|
shadow_decoder_on_frame(dec->shadow, dec->frame);
|
||||||
|
|
||||||
{
|
{
|
||||||
struct AVFrame *fr = dec->frame;
|
struct AVFrame *fr = dec->frame;
|
||||||
const AVPixFmtDescriptor *desc =
|
const AVPixFmtDescriptor *desc =
|
||||||
|
|||||||
@@ -21,6 +21,7 @@ struct ffmpeg_loader;
|
|||||||
struct AVCodecContext;
|
struct AVCodecContext;
|
||||||
struct AVPacket;
|
struct AVPacket;
|
||||||
struct AVFrame;
|
struct AVFrame;
|
||||||
|
struct shadow_decoder;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* struct daedalus_decoder - per-daemon decoder state
|
* struct daedalus_decoder - per-daemon decoder state
|
||||||
@@ -31,6 +32,10 @@ struct AVFrame;
|
|||||||
* @ctx_h264: lazily-opened H.264 AVCodecContext
|
* @ctx_h264: lazily-opened H.264 AVCodecContext
|
||||||
* @pkt: shared AVPacket reused across requests
|
* @pkt: shared AVPacket reused across requests
|
||||||
* @frame: shared AVFrame reused across requests
|
* @frame: shared AVFrame reused across requests
|
||||||
|
* @shadow: env-gated daedalus-decoder shadow path; NULL when
|
||||||
|
* DAEDALUS_SHADOW_MODE != "1" or libavcodec lacks the
|
||||||
|
* per-MB inspection callback. Production path doesn't
|
||||||
|
* care; all shadow_decoder_* entry points are NULL-safe.
|
||||||
*/
|
*/
|
||||||
struct daedalus_decoder {
|
struct daedalus_decoder {
|
||||||
struct ffmpeg_loader *loader;
|
struct ffmpeg_loader *loader;
|
||||||
@@ -39,6 +44,7 @@ struct daedalus_decoder {
|
|||||||
struct AVCodecContext *ctx_h264;
|
struct AVCodecContext *ctx_h264;
|
||||||
struct AVPacket *pkt;
|
struct AVPacket *pkt;
|
||||||
struct AVFrame *frame;
|
struct AVFrame *frame;
|
||||||
|
struct shadow_decoder *shadow;
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -109,6 +109,24 @@ int ffmpeg_loader_init(struct ffmpeg_loader *loader)
|
|||||||
RESOLVE(libavutil, LIBAVUTIL_SONAME, av_version_info);
|
RESOLVE(libavutil, LIBAVUTIL_SONAME, av_version_info);
|
||||||
RESOLVE(libavutil, LIBAVUTIL_SONAME, av_pix_fmt_desc_get);
|
RESOLVE(libavutil, LIBAVUTIL_SONAME, av_pix_fmt_desc_get);
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Optional symbols. Resolved NULL-tolerantly — stock libavcodec
|
||||||
|
* does not export these; the marfrit-packages
|
||||||
|
* ffmpeg-v4l2-request-fourier fork does (patches 0016/0017).
|
||||||
|
* Callers MUST NULL-check before invoking. Clear any stale
|
||||||
|
* dlerror() the previous lookups left behind so we read a clean
|
||||||
|
* status here.
|
||||||
|
*/
|
||||||
|
(void) dlerror();
|
||||||
|
*(void **) &loader->ff_h264_set_mb_inspect_cb =
|
||||||
|
dlsym(loader->libavcodec, "ff_h264_set_mb_inspect_cb");
|
||||||
|
if (!loader->ff_h264_set_mb_inspect_cb) {
|
||||||
|
log_info("libavcodec lacks ff_h264_set_mb_inspect_cb "
|
||||||
|
"(stock build, no daedalus-fourier 0016 patch) "
|
||||||
|
"— shadow-mode unavailable");
|
||||||
|
(void) dlerror(); /* discard the not-found message */
|
||||||
|
}
|
||||||
|
|
||||||
{
|
{
|
||||||
unsigned int v = loader->avformat_version();
|
unsigned int v = loader->avformat_version();
|
||||||
log_info("FFmpeg loaded: %s (libavformat %u.%u.%u)",
|
log_info("FFmpeg loaded: %s (libavformat %u.%u.%u)",
|
||||||
|
|||||||
@@ -35,6 +35,14 @@
|
|||||||
#include <libavutil/avutil.h>
|
#include <libavutil/avutil.h>
|
||||||
#include <libavutil/pixdesc.h>
|
#include <libavutil/pixdesc.h>
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Forward declaration must precede ff_h264_set_mb_inspect_cb's
|
||||||
|
* function-pointer signature below — otherwise the compiler treats
|
||||||
|
* `struct H264Context` as a parameter-scope declaration and the type
|
||||||
|
* is incompatible with later uses in shadow_decoder.c.
|
||||||
|
*/
|
||||||
|
struct H264Context; /* opaque outside libavcodec */
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* struct ffmpeg_loader - resolved FFmpeg API entry points
|
* struct ffmpeg_loader - resolved FFmpeg API entry points
|
||||||
* @libavformat: dlopen handle (close in cleanup)
|
* @libavformat: dlopen handle (close in cleanup)
|
||||||
@@ -88,6 +96,27 @@ struct ffmpeg_loader {
|
|||||||
const char *(*av_get_media_type_string)(enum AVMediaType);
|
const char *(*av_get_media_type_string)(enum AVMediaType);
|
||||||
const char *(*av_version_info)(void);
|
const char *(*av_version_info)(void);
|
||||||
const AVPixFmtDescriptor *(*av_pix_fmt_desc_get)(enum AVPixelFormat);
|
const AVPixFmtDescriptor *(*av_pix_fmt_desc_get)(enum AVPixelFormat);
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Optional libavcodec symbols. NULL when the loaded
|
||||||
|
* libavcodec.so doesn't carry the corresponding marfrit-packages
|
||||||
|
* patch. Callers must NULL-check before invoking.
|
||||||
|
*
|
||||||
|
* ff_h264_set_mb_inspect_cb — marfrit-packages patch 0016.
|
||||||
|
* Registers a per-MB callback that fires at the end of
|
||||||
|
* ff_h264_hl_decode_mb. Used by daedalus-v4l2's shadow-mode
|
||||||
|
* path to drive daedalus-decoder's frame-major dispatch
|
||||||
|
* alongside libavcodec's reference decode. H264Context stays
|
||||||
|
* opaque to the daemon — extraction of its private fields needs
|
||||||
|
* the patched FFmpeg source-tree headers (see the CLI in
|
||||||
|
* daedalus-decoder/tools/daedalus_decode_h264.c) and is
|
||||||
|
* deferred to PR-Q3a.2.
|
||||||
|
*/
|
||||||
|
void (*ff_h264_set_mb_inspect_cb)(struct AVCodecContext *avctx,
|
||||||
|
void (*cb)(void *opaque,
|
||||||
|
const struct H264Context *h,
|
||||||
|
int mb_x, int mb_y),
|
||||||
|
void *opaque);
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -0,0 +1,162 @@
|
|||||||
|
/* SPDX-License-Identifier: BSD-2-Clause */
|
||||||
|
/*
|
||||||
|
* shadow_decoder.c — env-gated parallel daedalus-decoder wiring.
|
||||||
|
*
|
||||||
|
* PR-Q3a.1 scope: prove the toolchain.
|
||||||
|
*
|
||||||
|
* 1. DAEDALUS_SHADOW_MODE=1 + libavcodec carries marfrit-packages
|
||||||
|
* 0016 (ff_h264_set_mb_inspect_cb) → shadow path active.
|
||||||
|
* 2. Per-MB callback fires on every macroblock libavcodec emits.
|
||||||
|
* We only count the firings here.
|
||||||
|
* 3. Frame boundary creates a daedalus_decoder context lazily
|
||||||
|
* (sized from the first AVFrame); destroy + recreate on
|
||||||
|
* resolution change.
|
||||||
|
* 4. Per-frame log line surfaces MB count + has_qpu state.
|
||||||
|
*
|
||||||
|
* No daedalus_decoder_append_mb / flush_frame calls yet — that
|
||||||
|
* needs H264Context introspection which depends on the patched
|
||||||
|
* FFmpeg source-tree headers (DAEDALUS_FFMPEG_SRC) and lands in
|
||||||
|
* PR-Q3a.2. This module's job here is to confirm the link
|
||||||
|
* survives, the callback resolves, the context creates, and
|
||||||
|
* tearing the path back down doesn't perturb the production
|
||||||
|
* AVFrame → V4L2 pipeline.
|
||||||
|
*/
|
||||||
|
#include "shadow_decoder.h"
|
||||||
|
|
||||||
|
#include "ffmpeg_loader.h"
|
||||||
|
#include "log.h"
|
||||||
|
|
||||||
|
#include <libavcodec/avcodec.h>
|
||||||
|
#include <libavutil/frame.h>
|
||||||
|
|
||||||
|
#include <daedalus_decoder.h>
|
||||||
|
|
||||||
|
#include <stdlib.h>
|
||||||
|
#include <string.h>
|
||||||
|
|
||||||
|
struct shadow_decoder {
|
||||||
|
struct ffmpeg_loader *loader;
|
||||||
|
daedalus_decoder *dec; /* lazily created on first frame */
|
||||||
|
int ctx_w; /* coded-frame width at last create */
|
||||||
|
int ctx_h;
|
||||||
|
uint64_t mbs_this_frame;
|
||||||
|
uint64_t total_frames;
|
||||||
|
uint64_t total_mbs;
|
||||||
|
};
|
||||||
|
|
||||||
|
static void shadow_mb_inspect(void *opaque,
|
||||||
|
const struct H264Context *h __attribute__((unused)),
|
||||||
|
int mb_x __attribute__((unused)),
|
||||||
|
int mb_y __attribute__((unused)))
|
||||||
|
{
|
||||||
|
struct shadow_decoder *sh = opaque;
|
||||||
|
sh->mbs_this_frame++;
|
||||||
|
}
|
||||||
|
|
||||||
|
struct shadow_decoder *shadow_decoder_create(struct ffmpeg_loader *loader)
|
||||||
|
{
|
||||||
|
const char *env = getenv("DAEDALUS_SHADOW_MODE");
|
||||||
|
|
||||||
|
if (!env || strcmp(env, "1") != 0)
|
||||||
|
return NULL;
|
||||||
|
|
||||||
|
if (!loader || !loader->ff_h264_set_mb_inspect_cb) {
|
||||||
|
log_warn("shadow_decoder: DAEDALUS_SHADOW_MODE=1 set but "
|
||||||
|
"libavcodec lacks ff_h264_set_mb_inspect_cb — disabled");
|
||||||
|
return NULL;
|
||||||
|
}
|
||||||
|
|
||||||
|
struct shadow_decoder *sh = calloc(1, sizeof(*sh));
|
||||||
|
if (!sh) {
|
||||||
|
log_err("shadow_decoder: out of memory");
|
||||||
|
return NULL;
|
||||||
|
}
|
||||||
|
sh->loader = loader;
|
||||||
|
log_info("shadow_decoder: enabled (DAEDALUS_SHADOW_MODE=1, "
|
||||||
|
"daedalus-decoder version %s)",
|
||||||
|
daedalus_decoder_version());
|
||||||
|
return sh;
|
||||||
|
}
|
||||||
|
|
||||||
|
void shadow_decoder_destroy(struct shadow_decoder *sh)
|
||||||
|
{
|
||||||
|
if (!sh)
|
||||||
|
return;
|
||||||
|
if (sh->dec)
|
||||||
|
daedalus_decoder_destroy(sh->dec);
|
||||||
|
log_info("shadow_decoder: shutdown — observed %llu frames / %llu MBs",
|
||||||
|
(unsigned long long) sh->total_frames,
|
||||||
|
(unsigned long long) sh->total_mbs);
|
||||||
|
free(sh);
|
||||||
|
}
|
||||||
|
|
||||||
|
void shadow_decoder_install_cb(struct shadow_decoder *sh,
|
||||||
|
struct AVCodecContext *avctx)
|
||||||
|
{
|
||||||
|
if (!sh || !avctx)
|
||||||
|
return;
|
||||||
|
/*
|
||||||
|
* Loader's optional-symbol pointer was checked at create time
|
||||||
|
* (we wouldn't be non-NULL otherwise), so the call is safe.
|
||||||
|
*/
|
||||||
|
sh->loader->ff_h264_set_mb_inspect_cb(avctx, shadow_mb_inspect, sh);
|
||||||
|
log_info("shadow_decoder: per-MB callback installed on H.264 ctx");
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Ensure the daedalus_decoder context matches the frame's dimensions.
|
||||||
|
* Rounds up to the H.264 macroblock grid (16-pixel multiples) — the
|
||||||
|
* coded picture is always 16-aligned even when the displayed crop
|
||||||
|
* isn't. Returns 0 on success, -1 on failure (ctx left NULL; caller
|
||||||
|
* logs and continues without shadow dispatch this frame).
|
||||||
|
*/
|
||||||
|
static int shadow_ensure_ctx(struct shadow_decoder *sh, int w, int h)
|
||||||
|
{
|
||||||
|
int coded_w = (w + 15) & ~15;
|
||||||
|
int coded_h = (h + 15) & ~15;
|
||||||
|
|
||||||
|
if (sh->dec && sh->ctx_w == coded_w && sh->ctx_h == coded_h)
|
||||||
|
return 0;
|
||||||
|
|
||||||
|
if (sh->dec) {
|
||||||
|
daedalus_decoder_destroy(sh->dec);
|
||||||
|
sh->dec = NULL;
|
||||||
|
}
|
||||||
|
|
||||||
|
sh->dec = daedalus_decoder_create(coded_w, coded_h);
|
||||||
|
if (!sh->dec) {
|
||||||
|
log_warn("shadow_decoder: daedalus_decoder_create(%dx%d) "
|
||||||
|
"failed — shadow dispatch skipped this stream",
|
||||||
|
coded_w, coded_h);
|
||||||
|
sh->ctx_w = sh->ctx_h = 0;
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
sh->ctx_w = coded_w;
|
||||||
|
sh->ctx_h = coded_h;
|
||||||
|
log_info("shadow_decoder: ctx ready (%dx%d coded, has_qpu=%d)",
|
||||||
|
coded_w, coded_h, daedalus_decoder_has_qpu(sh->dec));
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
void shadow_decoder_on_frame(struct shadow_decoder *sh,
|
||||||
|
const struct AVFrame *fr)
|
||||||
|
{
|
||||||
|
if (!sh || !fr)
|
||||||
|
return;
|
||||||
|
|
||||||
|
(void) shadow_ensure_ctx(sh, fr->width, fr->height);
|
||||||
|
|
||||||
|
sh->total_frames++;
|
||||||
|
sh->total_mbs += sh->mbs_this_frame;
|
||||||
|
|
||||||
|
uint64_t expected = (uint64_t) ((fr->width + 15) >> 4) *
|
||||||
|
(uint64_t) ((fr->height + 15) >> 4);
|
||||||
|
log_info("shadow_decoder: frame #%llu %dx%d — %llu MBs observed "
|
||||||
|
"(expected %llu)",
|
||||||
|
(unsigned long long) sh->total_frames,
|
||||||
|
fr->width, fr->height,
|
||||||
|
(unsigned long long) sh->mbs_this_frame,
|
||||||
|
(unsigned long long) expected);
|
||||||
|
|
||||||
|
sh->mbs_this_frame = 0;
|
||||||
|
}
|
||||||
@@ -0,0 +1,75 @@
|
|||||||
|
/* SPDX-License-Identifier: BSD-2-Clause */
|
||||||
|
/*
|
||||||
|
* shadow_decoder.h — env-gated parallel daedalus-decoder path.
|
||||||
|
*
|
||||||
|
* When the daemon is launched with DAEDALUS_SHADOW_MODE=1, shadow_decoder
|
||||||
|
* runs alongside libavcodec's normal H.264 decode: a per-MB inspection
|
||||||
|
* callback fires for every macroblock libavcodec emits, and a frame-
|
||||||
|
* boundary hook lets the shadow path observe and (in future PRs)
|
||||||
|
* dispatch the same frame's worth of work through daedalus-decoder's
|
||||||
|
* frame-major UMA pipeline. Production output (AVFrame → V4L2 NV12)
|
||||||
|
* is unchanged regardless of this module's state.
|
||||||
|
*
|
||||||
|
* PR-Q3a.1 scope: wiring only. The callback counts MBs and the per-
|
||||||
|
* frame hook logs the count. No daedalus-decoder dispatch yet; that
|
||||||
|
* lands in PR-Q3a.2 along with the H264Context-introspection path
|
||||||
|
* gated on the patched FFmpeg source-tree headers.
|
||||||
|
*
|
||||||
|
* Disabled state (env unset or libavcodec lacks ff_h264_set_mb_inspect_cb)
|
||||||
|
* is a hard NULL — shadow_decoder_create() returns NULL, all other
|
||||||
|
* entry points are safe with NULL and become no-ops.
|
||||||
|
*
|
||||||
|
* The daedalus-decoder context, when active, is created lazily on the
|
||||||
|
* first observed frame (dimensions come from libavcodec's AVFrame, not
|
||||||
|
* from the SPS — keeps init independent of stream-header bring-up
|
||||||
|
* order) and re-created on resolution change.
|
||||||
|
*/
|
||||||
|
#ifndef DAEDALUS_V4L2_SHADOW_DECODER_H
|
||||||
|
#define DAEDALUS_V4L2_SHADOW_DECODER_H
|
||||||
|
|
||||||
|
#include <stdint.h>
|
||||||
|
|
||||||
|
struct ffmpeg_loader;
|
||||||
|
struct AVCodecContext;
|
||||||
|
struct AVFrame;
|
||||||
|
struct shadow_decoder;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* shadow_decoder_create - allocate shadow state if env-enabled
|
||||||
|
* @loader: borrowed FFmpeg loader (must outlive the returned ctx)
|
||||||
|
*
|
||||||
|
* Probes DAEDALUS_SHADOW_MODE env var and the loader's optional
|
||||||
|
* ff_h264_set_mb_inspect_cb pointer. Returns NULL when shadow mode
|
||||||
|
* is disabled or unsupported; that's the normal production state.
|
||||||
|
* Returns a usable handle otherwise. Caller owns the handle and must
|
||||||
|
* call shadow_decoder_destroy.
|
||||||
|
*/
|
||||||
|
struct shadow_decoder *shadow_decoder_create(struct ffmpeg_loader *loader);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* shadow_decoder_destroy - tear down. Safe with NULL.
|
||||||
|
*/
|
||||||
|
void shadow_decoder_destroy(struct shadow_decoder *sh);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* shadow_decoder_install_cb - install the per-MB inspection callback
|
||||||
|
* on a freshly-opened H.264 AVCodecContext
|
||||||
|
*
|
||||||
|
* Safe with NULL @sh (NOP). Should be called once per H.264 codec
|
||||||
|
* open; repeated calls just reinstall and are harmless.
|
||||||
|
*/
|
||||||
|
void shadow_decoder_install_cb(struct shadow_decoder *sh,
|
||||||
|
struct AVCodecContext *avctx);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* shadow_decoder_on_frame - per-frame boundary hook
|
||||||
|
*
|
||||||
|
* Called after avcodec_receive_frame returns a frame. Logs the per-
|
||||||
|
* frame MB counter, resets it, and (in future PRs) drives
|
||||||
|
* daedalus_decoder_flush_frame + the AVFrame-vs-shadow diff. Safe
|
||||||
|
* with NULL @sh.
|
||||||
|
*/
|
||||||
|
void shadow_decoder_on_frame(struct shadow_decoder *sh,
|
||||||
|
const struct AVFrame *fr);
|
||||||
|
|
||||||
|
#endif /* DAEDALUS_V4L2_SHADOW_DECODER_H */
|
||||||
@@ -0,0 +1,479 @@
|
|||||||
|
/* 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;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Case 5: Temporal Delimiter is exactly 2 bytes 0x12 0x00. */
|
||||||
|
static int test_temporal_delimiter(void)
|
||||||
|
{
|
||||||
|
uint8_t out[4];
|
||||||
|
size_t n;
|
||||||
|
|
||||||
|
memset(out, 0xff, sizeof(out));
|
||||||
|
n = av1_synth_temporal_delimiter_obu(out, sizeof(out));
|
||||||
|
CHECK_EQ(n, 2, "TD length");
|
||||||
|
CHECK_EQ(out[0], 0x12, "TD obu header byte (obu_type=2, has_size=1)");
|
||||||
|
CHECK_EQ(out[1], 0x00, "TD leb128 size = 0");
|
||||||
|
printf(" temporal delimiter: OK\n");
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Test fixtures for Frame Header cases. */
|
||||||
|
static void mk_seq_1080p_p0(struct v4l2_ctrl_av1_sequence *seq)
|
||||||
|
{
|
||||||
|
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;
|
||||||
|
seq->max_frame_height_minus_1 = 1079;
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
|
||||||
|
static void mk_frame_key_1080p(struct v4l2_ctrl_av1_frame *fr)
|
||||||
|
{
|
||||||
|
memset(fr, 0, sizeof(*fr));
|
||||||
|
fr->frame_type = V4L2_AV1_KEY_FRAME;
|
||||||
|
fr->frame_width_minus_1 = 1919;
|
||||||
|
fr->frame_height_minus_1 = 1079;
|
||||||
|
fr->render_width_minus_1 = 1919;
|
||||||
|
fr->render_height_minus_1 = 1079;
|
||||||
|
fr->primary_ref_frame = 7; /* PRIMARY_REF_NONE */
|
||||||
|
fr->quantization.base_q_idx = 60;
|
||||||
|
fr->loop_filter.level[0] = 16;
|
||||||
|
fr->loop_filter.level[1] = 16;
|
||||||
|
fr->loop_filter.level[2] = 16;
|
||||||
|
fr->loop_filter.level[3] = 16;
|
||||||
|
fr->cdef.bits = 0;
|
||||||
|
fr->loop_restoration.frame_restoration_type[0] = V4L2_AV1_FRAME_RESTORE_NONE;
|
||||||
|
fr->loop_restoration.frame_restoration_type[1] = V4L2_AV1_FRAME_RESTORE_NONE;
|
||||||
|
fr->loop_restoration.frame_restoration_type[2] = V4L2_AV1_FRAME_RESTORE_NONE;
|
||||||
|
fr->interpolation_filter = 0;
|
||||||
|
fr->tx_mode = V4L2_AV1_TX_MODE_SELECT;
|
||||||
|
fr->flags = V4L2_AV1_FRAME_FLAG_SHOW_FRAME;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Case 6: KEY frame at 1080p — happy path, structural smoke. */
|
||||||
|
static int test_frame_header_key_1080p(void)
|
||||||
|
{
|
||||||
|
struct v4l2_ctrl_av1_sequence seq;
|
||||||
|
struct v4l2_ctrl_av1_frame fr;
|
||||||
|
uint8_t out[256];
|
||||||
|
size_t n;
|
||||||
|
struct br br;
|
||||||
|
uint32_t bit;
|
||||||
|
|
||||||
|
mk_seq_1080p_p0(&seq);
|
||||||
|
mk_frame_key_1080p(&fr);
|
||||||
|
|
||||||
|
n = av1_synth_frame_header_obu(&seq, &fr, out, sizeof(out));
|
||||||
|
CHECK(n > 0 && n <= sizeof(out), "FH synth returned %zu", n);
|
||||||
|
|
||||||
|
/* OBU header byte: obu_type=3 (FRAME_HEADER), has_size_field=1
|
||||||
|
* → 0_0011_0_1_0 = 0x1A. */
|
||||||
|
CHECK_EQ(out[0], 0x1A, "FH obu header byte");
|
||||||
|
CHECK((out[1] & 0x80) == 0, "leb128 single byte");
|
||||||
|
CHECK_EQ(n, 2 + (size_t)(out[1] & 0x7f), "total = header+leb+payload");
|
||||||
|
|
||||||
|
br_init(&br, out + 2, n - 2);
|
||||||
|
bit = br_get(&br, 1); CHECK_EQ(bit, 0, "show_existing_frame");
|
||||||
|
bit = br_get(&br, 2); CHECK_EQ(bit, 0, "frame_type=KEY");
|
||||||
|
bit = br_get(&br, 1); CHECK_EQ(bit, 1, "show_frame");
|
||||||
|
bit = br_get(&br, 1); CHECK_EQ(bit, 0, "disable_cdf_update");
|
||||||
|
bit = br_get(&br, 1); CHECK_EQ(bit, 0, "allow_screen_content_tools");
|
||||||
|
|
||||||
|
printf(" KEY frame 1080p: OK (%zu bytes)\n", n);
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Case 7: INTER frame — coverage smoke. */
|
||||||
|
static int test_frame_header_inter(void)
|
||||||
|
{
|
||||||
|
struct v4l2_ctrl_av1_sequence seq;
|
||||||
|
struct v4l2_ctrl_av1_frame fr;
|
||||||
|
uint8_t out[256];
|
||||||
|
size_t n;
|
||||||
|
int i;
|
||||||
|
|
||||||
|
mk_seq_1080p_p0(&seq);
|
||||||
|
mk_frame_key_1080p(&fr);
|
||||||
|
fr.frame_type = V4L2_AV1_INTER_FRAME;
|
||||||
|
fr.primary_ref_frame = 0;
|
||||||
|
for (i = 0; i < V4L2_AV1_REFS_PER_FRAME; i++)
|
||||||
|
fr.ref_frame_idx[i] = (int8_t)(i & 7);
|
||||||
|
fr.flags |= V4L2_AV1_FRAME_FLAG_REFERENCE_SELECT;
|
||||||
|
|
||||||
|
n = av1_synth_frame_header_obu(&seq, &fr, out, sizeof(out));
|
||||||
|
CHECK(n > 0, "INTER FH synth returned %zu", n);
|
||||||
|
CHECK_EQ(out[0], 0x1A, "FH obu header");
|
||||||
|
printf(" INTER frame: OK (%zu bytes)\n", n);
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Case 8: SWITCH frame should be rejected. */
|
||||||
|
static int test_frame_header_switch_rejected(void)
|
||||||
|
{
|
||||||
|
struct v4l2_ctrl_av1_sequence seq;
|
||||||
|
struct v4l2_ctrl_av1_frame fr;
|
||||||
|
uint8_t out[256];
|
||||||
|
size_t n;
|
||||||
|
|
||||||
|
mk_seq_1080p_p0(&seq);
|
||||||
|
mk_frame_key_1080p(&fr);
|
||||||
|
fr.frame_type = V4L2_AV1_SWITCH_FRAME;
|
||||||
|
n = av1_synth_frame_header_obu(&seq, &fr, out, sizeof(out));
|
||||||
|
CHECK_EQ(n, 0, "SWITCH frame should be out of scope");
|
||||||
|
printf(" SWITCH frame rejected: OK\n");
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Case 9: segmentation enabled should be rejected. */
|
||||||
|
static int test_frame_header_segmentation_rejected(void)
|
||||||
|
{
|
||||||
|
struct v4l2_ctrl_av1_sequence seq;
|
||||||
|
struct v4l2_ctrl_av1_frame fr;
|
||||||
|
uint8_t out[256];
|
||||||
|
size_t n;
|
||||||
|
|
||||||
|
mk_seq_1080p_p0(&seq);
|
||||||
|
mk_frame_key_1080p(&fr);
|
||||||
|
fr.segmentation.flags = V4L2_AV1_SEGMENTATION_FLAG_ENABLED;
|
||||||
|
n = av1_synth_frame_header_obu(&seq, &fr, out, sizeof(out));
|
||||||
|
CHECK_EQ(n, 0, "segmentation-enabled should be out of scope");
|
||||||
|
printf(" segmentation enabled rejected: 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();
|
||||||
|
|
||||||
|
printf("=== av1_synth_temporal_delimiter_obu ===\n");
|
||||||
|
fail |= test_temporal_delimiter();
|
||||||
|
|
||||||
|
printf("=== av1_synth_frame_header_obu ===\n");
|
||||||
|
fail |= test_frame_header_key_1080p();
|
||||||
|
fail |= test_frame_header_inter();
|
||||||
|
fail |= test_frame_header_switch_rejected();
|
||||||
|
fail |= test_frame_header_segmentation_rejected();
|
||||||
|
|
||||||
|
if (fail) {
|
||||||
|
fprintf(stderr, "AV1 OBU synth tests FAILED\n");
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
printf("AV1 OBU synth tests PASSED\n");
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
@@ -167,6 +167,26 @@ static int daedalus_chardev_release(struct inode *inode, struct file *file)
|
|||||||
}
|
}
|
||||||
mutex_unlock(&dev->req_lock);
|
mutex_unlock(&dev->req_lock);
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Drain the V4L2-side in-flight list before the daemon goes
|
||||||
|
* away. Any REQ_DECODE we already sent to the daemon won't
|
||||||
|
* get a matching RESP_FRAME — without this drain,
|
||||||
|
* v4l2_m2m_cancel_job() in the V4L2 consumer's close() path
|
||||||
|
* (or in vb2's STREAMOFF path) blocks forever waiting for a
|
||||||
|
* job_finish that will never arrive, and the consumer becomes
|
||||||
|
* unkillable D-state. Issue #146.
|
||||||
|
*
|
||||||
|
* Done AFTER draining the request queue: any REQ_DECODE still
|
||||||
|
* sitting in dev->req_queue is per definition not yet "in
|
||||||
|
* flight" (the kernel never released it to the daemon), so it
|
||||||
|
* doesn't need the m2m-job-finish dance — freeing the message
|
||||||
|
* is sufficient. The inflight list holds entries the kernel
|
||||||
|
* already committed to (added in device_run after the message
|
||||||
|
* was queued or written), which is exactly what needs to be
|
||||||
|
* failed back to vb2 here.
|
||||||
|
*/
|
||||||
|
daedalus_drain_inflight_on_disconnect();
|
||||||
|
|
||||||
mutex_lock(&dev->open_lock);
|
mutex_lock(&dev->open_lock);
|
||||||
dev->opened = 0;
|
dev->opened = 0;
|
||||||
mutex_unlock(&dev->open_lock);
|
mutex_unlock(&dev->open_lock);
|
||||||
|
|||||||
@@ -1005,6 +1005,65 @@ void daedalus_complete_resp_frame(u32 cookie,
|
|||||||
kfree(inf);
|
kfree(inf);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* -- daemon disconnect drain ----------------------------------------- */
|
||||||
|
|
||||||
|
void daedalus_drain_inflight_on_disconnect(void)
|
||||||
|
{
|
||||||
|
struct daedalus_dev *dev = g_daedalus_dev;
|
||||||
|
struct daedalus_inflight *inf, *tmp;
|
||||||
|
LIST_HEAD(local);
|
||||||
|
|
||||||
|
if (!dev)
|
||||||
|
return;
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Splice the in-flight list onto a local list under the lock,
|
||||||
|
* then process each entry with the lock dropped — every
|
||||||
|
* v4l2_m2m_buf_done_and_job_finish call may itself try to
|
||||||
|
* re-enter device_run via the scheduler (which would need to
|
||||||
|
* walk dev->inflight again on a future REQ_DECODE), and
|
||||||
|
* v4l2_m2m_buf_done can sleep via vb2's buffer-done dispatch.
|
||||||
|
* Holding inflight_lock across either is a deadlock invitation.
|
||||||
|
*/
|
||||||
|
mutex_lock(&dev->inflight_lock);
|
||||||
|
list_splice_init(&dev->inflight, &local);
|
||||||
|
mutex_unlock(&dev->inflight_lock);
|
||||||
|
|
||||||
|
list_for_each_entry_safe(inf, tmp, &local, list) {
|
||||||
|
list_del(&inf->list);
|
||||||
|
|
||||||
|
v4l2_warn(&dev->v4l2_dev,
|
||||||
|
"draining inflight cookie=%u (daemon disconnect)\n",
|
||||||
|
inf->cookie);
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Complete the per-request control state before
|
||||||
|
* buf_done_and_job_finish, same ordering as the success
|
||||||
|
* path in daedalus_complete_resp_frame(). For non-request
|
||||||
|
* flows inf->req is NULL and v4l2_ctrl_request_complete
|
||||||
|
* no-ops.
|
||||||
|
*/
|
||||||
|
if (inf->req)
|
||||||
|
v4l2_ctrl_request_complete(inf->req, &inf->ctx->hdl);
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Mark both buffers ERROR and clear the m2m scheduler's
|
||||||
|
* job_running flag. This is what unsticks
|
||||||
|
* v4l2_m2m_cancel_job() inside the consumer's close()
|
||||||
|
* path; without it, the consumer hangs in TASK_UNINTERRUPTIBLE
|
||||||
|
* forever (issue #146).
|
||||||
|
*/
|
||||||
|
v4l2_m2m_buf_done_and_job_finish(dev->m2m_dev,
|
||||||
|
inf->ctx->m2m_ctx,
|
||||||
|
VB2_BUF_STATE_ERROR);
|
||||||
|
|
||||||
|
if (inf->req)
|
||||||
|
media_request_put(inf->req);
|
||||||
|
|
||||||
|
kfree(inf);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/* -- v4l2_ioctl_ops -------------------------------------------------- */
|
/* -- v4l2_ioctl_ops -------------------------------------------------- */
|
||||||
|
|
||||||
static int daedalus_querycap(struct file *file, void *priv,
|
static int daedalus_querycap(struct file *file, void *priv,
|
||||||
|
|||||||
@@ -103,4 +103,27 @@ void daedalus_complete_resp_frame(u32 cookie,
|
|||||||
int daedalus_export_capture_dmabuf(u32 cookie, u32 plane, u32 flags,
|
int daedalus_export_capture_dmabuf(u32 cookie, u32 plane, u32 flags,
|
||||||
int *out_fd);
|
int *out_fd);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* daedalus_drain_inflight_on_disconnect() - fail all in-flight m2m jobs
|
||||||
|
*
|
||||||
|
* Called from daedalus_chardev_release() when the daemon disconnects
|
||||||
|
* (graceful close, SIGKILL, daemon crash — anything that triggers
|
||||||
|
* chardev release). Walks the in-flight list and, for every entry,
|
||||||
|
* marks both src+dst buffers VB2_BUF_STATE_ERROR and calls
|
||||||
|
* v4l2_m2m_buf_done_and_job_finish() to clear the m2m scheduler's
|
||||||
|
* "job_running" flag.
|
||||||
|
*
|
||||||
|
* Without this, v4l2_m2m_cancel_job() (called from
|
||||||
|
* v4l2_m2m_ctx_release() during the consumer's close() / task exit)
|
||||||
|
* blocks forever waiting for a job_finish that the dead daemon will
|
||||||
|
* never send — the consumer enters TASK_UNINTERRUPTIBLE and survives
|
||||||
|
* SIGKILL until reboot. See issue #146 for the full trace.
|
||||||
|
*
|
||||||
|
* Safe to call with an empty in-flight list; no-op in that case.
|
||||||
|
* Must NOT be called from atomic context — uses inflight_lock
|
||||||
|
* (sleeping mutex) and v4l2_m2m_buf_done_and_job_finish (which can
|
||||||
|
* sleep via vb2 buffer-done dispatch).
|
||||||
|
*/
|
||||||
|
void daedalus_drain_inflight_on_disconnect(void);
|
||||||
|
|
||||||
#endif /* DAEDALUS_V4L2_MAIN_H */
|
#endif /* DAEDALUS_V4L2_MAIN_H */
|
||||||
|
|||||||
Reference in New Issue
Block a user