From 1e0c161ef71421c3210a505ca34d7ea313ae1078 Mon Sep 17 00:00:00 2001 From: Andrey Norkin Date: Wed, 5 Aug 2026 14:24:38 -0700 Subject: [PATCH 1/5] Fix incorrect display_clock_tick calculation --- av2/common/level.c | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/av2/common/level.c b/av2/common/level.c index 2c33bc62d3..d3374e1ed1 100644 --- a/av2/common/level.c +++ b/av2/common/level.c @@ -826,8 +826,7 @@ void av2_decoder_model_init(const AV2_COMP *const cpi, AV2_LEVEL level, decoder_model->num_ticks_per_picture = cm->ci_params_encoder.timing_info.num_ticks_per_elemental_duration; decoder_model->display_clock_tick = - (double) - cm->ci_params_encoder.timing_info.num_ticks_per_elemental_duration / + (double)cm->ci_params_encoder.timing_info.num_units_in_display_tick / cm->ci_params_encoder.timing_info.time_scale; } else { decoder_model->num_ticks_per_picture = 1; From e950d853dd740325df5d4e73a680c4831876ebde Mon Sep 17 00:00:00 2001 From: Andrey Norkin Date: Wed, 5 Aug 2026 15:28:51 -0700 Subject: [PATCH 2/5] Decoder model implementation --- apps/avmdec.c | 33 +- av2/av2.cmake | 4 + av2/av2_dx_iface.c | 95 +- av2/common/av2_common_int.h | 2 + av2/common/decoder_model.c | 3910 ++++++++++++++++++++++++ av2/common/decoder_model.h | 458 +++ av2/decoder/annexF.c | 75 + av2/decoder/annexF.h | 9 + av2/decoder/decodeframe.c | 48 + av2/decoder/decoder.c | 64 +- av2/decoder/decoder.h | 4 + av2/decoder/decoder_model.c | 3654 ++++++++++++++++++++++ av2/decoder/decoder_model.h | 167 + av2/decoder/obu.c | 58 +- av2/decoder/obu_buf.c | 4 + av2/decoder/obu_ops.c | 9 +- avm/avmdx.h | 21 + avm_dsp/bitreader.c | 2 + avm_dsp/bitreader.h | 5 + test/brt_test.cc | 8 + test/decoder_model_integration_test.cc | 2305 ++++++++++++++ test/decoder_model_parser_test.cc | 762 +++++ test/decoder_model_test.cc | 2828 +++++++++++++++++ test/level_test.cc | 23 + test/ops_test.cc | 39 + test/test.cmake | 3 + 26 files changed, 14570 insertions(+), 20 deletions(-) create mode 100644 av2/common/decoder_model.c create mode 100644 av2/common/decoder_model.h create mode 100644 av2/decoder/decoder_model.c create mode 100644 av2/decoder/decoder_model.h create mode 100644 test/decoder_model_integration_test.cc create mode 100644 test/decoder_model_parser_test.cc create mode 100644 test/decoder_model_test.cc diff --git a/apps/avmdec.c b/apps/avmdec.c index 1d80f28558..8ed482ce32 100644 --- a/apps/avmdec.c +++ b/apps/avmdec.c @@ -113,6 +113,9 @@ static const arg_def_t verifyarg = ARG_DEF(NULL, "verify", 1, "Use Decoded Frame Hash Metadata to verify integrity of decoded " "frames (off, fatal, warn)"); +static const arg_def_t checkconformancearg = + ARG_DEF(NULL, "check-conformance", 1, + "Check decoder-model conformance (off, warn, fatal)"); static const arg_def_t framestatsarg = ARG_DEF(NULL, "framestats", 1, "Output per-frame stats (.csv format)"); static const arg_def_t outbitdeptharg = @@ -159,6 +162,7 @@ static const arg_def_t *all_args[] = { &help, &fb_arg, &md5arg, &verifyarg, + &checkconformancearg, &framestatsarg, &continuearg, &outbitdeptharg, @@ -654,6 +658,8 @@ static int main_loop(int argc, const char **argv_) { int frame_in = 0, frame_out = 0, flipuv = 0, noblit = 0; int do_md5 = 0, progress = 0; int do_verify = 0, error_on_verify = 0; + avm_decoder_model_check_mode_t decoder_model_check_mode = + AVM_DECODER_MODEL_CHECK_OFF; int stop_after = 0, summary = 0, quiet = 1; int arg_skip = 0; int num_streams = 1; @@ -812,6 +818,16 @@ static int main_loop(int argc, const char **argv_) { error_on_verify = 1; } else if (strcmp(arg.val, "off")) die("Error: Invalid argument for --verify (%s).\n", arg.val); + } else if (arg_match(&arg, &checkconformancearg, argi)) { + if (!strcmp(arg.val, "warn")) { + decoder_model_check_mode = AVM_DECODER_MODEL_CHECK_WARN; + } else if (!strcmp(arg.val, "fatal")) { + decoder_model_check_mode = AVM_DECODER_MODEL_CHECK_FATAL; + } else if (!strcmp(arg.val, "off")) { + decoder_model_check_mode = AVM_DECODER_MODEL_CHECK_OFF; + } else { + die("Error: Invalid argument for --check-conformance (%s).\n", arg.val); + } } else if (arg_match(&arg, &framestatsarg, argi)) { framestats_file = fopen(arg.val, "w"); if (!framestats_file) { @@ -1004,6 +1020,13 @@ static int main_loop(int argc, const char **argv_) { if (!quiet) fprintf(stderr, "%s\n", decoder.name); + if (AVM_CODEC_CONTROL_TYPECHECKED(&decoder, AV2D_SET_DECODER_MODEL_CHECK_MODE, + decoder_model_check_mode)) { + fprintf(stderr, "Failed to set decoder-model conformance mode: %s\n", + avm_codec_error(&decoder)); + goto fail; + } + // Only set selected OPS when explicitly requested via --select-ops. // Setting it to 0,0 by default enables sub-bitstream extraction (SBE), // which can incorrectly filter out frame OBUs in multi-layer bitstreams @@ -1096,13 +1119,19 @@ static int main_loop(int argc, const char **argv_) { avm_usec_timer_start(&timer); - if (avm_codec_decode(&decoder, buf, bytes_in_buffer, NULL)) { + const avm_codec_err_t decode_status = + avm_codec_decode(&decoder, buf, bytes_in_buffer, NULL); + if (decode_status != AVM_CODEC_OK) { const char *detail = avm_codec_error_detail(&decoder); warn("Failed to decode frame %d: %s", frame_in, avm_codec_error(&decoder)); if (detail) warn("Additional information: %s", detail); - if (!keep_going) goto fail; + if (!keep_going || + (detail != NULL && + !strcmp(detail, "Decoder model conformance violation"))) { + goto fail; + } } if (framestats_file) { diff --git a/av2/av2.cmake b/av2/av2.cmake index 57f639be12..8098bf851b 100644 --- a/av2/av2.cmake +++ b/av2/av2.cmake @@ -47,6 +47,8 @@ list( "${AVM_ROOT}/av2/common/common_data.h" "${AVM_ROOT}/av2/common/convolve.c" "${AVM_ROOT}/av2/common/convolve.h" + "${AVM_ROOT}/av2/common/decoder_model.c" + "${AVM_ROOT}/av2/common/decoder_model.h" "${AVM_ROOT}/av2/common/entropy.c" "${AVM_ROOT}/av2/common/entropy.h" "${AVM_ROOT}/av2/common/entropymode.c" @@ -157,6 +159,8 @@ list( "${AVM_ROOT}/av2/decoder/decodemv.h" "${AVM_ROOT}/av2/decoder/decoder.c" "${AVM_ROOT}/av2/decoder/decoder.h" + "${AVM_ROOT}/av2/decoder/decoder_model.c" + "${AVM_ROOT}/av2/decoder/decoder_model.h" "${AVM_ROOT}/av2/decoder/decodetxb.c" "${AVM_ROOT}/av2/decoder/decodetxb.h" "${AVM_ROOT}/av2/decoder/detokenize.c" diff --git a/av2/av2_dx_iface.c b/av2/av2_dx_iface.c index d65e86fff9..a393bdc289 100644 --- a/av2/av2_dx_iface.c +++ b/av2/av2_dx_iface.c @@ -34,6 +34,7 @@ #include "av2/decoder/decoder.h" #include "av2/decoder/decodeframe.h" +#include "av2/decoder/decoder_model.h" #include "av2/decoder/obu.h" #include "avm_dsp/bitwriter_buffer.h" @@ -62,6 +63,9 @@ struct avm_codec_alg_priv { int local_ops_selections[MAX_NUM_XLAYERS - 1][3]; int num_local_ops_selections; int output_all_layers; + avm_decoder_model_check_mode_t decoder_model_check_mode; + int compressed_input_started; + int decoder_model_fatal_latched; AVxWorker *frame_worker; @@ -116,6 +120,7 @@ static avm_codec_err_t decoder_init(avm_codec_ctx_t *ctx) { priv->random_access_point_index = 0; priv->enable_sub_bitstream_extraction = 0; priv->num_local_ops_selections = 0; + priv->decoder_model_check_mode = AVM_DECODER_MODEL_CHECK_OFF; init_ibp_info(ctx->priv->ibp_directional_weights); } @@ -401,6 +406,10 @@ static int frame_worker_hook(void *arg1, void *arg2) { if (result != 0) { // Check decode result in serial decode. + if (frame_worker_data->pbi->decoder_model_verifier != NULL && + !av2_decoder_model_verifier_should_stop(frame_worker_data->pbi)) { + av2_decoder_model_verifier_on_recovery_reset(frame_worker_data->pbi); + } frame_worker_data->pbi->need_resync = 1; } return !result; @@ -473,6 +482,11 @@ static avm_codec_err_t init_decoder(avm_codec_alg_priv_t *ctx) { } } frame_worker_data->pbi->output_all_layers = ctx->output_all_layers; + frame_worker_data->pbi->decoder_model_check_mode = + ctx->decoder_model_check_mode; + if (ctx->decoder_model_check_mode != AVM_DECODER_MODEL_CHECK_OFF) { + av2_decoder_model_verifier_init(frame_worker_data->pbi); + } frame_worker_data->pbi->row_mt = ctx->row_mt; frame_worker_data->pbi->is_fwd_kf_present = 0; frame_worker_data->pbi->enable_subgop_stats = ctx->enable_subgop_stats; @@ -578,6 +592,10 @@ static avm_codec_err_t decoder_inspect(avm_codec_alg_priv_t *ctx, frame_worker_data->pbi->inspect_tip_cb = ctx->inspect_tip_cb; frame_worker_data->pbi->inspect_ctx = ctx->inspect_ctx; res = av2_receive_compressed_data(frame_worker_data->pbi, data_sz, &data); + if (res != AVM_CODEC_OK && pbi->decoder_model_verifier != NULL && + !av2_decoder_model_verifier_should_stop(frame_worker_data->pbi)) { + av2_decoder_model_verifier_on_recovery_reset(frame_worker_data->pbi); + } check_resync(ctx, frame_worker_data->pbi); if (ctx->frame_worker->had_error) @@ -910,7 +928,26 @@ static avm_codec_err_t decoder_decode(avm_codec_alg_priv_t *ctx, #if CONFIG_INSPECTION if (user_priv != 0) { - return decoder_inspect(ctx, data, data_sz, user_priv); + if (data != NULL && data_sz != 0) { + ctx->compressed_input_started = 1; + if (ctx->decoder_model_fatal_latched) { + set_error_detail(ctx, "Decoder model conformance violation"); + return AVM_CODEC_UNSUP_BITSTREAM; + } + } + res = decoder_inspect(ctx, data, data_sz, user_priv); + if (ctx->frame_worker != NULL) { + FrameWorkerData *const frame_worker_data = + (FrameWorkerData *)ctx->frame_worker->data1; + AV2Decoder *const pbi = frame_worker_data->pbi; + if (av2_decoder_model_verifier_should_stop(pbi)) { + av2_decoder_model_verifier_finish(pbi); + ctx->decoder_model_fatal_latched = 1; + set_error_detail(ctx, "Decoder model conformance violation"); + return AVM_CODEC_UNSUP_BITSTREAM; + } + } + return res; } #endif @@ -945,6 +982,15 @@ static avm_codec_err_t decoder_decode(avm_codec_alg_priv_t *ctx, if (data == NULL && data_sz == 0) { AV2_COMMON *const cm = &pbi->common; avm_codec_err_t err = flush_all_xlayer_frames(pbi, cm, false); + if (pbi->decoder_model_verifier != NULL || + pbi->decoder_model_verifier_allocation_failed) { + av2_decoder_model_verifier_finish(pbi); + } + if (av2_decoder_model_verifier_should_stop(pbi)) { + ctx->decoder_model_fatal_latched = 1; + set_error_detail(ctx, "Decoder model conformance violation"); + return AVM_CODEC_UNSUP_BITSTREAM; + } bool global_lcr_present = false; bool local_lcr_present = false; @@ -976,6 +1022,12 @@ static avm_codec_err_t decoder_decode(avm_codec_alg_priv_t *ctx, } if (data == NULL || data_sz == 0) return AVM_CODEC_INVALID_PARAM; + ctx->compressed_input_started = 1; + if (ctx->decoder_model_fatal_latched) { + set_error_detail(ctx, "Decoder model conformance violation"); + return AVM_CODEC_UNSUP_BITSTREAM; + } + // Reset flushed when receiving a valid frame. ctx->flushed = 0; @@ -1105,13 +1157,29 @@ static avm_codec_err_t decoder_decode(avm_codec_alg_priv_t *ctx, // Decode in serial mode. + // This boundary is established by the raw pre-scan, before Annex F can + // remove a frame unit. It therefore remains unique even when consecutive + // source frames are not decoded. + if (pbi->decoder_model_verifier != NULL) { + av2_decoder_model_verifier_on_source_frame_unit_start( + pbi, xlayer_id, mlayer_id, tlayer_id); + } + res = decode_one(ctx, &data_start, frame_unit_size, user_priv); if (res != AVM_CODEC_OK) return res; set_last_frame_unit(frame_worker_data->pbi); free(frame_worker_data->pbi->obu_list); + frame_worker_data->pbi->obu_list = NULL; frame_worker_data->pbi->num_obus_with_frame_unit = 0; + + if (av2_decoder_model_verifier_should_stop(pbi)) { + av2_decoder_model_verifier_finish(pbi); + ctx->decoder_model_fatal_latched = 1; + set_error_detail(ctx, "Decoder model conformance violation"); + return AVM_CODEC_UNSUP_BITSTREAM; + } } if (data_start != data_end) { @@ -1974,6 +2042,30 @@ static avm_codec_err_t ctrl_set_output_all_layers(avm_codec_alg_priv_t *ctx, return AVM_CODEC_OK; } +static avm_codec_err_t ctrl_set_decoder_model_check_mode( + avm_codec_alg_priv_t *ctx, va_list args) { + const int raw_mode = va_arg(args, int); + if (raw_mode < AVM_DECODER_MODEL_CHECK_OFF || + raw_mode > AVM_DECODER_MODEL_CHECK_WARN || + ctx->compressed_input_started) { + return AVM_CODEC_INVALID_PARAM; + } + const avm_decoder_model_check_mode_t mode = + (avm_decoder_model_check_mode_t)raw_mode; + ctx->decoder_model_check_mode = mode; + if (ctx->frame_worker != NULL) { + FrameWorkerData *const frame_worker_data = + (FrameWorkerData *)ctx->frame_worker->data1; + AV2Decoder *const pbi = frame_worker_data->pbi; + av2_decoder_model_verifier_destroy(pbi); + pbi->decoder_model_check_mode = mode; + if (mode != AVM_DECODER_MODEL_CHECK_OFF) { + av2_decoder_model_verifier_init(pbi); + } + } + return AVM_CODEC_OK; +} + static avm_codec_err_t ctrl_set_sub_bitstream_extraction( avm_codec_alg_priv_t *ctx, va_list args) { ctx->enable_sub_bitstream_extraction = va_arg(args, int); @@ -2039,6 +2131,7 @@ static avm_codec_ctrl_fn_map_t decoder_ctrl_maps[] = { { AV2D_SET_SKIP_FILM_GRAIN, ctrl_set_skip_film_grain }, { AV2D_SET_RANDOM_ACCESS, ctrl_set_random_access }, { AV2D_SET_BRU_OPT_MODE, ctrl_set_bru_opt_mode }, + { AV2D_SET_DECODER_MODEL_CHECK_MODE, ctrl_set_decoder_model_check_mode }, { AV2D_ENABLE_SUBGOP_STATS, ctrl_enable_subgop_stats }, // Getters diff --git a/av2/common/av2_common_int.h b/av2/common/av2_common_int.h index e90a00f57b..c327d18994 100644 --- a/av2/common/av2_common_int.h +++ b/av2/common/av2_common_int.h @@ -973,6 +973,7 @@ typedef struct OperatingPoint { // Details per layer int ops_xlayer_map; + bool ops_initial_display_delay_present_flag; int ops_initial_display_delay; int ops_decoder_model_info_for_this_op_present_flag; int ops_mlayer_explicit_info_flag[MAX_NUM_XLAYERS]; @@ -2983,6 +2984,7 @@ typedef struct AV2Common { * Temporal point info */ avm_metadata_temporal_point_info_t temporal_point_info_metadata; + bool temporal_point_info_present; /*! * Order hint of the last encountered OLK diff --git a/av2/common/decoder_model.c b/av2/common/decoder_model.c new file mode 100644 index 0000000000..7c7578ef87 --- /dev/null +++ b/av2/common/decoder_model.c @@ -0,0 +1,3910 @@ +/* + * Copyright (c) 2026, Alliance for Open Media. All rights reserved + * + * This source code is subject to the terms of the BSD 3-Clause Clear License + * and the Alliance for Open Media Patent License 1.0. If the BSD 3-Clause Clear + * License was not distributed with this source code in the LICENSE file, you + * can obtain it at aomedia.org/license/software-license/bsd-3-c-c/. If the + * Alliance for Open Media Patent License 1.0 was not distributed with this + * source code in the PATENTS file, you can obtain it at + * aomedia.org/license/patent-license/. + */ + +#include "av2/common/decoder_model.h" + +#include +#include + +#include "avm_mem/avm_mem.h" + +_Static_assert(sizeof(uint32_t) * CHAR_BIT == 32, "uint32_t must be 32 bits"); +_Static_assert(sizeof(uint64_t) * CHAR_BIT == 64, "uint64_t must be 64 bits"); +_Static_assert(sizeof(Av2DmUnsignedWide) * CHAR_BIT == 256, + "Av2DmUnsignedWide must be 256 bits"); + +#define AV2_DM_WIDE_LIMBS 4 +#define AV2_DM_PRODUCT_LIMBS (2 * AV2_DM_WIDE_LIMBS) + +typedef struct Av2DmUnsignedProduct { + uint64_t limbs[AV2_DM_PRODUCT_LIMBS]; +} Av2DmUnsignedProduct; + +static Av2DmUnsignedWide wide_from_u64(uint64_t value) { + Av2DmUnsignedWide result = { { value, 0, 0, 0 } }; + return result; +} + +static bool wide_is_zero(Av2DmUnsignedWide value) { + for (uint32_t i = 0; i < AV2_DM_WIDE_LIMBS; ++i) { + if (value.limbs[i] != 0) return false; + } + return true; +} + +static bool wide_equals_u64(Av2DmUnsignedWide value, uint64_t expected) { + return value.limbs[0] == expected && value.limbs[1] == 0 && + value.limbs[2] == 0 && value.limbs[3] == 0; +} + +static bool wide_fits_u64(Av2DmUnsignedWide value) { + return value.limbs[1] == 0 && value.limbs[2] == 0 && value.limbs[3] == 0; +} + +static int wide_compare(Av2DmUnsignedWide left, Av2DmUnsignedWide right) { + for (int i = AV2_DM_WIDE_LIMBS - 1; i >= 0; --i) { + if (left.limbs[i] != right.limbs[i]) { + return left.limbs[i] < right.limbs[i] ? -1 : 1; + } + } + return 0; +} + +static bool wide_add(Av2DmUnsignedWide left, Av2DmUnsignedWide right, + Av2DmUnsignedWide *result) { + uint64_t carry = 0; + for (uint32_t i = 0; i < AV2_DM_WIDE_LIMBS; ++i) { + const uint64_t partial = left.limbs[i] + right.limbs[i]; + const uint64_t partial_carry = partial < left.limbs[i]; + const uint64_t sum = partial + carry; + const uint64_t carry_carry = sum < partial; + result->limbs[i] = sum; + carry = partial_carry | carry_carry; + } + return carry == 0; +} + +// Subtraction is modulo 2^256. Callers either establish left >= right or use +// the wraparound result as one step of long division with a 257th carry bit. +static Av2DmUnsignedWide wide_subtract(Av2DmUnsignedWide left, + Av2DmUnsignedWide right) { + Av2DmUnsignedWide result; + uint64_t borrow = 0; + for (uint32_t i = 0; i < AV2_DM_WIDE_LIMBS; ++i) { + const uint64_t partial = left.limbs[i] - right.limbs[i]; + const uint64_t partial_borrow = left.limbs[i] < right.limbs[i]; + result.limbs[i] = partial - borrow; + const uint64_t borrow_borrow = partial < borrow; + borrow = partial_borrow | borrow_borrow; + } + return result; +} + +static uint64_t wide_get_bit(Av2DmUnsignedWide value, uint32_t bit_index) { + return (value.limbs[bit_index / 64] >> (bit_index % 64)) & 1; +} + +static void wide_set_bit(Av2DmUnsignedWide *value, uint32_t bit_index) { + value->limbs[bit_index / 64] |= UINT64_C(1) << (bit_index % 64); +} + +static bool wide_shift_left_one(Av2DmUnsignedWide *value) { + const bool overflow = (value->limbs[AV2_DM_WIDE_LIMBS - 1] >> 63) != 0; + for (int i = AV2_DM_WIDE_LIMBS - 1; i > 0; --i) { + value->limbs[i] = (value->limbs[i] << 1) | (value->limbs[i - 1] >> 63); + } + value->limbs[0] <<= 1; + return overflow; +} + +// Binary long division over the complete two-limb-independent representation. +static bool wide_divide(Av2DmUnsignedWide dividend, Av2DmUnsignedWide divisor, + Av2DmUnsignedWide *quotient, + Av2DmUnsignedWide *remainder) { + if (wide_is_zero(divisor)) return false; + if (wide_fits_u64(dividend) && wide_fits_u64(divisor)) { + *quotient = wide_from_u64(dividend.limbs[0] / divisor.limbs[0]); + *remainder = wide_from_u64(dividend.limbs[0] % divisor.limbs[0]); + return true; + } + Av2DmUnsignedWide result = { { 0, 0, 0, 0 } }; + Av2DmUnsignedWide rem = { { 0, 0, 0, 0 } }; + for (int bit_index = 255; bit_index >= 0; --bit_index) { + const bool overflow = wide_shift_left_one(&rem); + rem.limbs[0] |= wide_get_bit(dividend, (uint32_t)bit_index); + if (overflow || wide_compare(rem, divisor) >= 0) { + rem = wide_subtract(rem, divisor); + wide_set_bit(&result, (uint32_t)bit_index); + } + } + *quotient = result; + *remainder = rem; + return true; +} + +static Av2DmUnsignedWide wide_gcd(Av2DmUnsignedWide left, + Av2DmUnsignedWide right) { + if (wide_fits_u64(left) && wide_fits_u64(right)) { + uint64_t a = left.limbs[0]; + uint64_t b = right.limbs[0]; + while (b != 0) { + const uint64_t remainder = a % b; + a = b; + b = remainder; + } + return wide_from_u64(a); + } + while (!wide_is_zero(right)) { + Av2DmUnsignedWide quotient; + Av2DmUnsignedWide remainder; + if (!wide_divide(left, right, "ient, &remainder)) { + return wide_from_u64(0); + } + left = right; + right = remainder; + } + return left; +} + +// Computes the complete 64-by-64-bit product using only fixed-width portable +// C arithmetic. Unsigned wraparound in the 32-bit partial-product assembly is +// intentional and defined by the C language. +static void multiply_64(uint64_t left, uint64_t right, uint64_t *product_low, + uint64_t *product_high) { + const uint64_t mask = UINT32_MAX; + const uint64_t left_low = left & mask; + const uint64_t left_high = left >> 32; + const uint64_t right_low = right & mask; + const uint64_t right_high = right >> 32; + const uint64_t low = left_low * right_low; + const uint64_t middle_1 = left_high * right_low + (low >> 32); + const uint64_t middle_2 = left_low * right_high + (middle_1 & mask); + *product_high = left_high * right_high + (middle_1 >> 32) + (middle_2 >> 32); + *product_low = (middle_2 << 32) | (low & mask); +} + +static bool product_add_at(Av2DmUnsignedProduct *product, uint32_t index, + uint64_t low, uint64_t high) { + if (index >= AV2_DM_PRODUCT_LIMBS) return low == 0 && high == 0; + const uint64_t old_low = product->limbs[index]; + product->limbs[index] += low; + uint64_t carry = product->limbs[index] < old_low; + ++index; + if (index >= AV2_DM_PRODUCT_LIMBS) return high == 0 && carry == 0; + + const uint64_t old_high = product->limbs[index]; + product->limbs[index] += high; + const uint64_t high_carry = product->limbs[index] < old_high; + const uint64_t partial = product->limbs[index]; + product->limbs[index] += carry; + const uint64_t carry_carry = product->limbs[index] < partial; + carry = high_carry | carry_carry; + ++index; + while (carry != 0 && index < AV2_DM_PRODUCT_LIMBS) { + ++product->limbs[index]; + carry = product->limbs[index] == 0; + ++index; + } + return carry == 0; +} + +static bool wide_multiply(Av2DmUnsignedWide left, Av2DmUnsignedWide right, + Av2DmUnsignedProduct *product) { + memset(product, 0, sizeof(*product)); + for (uint32_t i = 0; i < AV2_DM_WIDE_LIMBS; ++i) { + for (uint32_t j = 0; j < AV2_DM_WIDE_LIMBS; ++j) { + uint64_t low; + uint64_t high; + multiply_64(left.limbs[i], right.limbs[j], &low, &high); + if (!product_add_at(product, i + j, low, high)) return false; + } + } + return true; +} + +static int product_compare(Av2DmUnsignedProduct left, + Av2DmUnsignedProduct right) { + for (int i = AV2_DM_PRODUCT_LIMBS - 1; i >= 0; --i) { + if (left.limbs[i] != right.limbs[i]) { + return left.limbs[i] < right.limbs[i] ? -1 : 1; + } + } + return 0; +} + +static bool product_to_wide(Av2DmUnsignedProduct product, + Av2DmUnsignedWide *result) { + for (uint32_t i = AV2_DM_WIDE_LIMBS; i < AV2_DM_PRODUCT_LIMBS; ++i) { + if (product.limbs[i] != 0) return false; + } + for (uint32_t i = 0; i < AV2_DM_WIDE_LIMBS; ++i) { + result->limbs[i] = product.limbs[i]; + } + return true; +} + +static bool wide_multiply_checked(Av2DmUnsignedWide left, + Av2DmUnsignedWide right, + Av2DmUnsignedWide *result) { + if (wide_fits_u64(left) && wide_fits_u64(right)) { + memset(result, 0, sizeof(*result)); + multiply_64(left.limbs[0], right.limbs[0], &result->limbs[0], + &result->limbs[1]); + return true; + } + Av2DmUnsignedProduct product; + return wide_multiply(left, right, &product) && + product_to_wide(product, result); +} + +static bool rational_normalize(Av2DmRational *value) { + if (wide_is_zero(value->denominator)) return false; + if (wide_is_zero(value->magnitude)) { + value->denominator = wide_from_u64(1); + value->negative = false; + return true; + } + + const Av2DmUnsignedWide divisor = + wide_gcd(value->magnitude, value->denominator); + if (wide_is_zero(divisor)) return false; + if (!wide_equals_u64(divisor, 1)) { + Av2DmUnsignedWide remainder; + Av2DmUnsignedWide reduced; + if (!wide_divide(value->magnitude, divisor, &reduced, &remainder) || + !wide_is_zero(remainder)) { + return false; + } + value->magnitude = reduced; + if (!wide_divide(value->denominator, divisor, &reduced, &remainder) || + !wide_is_zero(remainder)) { + return false; + } + value->denominator = reduced; + } + return true; +} + +bool av2_dm_rational_make(uint64_t numerator, uint64_t denominator, + Av2DmRational *result) { + return av2_dm_rational_make_wide(wide_from_u64(numerator), denominator, false, + result); +} + +bool av2_dm_rational_make_wide(Av2DmUnsignedWide numerator, + uint64_t denominator, bool negative, + Av2DmRational *result) { + if (result == NULL || denominator == 0) return false; + result->magnitude = numerator; + result->denominator = wide_from_u64(denominator); + result->negative = negative; + return rational_normalize(result); +} + +static bool rational_compare_magnitudes(const Av2DmRational *left, + const Av2DmRational *right, + int *comparison) { + // Cancel factors common to both numerators and both denominators before + // forming the exact cross-products. The products are retained in 512 bits. + const Av2DmUnsignedWide numerator_gcd = + wide_gcd(left->magnitude, right->magnitude); + const Av2DmUnsignedWide denominator_gcd = + wide_gcd(left->denominator, right->denominator); + if (wide_is_zero(numerator_gcd) || wide_is_zero(denominator_gcd)) { + return false; + } + Av2DmUnsignedWide left_numerator; + Av2DmUnsignedWide right_numerator; + Av2DmUnsignedWide left_denominator; + Av2DmUnsignedWide right_denominator; + Av2DmUnsignedWide remainder; + if (!wide_divide(left->magnitude, numerator_gcd, &left_numerator, + &remainder) || + !wide_is_zero(remainder) || + !wide_divide(right->magnitude, numerator_gcd, &right_numerator, + &remainder) || + !wide_is_zero(remainder) || + !wide_divide(left->denominator, denominator_gcd, &left_denominator, + &remainder) || + !wide_is_zero(remainder) || + !wide_divide(right->denominator, denominator_gcd, &right_denominator, + &remainder) || + !wide_is_zero(remainder)) { + return false; + } + Av2DmUnsignedProduct left_product; + Av2DmUnsignedProduct right_product; + if (!wide_multiply(left_numerator, right_denominator, &left_product) || + !wide_multiply(right_numerator, left_denominator, &right_product)) { + return false; + } + *comparison = product_compare(left_product, right_product); + return true; +} + +bool av2_dm_rational_add(const Av2DmRational *left, const Av2DmRational *right, + Av2DmRational *result) { + if (left == NULL || right == NULL || result == NULL || + wide_is_zero(left->denominator) || wide_is_zero(right->denominator)) { + return false; + } + Av2DmRational normalized_left = *left; + Av2DmRational normalized_right = *right; + if (!rational_normalize(&normalized_left) || + !rational_normalize(&normalized_right)) { + return false; + } + + const Av2DmUnsignedWide denominator_gcd = + wide_gcd(normalized_left.denominator, normalized_right.denominator); + if (wide_is_zero(denominator_gcd)) return false; + Av2DmUnsignedWide left_multiplier; + Av2DmUnsignedWide right_multiplier; + Av2DmUnsignedWide remainder; + if (!wide_divide(normalized_right.denominator, denominator_gcd, + &left_multiplier, &remainder) || + !wide_is_zero(remainder) || + !wide_divide(normalized_left.denominator, denominator_gcd, + &right_multiplier, &remainder) || + !wide_is_zero(remainder) || + !wide_multiply_checked(normalized_left.denominator, left_multiplier, + &result->denominator)) { + return false; + } + Av2DmUnsignedWide scaled_left; + Av2DmUnsignedWide scaled_right; + if (!wide_multiply_checked(normalized_left.magnitude, left_multiplier, + &scaled_left) || + !wide_multiply_checked(normalized_right.magnitude, right_multiplier, + &scaled_right)) { + return false; + } + + if (normalized_left.negative == normalized_right.negative) { + if (!wide_add(scaled_left, scaled_right, &result->magnitude)) return false; + result->negative = normalized_left.negative; + } else { + const int comparison = wide_compare(scaled_left, scaled_right); + if (comparison >= 0) { + result->magnitude = wide_subtract(scaled_left, scaled_right); + result->negative = normalized_left.negative; + } else { + result->magnitude = wide_subtract(scaled_right, scaled_left); + result->negative = normalized_right.negative; + } + } + return rational_normalize(result); +} + +bool av2_dm_rational_subtract(const Av2DmRational *left, + const Av2DmRational *right, + Av2DmRational *result) { + if (right == NULL) return false; + Av2DmRational negated_right = *right; + if (!wide_is_zero(negated_right.magnitude)) { + negated_right.negative = !negated_right.negative; + } + return av2_dm_rational_add(left, &negated_right, result); +} + +bool av2_dm_rational_multiply_u64(const Av2DmRational *value, + uint64_t multiplier, Av2DmRational *result) { + if (value == NULL || result == NULL || wide_is_zero(value->denominator)) { + return false; + } + Av2DmRational normalized = *value; + if (!rational_normalize(&normalized)) return false; + const Av2DmUnsignedWide wide_multiplier = wide_from_u64(multiplier); + const Av2DmUnsignedWide divisor = + wide_gcd(wide_multiplier, normalized.denominator); + Av2DmUnsignedWide reduced_multiplier; + Av2DmUnsignedWide remainder; + if (!wide_divide(wide_multiplier, divisor, &reduced_multiplier, &remainder) || + !wide_is_zero(remainder) || + !wide_divide(normalized.denominator, divisor, &normalized.denominator, + &remainder) || + !wide_is_zero(remainder) || + !wide_multiply_checked(normalized.magnitude, reduced_multiplier, + &normalized.magnitude)) { + return false; + } + *result = normalized; + return rational_normalize(result); +} + +bool av2_dm_rational_divide_u64(const Av2DmRational *value, uint64_t divisor, + Av2DmRational *result) { + if (value == NULL || result == NULL || wide_is_zero(value->denominator) || + divisor == 0) { + return false; + } + Av2DmRational normalized = *value; + if (!rational_normalize(&normalized)) return false; + const Av2DmUnsignedWide wide_divisor = wide_from_u64(divisor); + const Av2DmUnsignedWide common_divisor = + wide_gcd(normalized.magnitude, wide_divisor); + Av2DmUnsignedWide reduced_divisor; + Av2DmUnsignedWide remainder; + if (!wide_divide(normalized.magnitude, common_divisor, &normalized.magnitude, + &remainder) || + !wide_is_zero(remainder) || + !wide_divide(wide_divisor, common_divisor, &reduced_divisor, + &remainder) || + !wide_is_zero(remainder) || + !wide_multiply_checked(normalized.denominator, reduced_divisor, + &normalized.denominator)) { + return false; + } + *result = normalized; + return rational_normalize(result); +} + +bool av2_dm_rational_compare(const Av2DmRational *left, + const Av2DmRational *right, int *comparison) { + if (left == NULL || right == NULL || comparison == NULL || + wide_is_zero(left->denominator) || wide_is_zero(right->denominator)) { + return false; + } + Av2DmRational normalized_left = *left; + Av2DmRational normalized_right = *right; + if (!rational_normalize(&normalized_left) || + !rational_normalize(&normalized_right)) { + return false; + } + if (wide_is_zero(normalized_left.magnitude) && + wide_is_zero(normalized_right.magnitude)) { + *comparison = 0; + return true; + } + if (wide_is_zero(normalized_left.magnitude)) { + *comparison = normalized_right.negative ? 1 : -1; + return true; + } + if (wide_is_zero(normalized_right.magnitude)) { + *comparison = normalized_left.negative ? -1 : 1; + return true; + } + if (normalized_left.negative != normalized_right.negative) { + *comparison = normalized_left.negative ? -1 : 1; + return true; + } + if (!rational_compare_magnitudes(&normalized_left, &normalized_right, + comparison)) { + return false; + } + if (normalized_left.negative) *comparison = -*comparison; + return true; +} + +bool av2_dm_rational_rebase(Av2DmRational *values, uint32_t value_count, + const Av2DmRational *origin) { + if ((values == NULL && value_count != 0) || origin == NULL) return false; + Av2DmRational fixed_origin = *origin; + if (!rational_normalize(&fixed_origin)) return false; + + // Preflight every subtraction so arithmetic failure cannot leave the array + // containing a mixture of old and new time origins. + for (uint32_t i = 0; i < value_count; ++i) { + Av2DmRational rebased; + if (!av2_dm_rational_subtract(&values[i], &fixed_origin, &rebased)) { + return false; + } + } + for (uint32_t i = 0; i < value_count; ++i) { + Av2DmRational rebased; + if (!av2_dm_rational_subtract(&values[i], &fixed_origin, &rebased)) { + return false; + } + values[i] = rebased; + } + return true; +} + +bool av2_dm_rational_is_zero(const Av2DmRational *value) { + return value != NULL && !wide_is_zero(value->denominator) && + wide_is_zero(value->magnitude); +} + +static void buffer_reset(Av2DmBuffer *buffer) { + memset(buffer, 0, sizeof(*buffer)); + buffer->display_index = -1; + av2_dm_rational_make(0, 1, &buffer->presentation_time); + av2_dm_rational_make(0, 1, &buffer->decode_completion_time); +} + +static bool valid_buffer_index(const Av2DmBufferPool *pool, + uint32_t buffer_index) { + return pool != NULL && buffer_index < pool->pool_size; +} + +bool av2_dm_buffer_pool_initialize(Av2DmBufferPool *pool, + uint32_t num_ref_frames) { + if (pool == NULL || num_ref_frames == 0 || + num_ref_frames > AV2_DM_MAX_REF_FRAMES) { + return false; + } + memset(pool, 0, sizeof(*pool)); + pool->num_ref_frames = num_ref_frames; + pool->pool_size = num_ref_frames + 2; + for (uint32_t i = 0; i < AV2_DM_MAX_REF_FRAMES; ++i) pool->vbi[i] = -1; + for (uint32_t i = 0; i < AV2_DM_MAX_BUFFER_POOL_SIZE; ++i) { + buffer_reset(&pool->buffers[i]); + } + return true; +} + +int32_t av2_dm_buffer_pool_get_free_buffer(const Av2DmBufferPool *pool) { + if (pool == NULL) return -1; + for (uint32_t i = 0; i < pool->pool_size; ++i) { + const Av2DmBuffer *const buffer = &pool->buffers[i]; + if (buffer->decoder_ref_count == 0 && buffer->player_ref_count == 0) { + return (int32_t)i; + } + } + return -1; +} + +bool av2_dm_buffer_pool_release(Av2DmBufferPool *pool, uint32_t buffer_index) { + if (!valid_buffer_index(pool, buffer_index)) return false; + Av2DmBuffer *const buffer = &pool->buffers[buffer_index]; + if (buffer->decoder_ref_count != 0 || buffer->player_ref_count != 0) { + return false; + } + buffer_reset(buffer); + return true; +} + +bool av2_dm_buffer_pool_add_decoder_ref(Av2DmBufferPool *pool, + uint32_t buffer_index) { + if (!valid_buffer_index(pool, buffer_index)) return false; + Av2DmBuffer *const buffer = &pool->buffers[buffer_index]; + if (buffer->decoder_ref_count == UINT32_MAX) return false; + ++buffer->decoder_ref_count; + return true; +} + +bool av2_dm_buffer_pool_remove_decoder_ref(Av2DmBufferPool *pool, + uint32_t buffer_index) { + if (!valid_buffer_index(pool, buffer_index)) return false; + Av2DmBuffer *const buffer = &pool->buffers[buffer_index]; + if (buffer->decoder_ref_count == 0) return false; + --buffer->decoder_ref_count; + if (buffer->decoder_ref_count == 0 && buffer->player_ref_count == 0) { + buffer_reset(buffer); + } + return true; +} + +bool av2_dm_buffer_pool_add_player_ref(Av2DmBufferPool *pool, + uint32_t buffer_index) { + if (!valid_buffer_index(pool, buffer_index)) return false; + Av2DmBuffer *const buffer = &pool->buffers[buffer_index]; + if (buffer->player_ref_count == UINT32_MAX) return false; + ++buffer->player_ref_count; + return true; +} + +bool av2_dm_buffer_pool_remove_player_ref(Av2DmBufferPool *pool, + uint32_t buffer_index) { + if (!valid_buffer_index(pool, buffer_index)) return false; + Av2DmBuffer *const buffer = &pool->buffers[buffer_index]; + if (buffer->player_ref_count == 0) return false; + --buffer->player_ref_count; + if (buffer->decoder_ref_count == 0 && buffer->player_ref_count == 0) { + buffer_reset(buffer); + } + return true; +} + +bool av2_dm_buffer_pool_set_vbi(Av2DmBufferPool *pool, uint32_t ref_index, + int32_t buffer_index) { + if (pool == NULL || ref_index >= pool->num_ref_frames || buffer_index < -1 || + (buffer_index >= 0 && (uint32_t)buffer_index >= pool->pool_size)) { + return false; + } + const int32_t old_buffer_index = pool->vbi[ref_index]; + if (old_buffer_index == buffer_index) return true; + if (buffer_index >= 0 && + pool->buffers[buffer_index].decoder_ref_count == UINT32_MAX) { + return false; + } + if (old_buffer_index >= 0 && !av2_dm_buffer_pool_remove_decoder_ref( + pool, (uint32_t)old_buffer_index)) { + return false; + } + if (buffer_index >= 0 && + !av2_dm_buffer_pool_add_decoder_ref(pool, (uint32_t)buffer_index)) { + return false; + } + pool->vbi[ref_index] = buffer_index; + return true; +} + +uint32_t av2_dm_buffer_pool_frames_in_use(const Av2DmBufferPool *pool) { + if (pool == NULL) return 0; + uint32_t frames_in_use = 0; + for (uint32_t i = 0; i < pool->pool_size; ++i) { + if (pool->buffers[i].decoder_ref_count != 0 || + pool->buffers[i].player_ref_count != 0) { + ++frames_in_use; + } + } + return frames_in_use; +} + +typedef struct Av2DmLevelRow { + uint64_t max_picture_size; + uint32_t max_dimension; + uint64_t max_display_rate; + uint64_t max_decode_rate; + uint32_t max_header_rate; + uint32_t main_kbps; + uint32_t high_kbps; + uint32_t main_cr; + uint32_t high_cr; + uint32_t max_tiles; + uint32_t max_tile_columns; +} Av2DmLevelRow; + +// Annex A Tables A-2 and A-3. Integer kilobits per second preserve the table +// values exactly and deliberately avoid the encoder's legacy double table. +static const Av2DmLevelRow decoder_model_level_rows[22] = { + { 147456, 640, 4423680, 5529600, 150, 1500, 0, 2, 0, 8, 4 }, + { 278784, 880, 8363520, 10454400, 150, 3000, 0, 2, 0, 8, 4 }, + { 665856, 1360, 19975680, 24969600, 150, 6000, 0, 2, 0, 16, 6 }, + { 1065024, 1720, 31950720, 39938400, 150, 10000, 0, 2, 0, 16, 6 }, + { 2359296, 2560, 70778880, 77856768, 300, 12000, 30000, 4, 4, 32, 8 }, + { 2359296, 2560, 141557760, 155713536, 300, 20000, 50000, 4, 4, 32, 8 }, + { 8912896, 4975, 267386880, 273715200, 300, 30000, 100000, 6, 4, 64, 8 }, + { 8912896, 4975, 534773760, 547430400, 300, 40000, 160000, 8, 4, 64, 8 }, + { 8912896, 4975, 1069547520, 1094860800, 300, 60000, 240000, 8, 4, 64, 8 }, + { 8912896, 4975, 1069547520, 1176502272, 300, 60000, 240000, 8, 4, 64, 8 }, + { 35651584, 9951, 1069547520, 1176502272, 300, 60000, 240000, 8, 4, 128, 16 }, + { 35651584, 9951, 2139095040, 2189721600, 300, 100000, 480000, 8, 4, 128, + 16 }, + { 35651584, 9951, 4278190080, 4379443200, 300, 160000, 800000, 8, 4, 128, + 16 }, + { 35651584, 9951, 4278190080, 4706009088, 300, 160000, 800000, 8, 4, 128, + 16 }, + { 142606336, 19902, 4278190080, 4706009088, 960, 160000, 800000, 8, 4, 256, + 32 }, + { 142606336, 19902, 8556380160, 8758886400, 960, 200000, 960000, 8, 4, 256, + 32 }, + { 142606336, 19902, 17112760320, 17517772800, 960, 320000, 1600000, 8, 4, 256, + 32 }, + { 142606336, 19902, 17112760320, 18824036352, 960, 320000, 1600000, 8, 4, 256, + 32 }, + { 530841600, 38400, 17112760320, 18824036352, 960, 320000, 1600000, 8, 4, 512, + 64 }, + { 530841600, 38400, 34225520640, 34910031052, 960, 400000, 1920000, 8, 4, 512, + 64 }, + { 530841600, 38400, 68451041280, 69820062105, 960, 640000, 3200000, 8, 4, 512, + 64 }, + { 530841600, 38400, 68451041280, 75296145408, 960, 640000, 3200000, 8, 4, 512, + 64 }, +}; + +static const uint8_t decoder_model_tile_width_scale[2][22] = { + { 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 8, 8, 8, 8 }, + { 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 8, 8, 8, 8, 16, 16, 16, 16 }, +}; + +static const uint8_t decoder_model_tile_area_scale[2][22] = { + { 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 8, 8, 8, 8, 16, 16, 16, 16 }, + { 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 16, 16, 16, 16, 32, 32, 32, 32 }, +}; + +typedef struct Av2DmDfgRecord { + uint64_t event_index; + uint64_t temporal_unit_index; + uint64_t generation; + uint64_t coded_bits; + uint64_t decode_order; + uint64_t rap_epoch; + uint64_t smoothing_epoch; + Av2DmLevelLimits limits; + uint32_t tier; + Av2DmMode mode; + Av2DmRational first_arrival; + Av2DmRational last_arrival; + Av2DmRational scheduled_removal; + Av2DmRational removal; + Av2DmRational decode_time; + Av2DmRational decode_completion; + bool random_access_point; + bool parameters_updated; + bool count_frame_header; + bool decode_count_two; + bool coded_as_closed_loop_key; + bool smoothing_overflow_reported; + uint64_t luma_samples; + uint32_t num_tiles; + uint64_t max_tile_area; + uint64_t compressed_size; + uint64_t frame_symbol_count; +} Av2DmDfgRecord; + +typedef struct Av2DmTuRecord { + uint64_t temporal_unit_index; + uint64_t event_index; + uint64_t output_luma_samples; + uint32_t output_frames; + uint32_t frame_headers; + bool header_complete; + bool header_window_checked; + bool header_rate_reported; + bool tile_header_rate_reported; + bool output_time_valid; + bool presentation_time_valid; + bool prior_presentation_interval_checked; + Av2DmRational output_time; + Av2DmRational presentation_time; + uint64_t header_window_headers; +} Av2DmTuRecord; + +typedef struct Av2DmLane { + Av2DmBufferPool pool; + Av2DmRational time; + Av2DmRational initial_presentation_delay; + bool initial_presentation_delay_known; + int32_t current_buffer_index; +} Av2DmLane; + +typedef struct Av2DmPendingOutputWitness { + bool valid; + uint64_t event_index; + Av2DmRational threshold; + Av2DmRational observed; + Av2DmRational presentation_offset; +} Av2DmPendingOutputWitness; + +typedef struct Av2DmRapPresentationAnchor { + bool valid; + uint64_t rap_epoch; + Av2DmRational presentation_offset; +} Av2DmRapPresentationAnchor; + +typedef struct Av2DmResolvedParameters { + Av2DmLevelLimits limits; + Av2DmRational decoder_buffer_delay; + Av2DmRational encoder_buffer_delay; + uint32_t decoder_buffer_delay_ticks; + bool low_delay_mode; + Av2DmRational dec_ct; + Av2DmRational disp_ct; +} Av2DmResolvedParameters; + +struct Av2DecoderModel { + Av2DmConfig config; + Av2DmLevelLimits limits; + Av2DmRational decoder_buffer_delay; + Av2DmRational encoder_buffer_delay; + uint32_t decoder_buffer_delay_ticks; + bool low_delay_mode; + Av2DmRational dec_ct; + Av2DmRational disp_ct; + Av2DmLane lane; + Av2DmLane resource_lane; + Av2DmResult result; + Av2DmReportFn report; + void *report_opaque; + // Live smoothing/fullness records only. The adjacent parsing DFG and + // generation output metadata have separate bounded homes below. + Av2DmDfgRecord *dfgs; + uint32_t dfg_count; + uint32_t dfg_capacity; + uint64_t dfg_number; + bool previous_dfg_valid; + Av2DmDfgRecord previous_dfg; + Av2DmTuRecord *tus; + uint32_t tu_count; + uint32_t tu_capacity; + uint64_t frame_number; + uint64_t shown_frame_number; + uint64_t rap_epoch; + uint64_t smoothing_epoch; + bool smoothing_epoch_prepared; + bool most_recent_rap_removal_valid; + Av2DmRational most_recent_rap_scheduled_removal; + bool previous_output_order_valid; + uint64_t previous_output_decode_order; + bool previous_output_presentation_valid; + Av2DmRational previous_output_presentation_offset; + uint64_t previous_output_rap_epoch; + bool last_presentation_offset_valid; + Av2DmRational last_presentation_offset; + bool last_presentation_valid; + Av2DmRational last_presentation; + Av2DmRapPresentationAnchor + rap_presentation_anchors[AV2_DM_MAX_BUFFER_POOL_SIZE + 2]; + uint64_t last_output_temporal_unit; + uint64_t latest_frame_event_index; + uint64_t latest_header_check_event_index; + bool last_frame_parsing_time_valid; + Av2DmRational last_frame_parsing_time; + bool last_display_duration_valid; + Av2DmRational last_display_duration; + bool last_output_tu_valid; + uint64_t last_output_tu; + bool latest_timed_tu_valid; + Av2DmRational latest_timed_tu_output_time; + bool coded_tu_valid; + uint64_t coded_tu; + bool retired_header_summary_valid; + bool retired_header_summary_reported; + uint64_t retired_max_frame_headers; + uint64_t retired_header_event_index; + bool retired_unresolved_tu; + Av2DmPendingOutputWitness pending_display_late; + Av2DmPendingOutputWitness pending_decode_deadline; + uint64_t maximum_tile_area; + bool any_decode_count_two_requires_reserved_buffer; + bool max_reference_frames_checked; + bool max_reference_frames_reserved; + bool max_reference_frames_violated; + bool processing_stopped; + uint64_t model_events; + bool violation_seen[AV2_DM_VIOLATION_TILE_SIZE_HEADER_RATE + 1]; + Av2DmStorageStats storage; +}; + +static bool invalidate_lane_reference_buffers(Av2DmLane *lane, + uint32_t ref_valid_mask); +static void check_smoothing_buffer_overflow(Av2DecoderModel *model, + uint64_t proving_event_index); +static void retire_closed_smoothing_records(Av2DecoderModel *model, + const Av2DmRational *frontier); +static void check_max_reference_frames(Av2DecoderModel *model, + uint64_t event_index); +static void check_header_rate_windows(Av2DecoderModel *model, + bool require_complete, + uint64_t proving_event_index); +static void update_storage_stats(Av2DecoderModel *model); +static void check_retired_tile_header_summary(Av2DecoderModel *model, + uint64_t proving_event_index); +static void retire_unresolvable_tus(Av2DecoderModel *model); +static void restart_tu_history(Av2DecoderModel *model, + uint64_t temporal_unit_index); +static bool update_latest_timed_tu(Av2DecoderModel *model, + const Av2DmRational *output_time); + +static bool rational_zero(Av2DmRational *value) { + return av2_dm_rational_make(0, 1, value); +} + +static bool rational_from_product(uint64_t left, uint64_t right, + Av2DmRational *value) { + Av2DmUnsignedWide product; + if (!wide_multiply_checked(wide_from_u64(left), wide_from_u64(right), + &product)) { + return false; + } + return av2_dm_rational_make_wide(product, 1, false, value); +} + +static bool rational_multiply(const Av2DmRational *left, + const Av2DmRational *right, + Av2DmRational *result) { + if (left == NULL || right == NULL || result == NULL || + wide_is_zero(left->denominator) || wide_is_zero(right->denominator)) { + return false; + } + Av2DmRational a = *left; + Av2DmRational b = *right; + if (!rational_normalize(&a) || !rational_normalize(&b)) return false; + const Av2DmUnsignedWide cross_a = wide_gcd(a.magnitude, b.denominator); + const Av2DmUnsignedWide cross_b = wide_gcd(b.magnitude, a.denominator); + Av2DmUnsignedWide remainder; + if (!wide_divide(a.magnitude, cross_a, &a.magnitude, &remainder) || + !wide_is_zero(remainder) || + !wide_divide(b.denominator, cross_a, &b.denominator, &remainder) || + !wide_is_zero(remainder) || + !wide_divide(b.magnitude, cross_b, &b.magnitude, &remainder) || + !wide_is_zero(remainder) || + !wide_divide(a.denominator, cross_b, &a.denominator, &remainder) || + !wide_is_zero(remainder) || + !wide_multiply_checked(a.magnitude, b.magnitude, &result->magnitude) || + !wide_multiply_checked(a.denominator, b.denominator, + &result->denominator)) { + return false; + } + result->negative = a.negative != b.negative; + return rational_normalize(result); +} + +static bool rational_max(const Av2DmRational *left, const Av2DmRational *right, + Av2DmRational *result) { + int comparison; + if (!av2_dm_rational_compare(left, right, &comparison)) return false; + *result = comparison >= 0 ? *left : *right; + return true; +} + +static bool rational_less(const Av2DmRational *left, const Av2DmRational *right, + bool *is_less) { + int comparison; + if (!av2_dm_rational_compare(left, right, &comparison)) return false; + *is_less = comparison < 0; + return true; +} + +static bool rational_greater(const Av2DmRational *left, + const Av2DmRational *right, bool *is_greater) { + int comparison; + if (!av2_dm_rational_compare(left, right, &comparison)) return false; + *is_greater = comparison > 0; + return true; +} + +static bool grow_array(void **array, uint32_t *capacity, uint32_t count, + size_t element_size) { + if (count < *capacity) return true; + const uint32_t new_capacity = *capacity == 0 ? 16 : *capacity * 2; + if (new_capacity < *capacity || element_size > SIZE_MAX / new_capacity) { + return false; + } + void *const replacement = avm_calloc(new_capacity, element_size); + if (replacement == NULL) return false; + if (*array != NULL) { + memcpy(replacement, *array, (size_t)count * element_size); + avm_free(*array); + } + *array = replacement; + *capacity = new_capacity; + return true; +} + +bool av2_dm_get_level_limits(uint32_t level_idx, uint32_t tier, + uint32_t profile, Av2DmLevelLimits *limits) { + if (limits == NULL || level_idx >= 22 || tier > 1 || profile > 4) { + return false; + } + const Av2DmLevelRow *const row = &decoder_model_level_rows[level_idx]; + const uint32_t kbps = tier == 0 ? row->main_kbps : row->high_kbps; + const uint32_t compression = tier == 0 ? row->main_cr : row->high_cr; + if (kbps == 0 || compression == 0) return false; + + uint32_t profile_numerator = 1; + uint32_t profile_denominator = 1; + uint32_t picture_factor = 15; + if (profile == 3) { + profile_numerator = 1667; + profile_denominator = 1000; + picture_factor = 20; + } else if (profile == 4) { + profile_numerator = 5; + profile_denominator = 2; + picture_factor = 30; + } + + memset(limits, 0, sizeof(*limits)); + limits->max_picture_size = row->max_picture_size; + limits->max_horizontal_size = row->max_dimension; + limits->max_vertical_size = row->max_dimension; + limits->max_display_rate = row->max_display_rate; + limits->max_decode_rate = row->max_decode_rate; + limits->max_header_rate = row->max_header_rate; + limits->max_tiles = row->max_tiles; + limits->max_tile_columns = row->max_tile_columns; + limits->max_tile_width = + (uint64_t)decoder_model_tile_width_scale[tier][level_idx] * 4096 / 4; + limits->max_tile_area = + (uint64_t)decoder_model_tile_area_scale[tier][level_idx] * 4096 * 2304 / + 4; + limits->max_tile_size_header_rate_product = + (uint64_t)decoder_model_tile_area_scale[tier][level_idx] * 547430400 / 4; + limits->picture_size_profile_factor = picture_factor; + limits->min_compression_basis = compression; + + Av2DmRational base_rate; + Av2DmRational profile_factor; + if (!rational_from_product(kbps, 1000, &base_rate) || + !av2_dm_rational_make(profile_numerator, profile_denominator, + &profile_factor) || + !rational_multiply(&base_rate, &profile_factor, &limits->bit_rate)) { + return false; + } + // Annex A defines MaxBufferSize as one second of MaxBitrate. + limits->buffer_size = limits->bit_rate; + return true; +} + +typedef struct Av2DmSubstreamRow { + uint32_t max_horizontal_size; + uint32_t max_vertical_size; + uint32_t max_tile_columns; +} Av2DmSubstreamRow; + +static const Av2DmSubstreamRow decoder_model_substream_rows[5][3] = { + { { 896, 1600, 7 }, { 576, 960, 4 }, { 384, 640, 3 } }, + { { 1472, 2560, 7 }, { 1088, 1920, 4 }, { 768, 1280, 3 } }, + { { 2280, 5120, 13 }, { 2176, 3840, 8 }, { 1472, 2560, 5 } }, + { { 5760, 10240, 26 }, { 4320, 7680, 16 }, { 2880, 5120, 11 } }, + { { 11520, 20480, 52 }, { 8640, 15360, 32 }, { 5760, 10240, 21 } }, +}; + +static bool scaled_integer(uint64_t value, uint32_t scale_numerator, + uint32_t scale_denominator, uint64_t *scaled) { + Av2DmRational rational; + if (!av2_dm_rational_make(value, 1, &rational) || + !av2_dm_rational_multiply_u64(&rational, scale_denominator, &rational) || + !av2_dm_rational_divide_u64(&rational, scale_numerator, &rational) || + rational.negative || !wide_equals_u64(rational.denominator, 1) || + rational.magnitude.limbs[1] != 0 || rational.magnitude.limbs[2] != 0 || + rational.magnitude.limbs[3] != 0) { + return false; + } + *scaled = rational.magnitude.limbs[0]; + return true; +} + +bool av2_dm_apply_multistream_limits(uint32_t level_idx, uint32_t tier, + uint32_t profile, uint32_t scale_numerator, + uint32_t scale_denominator, + Av2DmLevelLimits *limits) { + // Annex A does not define substream limits below Level 4.0. + if (limits == NULL || scale_denominator == 0 || level_idx < 4 || + level_idx >= 22) { + return false; + } + uint32_t scale_index; + if (scale_numerator == 3 && scale_denominator == 2) { + scale_index = 0; + } else if (scale_numerator == 4 && scale_denominator == 1) { + scale_index = 1; + } else if (scale_numerator == 9 && scale_denominator == 1) { + scale_index = 2; + } else { + return false; + } + const uint32_t group = + level_idx < 6 ? 0 : (level_idx < 10 ? 1 : (level_idx - 10) / 4 + 2); + if (group >= 5) return false; + const Av2DmSubstreamRow *const row = + &decoder_model_substream_rows[group][scale_index]; + Av2DmLevelLimits multistream; + if (!av2_dm_get_level_limits(level_idx, tier, profile, &multistream)) { + return false; + } + uint64_t scaled_display; + uint64_t scaled_decode; + if (!scaled_integer(multistream.max_display_rate, scale_numerator, + scale_denominator, &scaled_display) || + !scaled_integer(multistream.max_decode_rate, scale_numerator, + scale_denominator, &scaled_decode) || + !av2_dm_rational_multiply_u64(&multistream.bit_rate, scale_denominator, + &multistream.bit_rate) || + !av2_dm_rational_divide_u64(&multistream.bit_rate, scale_numerator, + &multistream.bit_rate) || + !av2_dm_rational_multiply_u64(&multistream.buffer_size, scale_denominator, + &multistream.buffer_size) || + !av2_dm_rational_divide_u64(&multistream.buffer_size, scale_numerator, + &multistream.buffer_size)) { + return false; + } + multistream.max_picture_size = + (uint64_t)row->max_horizontal_size * row->max_vertical_size; + multistream.max_horizontal_size = row->max_horizontal_size; + multistream.max_vertical_size = row->max_vertical_size; + multistream.max_display_rate = scaled_display; + multistream.max_decode_rate = scaled_decode; + multistream.max_header_rate = 132; + multistream.max_tiles = (uint32_t)((uint64_t)multistream.max_tiles * + scale_denominator / scale_numerator); + multistream.max_tile_columns = row->max_tile_columns; + +#define MIN_LIMIT(member) \ + do { \ + if (multistream.member < limits->member) \ + limits->member = multistream.member; \ + } while (0) + MIN_LIMIT(max_picture_size); + MIN_LIMIT(max_horizontal_size); + MIN_LIMIT(max_vertical_size); + MIN_LIMIT(max_display_rate); + MIN_LIMIT(max_decode_rate); + MIN_LIMIT(max_header_rate); + MIN_LIMIT(max_tiles); + MIN_LIMIT(max_tile_columns); +#undef MIN_LIMIT + int comparison; + if (!av2_dm_rational_compare(&multistream.bit_rate, &limits->bit_rate, + &comparison)) { + return false; + } + if (comparison < 0) limits->bit_rate = multistream.bit_rate; + if (!av2_dm_rational_compare(&multistream.buffer_size, &limits->buffer_size, + &comparison)) { + return false; + } + if (comparison < 0) limits->buffer_size = multistream.buffer_size; + if (multistream.min_compression_basis > limits->min_compression_basis) { + limits->min_compression_basis = multistream.min_compression_basis; + } + return true; +} + +static void update_result_status(Av2DecoderModel *model) { + if (model->result.applicability == AV2_DM_NOT_APPLICABLE) { + model->result.status = AV2_DM_RESULT_NOT_APPLICABLE; + } else if (model->result.violations != 0) { + model->result.status = AV2_DM_RESULT_NON_CONFORMANT; + } else if (model->result.arithmetic_failed || + model->result.missing_required_input || + model->result.applicability == AV2_DM_MISSING_REQUIRED_INPUT) { + model->result.status = AV2_DM_RESULT_INDETERMINATE; + } else { + model->result.status = AV2_DM_RESULT_CONFORMANT; + } +} + +static void arithmetic_failure(Av2DecoderModel *model) { + model->result.arithmetic_failed = true; + model->processing_stopped = true; + update_result_status(model); +} + +void av2_decoder_model_fail_arithmetic_for_testing(Av2DecoderModel *model) { + if (model != NULL) arithmetic_failure(model); +} + +static bool increment_model_u64(Av2DecoderModel *model, uint64_t *value) { + if (*value == UINT64_MAX) { + arithmetic_failure(model); + return false; + } + ++*value; + return true; +} + +static bool increment_output_count(Av2DecoderModel *model) { + if (model->shown_frame_number == UINT64_MAX || + model->result.output_frames == UINT64_MAX) { + arithmetic_failure(model); + return false; + } + ++model->shown_frame_number; + ++model->result.output_frames; + return true; +} + +static void missing_input(Av2DecoderModel *model) { + model->result.missing_required_input = true; + model->processing_stopped = true; + if (model->result.applicability == AV2_DM_APPLICABLE) { + model->result.applicability = AV2_DM_MISSING_REQUIRED_INPUT; + } + update_result_status(model); +} + +static void incomplete_verification(Av2DecoderModel *model) { + model->result.missing_required_input = true; + if (model->result.applicability == AV2_DM_APPLICABLE) { + model->result.applicability = AV2_DM_MISSING_REQUIRED_INPUT; + } + update_result_status(model); +} + +static bool violation_seen(const Av2DecoderModel *model, + Av2DmViolationCode code) { + return code <= AV2_DM_VIOLATION_TILE_SIZE_HEADER_RATE && + model->violation_seen[code]; +} + +static void report_violation_for_affected( + Av2DecoderModel *model, Av2DmViolationCode code, uint64_t event_index, + Av2DmViolationAffectedKind affected_kind, uint64_t affected_index, + const Av2DmRational *observed, const Av2DmRational *limit, + const Av2DmViolationDetail *detail) { + if (model->processing_stopped) return; + if (code > AV2_DM_VIOLATION_TILE_SIZE_HEADER_RATE) { + arithmetic_failure(model); + return; + } + model->violation_seen[code] = true; + if (model->result.violations != UINT64_MAX) { + ++model->result.violations; + } + model->result.status = AV2_DM_RESULT_NON_CONFORMANT; + if (code == AV2_DM_VIOLATION_TILE_SIZE_HEADER_RATE) { + // Once this code is proven, no retained header-window witness can change + // the CVS or bitstream verdict. Direct later occurrences may still be + // reported without retaining the old windows. + model->retired_header_summary_valid = false; + } + if (model->report != NULL) { + Av2DmViolation violation; + memset(&violation, 0, sizeof(violation)); + violation.code = code; + violation.scope = model->config.scope; + violation.event_index = event_index; + violation.affected_kind = affected_kind; + violation.affected_index = affected_index; + violation.observed_present = observed != NULL; + violation.limit_present = limit != NULL; + if (observed != NULL) violation.observed = *observed; + if (limit != NULL) violation.limit = *limit; + if (detail != NULL) violation.detail = *detail; + model->report(model->report_opaque, &violation); + } + if (model->config.stop_after_first_violation) { + model->processing_stopped = true; + } +} + +static void report_violation(Av2DecoderModel *model, Av2DmViolationCode code, + uint64_t event_index, + const Av2DmRational *observed, + const Av2DmRational *limit) { + report_violation_for_affected(model, code, event_index, + AV2_DM_VIOLATION_AFFECTED_EVENT, event_index, + observed, limit, NULL); +} + +static bool lane_initialize(Av2DmLane *lane, uint32_t num_ref_frames) { + memset(lane, 0, sizeof(*lane)); + lane->current_buffer_index = -1; + return av2_dm_buffer_pool_initialize(&lane->pool, num_ref_frames) && + rational_zero(&lane->time) && + rational_zero(&lane->initial_presentation_delay); +} + +static bool seed_ras_buffers(Av2DecoderModel *model, Av2DmLane *lane) { + for (uint32_t i = 0; i < model->config.ras_seed_count; ++i) { + const Av2DmRasSeed *const seed = &model->config.ras_seeds[i]; + if (seed->ref_index >= lane->pool.num_ref_frames) return false; + int32_t buffer_index = -1; + for (uint32_t j = 0; j < lane->pool.pool_size; ++j) { + if (lane->pool.buffers[j].generation_valid && + lane->pool.buffers[j].generation == seed->generation) { + buffer_index = (int32_t)j; + break; + } + } + if (buffer_index == -1) { + buffer_index = av2_dm_buffer_pool_get_free_buffer(&lane->pool); + if (buffer_index == -1) return false; + lane->pool.buffers[buffer_index].generation_valid = true; + lane->pool.buffers[buffer_index].generation = seed->generation; + } + if (!av2_dm_buffer_pool_set_vbi(&lane->pool, seed->ref_index, + buffer_index)) { + return false; + } + } + return true; +} + +static bool resolve_parameters(const Av2DmConfig *config, + Av2DmResolvedParameters *parameters) { + memset(parameters, 0, sizeof(*parameters)); + if (config->level_limits_present) { + parameters->limits = config->level_limits; + } else if (!av2_dm_get_level_limits(config->level_idx, config->tier, + config->profile, ¶meters->limits)) { + return false; + } + if (!rational_normalize(¶meters->limits.bit_rate) || + !rational_normalize(¶meters->limits.buffer_size) || + wide_is_zero(parameters->limits.bit_rate.magnitude) || + parameters->limits.max_decode_rate == 0 || + parameters->limits.max_display_rate == 0 || + parameters->limits.max_header_rate == 0 || + parameters->limits.picture_size_profile_factor == 0 || + parameters->limits.min_compression_basis == 0 || + !config->timing_info_present || config->time_scale == 0 || + config->num_units_in_display_tick == 0 || + (config->mode == AV2_DM_DECODING_SCHEDULE_MODE && + config->num_units_in_decoding_tick == 0) || + (config->equal_picture_interval && config->ticks_per_picture == 0) || + config->initial_display_delay == 0 || + !av2_dm_rational_make(config->num_units_in_display_tick, + config->time_scale, ¶meters->disp_ct)) { + return false; + } + if (config->mode == AV2_DM_DECODING_SCHEDULE_MODE && + !av2_dm_rational_make(config->num_units_in_decoding_tick, + config->time_scale, ¶meters->dec_ct)) { + return false; + } + + uint32_t decoder_delay = 70000; + uint32_t encoder_delay = 20000; + if (config->mode == AV2_DM_DECODING_SCHEDULE_MODE) { + if (!config->scope.whole_xlayer && + config->operating_point_parameters_present) { + decoder_delay = config->operating_point_decoder_buffer_delay; + encoder_delay = config->operating_point_encoder_buffer_delay; + parameters->low_delay_mode = config->operating_point_low_delay_mode; + } else if (config->sequence_parameters_present) { + // DM-SPEC-1: operating-point selection falls back to the associated + // sequence-header parameters when OP parameters are absent. + decoder_delay = config->sequence_decoder_buffer_delay; + encoder_delay = config->sequence_encoder_buffer_delay; + parameters->low_delay_mode = config->sequence_low_delay_mode; + } else { + return false; + } + } else if (!config->equal_picture_interval) { + return false; + } + if (!av2_dm_rational_make(decoder_delay, 90000, + ¶meters->decoder_buffer_delay) || + !av2_dm_rational_make(encoder_delay, 90000, + ¶meters->encoder_buffer_delay)) { + return false; + } + parameters->decoder_buffer_delay_ticks = decoder_delay; + return true; +} + +static void apply_parameters(Av2DecoderModel *model, const Av2DmConfig *config, + const Av2DmResolvedParameters *parameters) { + model->config = *config; + model->limits = parameters->limits; + model->decoder_buffer_delay = parameters->decoder_buffer_delay; + model->encoder_buffer_delay = parameters->encoder_buffer_delay; + model->decoder_buffer_delay_ticks = parameters->decoder_buffer_delay_ticks; + model->low_delay_mode = parameters->low_delay_mode; + model->dec_ct = parameters->dec_ct; + model->disp_ct = parameters->disp_ct; +} + +Av2DecoderModel *av2_decoder_model_create(const Av2DmConfig *config, + Av2DmReportFn report, + void *report_opaque) { + if (config == NULL) return NULL; + Av2DecoderModel *const model = avm_calloc(1, sizeof(*model)); + if (model == NULL) return NULL; + model->config = *config; + model->report = report; + model->report_opaque = report_opaque; + model->result.status = AV2_DM_RESULT_CONFORMANT; + model->result.applicability = config->applicability; + model->result.mode = config->mode; + model->result.scope = config->scope; + + if (config->level_idx == 31 || + config->applicability == AV2_DM_NOT_APPLICABLE) { + model->result.applicability = AV2_DM_NOT_APPLICABLE; + update_result_status(model); + return model; + } + if (config->applicability == AV2_DM_MISSING_REQUIRED_INPUT) { + missing_input(model); + return model; + } + if (config->num_ref_frames == 0 || + config->num_ref_frames > AV2_DM_MAX_REF_FRAMES || + !lane_initialize(&model->lane, config->num_ref_frames) || + !lane_initialize(&model->resource_lane, config->num_ref_frames)) { + av2_decoder_model_destroy(model); + return NULL; + } + Av2DmResolvedParameters parameters; + if (!resolve_parameters(config, ¶meters)) { + missing_input(model); + } else { + apply_parameters(model, config, ¶meters); + } + + if (config->ras_start) { + if (!config->ras_seed_complete || !seed_ras_buffers(model, &model->lane) || + !seed_ras_buffers(model, &model->resource_lane)) { + // DM-SPEC-6: a RAS run is provable only when all established long-term + // slot/generation relationships can be reconstructed. + missing_input(model); + } + } + update_result_status(model); + update_storage_stats(model); + return model; +} + +void av2_decoder_model_destroy(Av2DecoderModel *model) { + if (model == NULL) return; + avm_free(model->dfgs); + avm_free(model->tus); + avm_free(model); +} + +static bool rational_multiply_wide(const Av2DmRational *value, + Av2DmUnsignedWide multiplier, + Av2DmRational *result) { + if (value == NULL || result == NULL || wide_is_zero(value->denominator)) { + return false; + } + Av2DmRational normalized = *value; + if (!rational_normalize(&normalized)) return false; + const Av2DmUnsignedWide divisor = + wide_gcd(multiplier, normalized.denominator); + Av2DmUnsignedWide remainder; + if (!wide_divide(multiplier, divisor, &multiplier, &remainder) || + !wide_is_zero(remainder) || + !wide_divide(normalized.denominator, divisor, &normalized.denominator, + &remainder) || + !wide_is_zero(remainder) || + !wide_multiply_checked(normalized.magnitude, multiplier, + &normalized.magnitude)) { + return false; + } + *result = normalized; + return rational_normalize(result); +} + +static bool rational_ceil_ratio_to_tick(const Av2DmRational *time, + const Av2DmRational *tick, + Av2DmRational *result) { + if (time->negative || tick->negative || wide_is_zero(tick->magnitude)) { + return false; + } + Av2DmRational reciprocal; + reciprocal.magnitude = tick->denominator; + reciprocal.denominator = tick->magnitude; + reciprocal.negative = false; + Av2DmRational ratio; + if (!rational_multiply(time, &reciprocal, &ratio)) return false; + Av2DmUnsignedWide quotient; + Av2DmUnsignedWide remainder; + if (!wide_divide(ratio.magnitude, ratio.denominator, "ient, &remainder)) { + return false; + } + if (!wide_is_zero(remainder)) { + const Av2DmUnsignedWide one = wide_from_u64(1); + if (!wide_add(quotient, one, "ient)) return false; + } + return rational_multiply_wide(tick, quotient, result); +} + +static bool rational_ceil_to_integer(const Av2DmRational *value, + Av2DmRational *result) { + if (value == NULL || result == NULL || wide_is_zero(value->denominator)) { + return false; + } + Av2DmUnsignedWide quotient; + Av2DmUnsignedWide remainder; + if (!wide_divide(value->magnitude, value->denominator, "ient, + &remainder)) { + return false; + } + if (!value->negative && !wide_is_zero(remainder)) { + if (!wide_add(quotient, wide_from_u64(1), "ient)) return false; + } + return av2_dm_rational_make_wide(quotient, 1, value->negative, result); +} + +static void compare_upper_limit(Av2DecoderModel *model, Av2DmViolationCode code, + uint64_t event_index, + const Av2DmRational *observed, + const Av2DmRational *limit) { + bool greater; + if (!rational_greater(observed, limit, &greater)) { + arithmetic_failure(model); + } else if (greater) { + report_violation(model, code, event_index, observed, limit); + } +} + +static void compare_upper_limit_for_affected( + Av2DecoderModel *model, Av2DmViolationCode code, uint64_t event_index, + Av2DmViolationAffectedKind affected_kind, uint64_t affected_index, + const Av2DmRational *observed, const Av2DmRational *limit) { + bool greater; + if (!rational_greater(observed, limit, &greater)) { + arithmetic_failure(model); + } else if (greater) { + report_violation_for_affected(model, code, event_index, affected_kind, + affected_index, observed, limit, NULL); + } +} + +static void compare_upper_limit_for_affected_with_detail( + Av2DecoderModel *model, Av2DmViolationCode code, uint64_t event_index, + Av2DmViolationAffectedKind affected_kind, uint64_t affected_index, + const Av2DmRational *observed, const Av2DmRational *limit, + const Av2DmViolationDetail *detail) { + bool greater; + if (!rational_greater(observed, limit, &greater)) { + arithmetic_failure(model); + } else if (greater) { + report_violation_for_affected(model, code, event_index, affected_kind, + affected_index, observed, limit, detail); + } +} + +static void compare_lower_limit(Av2DecoderModel *model, Av2DmViolationCode code, + uint64_t event_index, + const Av2DmRational *observed, + const Av2DmRational *limit) { + bool less; + if (!rational_less(observed, limit, &less)) { + arithmetic_failure(model); + } else if (less) { + report_violation(model, code, event_index, observed, limit); + } +} + +static void compare_lower_limit_for_affected( + Av2DecoderModel *model, Av2DmViolationCode code, uint64_t event_index, + Av2DmViolationAffectedKind affected_kind, uint64_t affected_index, + const Av2DmRational *observed, const Av2DmRational *limit) { + bool less; + if (!rational_less(observed, limit, &less)) { + arithmetic_failure(model); + } else if (less) { + report_violation_for_affected(model, code, event_index, affected_kind, + affected_index, observed, limit, NULL); + } +} + +static void compare_lower_limit_for_affected_with_detail( + Av2DecoderModel *model, Av2DmViolationCode code, uint64_t event_index, + Av2DmViolationAffectedKind affected_kind, uint64_t affected_index, + const Av2DmRational *observed, const Av2DmRational *limit, + const Av2DmViolationDetail *detail) { + bool less; + if (!rational_less(observed, limit, &less)) { + arithmetic_failure(model); + } else if (less) { + report_violation_for_affected(model, code, event_index, affected_kind, + affected_index, observed, limit, detail); + } +} + +static Av2DmViolationDetail buffer_pool_violation_detail( + const Av2DmBufferPool *pool, bool resource_lane) { + Av2DmViolationDetail detail; + memset(&detail, 0, sizeof(detail)); + detail.kind = AV2_DM_VIOLATION_DETAIL_BUFFER_POOL; + detail.value.buffer_pool.resource_lane = resource_lane; + detail.value.buffer_pool.pool_size = pool->pool_size; + detail.value.buffer_pool.frames_in_use = + av2_dm_buffer_pool_frames_in_use(pool); + detail.value.buffer_pool.free_buffers = + pool->pool_size - detail.value.buffer_pool.frames_in_use; + for (uint32_t i = 0; i < pool->pool_size; ++i) { + if (pool->buffers[i].decoder_ref_count != 0) { + ++detail.value.buffer_pool.decoder_held_buffers; + } + if (pool->buffers[i].player_ref_count != 0) { + ++detail.value.buffer_pool.player_held_buffers; + } + } + return detail; +} + +static Av2DmTuRecord *find_tu(Av2DecoderModel *model, + uint64_t temporal_unit_index) { + for (uint32_t i = model->tu_count; i > 0; --i) { + if (model->tus[i - 1].temporal_unit_index == temporal_unit_index) { + return &model->tus[i - 1]; + } + } + return NULL; +} + +static Av2DmTuRecord *get_tu(Av2DecoderModel *model, + uint64_t temporal_unit_index, + uint64_t event_index) { + Av2DmTuRecord *const existing = find_tu(model, temporal_unit_index); + if (existing != NULL) return existing; + if (model->tu_count == UINT32_MAX || + !grow_array((void **)&model->tus, &model->tu_capacity, model->tu_count, + sizeof(*model->tus))) { + arithmetic_failure(model); + return NULL; + } + Av2DmTuRecord *const tu = &model->tus[model->tu_count++]; + memset(tu, 0, sizeof(*tu)); + tu->temporal_unit_index = temporal_unit_index; + tu->event_index = event_index; + return tu; +} + +static bool update_latest_timed_tu(Av2DecoderModel *model, + const Av2DmRational *output_time) { + if (!model->latest_timed_tu_valid) { + model->latest_timed_tu_output_time = *output_time; + model->latest_timed_tu_valid = true; + return true; + } + int comparison; + if (!av2_dm_rational_compare(output_time, &model->latest_timed_tu_output_time, + &comparison)) { + return false; + } + if (comparison > 0) model->latest_timed_tu_output_time = *output_time; + return true; +} + +static void check_static_level_limits(Av2DecoderModel *model, + const Av2DmFrameEvent *event) { + Av2DmRational observed; + Av2DmRational limit; + if (!rational_from_product(event->frame_width, event->frame_height, + &observed) || + !av2_dm_rational_make(model->limits.max_picture_size, 1, &limit)) { + arithmetic_failure(model); + return; + } + compare_upper_limit(model, AV2_DM_VIOLATION_MAX_PICTURE_SIZE, + event->event_index, &observed, &limit); + +#define CHECK_INTEGER_LIMIT(field, member, violation_code) \ + do { \ + if (!av2_dm_rational_make((field), 1, &observed) || \ + !av2_dm_rational_make(model->limits.member, 1, &limit)) { \ + arithmetic_failure(model); \ + } else { \ + compare_upper_limit(model, (violation_code), event->event_index, \ + &observed, &limit); \ + } \ + } while (0) + CHECK_INTEGER_LIMIT(event->frame_width, max_horizontal_size, + AV2_DM_VIOLATION_MAX_HORIZONTAL_SIZE); + CHECK_INTEGER_LIMIT(event->frame_height, max_vertical_size, + AV2_DM_VIOLATION_MAX_VERTICAL_SIZE); + CHECK_INTEGER_LIMIT(event->num_tiles, max_tiles, AV2_DM_VIOLATION_MAX_TILES); + CHECK_INTEGER_LIMIT(event->tile_columns, max_tile_columns, + AV2_DM_VIOLATION_MAX_TILE_COLUMNS); + CHECK_INTEGER_LIMIT(event->max_tile_width, max_tile_width, + AV2_DM_VIOLATION_MAX_TILE_WIDTH); + CHECK_INTEGER_LIMIT(event->max_tile_area, max_tile_area, + AV2_DM_VIOLATION_MAX_TILE_AREA); +#undef CHECK_INTEGER_LIMIT + + if (event->frame_width < 16) { + av2_dm_rational_make(event->frame_width, 1, &observed); + av2_dm_rational_make(16, 1, &limit); + report_violation(model, AV2_DM_VIOLATION_MIN_HORIZONTAL_SIZE, + event->event_index, &observed, &limit); + } + if (event->frame_height < 16) { + av2_dm_rational_make(event->frame_height, 1, &observed); + av2_dm_rational_make(16, 1, &limit); + report_violation(model, AV2_DM_VIOLATION_MIN_VERTICAL_SIZE, + event->event_index, &observed, &limit); + } + if (!event->non_rightmost_tile_width_valid) { + report_violation(model, AV2_DM_VIOLATION_MIN_TILE_WIDTH, event->event_index, + NULL, NULL); + } +} + +static bool release_presented_buffers(Av2DmLane *lane, + const Av2DmRational *removal) { + for (uint32_t i = 0; i < lane->pool.pool_size; ++i) { + Av2DmBuffer *const buffer = &lane->pool.buffers[i]; + if (buffer->player_ref_count == 0 || !buffer->presentation_time_valid) { + continue; + } + int comparison; + if (!av2_dm_rational_compare(&buffer->presentation_time, removal, + &comparison)) { + return false; + } + if (comparison <= 0) { + buffer->player_ref_count = 0; + if (buffer->decoder_ref_count == 0) buffer_reset(buffer); + } + } + return true; +} + +static bool next_resource_removal(Av2DecoderModel *model, Av2DmLane *lane, + uint64_t dfg_index, Av2DmRational *removal) { + if (dfg_index == 0) { + *removal = model->decoder_buffer_delay; + return true; + } + if (!release_presented_buffers(lane, &lane->time)) return false; + if (av2_dm_buffer_pool_get_free_buffer(&lane->pool) >= 0) { + *removal = lane->time; + return true; + } + bool found = false; + Av2DmRational earliest; + for (uint32_t i = 0; i < lane->pool.pool_size; ++i) { + const Av2DmBuffer *const buffer = &lane->pool.buffers[i]; + if (buffer->decoder_ref_count != 0 || buffer->player_ref_count == 0) { + continue; + } + if (!buffer->presentation_time_valid) { + missing_input(model); + return false; + } + if (!found) { + earliest = buffer->presentation_time; + found = true; + } else { + bool less; + if (!rational_less(&buffer->presentation_time, &earliest, &less)) { + return false; + } + if (less) earliest = buffer->presentation_time; + } + } + if (!found) return false; + *removal = earliest; + return true; +} + +static bool lane_start_decode(Av2DmLane *lane, const Av2DmRational *removal, + const Av2DmRational *decode_time, + uint64_t generation, int32_t *buffer_index) { + if (!release_presented_buffers(lane, removal)) return false; + lane->time = *removal; + const int32_t free_buffer = av2_dm_buffer_pool_get_free_buffer(&lane->pool); + *buffer_index = free_buffer; + lane->current_buffer_index = free_buffer; + if (!av2_dm_rational_add(&lane->time, decode_time, &lane->time)) { + return false; + } + if (free_buffer < 0) return true; + Av2DmBuffer *const buffer = &lane->pool.buffers[free_buffer]; + buffer_reset(buffer); + buffer->generation_valid = true; + buffer->generation = generation; + buffer->decode_completion_time = lane->time; + buffer->decode_completion_time_valid = true; + return true; +} + +static bool calculate_decode_time(Av2DecoderModel *model, + const Av2DmFrameEvent *event, + uint64_t *luma_samples, + Av2DmRational *decode_time) { + uint64_t samples; + if (event->frame_is_intra) { + Av2DmRational product; + if (!rational_from_product(event->frame_width, event->frame_height, + &product) || + product.magnitude.limbs[1] != 0 || product.magnitude.limbs[2] != 0 || + product.magnitude.limbs[3] != 0) { + return false; + } + samples = product.magnitude.limbs[0]; + if (event->allow_global_intrabc && event->inloop_filtering_enabled) { + if (samples > UINT64_MAX / 2) return false; + samples *= 2; + } + } else { + Av2DmRational product; + if (!rational_from_product(model->config.max_frame_width, + model->config.max_frame_height, &product) || + product.magnitude.limbs[1] != 0 || product.magnitude.limbs[2] != 0 || + product.magnitude.limbs[3] != 0) { + return false; + } + samples = product.magnitude.limbs[0]; + } + *luma_samples = samples; + return av2_dm_rational_make(samples, model->limits.max_decode_rate, + decode_time); +} + +static void check_frame_parsing_constraints(Av2DecoderModel *model, + Av2DmDfgRecord *dfg, + const Av2DmRational *interval, + uint64_t proving_event_index) { + if (model->config.still_picture) return; + Av2DmViolationDetail detail; + memset(&detail, 0, sizeof(detail)); + detail.kind = AV2_DM_VIOLATION_DETAIL_FRAME_INTERVAL; + detail.value.frame_interval = *interval; + const Av2DmLevelLimits *const limits = &dfg->limits; + Av2DmRational limit; + Av2DmRational observed; + if (!av2_dm_rational_multiply_u64(interval, limits->max_decode_rate, + &limit) || + !av2_dm_rational_make(dfg->luma_samples, 1, &observed)) { + arithmetic_failure(model); + return; + } + compare_upper_limit_for_affected_with_detail( + model, AV2_DM_VIOLATION_FRAME_DECODE_RATE, proving_event_index, + AV2_DM_VIOLATION_AFFECTED_DFG, dfg->event_index, &observed, &limit, + &detail); + + Av2DmRational dynamic_tiles; + Av2DmRational one; + Av2DmRational max_tiles; + if (!av2_dm_rational_multiply_u64(interval, (uint64_t)limits->max_tiles * 120, + &dynamic_tiles) || + !av2_dm_rational_make(1, 1, &one) || + !av2_dm_rational_make(limits->max_tiles, 1, &max_tiles) || + !rational_max(&dynamic_tiles, &one, &dynamic_tiles)) { + arithmetic_failure(model); + return; + } + bool greater; + if (!rational_greater(&dynamic_tiles, &max_tiles, &greater)) { + arithmetic_failure(model); + return; + } + if (greater) dynamic_tiles = max_tiles; + if (!av2_dm_rational_make(dfg->num_tiles, 1, &observed)) { + arithmetic_failure(model); + return; + } + compare_upper_limit_for_affected_with_detail( + model, AV2_DM_VIOLATION_FRAME_TILE_RATE, proving_event_index, + AV2_DM_VIOLATION_AFFECTED_DFG, dfg->event_index, &observed, + &dynamic_tiles, &detail); + + Av2DmRational compressed_limit_1; + Av2DmRational compressed_limit_2; + if (dfg->luma_samples > UINT64_MAX / limits->picture_size_profile_factor) { + arithmetic_failure(model); + return; + } + const uint64_t picture_units = + dfg->luma_samples * limits->picture_size_profile_factor / 8; + if (!av2_dm_rational_make(picture_units, 1, &compressed_limit_1) || + !av2_dm_rational_multiply_u64(&compressed_limit_1, 5, + &compressed_limit_1) || + !av2_dm_rational_divide_u64(&compressed_limit_1, 4, + &compressed_limit_1) || + !av2_dm_rational_multiply_u64(interval, limits->max_decode_rate, + &compressed_limit_2) || + !av2_dm_rational_multiply_u64(&compressed_limit_2, + limits->picture_size_profile_factor, + &compressed_limit_2) || + !av2_dm_rational_divide_u64(&compressed_limit_2, + (uint64_t)8 * limits->min_compression_basis, + &compressed_limit_2)) { + arithmetic_failure(model); + return; + } + bool first_is_greater; + if (!rational_greater(&compressed_limit_1, &compressed_limit_2, + &first_is_greater)) { + arithmetic_failure(model); + return; + } + limit = first_is_greater ? compressed_limit_2 : compressed_limit_1; + if (!av2_dm_rational_make(dfg->compressed_size, 1, &observed)) { + arithmetic_failure(model); + return; + } + compare_upper_limit_for_affected_with_detail( + model, AV2_DM_VIOLATION_MAX_COMPRESSED_SIZE, proving_event_index, + AV2_DM_VIOLATION_AFFECTED_DFG, dfg->event_index, &observed, &limit, + &detail); + + Av2DmRational symbol_factor_a; + Av2DmRational symbol_factor_b; + Av2DmRational symbol_factor; + if (!av2_dm_rational_make(8, (uint64_t)9 * limits->min_compression_basis, + &symbol_factor_a) || + !av2_dm_rational_make(1, 48, &symbol_factor_b) || + !av2_dm_rational_add(&symbol_factor_a, &symbol_factor_b, + &symbol_factor) || + !av2_dm_rational_multiply_u64(interval, limits->max_decode_rate, + &limit) || + !av2_dm_rational_multiply_u64(&limit, limits->picture_size_profile_factor, + &limit) || + !rational_multiply(&limit, &symbol_factor, &limit) || + !av2_dm_rational_make(dfg->frame_symbol_count, 1, &observed)) { + arithmetic_failure(model); + return; + } + compare_upper_limit_for_affected_with_detail( + model, AV2_DM_VIOLATION_MAX_FRAME_SYMBOLS, proving_event_index, + AV2_DM_VIOLATION_AFFECTED_DFG, dfg->event_index, &observed, &limit, + &detail); +} + +static void check_previous_dfg_interval(Av2DecoderModel *model, + Av2DmDfgRecord *previous, + const Av2DmDfgRecord *current) { + Av2DmRational interval; + if (!av2_dm_rational_subtract(¤t->removal, &previous->removal, + &interval) || + !av2_dm_rational_divide_u64(&interval, previous->decode_count_two ? 2 : 1, + &interval)) { + arithmetic_failure(model); + return; + } + model->last_frame_parsing_time = interval; + model->last_frame_parsing_time_valid = true; + // The previous DFG retains the affected frame/generation identity; the + // current DFG supplies the removal interval that proves these constraints. + check_frame_parsing_constraints(model, previous, &interval, + current->event_index); + + if (previous->mode == AV2_DM_DECODING_SCHEDULE_MODE) { + Av2DmRational available; + Av2DmRational one_header_time; + Av2DmRational required; + const uint64_t max_headers = (uint64_t)previous->limits.max_header_rate * + (1 + ((uint64_t)previous->tier << 1)); + if (!av2_dm_rational_subtract(¤t->scheduled_removal, + &previous->removal, &available) || + !av2_dm_rational_make(1, max_headers, &one_header_time) || + !rational_max(&previous->decode_time, &one_header_time, &required)) { + arithmetic_failure(model); + return; + } + Av2DmViolationDetail detail; + memset(&detail, 0, sizeof(detail)); + detail.kind = AV2_DM_VIOLATION_DETAIL_MINIMUM_DECODE_TIME; + detail.value.minimum_decode_time.frame_decode_time = previous->decode_time; + detail.value.minimum_decode_time.one_header_time = one_header_time; + compare_lower_limit_for_affected_with_detail( + model, AV2_DM_VIOLATION_MINIMUM_DECODE_TIME, current->event_index, + AV2_DM_VIOLATION_AFFECTED_DFG, previous->event_index, &available, + &required, &detail); + } +} + +static bool calculate_arrival_times(Av2DecoderModel *model, + Av2DmDfgRecord *dfg) { + if (model->dfg_number == 1 || dfg->parameters_updated) { + if (!rational_zero(&dfg->first_arrival)) return false; + } else { + if (!model->previous_dfg_valid) return false; + Av2DmRational total_delay; + Av2DmRational latest; + if (!av2_dm_rational_add(&model->encoder_buffer_delay, + &model->decoder_buffer_delay, &total_delay) || + !av2_dm_rational_subtract(&dfg->scheduled_removal, &total_delay, + &latest) || + !rational_max(&model->previous_dfg.last_arrival, &latest, + &dfg->first_arrival)) { + return false; + } + } + Av2DmRational coded_bits; + Av2DmRational reciprocal_rate; + Av2DmRational arrival_duration; + if (wide_is_zero(model->limits.bit_rate.magnitude) || + !av2_dm_rational_make(dfg->coded_bits, 1, &coded_bits)) { + return false; + } + reciprocal_rate.magnitude = model->limits.bit_rate.denominator; + reciprocal_rate.denominator = model->limits.bit_rate.magnitude; + reciprocal_rate.negative = false; + return rational_multiply(&coded_bits, &reciprocal_rate, &arrival_duration) && + av2_dm_rational_add(&dfg->first_arrival, &arrival_duration, + &dfg->last_arrival); +} + +static bool calculate_scheduled_removal(Av2DecoderModel *model, + const Av2DmFrameEvent *event, + Av2DmDfgRecord *dfg) { + if (model->config.mode == AV2_DM_RESOURCE_AVAILABILITY_MODE) { + return next_resource_removal(model, &model->lane, model->dfg_number - 1, + &dfg->scheduled_removal); + } + if (!event->buffer_removal_time_present) { + missing_input(model); + return false; + } + if (model->dfg_number == 1) { + dfg->scheduled_removal = model->decoder_buffer_delay; + return true; + } + if (!model->most_recent_rap_removal_valid) { + missing_input(model); + return false; + } + Av2DmRational offset; + return av2_dm_rational_multiply_u64(&model->dec_ct, + event->buffer_removal_time, &offset) && + av2_dm_rational_add(&model->most_recent_rap_scheduled_removal, &offset, + &dfg->scheduled_removal); +} + +static void check_schedule_delay_limits(Av2DecoderModel *model, + uint64_t event_index) { + if (model->dfg_number != 1 || + model->config.mode != AV2_DM_DECODING_SCHEDULE_MODE) { + return; + } + Av2DmRational zero; + rational_zero(&zero); + if (av2_dm_rational_is_zero(&model->decoder_buffer_delay)) { + report_violation(model, AV2_DM_VIOLATION_DECODER_BUFFER_DELAY_ZERO, + event_index, &model->decoder_buffer_delay, &zero); + } + Av2DmRational reciprocal_rate; + Av2DmRational maximum_delay; + reciprocal_rate.magnitude = model->limits.bit_rate.denominator; + reciprocal_rate.denominator = model->limits.bit_rate.magnitude; + reciprocal_rate.negative = false; + if (!rational_multiply(&model->limits.buffer_size, &reciprocal_rate, + &maximum_delay)) { + arithmetic_failure(model); + return; + } + compare_upper_limit(model, AV2_DM_VIOLATION_DECODER_BUFFER_DELAY_TOO_LARGE, + event_index, &model->decoder_buffer_delay, + &maximum_delay); +} + +static void check_delay_consistency(Av2DecoderModel *model, + Av2DmDfgRecord *dfg) { + if (model->config.mode != AV2_DM_DECODING_SCHEDULE_MODE || + !dfg->random_access_point || !model->previous_dfg_valid) { + return; + } + Av2DmRational time_delta; + Av2DmRational threshold; + if (!av2_dm_rational_subtract(&dfg->scheduled_removal, + &model->previous_dfg.last_arrival, + &time_delta) || + !av2_dm_rational_multiply_u64(&time_delta, 90000, &time_delta) || + model->decoder_buffer_delay_ticks == 0 || + !av2_dm_rational_make(model->decoder_buffer_delay_ticks - 1, 1, + &threshold)) { + arithmetic_failure(model); + return; + } + int comparison; + if (!av2_dm_rational_compare(&time_delta, &threshold, &comparison)) { + arithmetic_failure(model); + } else if (comparison <= 0) { + Av2DmViolationDetail detail; + memset(&detail, 0, sizeof(detail)); + detail.kind = AV2_DM_VIOLATION_DETAIL_DELAY_CONSISTENCY; + detail.value.delay_consistency.decoder_buffer_delay_ticks = + model->decoder_buffer_delay_ticks; + detail.value.delay_consistency.ceil_time_delta_present = + rational_ceil_to_integer( + &time_delta, &detail.value.delay_consistency.ceil_time_delta_ticks); + report_violation_for_affected( + model, AV2_DM_VIOLATION_DECODER_BUFFER_DELAY_INCONSISTENT, + dfg->event_index, AV2_DM_VIOLATION_AFFECTED_EVENT, dfg->event_index, + &time_delta, &threshold, &detail); + } +} + +typedef struct Av2DmPreparedRebase { + Av2DmRational **targets; + Av2DmRational *values; + uint32_t count; +} Av2DmPreparedRebase; + +static bool add_rebase_target(Av2DmPreparedRebase *prepared, + Av2DmRational *target, uint32_t capacity) { + if (prepared->count >= capacity) return false; + prepared->targets[prepared->count] = target; + prepared->values[prepared->count] = *target; + ++prepared->count; + return true; +} + +static bool prepare_lane_rebase(Av2DecoderModel *model, Av2DmLane *lane, + bool primary, const Av2DmRational *origin, + Av2DmPreparedRebase *prepared) { + uint64_t capacity = 2 + 2 * lane->pool.pool_size; + if (primary) { + capacity += (uint64_t)5 * model->dfg_count + 16; + } + if (capacity > UINT32_MAX || + capacity > SIZE_MAX / sizeof(*prepared->values)) { + return false; + } + prepared->targets = avm_calloc((size_t)capacity, sizeof(*prepared->targets)); + prepared->values = avm_calloc((size_t)capacity, sizeof(*prepared->values)); + if (prepared->targets == NULL || prepared->values == NULL) return false; + const uint32_t count_limit = (uint32_t)capacity; + if (!add_rebase_target(prepared, &lane->time, count_limit) || + !add_rebase_target(prepared, &lane->initial_presentation_delay, + count_limit)) { + return false; + } + for (uint32_t i = 0; i < lane->pool.pool_size; ++i) { + Av2DmBuffer *const buffer = &lane->pool.buffers[i]; + if (buffer->presentation_time_valid && + !add_rebase_target(prepared, &buffer->presentation_time, count_limit)) { + return false; + } + if (buffer->decode_completion_time_valid && + !add_rebase_target(prepared, &buffer->decode_completion_time, + count_limit)) { + return false; + } + } + if (primary) { + for (uint32_t i = 0; i < model->dfg_count; ++i) { + Av2DmDfgRecord *const dfg = &model->dfgs[i]; + if (!add_rebase_target(prepared, &dfg->first_arrival, count_limit) || + !add_rebase_target(prepared, &dfg->last_arrival, count_limit) || + !add_rebase_target(prepared, &dfg->scheduled_removal, count_limit) || + !add_rebase_target(prepared, &dfg->removal, count_limit) || + !add_rebase_target(prepared, &dfg->decode_completion, count_limit)) { + return false; + } + } + if (model->previous_dfg_valid) { + Av2DmDfgRecord *const dfg = &model->previous_dfg; + if (!add_rebase_target(prepared, &dfg->first_arrival, count_limit) || + !add_rebase_target(prepared, &dfg->last_arrival, count_limit) || + !add_rebase_target(prepared, &dfg->scheduled_removal, count_limit) || + !add_rebase_target(prepared, &dfg->removal, count_limit) || + !add_rebase_target(prepared, &dfg->decode_completion, count_limit)) { + return false; + } + } + if (model->most_recent_rap_removal_valid && + !add_rebase_target(prepared, &model->most_recent_rap_scheduled_removal, + count_limit)) { + return false; + } + if (model->last_presentation_valid && + !add_rebase_target(prepared, &model->last_presentation, count_limit)) { + return false; + } + if (model->pending_display_late.valid && + (!add_rebase_target(prepared, &model->pending_display_late.threshold, + count_limit) || + !add_rebase_target(prepared, &model->pending_display_late.observed, + count_limit))) { + return false; + } + if (model->pending_decode_deadline.valid && + (!add_rebase_target(prepared, &model->pending_decode_deadline.threshold, + count_limit) || + !add_rebase_target(prepared, &model->pending_decode_deadline.observed, + count_limit))) { + return false; + } + } + return av2_dm_rational_rebase(prepared->values, prepared->count, origin); +} + +static void free_prepared_rebase(Av2DmPreparedRebase *prepared) { + avm_free(prepared->targets); + avm_free(prepared->values); + memset(prepared, 0, sizeof(*prepared)); +} + +static void commit_prepared_rebase(const Av2DmPreparedRebase *prepared) { + for (uint32_t i = 0; i < prepared->count; ++i) { + *prepared->targets[i] = prepared->values[i]; + } +} + +static void maybe_rebase_model(Av2DecoderModel *model) { + const uint32_t interval = model->config.rebase_interval_events == 0 + ? 4096 + : model->config.rebase_interval_events; + if (model->model_events == 0 || model->model_events % interval != 0 || + !model->lane.initial_presentation_delay_known || + !model->resource_lane.initial_presentation_delay_known) { + return; + } + Av2DmPreparedRebase primary = { 0 }; + Av2DmPreparedRebase resource = { 0 }; + // Both lanes participate in the Annex E schedule-vs-resource ordering + // comparison, so every absolute lane time must retain one shared origin. + const Av2DmRational origin = model->lane.time; + if (!prepare_lane_rebase(model, &model->lane, true, &origin, &primary) || + !prepare_lane_rebase(model, &model->resource_lane, false, &origin, + &resource)) { + free_prepared_rebase(&primary); + free_prepared_rebase(&resource); + arithmetic_failure(model); + return; + } + commit_prepared_rebase(&primary); + commit_prepared_rebase(&resource); + free_prepared_rebase(&primary); + free_prepared_rebase(&resource); +} + +static bool lane_buffer_is_live(const Av2DmLane *lane, uint32_t buffer_index) { + const Av2DmBuffer *const buffer = &lane->pool.buffers[buffer_index]; + return buffer->generation_valid && + (lane->current_buffer_index == (int32_t)buffer_index || + buffer->decoder_ref_count != 0 || buffer->player_ref_count != 0); +} + +static bool earlier_lane_has_generation(const Av2DmLane *const lanes[2], + uint32_t lane_index, + uint32_t buffer_index, + uint64_t generation) { + for (uint32_t i = 0; i <= lane_index; ++i) { + const uint32_t limit = + i == lane_index ? buffer_index : lanes[i]->pool.pool_size; + for (uint32_t j = 0; j < limit; ++j) { + const Av2DmBuffer *const buffer = &lanes[i]->pool.buffers[j]; + if (lane_buffer_is_live(lanes[i], j) && + buffer->generation == generation) { + return true; + } + } + } + return false; +} + +static uint32_t active_generation_count(const Av2DecoderModel *model) { + const Av2DmLane *const lanes[2] = { &model->lane, &model->resource_lane }; + uint32_t count = 0; + for (uint32_t i = 0; i < 2; ++i) { + for (uint32_t j = 0; j < lanes[i]->pool.pool_size; ++j) { + const Av2DmBuffer *const buffer = &lanes[i]->pool.buffers[j]; + if (lane_buffer_is_live(lanes[i], j) && + !earlier_lane_has_generation(lanes, i, j, buffer->generation)) { + ++count; + } + } + } + return count; +} + +static void update_storage_high_water(uint32_t active, uint32_t *current, + uint32_t *high_water) { + *current = active; + if (active > *high_water) *high_water = active; +} + +static void update_storage_stats(Av2DecoderModel *model) { + if (model->result.finished) { + model->storage.active_dfgs = 0; + model->storage.active_outputs = 0; + model->storage.active_tus = 0; + model->storage.active_generations = 0; + model->storage.active_cvs = 0; + model->storage.active_rap_runs = 0; + return; + } + const uint32_t active_outputs = + (uint32_t)model->pending_display_late.valid + + (uint32_t)model->pending_decode_deadline.valid; + const uint32_t active_run = + !model->result.finished && + model->result.applicability == AV2_DM_APPLICABLE + ? 1 + : 0; + uint32_t active_dfgs = model->dfg_count; + if (model->previous_dfg_valid && active_dfgs != UINT32_MAX) ++active_dfgs; + update_storage_high_water(active_dfgs, &model->storage.active_dfgs, + &model->storage.high_water_dfgs); + update_storage_high_water(active_outputs, &model->storage.active_outputs, + &model->storage.high_water_outputs); + update_storage_high_water(model->tu_count, &model->storage.active_tus, + &model->storage.high_water_tus); + update_storage_high_water(active_generation_count(model), + &model->storage.active_generations, + &model->storage.high_water_generations); + update_storage_high_water(active_run, &model->storage.active_cvs, + &model->storage.high_water_cvs); + update_storage_high_water(active_run, &model->storage.active_rap_runs, + &model->storage.high_water_rap_runs); +} + +static void model_event_complete(Av2DecoderModel *model) { + if (!increment_model_u64(model, &model->model_events)) return; + maybe_rebase_model(model); + update_storage_stats(model); +} + +void av2_decoder_model_start_frame(Av2DecoderModel *model, + const Av2DmFrameEvent *event) { + if (model == NULL || event == NULL || model->result.finished || + model->result.applicability == AV2_DM_NOT_APPLICABLE || + model->processing_stopped) { + return; + } + if (model->frame_number == UINT64_MAX) { + arithmetic_failure(model); + return; + } + model->latest_frame_event_index = event->event_index; + // Annex E synchronizes VBI with RefValid at every start_frame_decode(), + // before FrameNum advances or a current buffer is selected. + if (!invalidate_lane_reference_buffers(&model->lane, event->ref_valid_mask) || + !invalidate_lane_reference_buffers(&model->resource_lane, + event->ref_valid_mask)) { + arithmetic_failure(model); + return; + } + ++model->frame_number; + if (model->coded_tu_valid && model->coded_tu != event->temporal_unit_index) { + Av2DmTuRecord *const previous_coded = find_tu(model, model->coded_tu); + if (previous_coded == NULL) { + arithmetic_failure(model); + return; + } + previous_coded->header_complete = true; + } + model->coded_tu = event->temporal_unit_index; + model->coded_tu_valid = true; + Av2DmTuRecord *const tu = + get_tu(model, event->temporal_unit_index, event->event_index); + if (tu != NULL && event->temporal_unit_output_time_present) { + tu->output_time = event->temporal_unit_output_time; + tu->output_time_valid = true; + if (!update_latest_timed_tu(model, &tu->output_time)) { + arithmetic_failure(model); + return; + } + } + if (tu != NULL && event->count_frame_header) { + if (tu->frame_headers == UINT32_MAX) { + arithmetic_failure(model); + return; + } + ++tu->frame_headers; + } + if (event->show_existing_frame) { + if (!model->config.defer_nonterminal_checks_for_testing) { + check_header_rate_windows(model, false, event->event_index); + } + retire_unresolvable_tus(model); + model_event_complete(model); + return; + } + if (event->max_tile_area > model->maximum_tile_area) { + // Annex A MaxTileSizeInLumaSamples covers every tile in the coded video + // sequence, independently of CountFrameHeaderForLevelConstraint. + model->maximum_tile_area = event->max_tile_area; + check_retired_tile_header_summary(model, event->event_index); + } + if (!model->config.defer_nonterminal_checks_for_testing) { + check_header_rate_windows(model, false, event->event_index); + } + if (model->processing_stopped) return; + if (model->smoothing_epoch_prepared && + !event->decoder_model_parameters_updated) { + arithmetic_failure(model); + return; + } + if (event->decoder_model_parameters_updated && model->dfg_number != 0 && + !model->smoothing_epoch_prepared) { + // FirstBitArrival restarts at zero when decoder-model parameters change. + // Close the prior smoothing epoch before accepting the new epoch so its + // occupancy cannot be combined with the reset timeline. + if (!model->config.defer_nonterminal_checks_for_testing) { + check_smoothing_buffer_overflow(model, event->event_index); + model->dfg_count = 0; + } + if (model->smoothing_epoch == UINT64_MAX) { + arithmetic_failure(model); + return; + } + ++model->smoothing_epoch; + } + model->smoothing_epoch_prepared = false; + if (model->dfg_count == UINT32_MAX || model->dfg_number == UINT64_MAX || + (event->random_access_point && model->rap_epoch == UINT64_MAX) || + !grow_array((void **)&model->dfgs, &model->dfg_capacity, model->dfg_count, + sizeof(*model->dfgs))) { + arithmetic_failure(model); + return; + } + Av2DmDfgRecord *const dfg = &model->dfgs[model->dfg_count++]; + memset(dfg, 0, sizeof(*dfg)); + dfg->event_index = event->event_index; + dfg->temporal_unit_index = event->temporal_unit_index; + dfg->generation = event->generation; + dfg->coded_bits = event->coded_bits; + dfg->decode_order = model->result.decoded_frames; + dfg->smoothing_epoch = model->smoothing_epoch; + dfg->limits = model->limits; + dfg->tier = model->config.tier; + dfg->mode = model->config.mode; + dfg->random_access_point = event->random_access_point; + dfg->parameters_updated = event->decoder_model_parameters_updated; + dfg->count_frame_header = event->count_frame_header; + dfg->decode_count_two = + event->allow_global_intrabc && event->inloop_filtering_enabled; + dfg->coded_as_closed_loop_key = event->coded_as_closed_loop_key; + dfg->num_tiles = event->num_tiles; + dfg->max_tile_area = event->max_tile_area; + dfg->compressed_size = event->compressed_size_bytes > 128 + ? event->compressed_size_bytes - 128 + : 0; + dfg->frame_symbol_count = event->frame_symbol_count; + if (event->random_access_point) ++model->rap_epoch; + dfg->rap_epoch = model->rap_epoch; + ++model->dfg_number; + + check_static_level_limits(model, event); + if (model->processing_stopped) return; + uint64_t decode_luma_samples; + if (!calculate_decode_time(model, event, &decode_luma_samples, + &dfg->decode_time)) { + arithmetic_failure(model); + return; + } + dfg->luma_samples = + dfg->decode_count_two ? decode_luma_samples / 2 : decode_luma_samples; + if (dfg->decode_count_two && + (model->config.max_mlayer_id != 0 || !dfg->coded_as_closed_loop_key)) { + model->any_decode_count_two_requires_reserved_buffer = true; + } + if (!model->config.defer_nonterminal_checks_for_testing) { + check_max_reference_frames(model, event->event_index); + } + if (model->processing_stopped) return; + if (!calculate_scheduled_removal(model, event, dfg) || + !calculate_arrival_times(model, dfg)) { + if (!model->result.missing_required_input) arithmetic_failure(model); + return; + } + dfg->removal = dfg->scheduled_removal; + bool scheduled_before_arrival; + if (!rational_less(&dfg->scheduled_removal, &dfg->last_arrival, + &scheduled_before_arrival)) { + arithmetic_failure(model); + return; + } + if (scheduled_before_arrival && !model->low_delay_mode) { + // DM-SPEC-2: availability at the scheduled time is required only in + // strict-arrival mode. + report_violation(model, AV2_DM_VIOLATION_SMOOTHING_BUFFER_UNDERFLOW, + event->event_index, &dfg->scheduled_removal, + &dfg->last_arrival); + } else if (scheduled_before_arrival && + model->config.mode == AV2_DM_DECODING_SCHEDULE_MODE && + !rational_ceil_ratio_to_tick(&dfg->last_arrival, &model->dec_ct, + &dfg->removal)) { + arithmetic_failure(model); + return; + } + if (!model->config.defer_nonterminal_checks_for_testing) { + check_smoothing_buffer_overflow(model, event->event_index); + } + if (model->processing_stopped) return; + + Av2DmRational resource_removal; + if (!next_resource_removal(model, &model->resource_lane, + model->dfg_number - 1, &resource_removal)) { + if (!model->result.missing_required_input) arithmetic_failure(model); + return; + } + int32_t resource_buffer_index; + if (!lane_start_decode(&model->resource_lane, &resource_removal, + &dfg->decode_time, event->generation, + &resource_buffer_index)) { + arithmetic_failure(model); + return; + } + if (resource_buffer_index < 0 && + model->config.mode == AV2_DM_RESOURCE_AVAILABILITY_MODE) { + const Av2DmViolationDetail detail = + buffer_pool_violation_detail(&model->resource_lane.pool, true); + report_violation_for_affected( + model, AV2_DM_VIOLATION_DECODE_FRAME_BUFFER_UNAVAILABLE, + event->event_index, AV2_DM_VIOLATION_AFFECTED_EVENT, event->event_index, + NULL, NULL, &detail); + } + + int32_t buffer_index; + if (!lane_start_decode(&model->lane, &dfg->removal, &dfg->decode_time, + event->generation, &buffer_index)) { + arithmetic_failure(model); + return; + } + if (buffer_index < 0) { + const Av2DmViolationDetail detail = + buffer_pool_violation_detail(&model->lane.pool, false); + report_violation_for_affected( + model, AV2_DM_VIOLATION_DECODE_FRAME_BUFFER_UNAVAILABLE, + event->event_index, AV2_DM_VIOLATION_AFFECTED_EVENT, event->event_index, + NULL, NULL, &detail); + } + dfg->decode_completion = model->lane.time; + if (buffer_index >= 0) { + Av2DmBuffer *const buffer = &model->lane.pool.buffers[buffer_index]; + buffer->decode_order = dfg->decode_order; + buffer->rap_epoch = dfg->rap_epoch; + buffer->random_access_point = dfg->random_access_point; + buffer->coded_temporal_unit_index = dfg->temporal_unit_index; + buffer->coded_temporal_unit_valid = true; + } + if (resource_buffer_index >= 0) { + Av2DmBuffer *const buffer = + &model->resource_lane.pool.buffers[resource_buffer_index]; + buffer->decode_order = dfg->decode_order; + buffer->rap_epoch = dfg->rap_epoch; + buffer->random_access_point = dfg->random_access_point; + buffer->coded_temporal_unit_index = dfg->temporal_unit_index; + buffer->coded_temporal_unit_valid = true; + } + + if (model->config.mode == AV2_DM_DECODING_SCHEDULE_MODE) { + compare_lower_limit( + model, AV2_DM_VIOLATION_SCHEDULE_BEFORE_RESOURCE_REMOVAL, + event->event_index, &dfg->scheduled_removal, &resource_removal); + } + if (model->previous_dfg_valid) { + check_previous_dfg_interval(model, &model->previous_dfg, dfg); + } + check_schedule_delay_limits(model, event->event_index); + check_delay_consistency(model, dfg); + + if (model->dfg_number == 1 || event->random_access_point) { + model->most_recent_rap_scheduled_removal = dfg->scheduled_removal; + model->most_recent_rap_removal_valid = true; + } + model->previous_dfg = *dfg; + model->previous_dfg_valid = true; + if (!model->config.defer_nonterminal_checks_for_testing) { + retire_closed_smoothing_records(model, &dfg->last_arrival); + } + retire_unresolvable_tus(model); + if (!increment_model_u64(model, &model->result.decoded_frames)) return; + model_event_complete(model); + update_result_status(model); +} + +static bool update_lane_reference_buffers( + Av2DmLane *lane, const Av2DmReferenceUpdateEvent *event) { + for (uint32_t i = 0; i < lane->pool.num_ref_frames; ++i) { + if (((event->refresh_frame_flags >> i) & 1) == 0) continue; + int32_t buffer_index = -1; + if (((event->ref_valid_mask >> i) & 1) != 0) { + buffer_index = lane->current_buffer_index; + } + if (!av2_dm_buffer_pool_set_vbi(&lane->pool, i, buffer_index)) { + return false; + } + } + return true; +} + +void av2_decoder_model_update_reference_buffers( + Av2DecoderModel *model, const Av2DmReferenceUpdateEvent *event) { + if (model == NULL || event == NULL || model->result.finished || + model->result.applicability == AV2_DM_NOT_APPLICABLE || + model->processing_stopped) { + return; + } + if (model->shown_frame_number == UINT64_MAX || + model->result.output_frames == UINT64_MAX) { + arithmetic_failure(model); + return; + } + if (!update_lane_reference_buffers(&model->lane, event) || + !update_lane_reference_buffers(&model->resource_lane, event)) { + arithmetic_failure(model); + } + retire_unresolvable_tus(model); + model_event_complete(model); +} + +static bool invalidate_lane_reference_buffers(Av2DmLane *lane, + uint32_t ref_valid_mask) { + for (uint32_t i = 0; i < lane->pool.num_ref_frames; ++i) { + if (((ref_valid_mask >> i) & 1) == 0 && lane->pool.vbi[i] != -1 && + !av2_dm_buffer_pool_set_vbi(&lane->pool, i, -1)) { + return false; + } + } + return true; +} + +void av2_decoder_model_invalidate_olk_reference_buffers( + Av2DecoderModel *model, uint32_t ref_valid_mask) { + if (model == NULL || model->result.finished || + model->result.applicability == AV2_DM_NOT_APPLICABLE || + model->processing_stopped) { + return; + } + // DM-SPEC-5 / Annex E invalidate_olk_ref_buffers(): RefValid has already + // been updated by frame_header_info(), so every invalid slot is mirrored, + // including slots absent from the current refresh_frame_flags. + if (!invalidate_lane_reference_buffers(&model->lane, ref_valid_mask) || + !invalidate_lane_reference_buffers(&model->resource_lane, + ref_valid_mask)) { + arithmetic_failure(model); + } + model_event_complete(model); +} + +static void complete_output_checks(Av2DecoderModel *model, + uint64_t affected_event_index, + uint64_t proving_event_index, + const Av2DmRational *output_time, + const Av2DmRational *decode_completion, + const Av2DmRational *presentation) { + bool late; + if (!rational_greater(output_time, presentation, &late)) { + arithmetic_failure(model); + return; + } + if (late) { + report_violation_for_affected( + model, AV2_DM_VIOLATION_DISPLAY_FRAME_LATE, proving_event_index, + AV2_DM_VIOLATION_AFFECTED_OUTPUT, affected_event_index, output_time, + presentation, NULL); + } + if (decode_completion == NULL) return; + bool missed_deadline; + if (!rational_greater(decode_completion, presentation, &missed_deadline)) { + arithmetic_failure(model); + return; + } + if (missed_deadline) { + // DM-SPEC-3 associates the three times by decoded generation, not by + // their positions in the decode- and presentation-order arrays. + report_violation_for_affected( + model, AV2_DM_VIOLATION_DECODE_DEADLINE, proving_event_index, + AV2_DM_VIOLATION_AFFECTED_OUTPUT, affected_event_index, + decode_completion, presentation, NULL); + } +} + +static bool update_pending_output_witness( + Av2DmPendingOutputWitness *pending, uint64_t event_index, + const Av2DmRational *observed, const Av2DmRational *presentation_offset) { + Av2DmRational threshold; + if (!av2_dm_rational_subtract(observed, presentation_offset, &threshold)) { + return false; + } + if (pending->valid) { + int comparison; + if (!av2_dm_rational_compare(&threshold, &pending->threshold, + &comparison)) { + return false; + } + if (comparison <= 0) return true; + } + pending->valid = true; + pending->event_index = event_index; + pending->threshold = threshold; + pending->observed = *observed; + pending->presentation_offset = *presentation_offset; + return true; +} + +static bool complete_pending_output_check(Av2DecoderModel *model, + Av2DmPendingOutputWitness *pending, + Av2DmViolationCode code, + const Av2DmRational *initial_delay, + uint64_t proving_event_index) { + if (!pending->valid) return true; + bool violated; + if (!rational_greater(&pending->threshold, initial_delay, &violated)) { + return false; + } + if (violated) { + Av2DmRational presentation; + if (!av2_dm_rational_add(&pending->presentation_offset, initial_delay, + &presentation)) { + return false; + } + report_violation_for_affected( + model, code, proving_event_index, AV2_DM_VIOLATION_AFFECTED_OUTPUT, + pending->event_index, &pending->observed, &presentation, NULL); + } + pending->valid = false; + return true; +} + +static bool set_lane_initial_presentation_delay(Av2DecoderModel *model, + Av2DmLane *lane, + bool primary_lane, + uint64_t proving_event_index) { + if (lane->initial_presentation_delay_known || + av2_dm_buffer_pool_frames_in_use(&lane->pool) < + model->config.initial_display_delay) { + return true; + } + lane->initial_presentation_delay = lane->time; + lane->initial_presentation_delay_known = true; + for (uint32_t i = 0; i < lane->pool.pool_size; ++i) { + Av2DmBuffer *const buffer = &lane->pool.buffers[i]; + if (buffer->player_ref_count != 0 && !buffer->presentation_time_valid) { + if (!av2_dm_rational_add(&buffer->presentation_time, + &lane->initial_presentation_delay, + &buffer->presentation_time)) { + return false; + } + buffer->presentation_time_valid = true; + } + } + if (primary_lane && + (!complete_pending_output_check(model, &model->pending_display_late, + AV2_DM_VIOLATION_DISPLAY_FRAME_LATE, + &lane->initial_presentation_delay, + proving_event_index) || + !complete_pending_output_check(model, &model->pending_decode_deadline, + AV2_DM_VIOLATION_DECODE_DEADLINE, + &lane->initial_presentation_delay, + proving_event_index))) { + return false; + } + if (primary_lane && model->last_presentation_offset_valid) { + if (!av2_dm_rational_add(&model->last_presentation_offset, + &lane->initial_presentation_delay, + &model->last_presentation)) { + return false; + } + model->last_presentation_valid = true; + } + return true; +} + +void av2_decoder_model_set_initial_presentation_delay(Av2DecoderModel *model, + uint64_t event_index) { + if (model == NULL || model->result.finished || + model->result.applicability == AV2_DM_NOT_APPLICABLE || + model->processing_stopped) { + return; + } + if (!set_lane_initial_presentation_delay(model, &model->lane, true, + event_index) || + !set_lane_initial_presentation_delay(model, &model->resource_lane, false, + event_index)) { + arithmetic_failure(model); + } + model_event_complete(model); +} + +static void check_tu_display_rate(Av2DecoderModel *model, Av2DmTuRecord *tu, + const Av2DmRational *duration, + uint64_t proving_event_index) { + if (model->config.still_picture) return; + Av2DmRational observed; + Av2DmRational capacity; + if (!av2_dm_rational_multiply_u64(duration, model->limits.max_display_rate, + &capacity) || + !av2_dm_rational_make(tu->output_luma_samples, 1, &observed)) { + arithmetic_failure(model); + return; + } + compare_upper_limit_for_affected( + model, AV2_DM_VIOLATION_MAX_DISPLAY_RATE, proving_event_index, + AV2_DM_VIOLATION_AFFECTED_TEMPORAL_UNIT, tu->temporal_unit_index, + &observed, &capacity); +} + +static void check_tu_minimum_presentation_interval( + Av2DecoderModel *model, Av2DmTuRecord *tu, const Av2DmRational *interval, + uint64_t proving_event_index) { + if (model->config.still_picture) return; + Av2DmRational limit; + const uint64_t max_headers = (uint64_t)model->limits.max_header_rate * + (1 + ((uint64_t)model->config.tier << 1)); + Av2DmRational sample_interval; + Av2DmRational min_frame_time; + if (!rational_from_product(model->config.max_frame_width, + model->config.max_frame_height, + &sample_interval) || + !av2_dm_rational_multiply_u64(&sample_interval, tu->output_frames, + &sample_interval) || + !av2_dm_rational_divide_u64( + &sample_interval, model->limits.max_display_rate, &sample_interval) || + !av2_dm_rational_make(model->limits.max_decode_rate, + model->limits.max_display_rate, &min_frame_time) || + !av2_dm_rational_divide_u64(&min_frame_time, max_headers, + &min_frame_time) || + !rational_max(&sample_interval, &min_frame_time, &limit)) { + arithmetic_failure(model); + return; + } + compare_lower_limit_for_affected( + model, AV2_DM_VIOLATION_MINIMUM_PRESENTATION_INTERVAL, + proving_event_index, AV2_DM_VIOLATION_AFFECTED_TEMPORAL_UNIT, + tu->temporal_unit_index, interval, &limit); +} + +static void update_tu_for_output(Av2DecoderModel *model, + const Av2DmOutputEvent *event, + const Av2DmRational *presentation_offset) { + Av2DmTuRecord *const tu = + get_tu(model, event->temporal_unit_index, event->event_index); + if (tu == NULL) return; + bool output_time_regressed = false; + if (UINT64_MAX - tu->output_luma_samples < event->output_luma_samples || + tu->output_frames == UINT32_MAX) { + arithmetic_failure(model); + return; + } + tu->output_luma_samples += event->output_luma_samples; + ++tu->output_frames; + if (!tu->presentation_time_valid) { + tu->presentation_time = *presentation_offset; + tu->presentation_time_valid = true; + } + if (!tu->output_time_valid) { + // When no external TU output time was supplied, the first actual output + // event establishes the TU output time in display order. Coding-order TU + // indices are identifiers and are not timestamps. + tu->output_time = *presentation_offset; + tu->output_time_valid = true; + } + if (!update_latest_timed_tu(model, &tu->output_time)) { + arithmetic_failure(model); + return; + } + if (model->last_output_tu_valid && + model->last_output_tu != tu->temporal_unit_index) { + Av2DmTuRecord *const previous = find_tu(model, model->last_output_tu); + if (previous == NULL) { + arithmetic_failure(model); + return; + } + if (previous->presentation_time_valid) { + Av2DmRational presentation_interval; + if (!av2_dm_rational_subtract(presentation_offset, + &previous->presentation_time, + &presentation_interval)) { + arithmetic_failure(model); + return; + } + check_tu_minimum_presentation_interval( + model, previous, &presentation_interval, event->event_index); + previous->prior_presentation_interval_checked = true; + } + if (previous->output_time_valid && tu->output_time_valid) { + int ordering; + if (!av2_dm_rational_compare(&tu->output_time, &previous->output_time, + &ordering)) { + arithmetic_failure(model); + return; + } + output_time_regressed = ordering <= 0; + Av2DmRational display_duration; + if (!av2_dm_rational_subtract(&tu->output_time, &previous->output_time, + &display_duration)) { + arithmetic_failure(model); + return; + } + check_tu_display_rate(model, previous, &display_duration, + event->event_index); + model->last_display_duration = display_duration; + model->last_display_duration_valid = true; + } + } + model->last_output_tu = tu->temporal_unit_index; + model->last_output_tu_valid = true; + if (output_time_regressed && + (violation_seen(model, AV2_DM_VIOLATION_MAX_DISPLAY_RATE) || + violation_seen(model, AV2_DM_VIOLATION_MINIMUM_PRESENTATION_INTERVAL) || + violation_seen(model, AV2_DM_VIOLATION_PRESENTATION_TIME_DECREASE))) { + // A non-increasing output timeline has already proven conformance failure. + // Start a new bounded rate-window segment while retaining any generations + // that can still be output and checked for other violation classes. + restart_tu_history(model, tu->temporal_unit_index); + } +} + +static const Av2DmRapPresentationAnchor *find_rap_presentation_anchor( + const Av2DecoderModel *model, uint64_t rap_epoch) { + for (uint32_t i = 0; i < AV2_DM_MAX_BUFFER_POOL_SIZE + 2; ++i) { + if (model->rap_presentation_anchors[i].valid && + model->rap_presentation_anchors[i].rap_epoch == rap_epoch) { + return &model->rap_presentation_anchors[i]; + } + } + return NULL; +} + +static void store_rap_presentation_anchor(Av2DecoderModel *model, + uint64_t rap_epoch, + const Av2DmRational *offset) { + Av2DmRapPresentationAnchor *free_anchor = NULL; + for (uint32_t i = 0; i < AV2_DM_MAX_BUFFER_POOL_SIZE + 2; ++i) { + if (model->rap_presentation_anchors[i].valid && + model->rap_presentation_anchors[i].rap_epoch == rap_epoch) { + model->rap_presentation_anchors[i].presentation_offset = *offset; + return; + } + if (!model->rap_presentation_anchors[i].valid && free_anchor == NULL) { + free_anchor = &model->rap_presentation_anchors[i]; + } + } + if (free_anchor == NULL) { + // At most one anchor is needed per DPB generation epoch, plus the current + // and immediately preceding RAP. Reclaim an epoch that no live generation + // can present again before treating exhaustion as an internal failure. + for (uint32_t i = 0; i < AV2_DM_MAX_BUFFER_POOL_SIZE + 2; ++i) { + Av2DmRapPresentationAnchor *const candidate = + &model->rap_presentation_anchors[i]; + bool live = candidate->rap_epoch == model->rap_epoch || + (model->rap_epoch != 0 && + candidate->rap_epoch == model->rap_epoch - 1); + for (uint32_t j = 0; j < model->lane.pool.pool_size && !live; ++j) { + const Av2DmBuffer *const buffer = &model->lane.pool.buffers[j]; + live = buffer->generation_valid && + buffer->rap_epoch == candidate->rap_epoch; + } + if (!live) { + free_anchor = candidate; + break; + } + } + } + if (free_anchor == NULL) { + arithmetic_failure(model); + return; + } + free_anchor->valid = true; + free_anchor->rap_epoch = rap_epoch; + free_anchor->presentation_offset = *offset; +} + +static bool calculate_presentation_offset(Av2DecoderModel *model, + const Av2DmOutputEvent *event, + const Av2DmBuffer *buffer, + Av2DmRational *offset) { + if (model->config.equal_picture_interval) { + if (!model->last_presentation_offset_valid) return rational_zero(offset); + if (event->temporal_unit_index == model->last_output_temporal_unit) { + *offset = model->last_presentation_offset; + return true; + } + Av2DmRational increment; + return av2_dm_rational_multiply_u64( + &model->disp_ct, model->config.ticks_per_picture, &increment) && + av2_dm_rational_add(&model->last_presentation_offset, &increment, + offset); + } + if (!event->presentation_time_present) { + missing_input(model); + return false; + } + if (model->shown_frame_number == 0) return rational_zero(offset); + Av2DmRational base; + bool base_found = false; + uint64_t presentation_epoch = model->rap_epoch; + bool random_access_point = event->presentation_random_access_point; + if (!event->presentation_uses_current_frame) { + presentation_epoch = buffer->rap_epoch; + random_access_point = buffer->random_access_point; + } + if (buffer->generation_valid || event->presentation_uses_current_frame || + model->config.ras_start) { + const uint64_t base_epoch = + (random_access_point || event->leading_frame) + ? (presentation_epoch == 0 ? 0 : presentation_epoch - 1) + : presentation_epoch; + if (base_epoch == 0) { + base_found = rational_zero(&base); + } else { + const Av2DmRapPresentationAnchor *const anchor = + find_rap_presentation_anchor(model, base_epoch); + if (anchor != NULL) { + base = anchor->presentation_offset; + base_found = true; + } + } + } + if (!base_found && event->presentation_base_offset_present) { + // Externally seeded RAS frames have no decode record in this model run. + base = event->presentation_base_offset; + base_found = true; + } + if (!base_found) { + missing_input(model); + return false; + } + Av2DmRational increment; + return av2_dm_rational_multiply_u64( + &model->disp_ct, event->presentation_time_ticks, &increment) && + av2_dm_rational_add(&base, &increment, offset); +} + +static int32_t select_output_buffer(Av2DecoderModel *model, Av2DmLane *lane, + const Av2DmOutputEvent *event, + bool report_error) { + if (event->frame_to_show_map_idx == -1) { + return lane->current_buffer_index; + } + if (event->frame_to_show_map_idx < 0 || + (uint32_t)event->frame_to_show_map_idx >= lane->pool.num_ref_frames || + ((event->ref_valid_mask >> event->frame_to_show_map_idx) & 1) == 0 || + lane->pool.vbi[event->frame_to_show_map_idx] == -1) { + if (report_error) { + Av2DmViolationDetail detail; + memset(&detail, 0, sizeof(detail)); + detail.kind = AV2_DM_VIOLATION_DETAIL_REFERENCE_SLOT; + Av2DmReferenceSlotViolationDetail *const slot = + &detail.value.reference_slot; + slot->requested_slot = event->frame_to_show_map_idx; + slot->slot_in_range = + event->frame_to_show_map_idx >= 0 && + (uint32_t)event->frame_to_show_map_idx < lane->pool.num_ref_frames; + slot->buffer_index = -1; + if (slot->slot_in_range) { + slot->reference_valid = + ((event->ref_valid_mask >> event->frame_to_show_map_idx) & 1) != 0; + slot->buffer_index = lane->pool.vbi[event->frame_to_show_map_idx]; + } + slot->pool = + buffer_pool_violation_detail(&lane->pool, false).value.buffer_pool; + report_violation_for_affected( + model, AV2_DM_VIOLATION_DECODE_EXISTING_FRAME_BUFFER_EMPTY, + event->event_index, AV2_DM_VIOLATION_AFFECTED_OUTPUT, + event->event_index, NULL, NULL, &detail); + } + return -1; + } + return lane->pool.vbi[event->frame_to_show_map_idx]; +} + +void av2_decoder_model_output_frame(Av2DecoderModel *model, + const Av2DmOutputEvent *event) { + if (model == NULL || event == NULL || model->result.finished || + model->result.applicability == AV2_DM_NOT_APPLICABLE || + model->processing_stopped) { + return; + } + const int32_t buffer_index = + select_output_buffer(model, &model->lane, event, true); + const int32_t resource_buffer_index = + select_output_buffer(model, &model->resource_lane, event, false); + if (buffer_index < 0 || resource_buffer_index < 0) { + if (!increment_output_count(model)) return; + model_event_complete(model); + update_result_status(model); + return; + } + Av2DmBuffer *const buffer = &model->lane.pool.buffers[buffer_index]; + Av2DmBuffer *const resource_buffer = + &model->resource_lane.pool.buffers[resource_buffer_index]; + if (!buffer->generation_valid || !resource_buffer->generation_valid || + buffer->generation != event->generation || + resource_buffer->generation != event->generation) { + missing_input(model); + return; + } + Av2DmRational presentation_offset; + if (!calculate_presentation_offset(model, event, buffer, + &presentation_offset)) { + if (!model->result.missing_required_input) arithmetic_failure(model); + return; + } + const uint64_t presentation_epoch = event->presentation_uses_current_frame + ? model->rap_epoch + : buffer->rap_epoch; + const uint64_t output_rap_epoch = + event->leading_frame && presentation_epoch != 0 ? presentation_epoch - 1 + : presentation_epoch; + const bool random_access_point = event->presentation_uses_current_frame + ? event->presentation_random_access_point + : buffer->random_access_point; + Av2DmRational presentation = presentation_offset; + if (model->lane.initial_presentation_delay_known) { + if (!av2_dm_rational_add(&presentation, + &model->lane.initial_presentation_delay, + &presentation)) { + arithmetic_failure(model); + return; + } + } + buffer->presentation_time = presentation; + buffer->presentation_time_valid = + model->lane.initial_presentation_delay_known; + if (!av2_dm_buffer_pool_add_player_ref(&model->lane.pool, + (uint32_t)buffer_index)) { + arithmetic_failure(model); + return; + } + + resource_buffer->presentation_time = presentation_offset; + resource_buffer->presentation_time_valid = false; + if (model->resource_lane.initial_presentation_delay_known) { + if (!av2_dm_rational_add(&resource_buffer->presentation_time, + &model->resource_lane.initial_presentation_delay, + &resource_buffer->presentation_time)) { + arithmetic_failure(model); + return; + } + resource_buffer->presentation_time_valid = true; + } + if (!av2_dm_buffer_pool_add_player_ref(&model->resource_lane.pool, + (uint32_t)resource_buffer_index)) { + arithmetic_failure(model); + return; + } + + if (model->previous_output_presentation_valid && + model->previous_output_rap_epoch == output_rap_epoch) { + compare_lower_limit(model, AV2_DM_VIOLATION_PRESENTATION_TIME_DECREASE, + event->event_index, &presentation_offset, + &model->previous_output_presentation_offset); + } + if (buffer->decode_completion_time_valid) { + if (model->previous_output_order_valid && + buffer->decode_order < model->previous_output_decode_order) { + if (!increment_model_u64(model, &model->result.reordered_outputs)) { + return; + } + } + model->previous_output_decode_order = buffer->decode_order; + model->previous_output_order_valid = true; + } + model->previous_output_presentation_offset = presentation_offset; + model->previous_output_presentation_valid = true; + model->previous_output_rap_epoch = output_rap_epoch; + model->last_presentation_offset = presentation_offset; + model->last_presentation_offset_valid = true; + if (model->lane.initial_presentation_delay_known) { + model->last_presentation = presentation; + model->last_presentation_valid = true; + } + model->last_output_temporal_unit = event->temporal_unit_index; + if (random_access_point) { + store_rap_presentation_anchor(model, output_rap_epoch, + &presentation_offset); + } + update_tu_for_output(model, event, &presentation_offset); + if (model->processing_stopped) return; + if (!model->config.defer_nonterminal_checks_for_testing) { + check_header_rate_windows(model, false, event->event_index); + } + if (model->processing_stopped) return; + if (model->lane.initial_presentation_delay_known) { + complete_output_checks( + model, event->event_index, event->event_index, &model->lane.time, + buffer->decode_completion_time_valid ? &buffer->decode_completion_time + : NULL, + &presentation); + } else if (!update_pending_output_witness( + &model->pending_display_late, event->event_index, + &model->lane.time, &presentation_offset) || + (buffer->decode_completion_time_valid && + !update_pending_output_witness( + &model->pending_decode_deadline, event->event_index, + &buffer->decode_completion_time, &presentation_offset))) { + arithmetic_failure(model); + return; + } + + if (!increment_output_count(model)) return; + model_event_complete(model); + update_result_status(model); +} + +static void check_smoothing_fullness_at(Av2DecoderModel *model, + const Av2DmRational *time, + Av2DmDfgRecord *breakpoint, + uint64_t proving_event_index) { + Av2DmRational fullness; + if (!rational_zero(&fullness)) { + arithmetic_failure(model); + return; + } + for (uint32_t i = 0; i < model->dfg_count; ++i) { + const Av2DmDfgRecord *const dfg = &model->dfgs[i]; + if (dfg->smoothing_epoch != breakpoint->smoothing_epoch) continue; + int before_first; + int after_removal; + if (!av2_dm_rational_compare(time, &dfg->first_arrival, &before_first) || + !av2_dm_rational_compare(time, &dfg->removal, &after_removal)) { + arithmetic_failure(model); + return; + } + if (before_first < 0 || after_removal > 0) continue; + Av2DmRational duration; + Av2DmRational arrived; + Av2DmRational coded_bits; + if (!av2_dm_rational_subtract(time, &dfg->first_arrival, &duration) || + !rational_multiply(&duration, &breakpoint->limits.bit_rate, &arrived) || + !av2_dm_rational_make(dfg->coded_bits, 1, &coded_bits)) { + arithmetic_failure(model); + return; + } + bool too_many; + if (!rational_greater(&arrived, &coded_bits, &too_many)) { + arithmetic_failure(model); + return; + } + if (too_many) arrived = coded_bits; + if (!av2_dm_rational_add(&fullness, &arrived, &fullness)) { + arithmetic_failure(model); + return; + } + } + bool overflow; + if (!rational_greater(&fullness, &breakpoint->limits.buffer_size, + &overflow)) { + arithmetic_failure(model); + return; + } + if (overflow && !breakpoint->smoothing_overflow_reported) { + breakpoint->smoothing_overflow_reported = true; + report_violation_for_affected( + model, AV2_DM_VIOLATION_SMOOTHING_BUFFER_OVERFLOW, proving_event_index, + AV2_DM_VIOLATION_AFFECTED_DFG, breakpoint->event_index, &fullness, + &breakpoint->limits.buffer_size, NULL); + } +} + +static void retire_closed_smoothing_records(Av2DecoderModel *model, + const Av2DmRational *frontier) { + uint32_t write_index = 0; + for (uint32_t i = 0; i < model->dfg_count; ++i) { + Av2DmDfgRecord *const dfg = &model->dfgs[i]; + int comparison; + if (!av2_dm_rational_compare(&dfg->removal, frontier, &comparison)) { + arithmetic_failure(model); + return; + } + // Equality remains live because a later DFG may start arriving at exactly + // this frontier and introduce another breakpoint at the same instant. + if (comparison < 0) continue; + if (write_index != i) model->dfgs[write_index] = *dfg; + ++write_index; + } + model->dfg_count = write_index; +} + +static void check_smoothing_buffer_overflow(Av2DecoderModel *model, + uint64_t proving_event_index) { + if (violation_seen(model, AV2_DM_VIOLATION_SMOOTHING_BUFFER_OVERFLOW)) { + model->dfg_count = 0; + return; + } + for (uint32_t i = 0; i < model->dfg_count; ++i) { + Av2DmDfgRecord *const breakpoint = &model->dfgs[i]; + check_smoothing_fullness_at(model, &breakpoint->last_arrival, breakpoint, + proving_event_index); + check_smoothing_fullness_at(model, &breakpoint->removal, breakpoint, + proving_event_index); + } + if (violation_seen(model, AV2_DM_VIOLATION_SMOOTHING_BUFFER_OVERFLOW)) { + // Fullness history cannot prove a different code after overflow has made + // this CVS non-conformant. Adjacent-DFG and per-frame checks retain their + // independent scalar state and continue online. + model->dfg_count = 0; + } +} + +static bool same_scope(const Av2DmScope *a, const Av2DmScope *b) { + return a->xlayer_id == b->xlayer_id && a->ops_xlayer_id == b->ops_xlayer_id && + a->ops_id == b->ops_id && a->operating_point == b->operating_point && + a->whole_xlayer == b->whole_xlayer; +} + +static bool same_model_topology_and_clock(const Av2DmConfig *a, + const Av2DmConfig *b) { + // Section 7 keeps the active sequence header fixed until the next CLK, + // which starts a new CVS and therefore a new model. In-place RAP updates + // can replace OPS parameters, but not the active sequence-level fallback. + return same_scope(&a->scope, &b->scope) && + a->num_ref_frames == b->num_ref_frames && + a->max_frame_width == b->max_frame_width && + a->max_frame_height == b->max_frame_height && + a->max_mlayer_id == b->max_mlayer_id && + a->still_picture == b->still_picture && + a->explicit_num_ref_frames == b->explicit_num_ref_frames && + a->timing_info_present == b->timing_info_present && + a->num_units_in_display_tick == b->num_units_in_display_tick && + a->time_scale == b->time_scale && + a->num_units_in_decoding_tick == b->num_units_in_decoding_tick && + a->equal_picture_interval == b->equal_picture_interval && + a->ticks_per_picture == b->ticks_per_picture && + a->sequence_parameters_present == b->sequence_parameters_present && + a->sequence_decoder_buffer_delay == b->sequence_decoder_buffer_delay && + a->sequence_encoder_buffer_delay == b->sequence_encoder_buffer_delay && + a->sequence_low_delay_mode == b->sequence_low_delay_mode && + a->rebase_interval_events == b->rebase_interval_events && + a->defer_nonterminal_checks_for_testing == + b->defer_nonterminal_checks_for_testing && + a->stop_after_first_violation == b->stop_after_first_violation; +} + +bool av2_decoder_model_update_parameters(Av2DecoderModel *model, + const Av2DmConfig *config, + uint64_t event_index) { + if (model == NULL || config == NULL || model->result.finished || + model->processing_stopped) { + return false; + } + if (model->result.applicability != AV2_DM_APPLICABLE || + config->applicability != AV2_DM_APPLICABLE || + !same_model_topology_and_clock(&model->config, config)) { + missing_input(model); + return false; + } + + Av2DmResolvedParameters parameters; + if (!resolve_parameters(config, ¶meters)) { + missing_input(model); + return false; + } + if (!model->config.defer_nonterminal_checks_for_testing) { + // The old smoothing epoch is evaluated with the old BitRate and + // BufferSize before the replacement parameters take effect. + check_smoothing_buffer_overflow(model, event_index); + model->dfg_count = 0; + } + if (model->processing_stopped) return false; + if (model->smoothing_epoch == UINT64_MAX) { + arithmetic_failure(model); + return false; + } + ++model->smoothing_epoch; + model->smoothing_epoch_prepared = true; + + Av2DmConfig updated = *config; + updated.initial_display_delay = model->config.initial_display_delay; + updated.ras_start = model->config.ras_start; + updated.ras_seed_complete = model->config.ras_seed_complete; + updated.ras_seed_count = model->config.ras_seed_count; + memcpy(updated.ras_seeds, model->config.ras_seeds, sizeof(updated.ras_seeds)); + const Av2DmMode old_mode = model->config.mode; + apply_parameters(model, &updated, ¶meters); + if (!model->max_reference_frames_violated) { + // The maximum depends on the active level limits and must be reconsidered + // for the RAP frame after an OPS parameter update. + model->max_reference_frames_checked = false; + } + if (old_mode == AV2_DM_DECODING_SCHEDULE_MODE && + updated.mode == AV2_DM_RESOURCE_AVAILABILITY_MODE) { + // The resource lane is maintained for every event. It is therefore the + // continuous resource-availability state when that mode becomes active. + model->lane = model->resource_lane; + } + model->result.mode = updated.mode; + update_result_status(model); + update_storage_stats(model); + return true; +} + +void av2_decoder_model_mark_incomplete(Av2DecoderModel *model) { + if (model == NULL || model->result.finished || model->processing_stopped) { + return; + } + missing_input(model); +} + +static void check_max_reference_frames(Av2DecoderModel *model, + uint64_t event_index) { + if (model->config.still_picture) return; + if (model->max_reference_frames_violated) return; + if (model->max_reference_frames_checked && + model->max_reference_frames_reserved == + model->any_decode_count_two_requires_reserved_buffer) { + return; + } + model->max_reference_frames_checked = true; + model->max_reference_frames_reserved = + model->any_decode_count_two_requires_reserved_buffer; + const uint64_t frame_size = + (uint64_t)model->config.max_frame_width * model->config.max_frame_height; + if (frame_size == 0) { + missing_input(model); + return; + } + if (model->limits.max_picture_size > UINT64_MAX / 8) { + arithmetic_failure(model); + return; + } + uint64_t maximum = 8 * model->limits.max_picture_size / frame_size; + if (model->any_decode_count_two_requires_reserved_buffer && maximum != 0) { + --maximum; + } + const uint64_t syntax_maximum = + model->config.explicit_num_ref_frames ? 16 : 8; + if (maximum > syntax_maximum) maximum = syntax_maximum; + Av2DmRational observed; + Av2DmRational limit; + if (!av2_dm_rational_make(model->config.num_ref_frames, 1, &observed) || + !av2_dm_rational_make(maximum, 1, &limit)) { + arithmetic_failure(model); + return; + } + bool too_many_reference_frames; + if (!rational_greater(&observed, &limit, &too_many_reference_frames)) { + arithmetic_failure(model); + } else if (too_many_reference_frames) { + model->max_reference_frames_violated = true; + report_violation(model, AV2_DM_VIOLATION_MAX_REFERENCE_FRAMES, event_index, + &observed, &limit); + } +} + +static void check_header_rate_at(Av2DecoderModel *model, Av2DmTuRecord *end_tu, + uint64_t frame_headers, + uint64_t maximum_headers, + uint64_t proving_event_index) { + Av2DmRational observed; + Av2DmRational limit; + if (!av2_dm_rational_make(frame_headers, 1, &observed) || + !av2_dm_rational_make(maximum_headers, 1, &limit)) { + arithmetic_failure(model); + return; + } + bool violated; + if (!rational_greater(&observed, &limit, &violated)) { + arithmetic_failure(model); + return; + } + if (violated && !end_tu->header_rate_reported) { + end_tu->header_rate_reported = true; + report_violation_for_affected( + model, AV2_DM_VIOLATION_MAX_HEADER_RATE, proving_event_index, + AV2_DM_VIOLATION_AFFECTED_TEMPORAL_UNIT, end_tu->temporal_unit_index, + &observed, &limit, NULL); + } + if (!rational_from_product(model->maximum_tile_area, frame_headers, + &observed) || + !av2_dm_rational_make(model->limits.max_tile_size_header_rate_product, 1, + &limit)) { + arithmetic_failure(model); + return; + } + if (!rational_greater(&observed, &limit, &violated)) { + arithmetic_failure(model); + return; + } + if (violated && !end_tu->tile_header_rate_reported) { + end_tu->tile_header_rate_reported = true; + report_violation_for_affected( + model, AV2_DM_VIOLATION_TILE_SIZE_HEADER_RATE, proving_event_index, + AV2_DM_VIOLATION_AFFECTED_TEMPORAL_UNIT, end_tu->temporal_unit_index, + &observed, &limit, NULL); + } +} + +static void check_retired_tile_header_summary(Av2DecoderModel *model, + uint64_t proving_event_index) { + if (violation_seen(model, AV2_DM_VIOLATION_TILE_SIZE_HEADER_RATE) || + !model->retired_header_summary_valid || + model->retired_header_summary_reported) { + return; + } + Av2DmRational observed; + Av2DmRational limit; + if (!rational_from_product(model->maximum_tile_area, + model->retired_max_frame_headers, &observed) || + !av2_dm_rational_make(model->limits.max_tile_size_header_rate_product, 1, + &limit)) { + arithmetic_failure(model); + return; + } + bool violated; + if (!rational_greater(&observed, &limit, &violated)) { + arithmetic_failure(model); + return; + } + if (violated) { + model->retired_header_summary_reported = true; + report_violation_for_affected( + model, AV2_DM_VIOLATION_TILE_SIZE_HEADER_RATE, proving_event_index, + AV2_DM_VIOLATION_AFFECTED_TEMPORAL_UNIT, + model->retired_header_event_index, &observed, &limit, NULL); + } +} + +static bool order_tus_by_output_time(const Av2DecoderModel *model, + uint32_t **ordered_tus, + uint32_t *ordered_tu_count) { + *ordered_tus = NULL; + *ordered_tu_count = 0; + if (model->tu_count == 0) return true; + const uint64_t capacity = model->tu_count; + if (capacity > SIZE_MAX / sizeof(**ordered_tus)) return false; + const size_t allocation_size = (size_t)capacity * sizeof(**ordered_tus); + uint32_t *source = avm_malloc(allocation_size); + uint32_t *destination = avm_malloc(allocation_size); + if (source == NULL || destination == NULL) { + avm_free(source); + avm_free(destination); + return false; + } + uint32_t count = 0; + for (uint32_t i = 0; i < model->tu_count; ++i) { + if (model->tus[i].output_time_valid) source[count++] = i; + } + for (size_t width = 1; width < count; width *= 2) { + const size_t block_width = 2 * width; + for (size_t left = 0; left < count; left += block_width) { + const size_t middle = left + width < count ? left + width : count; + const size_t right = + left + block_width < count ? left + block_width : count; + size_t first = left; + size_t second = middle; + size_t output = left; + while (first < middle && second < right) { + int comparison; + if (!av2_dm_rational_compare(&model->tus[source[first]].output_time, + &model->tus[source[second]].output_time, + &comparison)) { + avm_free(source); + avm_free(destination); + return false; + } + destination[output++] = + comparison <= 0 ? source[first++] : source[second++]; + } + while (first < middle) destination[output++] = source[first++]; + while (second < right) destination[output++] = source[second++]; + } + uint32_t *const swap = source; + source = destination; + destination = swap; + if (width > count / 2) break; + } + avm_free(destination); + *ordered_tus = source; + *ordered_tu_count = count; + return true; +} + +static void check_header_rate_windows_in_output_order( + Av2DecoderModel *model, const uint32_t *ordered_tus, + uint32_t ordered_tu_count, const Av2DmRational *one_second, + uint64_t maximum_headers, uint64_t proving_event_index) { + uint32_t first_tu = 0; + uint64_t frame_headers = 0; + for (uint32_t i = 0; i < ordered_tu_count; ++i) { + Av2DmTuRecord *const end_tu = &model->tus[ordered_tus[i]]; + if (UINT64_MAX - frame_headers < end_tu->frame_headers) { + arithmetic_failure(model); + return; + } + frame_headers += end_tu->frame_headers; + Av2DmRational window_start; + if (!av2_dm_rational_subtract(&end_tu->output_time, one_second, + &window_start)) { + arithmetic_failure(model); + return; + } + while (first_tu <= i) { + const Av2DmTuRecord *const candidate = &model->tus[ordered_tus[first_tu]]; + int comparison; + if (!av2_dm_rational_compare(&candidate->output_time, &window_start, + &comparison)) { + arithmetic_failure(model); + return; + } + if (comparison >= 0) break; + if (frame_headers < candidate->frame_headers) { + arithmetic_failure(model); + return; + } + frame_headers -= candidate->frame_headers; + ++first_tu; + } + if (end_tu->header_complete && !end_tu->header_window_checked) { + end_tu->header_window_checked = true; + end_tu->header_window_headers = frame_headers; + } + check_header_rate_at(model, end_tu, + end_tu->header_window_checked + ? end_tu->header_window_headers + : frame_headers, + maximum_headers, proving_event_index); + if (model->processing_stopped) return; + } +} + +static bool lane_has_live_coded_tu(const Av2DmLane *lane, + uint64_t temporal_unit_index) { + for (uint32_t i = 0; i < lane->pool.pool_size; ++i) { + const Av2DmBuffer *const buffer = &lane->pool.buffers[i]; + if (lane_buffer_is_live(lane, i) && buffer->coded_temporal_unit_valid && + buffer->coded_temporal_unit_index == temporal_unit_index) { + return true; + } + } + return false; +} + +static bool tu_has_live_generation(const Av2DecoderModel *model, + uint64_t temporal_unit_index) { + return lane_has_live_coded_tu(&model->lane, temporal_unit_index) || + lane_has_live_coded_tu(&model->resource_lane, temporal_unit_index); +} + +static void remember_retired_tu(Av2DecoderModel *model, + const Av2DmTuRecord *tu) { + if (!tu->output_time_valid && + (tu->frame_headers != 0 || tu->output_frames != 0)) { + model->retired_unresolved_tu = true; + } + if (violation_seen(model, AV2_DM_VIOLATION_TILE_SIZE_HEADER_RATE) || + !tu->header_window_checked || tu->tile_header_rate_reported) { + return; + } + if (!model->retired_header_summary_valid || + tu->header_window_headers > model->retired_max_frame_headers) { + model->retired_header_summary_valid = true; + model->retired_max_frame_headers = tu->header_window_headers; + model->retired_header_event_index = tu->temporal_unit_index; + } +} + +static void retire_unresolvable_tus(Av2DecoderModel *model) { + uint32_t write_index = 0; + for (uint32_t i = 0; i < model->tu_count; ++i) { + Av2DmTuRecord *const tu = &model->tus[i]; + const bool current_coded = + model->coded_tu_valid && tu->temporal_unit_index == model->coded_tu; + const bool retire = tu->header_complete && !tu->output_time_valid && + !current_coded && + !tu_has_live_generation(model, tu->temporal_unit_index); + if (retire) { + remember_retired_tu(model, tu); + continue; + } + if (write_index != i) model->tus[write_index] = *tu; + ++write_index; + } + model->tu_count = write_index; +} + +static void restart_tu_history(Av2DecoderModel *model, + uint64_t temporal_unit_index) { + uint32_t write_index = 0; + for (uint32_t i = 0; i < model->tu_count; ++i) { + Av2DmTuRecord *const tu = &model->tus[i]; + const bool keep_current = + tu->temporal_unit_index == temporal_unit_index || + (model->coded_tu_valid && tu->temporal_unit_index == model->coded_tu); + const bool keep_pending = + !tu->output_time_valid && + tu_has_live_generation(model, tu->temporal_unit_index); + if (!keep_current && !keep_pending) { + remember_retired_tu(model, tu); + continue; + } + if (write_index != i) model->tus[write_index] = *tu; + ++write_index; + } + model->tu_count = write_index; + const Av2DmTuRecord *const current = find_tu(model, temporal_unit_index); + if (current != NULL && current->output_time_valid) { + model->latest_timed_tu_output_time = current->output_time; + model->latest_timed_tu_valid = true; + } +} + +static void retire_closed_tus(Av2DecoderModel *model, + const Av2DmRational *one_second) { + const bool header_history_proven = + violation_seen(model, AV2_DM_VIOLATION_MAX_HEADER_RATE) && + violation_seen(model, AV2_DM_VIOLATION_TILE_SIZE_HEADER_RATE); + if (!header_history_proven && !model->latest_timed_tu_valid) return; + Av2DmRational frontier; + if (!header_history_proven && + !av2_dm_rational_subtract(&model->latest_timed_tu_output_time, one_second, + &frontier)) { + arithmetic_failure(model); + return; + } + uint32_t write_index = 0; + for (uint32_t i = 0; i < model->tu_count; ++i) { + Av2DmTuRecord *const tu = &model->tus[i]; + bool retire = false; + if (tu->header_complete && + (!model->coded_tu_valid || + tu->temporal_unit_index != model->coded_tu) && + tu->temporal_unit_index != model->last_output_tu && + (tu->output_time_valid || + !tu_has_live_generation(model, tu->temporal_unit_index))) { + if (header_history_proven) { + retire = true; + } else if (tu->header_window_checked && tu->output_time_valid) { + int comparison; + if (!av2_dm_rational_compare(&tu->output_time, &frontier, + &comparison)) { + arithmetic_failure(model); + return; + } + retire = comparison < 0; + } + } + if (retire) { + remember_retired_tu(model, tu); + continue; + } + if (write_index != i) model->tus[write_index] = *tu; + ++write_index; + } + model->tu_count = write_index; +} + +static void check_header_rate_windows(Av2DecoderModel *model, + bool require_complete, + uint64_t proving_event_index) { + if (model->config.still_picture) return; + if (!require_complete) { + model->latest_header_check_event_index = proving_event_index; + } + if (require_complete) { + for (uint32_t i = 0; i < model->tu_count; ++i) { + if (model->tus[i].frame_headers != 0 && + !model->tus[i].output_time_valid) { + incomplete_verification(model); + return; + } + } + } + Av2DmRational one_second; + if (!av2_dm_rational_make(1, 1, &one_second)) { + arithmetic_failure(model); + return; + } + const uint64_t maximum_headers = (uint64_t)model->limits.max_header_rate * + (1 + ((uint64_t)model->config.tier << 1)); + uint32_t *ordered_tus; + uint32_t ordered_tu_count; + if (!order_tus_by_output_time(model, &ordered_tus, &ordered_tu_count)) { + arithmetic_failure(model); + return; + } + check_header_rate_windows_in_output_order( + model, ordered_tus, ordered_tu_count, &one_second, maximum_headers, + proving_event_index); + avm_free(ordered_tus); + if (!model->processing_stopped && !require_complete) { + retire_closed_tus(model, &one_second); + } +} + +void av2_decoder_model_finish(Av2DecoderModel *model) { + if (model == NULL || model->result.finished) return; + if (model->result.applicability != AV2_DM_NOT_APPLICABLE && + !model->processing_stopped) { + if (model->coded_tu_valid) { + Av2DmTuRecord *const coded_tu = find_tu(model, model->coded_tu); + if (coded_tu == NULL) { + arithmetic_failure(model); + } else { + coded_tu->header_complete = true; + } + } + if (!model->lane.initial_presentation_delay_known && + model->shown_frame_number != 0) { + incomplete_verification(model); + } + if (!model->processing_stopped && !model->config.still_picture && + model->previous_dfg_valid) { + if (model->last_frame_parsing_time_valid) { + check_frame_parsing_constraints(model, &model->previous_dfg, + &model->last_frame_parsing_time, + model->previous_dfg.event_index); + } else { + incomplete_verification(model); + } + if (!model->processing_stopped) { + Av2DmTuRecord *const last_output_tu = + model->last_output_tu_valid ? find_tu(model, model->last_output_tu) + : NULL; + if (last_output_tu != NULL) { + if (model->last_display_duration_valid) { + // Annex A reuses the preceding output duration for the last TU. + // Annex E does not synthesize another presentation interval. + check_tu_display_rate(model, last_output_tu, + &model->last_display_duration, + last_output_tu->event_index); + } else { + incomplete_verification(model); + } + } + } + } + if (!model->processing_stopped && + model->config.defer_nonterminal_checks_for_testing) { + check_smoothing_buffer_overflow(model, model->latest_frame_event_index); + } + if (!model->processing_stopped && + model->config.defer_nonterminal_checks_for_testing) { + check_max_reference_frames(model, model->latest_frame_event_index); + } + if (!model->processing_stopped && model->retired_unresolved_tu) { + incomplete_verification(model); + } + if (!model->processing_stopped) { + check_header_rate_windows(model, true, + model->latest_header_check_event_index); + } + } + model->result.finished = true; + update_result_status(model); + update_storage_stats(model); +} + +bool av2_decoder_model_get_result(const Av2DecoderModel *model, + Av2DmResult *result) { + if (model == NULL || result == NULL) return false; + *result = model->result; + return true; +} + +bool av2_decoder_model_get_state(const Av2DecoderModel *model, + Av2DmState *state) { + if (model == NULL || state == NULL) return false; + memset(state, 0, sizeof(*state)); + state->time = model->lane.time; + state->initial_presentation_delay_known = + model->lane.initial_presentation_delay_known; + state->initial_presentation_delay = model->lane.initial_presentation_delay; + state->current_buffer_index = model->lane.current_buffer_index; + state->frame_number = model->frame_number; + state->dfg_number = model->dfg_number; + state->shown_frame_number = model->shown_frame_number; + state->buffer_pool = model->lane.pool; + if (model->previous_dfg_valid) { + const Av2DmDfgRecord *const dfg = &model->previous_dfg; + state->last_dfg_valid = true; + state->first_bit_arrival = dfg->first_arrival; + state->last_bit_arrival = dfg->last_arrival; + state->scheduled_removal = dfg->scheduled_removal; + state->removal = dfg->removal; + state->time_to_decode = dfg->decode_time; + state->decode_completion = dfg->decode_completion; + } + if (model->shown_frame_number != 0) { + state->last_presentation_valid = model->last_presentation_valid; + state->last_presentation = model->last_presentation; + state->last_presentation_offset_valid = + model->last_presentation_offset_valid; + state->last_presentation_offset = model->last_presentation_offset; + state->last_output_temporal_unit_valid = true; + state->last_output_temporal_unit = model->last_output_temporal_unit; + } + if (model->last_output_tu_valid) { + const Av2DmTuRecord *tu = NULL; + for (uint32_t i = model->tu_count; i > 0; --i) { + if (model->tus[i - 1].temporal_unit_index == model->last_output_tu) { + tu = &model->tus[i - 1]; + break; + } + } + if (tu == NULL) return false; + state->last_temporal_unit_output_time_valid = tu->output_time_valid; + state->last_temporal_unit_output_time = tu->output_time; + state->last_temporal_unit_output_luma_samples = tu->output_luma_samples; + state->last_temporal_unit_output_frames = tu->output_frames; + } + return true; +} + +bool av2_decoder_model_get_storage_stats(const Av2DecoderModel *model, + Av2DmStorageStats *stats) { + if (model == NULL || stats == NULL) return false; + *stats = model->storage; + return true; +} + +const char *av2_dm_violation_code_name(Av2DmViolationCode code) { + static const char *const names[] = { + "DECODE_FRAME_BUFFER_UNAVAILABLE", + "DECODE_EXISTING_FRAME_BUFFER_EMPTY", + "DISPLAY_FRAME_LATE", + "SMOOTHING_BUFFER_UNDERFLOW", + "SMOOTHING_BUFFER_OVERFLOW", + "PRESENTATION_TIME_DECREASE", + "SCHEDULE_BEFORE_RESOURCE_REMOVAL", + "DECODER_BUFFER_DELAY_INCONSISTENT", + "MINIMUM_DECODE_TIME", + "MINIMUM_PRESENTATION_INTERVAL", + "DECODE_DEADLINE", + "DECODER_BUFFER_DELAY_ZERO", + "DECODER_BUFFER_DELAY_TOO_LARGE", + "MAX_PICTURE_SIZE", + "MAX_HORIZONTAL_SIZE", + "MAX_VERTICAL_SIZE", + "MIN_HORIZONTAL_SIZE", + "MIN_VERTICAL_SIZE", + "MAX_TILES", + "MAX_TILE_COLUMNS", + "MAX_TILE_WIDTH", + "MIN_TILE_WIDTH", + "MAX_TILE_AREA", + "MAX_DISPLAY_RATE", + "MAX_HEADER_RATE", + "MAX_REFERENCE_FRAMES", + "FRAME_DECODE_RATE", + "FRAME_TILE_RATE", + "MAX_COMPRESSED_SIZE", + "MAX_FRAME_SYMBOLS", + "TILE_SIZE_HEADER_RATE", + }; + if (code > AV2_DM_VIOLATION_TILE_SIZE_HEADER_RATE) { + return "UNKNOWN"; + } + return names[code]; +} diff --git a/av2/common/decoder_model.h b/av2/common/decoder_model.h new file mode 100644 index 0000000000..bcd26cb292 --- /dev/null +++ b/av2/common/decoder_model.h @@ -0,0 +1,458 @@ +/* + * Copyright (c) 2026, Alliance for Open Media. All rights reserved + * + * This source code is subject to the terms of the BSD 3-Clause Clear License + * and the Alliance for Open Media Patent License 1.0. If the BSD 3-Clause Clear + * License was not distributed with this source code in the LICENSE file, you + * can obtain it at aomedia.org/license/software-license/bsd-3-c-c/. If the + * Alliance for Open Media Patent License 1.0 was not distributed with this + * source code in the PATENTS file, you can obtain it at + * aomedia.org/license/patent-license/. + */ + +#ifndef AVM_AV2_COMMON_DECODER_MODEL_H_ +#define AVM_AV2_COMMON_DECODER_MODEL_H_ + +#include +#include + +#ifdef __cplusplus +extern "C" { +#endif + +#define AV2_DM_MAX_REF_FRAMES 16 +#define AV2_DM_MAX_BUFFER_POOL_SIZE (AV2_DM_MAX_REF_FRAMES + 2) + +// A portable unsigned 256-bit value, stored least-significant limb first. The +// decoder model never exposes a +// compiler-specific wide-integer type through its internal C interface. +typedef struct Av2DmUnsignedWide { + uint64_t limbs[4]; +} Av2DmUnsignedWide; + +// Exact signed rational used for all normative decoder-model decisions. +// denominator is positive. Zero is canonicalized to 0/1 with negative false. +typedef struct Av2DmRational { + Av2DmUnsignedWide magnitude; + Av2DmUnsignedWide denominator; + bool negative; +} Av2DmRational; + +bool av2_dm_rational_make(uint64_t numerator, uint64_t denominator, + Av2DmRational *result); +bool av2_dm_rational_make_wide(Av2DmUnsignedWide numerator, + uint64_t denominator, bool negative, + Av2DmRational *result); +bool av2_dm_rational_add(const Av2DmRational *left, const Av2DmRational *right, + Av2DmRational *result); +bool av2_dm_rational_subtract(const Av2DmRational *left, + const Av2DmRational *right, + Av2DmRational *result); +bool av2_dm_rational_multiply_u64(const Av2DmRational *value, + uint64_t multiplier, Av2DmRational *result); +bool av2_dm_rational_divide_u64(const Av2DmRational *value, uint64_t divisor, + Av2DmRational *result); +bool av2_dm_rational_compare(const Av2DmRational *left, + const Av2DmRational *right, int *comparison); +bool av2_dm_rational_rebase(Av2DmRational *values, uint32_t value_count, + const Av2DmRational *origin); +bool av2_dm_rational_is_zero(const Av2DmRational *value); + +typedef struct Av2DmBuffer { + uint32_t decoder_ref_count; + uint32_t player_ref_count; + int32_t display_index; + bool presentation_time_valid; + Av2DmRational presentation_time; + bool generation_valid; + uint64_t generation; + bool decode_completion_time_valid; + Av2DmRational decode_completion_time; + uint64_t decode_order; + uint64_t rap_epoch; + bool random_access_point; + uint64_t coded_temporal_unit_index; + bool coded_temporal_unit_valid; +} Av2DmBuffer; + +typedef struct Av2DmBufferPool { + uint32_t num_ref_frames; + uint32_t pool_size; + int32_t vbi[AV2_DM_MAX_REF_FRAMES]; + Av2DmBuffer buffers[AV2_DM_MAX_BUFFER_POOL_SIZE]; +} Av2DmBufferPool; + +bool av2_dm_buffer_pool_initialize(Av2DmBufferPool *pool, + uint32_t num_ref_frames); +int32_t av2_dm_buffer_pool_get_free_buffer(const Av2DmBufferPool *pool); +bool av2_dm_buffer_pool_release(Av2DmBufferPool *pool, uint32_t buffer_index); +bool av2_dm_buffer_pool_add_decoder_ref(Av2DmBufferPool *pool, + uint32_t buffer_index); +bool av2_dm_buffer_pool_remove_decoder_ref(Av2DmBufferPool *pool, + uint32_t buffer_index); +bool av2_dm_buffer_pool_add_player_ref(Av2DmBufferPool *pool, + uint32_t buffer_index); +bool av2_dm_buffer_pool_remove_player_ref(Av2DmBufferPool *pool, + uint32_t buffer_index); +bool av2_dm_buffer_pool_set_vbi(Av2DmBufferPool *pool, uint32_t ref_index, + int32_t buffer_index); +uint32_t av2_dm_buffer_pool_frames_in_use(const Av2DmBufferPool *pool); + +typedef struct Av2DecoderModel Av2DecoderModel; + +typedef enum Av2DmMode { + AV2_DM_RESOURCE_AVAILABILITY_MODE, + AV2_DM_DECODING_SCHEDULE_MODE +} Av2DmMode; + +typedef enum Av2DmApplicability { + AV2_DM_APPLICABLE, + AV2_DM_NOT_APPLICABLE, + AV2_DM_MISSING_REQUIRED_INPUT +} Av2DmApplicability; + +typedef enum Av2DmResultStatus { + AV2_DM_RESULT_CONFORMANT, + AV2_DM_RESULT_NON_CONFORMANT, + AV2_DM_RESULT_INDETERMINATE, + AV2_DM_RESULT_NOT_APPLICABLE +} Av2DmResultStatus; + +typedef enum Av2DmViolationCode { + AV2_DM_VIOLATION_DECODE_FRAME_BUFFER_UNAVAILABLE, + AV2_DM_VIOLATION_DECODE_EXISTING_FRAME_BUFFER_EMPTY, + AV2_DM_VIOLATION_DISPLAY_FRAME_LATE, + AV2_DM_VIOLATION_SMOOTHING_BUFFER_UNDERFLOW, + AV2_DM_VIOLATION_SMOOTHING_BUFFER_OVERFLOW, + AV2_DM_VIOLATION_PRESENTATION_TIME_DECREASE, + AV2_DM_VIOLATION_SCHEDULE_BEFORE_RESOURCE_REMOVAL, + AV2_DM_VIOLATION_DECODER_BUFFER_DELAY_INCONSISTENT, + AV2_DM_VIOLATION_MINIMUM_DECODE_TIME, + AV2_DM_VIOLATION_MINIMUM_PRESENTATION_INTERVAL, + AV2_DM_VIOLATION_DECODE_DEADLINE, + AV2_DM_VIOLATION_DECODER_BUFFER_DELAY_ZERO, + AV2_DM_VIOLATION_DECODER_BUFFER_DELAY_TOO_LARGE, + AV2_DM_VIOLATION_MAX_PICTURE_SIZE, + AV2_DM_VIOLATION_MAX_HORIZONTAL_SIZE, + AV2_DM_VIOLATION_MAX_VERTICAL_SIZE, + AV2_DM_VIOLATION_MIN_HORIZONTAL_SIZE, + AV2_DM_VIOLATION_MIN_VERTICAL_SIZE, + AV2_DM_VIOLATION_MAX_TILES, + AV2_DM_VIOLATION_MAX_TILE_COLUMNS, + AV2_DM_VIOLATION_MAX_TILE_WIDTH, + AV2_DM_VIOLATION_MIN_TILE_WIDTH, + AV2_DM_VIOLATION_MAX_TILE_AREA, + AV2_DM_VIOLATION_MAX_DISPLAY_RATE, + AV2_DM_VIOLATION_MAX_HEADER_RATE, + AV2_DM_VIOLATION_MAX_REFERENCE_FRAMES, + AV2_DM_VIOLATION_FRAME_DECODE_RATE, + AV2_DM_VIOLATION_FRAME_TILE_RATE, + AV2_DM_VIOLATION_MAX_COMPRESSED_SIZE, + AV2_DM_VIOLATION_MAX_FRAME_SYMBOLS, + AV2_DM_VIOLATION_TILE_SIZE_HEADER_RATE +} Av2DmViolationCode; + +typedef enum Av2DmViolationAffectedKind { + AV2_DM_VIOLATION_AFFECTED_EVENT, + AV2_DM_VIOLATION_AFFECTED_DFG, + AV2_DM_VIOLATION_AFFECTED_OUTPUT, + AV2_DM_VIOLATION_AFFECTED_TEMPORAL_UNIT +} Av2DmViolationAffectedKind; + +typedef enum Av2DmViolationDetailKind { + AV2_DM_VIOLATION_DETAIL_NONE, + AV2_DM_VIOLATION_DETAIL_BUFFER_POOL, + AV2_DM_VIOLATION_DETAIL_REFERENCE_SLOT, + AV2_DM_VIOLATION_DETAIL_DELAY_CONSISTENCY, + AV2_DM_VIOLATION_DETAIL_MINIMUM_DECODE_TIME, + AV2_DM_VIOLATION_DETAIL_FRAME_INTERVAL +} Av2DmViolationDetailKind; + +typedef struct Av2DmBufferPoolViolationDetail { + bool resource_lane; + uint32_t pool_size; + uint32_t frames_in_use; + uint32_t free_buffers; + uint32_t decoder_held_buffers; + uint32_t player_held_buffers; +} Av2DmBufferPoolViolationDetail; + +typedef struct Av2DmReferenceSlotViolationDetail { + int32_t requested_slot; + bool slot_in_range; + bool reference_valid; + int32_t buffer_index; + Av2DmBufferPoolViolationDetail pool; +} Av2DmReferenceSlotViolationDetail; + +typedef struct Av2DmDelayConsistencyViolationDetail { + uint32_t decoder_buffer_delay_ticks; + bool ceil_time_delta_present; + Av2DmRational ceil_time_delta_ticks; +} Av2DmDelayConsistencyViolationDetail; + +typedef struct Av2DmMinimumDecodeTimeViolationDetail { + Av2DmRational frame_decode_time; + Av2DmRational one_header_time; +} Av2DmMinimumDecodeTimeViolationDetail; + +typedef struct Av2DmViolationDetail { + Av2DmViolationDetailKind kind; + union { + Av2DmBufferPoolViolationDetail buffer_pool; + Av2DmReferenceSlotViolationDetail reference_slot; + Av2DmDelayConsistencyViolationDetail delay_consistency; + Av2DmMinimumDecodeTimeViolationDetail minimum_decode_time; + Av2DmRational frame_interval; + } value; +} Av2DmViolationDetail; + +typedef struct Av2DmScope { + int32_t xlayer_id; + // Xlayer carrying the OPS syntax. This is -1 for a whole-xlayer model and + // GLOBAL_XLAYER_ID for an operating point from a global OPS. + int32_t ops_xlayer_id; + int32_t ops_id; + int32_t operating_point; + bool whole_xlayer; +} Av2DmScope; + +typedef struct Av2DmLevelLimits { + uint64_t max_picture_size; + uint32_t max_horizontal_size; + uint32_t max_vertical_size; + uint64_t max_display_rate; + uint64_t max_decode_rate; + uint32_t max_header_rate; + uint32_t max_tiles; + uint32_t max_tile_columns; + uint64_t max_tile_width; + uint64_t max_tile_area; + uint64_t max_tile_size_header_rate_product; + uint32_t picture_size_profile_factor; + uint32_t min_compression_basis; + Av2DmRational bit_rate; + Av2DmRational buffer_size; +} Av2DmLevelLimits; + +typedef struct Av2DmRasSeed { + uint32_t ref_index; + uint64_t generation; +} Av2DmRasSeed; + +typedef struct Av2DmConfig { + Av2DmScope scope; + Av2DmMode mode; + Av2DmApplicability applicability; + uint32_t level_idx; + uint32_t tier; + uint32_t profile; + uint32_t num_ref_frames; + uint32_t max_frame_width; + uint32_t max_frame_height; + uint32_t max_mlayer_id; + bool still_picture; + bool explicit_num_ref_frames; + + bool timing_info_present; + uint32_t num_units_in_display_tick; + uint32_t time_scale; + uint32_t num_units_in_decoding_tick; + bool equal_picture_interval; + uint32_t ticks_per_picture; + uint32_t initial_display_delay; + + bool sequence_parameters_present; + uint32_t sequence_decoder_buffer_delay; + uint32_t sequence_encoder_buffer_delay; + bool sequence_low_delay_mode; + bool operating_point_parameters_present; + uint32_t operating_point_decoder_buffer_delay; + uint32_t operating_point_encoder_buffer_delay; + bool operating_point_low_delay_mode; + + bool level_limits_present; + Av2DmLevelLimits level_limits; + + bool ras_start; + bool ras_seed_complete; + uint32_t ras_seed_count; + Av2DmRasSeed ras_seeds[AV2_DM_MAX_REF_FRAMES]; + + // Zero selects the implementation default. Tests may request a smaller + // interval to exercise periodic rebasing without a huge input. + uint32_t rebase_interval_events; + + // Temporary Commit-7 differential oracle. Decoder contexts leave this + // false; tests may defer the checks moved online until finish(). + bool defer_nonterminal_checks_for_testing; + + // Decoder fatal mode stops this model after its first proven violation. + bool stop_after_first_violation; +} Av2DmConfig; + +typedef struct Av2DmFrameEvent { + uint64_t event_index; + uint64_t temporal_unit_index; + uint32_t ref_valid_mask; + bool temporal_unit_output_time_present; + Av2DmRational temporal_unit_output_time; + uint64_t generation; + uint64_t coded_bits; + bool show_existing_frame; + bool random_access_point; + bool coded_as_closed_loop_key; + bool frame_is_intra; + bool allow_global_intrabc; + bool inloop_filtering_enabled; + uint32_t frame_width; + uint32_t frame_height; + uint32_t num_tiles; + uint32_t tile_columns; + uint64_t max_tile_width; + uint64_t max_tile_area; + bool non_rightmost_tile_width_valid; + bool buffer_removal_time_present; + uint32_t buffer_removal_time; + bool decoder_model_parameters_updated; + bool count_frame_header; + uint64_t compressed_size_bytes; + uint64_t frame_symbol_count; +} Av2DmFrameEvent; + +typedef struct Av2DmReferenceUpdateEvent { + uint32_t refresh_frame_flags; + uint32_t ref_valid_mask; +} Av2DmReferenceUpdateEvent; + +typedef struct Av2DmOutputEvent { + uint64_t event_index; + uint64_t temporal_unit_index; + uint64_t generation; + int32_t frame_to_show_map_idx; + uint32_t ref_valid_mask; + uint64_t output_luma_samples; + bool leading_frame; + bool presentation_uses_current_frame; + bool presentation_random_access_point; + bool presentation_time_present; + uint64_t presentation_time_ticks; + bool presentation_base_offset_present; + Av2DmRational presentation_base_offset; +} Av2DmOutputEvent; + +typedef struct Av2DmViolation { + Av2DmViolationCode code; + Av2DmScope scope; + uint64_t event_index; + Av2DmViolationAffectedKind affected_kind; + uint64_t affected_index; + bool observed_present; + Av2DmRational observed; + bool limit_present; + Av2DmRational limit; + Av2DmViolationDetail detail; +} Av2DmViolation; + +typedef struct Av2DmResult { + Av2DmResultStatus status; + Av2DmApplicability applicability; + Av2DmMode mode; + Av2DmScope scope; + uint64_t decoded_frames; + uint64_t output_frames; + uint64_t reordered_outputs; + uint64_t violations; + bool arithmetic_failed; + bool missing_required_input; + bool finished; +} Av2DmResult; + +typedef struct Av2DmState { + Av2DmRational time; + bool last_dfg_valid; + Av2DmRational first_bit_arrival; + Av2DmRational last_bit_arrival; + Av2DmRational scheduled_removal; + Av2DmRational removal; + Av2DmRational time_to_decode; + Av2DmRational decode_completion; + bool last_presentation_valid; + Av2DmRational last_presentation; + bool last_presentation_offset_valid; + Av2DmRational last_presentation_offset; + bool last_output_temporal_unit_valid; + uint64_t last_output_temporal_unit; + bool last_temporal_unit_output_time_valid; + Av2DmRational last_temporal_unit_output_time; + uint64_t last_temporal_unit_output_luma_samples; + uint32_t last_temporal_unit_output_frames; + bool initial_presentation_delay_known; + Av2DmRational initial_presentation_delay; + int32_t current_buffer_index; + uint64_t frame_number; + uint64_t dfg_number; + uint64_t shown_frame_number; + Av2DmBufferPool buffer_pool; +} Av2DmState; + +// Private verifier-storage instrumentation used by decoder-model tests. These +// counters describe live normative state, not allocated capacity or lifetime +// event totals, and do not affect conformance decisions. +typedef struct Av2DmStorageStats { + uint32_t active_dfgs; + uint32_t high_water_dfgs; + uint32_t active_outputs; + uint32_t high_water_outputs; + uint32_t active_tus; + uint32_t high_water_tus; + uint32_t active_generations; + uint32_t high_water_generations; + uint32_t active_cvs; + uint32_t high_water_cvs; + uint32_t active_rap_runs; + uint32_t high_water_rap_runs; +} Av2DmStorageStats; + +typedef void (*Av2DmReportFn)(void *opaque, const Av2DmViolation *violation); + +bool av2_dm_get_level_limits(uint32_t level_idx, uint32_t tier, + uint32_t profile, Av2DmLevelLimits *limits); +bool av2_dm_apply_multistream_limits(uint32_t level_idx, uint32_t tier, + uint32_t profile, uint32_t scale_numerator, + uint32_t scale_denominator, + Av2DmLevelLimits *limits); + +Av2DecoderModel *av2_decoder_model_create(const Av2DmConfig *config, + Av2DmReportFn report, + void *report_opaque); +void av2_decoder_model_destroy(Av2DecoderModel *model); +bool av2_decoder_model_update_parameters(Av2DecoderModel *model, + const Av2DmConfig *config, + uint64_t event_index); +void av2_decoder_model_mark_incomplete(Av2DecoderModel *model); +void av2_decoder_model_fail_arithmetic_for_testing(Av2DecoderModel *model); +void av2_decoder_model_start_frame(Av2DecoderModel *model, + const Av2DmFrameEvent *event); +void av2_decoder_model_update_reference_buffers( + Av2DecoderModel *model, const Av2DmReferenceUpdateEvent *event); +void av2_decoder_model_invalidate_olk_reference_buffers( + Av2DecoderModel *model, uint32_t ref_valid_mask); +void av2_decoder_model_set_initial_presentation_delay(Av2DecoderModel *model, + uint64_t event_index); +void av2_decoder_model_output_frame(Av2DecoderModel *model, + const Av2DmOutputEvent *event); +void av2_decoder_model_finish(Av2DecoderModel *model); +bool av2_decoder_model_get_result(const Av2DecoderModel *model, + Av2DmResult *result); +bool av2_decoder_model_get_state(const Av2DecoderModel *model, + Av2DmState *state); +bool av2_decoder_model_get_storage_stats(const Av2DecoderModel *model, + Av2DmStorageStats *stats); +const char *av2_dm_violation_code_name(Av2DmViolationCode code); + +#ifdef __cplusplus +} // extern "C" +#endif + +#endif // AVM_AV2_COMMON_DECODER_MODEL_H_ diff --git a/av2/decoder/annexF.c b/av2/decoder/annexF.c index 4c3e00fc8d..3ef9511147 100644 --- a/av2/decoder/annexF.c +++ b/av2/decoder/annexF.c @@ -495,6 +495,81 @@ int av2_sbe_should_retain_obu(const SubBitstreamExtractionState *sbe, return 1; // Retain: layer combination is in retention map } +static void retain_all_layers(SubBitstreamExtractionState *sbe, int xlayer_id) { + sbe->xlayer_is_selected[xlayer_id] = 1; + for (int m = 0; m < MAX_NUM_MLAYERS; ++m) { + for (int t = 0; t < MAX_NUM_TLAYERS; ++t) { + sbe->retention_map[xlayer_id][m][t] = 1; + } + } +} + +static void retain_operating_point_layers(SubBitstreamExtractionState *sbe, + const OperatingPointSet *ops, + const OperatingPoint *op, + int xlayer_id) { + sbe->xlayer_is_selected[xlayer_id] = 1; + if (ops->ops_mlayer_info_idc == 0) { + retain_all_layers(sbe, xlayer_id); + return; + } + + const int mlayer_map = op->mlayer_info.ops_mlayer_map[xlayer_id]; + for (int m = 0; m < MAX_NUM_MLAYERS; ++m) { + if ((mlayer_map & (1 << m)) == 0) continue; + const int tlayer_map = op->mlayer_info.ops_tlayer_map[xlayer_id][m]; + for (int t = 0; t < MAX_NUM_TLAYERS; ++t) { + if (tlayer_map & (1 << t)) { + sbe->retention_map[xlayer_id][m][t] = 1; + } + } + } +} + +int av2_sbe_configure_decoder_model_scope(SubBitstreamExtractionState *sbe, + int xlayer_id, + const OperatingPointSet *ops, + int op_index, int whole_xlayer) { + if (sbe == NULL || xlayer_id < 0 || xlayer_id >= MAX_NUM_XLAYERS) return 0; + + memset(sbe, 0, sizeof(*sbe)); + sbe->extraction_enabled = 1; + av2_sbe_init(sbe); + sbe->retention_map_ready = 1; + + // Annex F retains global structural OBUs in extracted multistreams. Setting + // the global base-layer entry is harmless for singlestreams, which contain + // no such OBU, and makes the membership decision independent of parse order. + sbe->xlayer_is_selected[GLOBAL_XLAYER_ID] = 1; + sbe->retention_map[GLOBAL_XLAYER_ID][0][0] = 1; + + if (whole_xlayer) { + if (xlayer_id == GLOBAL_XLAYER_ID) return 0; + retain_all_layers(sbe, xlayer_id); + return 1; + } + + if (ops == NULL || !ops->valid || op_index < 0 || op_index >= ops->ops_cnt) { + return 0; + } + + const OperatingPoint *const op = &ops->op[op_index]; + if (ops->obu_xlayer_id != GLOBAL_XLAYER_ID) { + if (ops->obu_xlayer_id != xlayer_id) return 0; + retain_operating_point_layers(sbe, ops, op, xlayer_id); + return 1; + } + + // Annex E checks each xlayer selected by a global operating point + // independently. The caller creates one scope per selected xlayer. + if (xlayer_id == GLOBAL_XLAYER_ID || + (op->ops_xlayer_map & (1 << xlayer_id)) == 0) { + return 0; + } + retain_operating_point_layers(sbe, ops, op, xlayer_id); + return 1; +} + // Step 5 fallback: extract profile/level/tier from sequence header. void av2_sbe_extract_seq_header_params(SubBitstreamExtractionState *sbe, int xlayer_id, int seq_profile_idc, diff --git a/av2/decoder/annexF.h b/av2/decoder/annexF.h index c197bb0621..9a8cf089f3 100644 --- a/av2/decoder/annexF.h +++ b/av2/decoder/annexF.h @@ -79,6 +79,7 @@ typedef struct SubBitstreamExtractionState { } SubBitstreamExtractionState; struct AV2Decoder; +struct OperatingPointSet; // clang-format off // Operating point selection and analysis process (Annex F, Section F.3.1): @@ -143,6 +144,14 @@ int av2_sbe_should_retain_obu(const SubBitstreamExtractionState *sbe, OBU_TYPE obu_type, int obu_xlayer_id, int obu_mlayer_id, int obu_tlayer_id); +// Build the exact Annex F retention map used to account CodedBits for one +// decoder-model scope. A whole-xlayer scope retains every embedded and temporal +// layer of xlayer_id. An operating-point scope uses op_index from ops. +int av2_sbe_configure_decoder_model_scope(SubBitstreamExtractionState *sbe, + int xlayer_id, + const struct OperatingPointSet *ops, + int op_index, int whole_xlayer); + // Step 5 fallback: extract profile/level/tier from sequence header // when no OPS or LCR provides this information. void av2_sbe_extract_seq_header_params(SubBitstreamExtractionState *sbe, diff --git a/av2/decoder/decodeframe.c b/av2/decoder/decodeframe.c index 4b785a8d36..6a3c973f6c 100644 --- a/av2/decoder/decodeframe.c +++ b/av2/decoder/decodeframe.c @@ -73,6 +73,7 @@ #include "av2/decoder/decodeframe.h" #include "av2/decoder/decodemv.h" #include "av2/decoder/decoder.h" +#include "av2/decoder/decoder_model.h" #include "av2/decoder/decodetxb.h" #include "av2/decoder/detokenize.h" #include "av2/decoder/obu.h" @@ -4787,6 +4788,7 @@ static const uint8_t *decode_tiles(AV2Decoder *pbi, const uint8_t *data, td->dcb.xd.current_base_qindex = cm->quant_params.base_qindex; setup_bool_decoder(tile_bs_buf->data, data_end, tile_bs_buf->size, &cm->error, td->bit_reader, allow_update_cdf); + td->bit_reader->count_frame_symbols = pbi->decoder_model_verifier != NULL; #if CONFIG_ACCOUNTING if (pbi->acct_enabled) { td->bit_reader->accounting = &pbi->accounting; @@ -4862,6 +4864,7 @@ static AVM_INLINE void tile_worker_hook_init( setup_bool_decoder(tile_buffer->data, thread_data->data_end, tile_buffer->size, &thread_data->error_info, td->bit_reader, allow_update_cdf); + td->bit_reader->count_frame_symbols = pbi->decoder_model_verifier != NULL; #if CONFIG_ACCOUNTING if (pbi->acct_enabled) { td->bit_reader->accounting = &pbi->accounting; @@ -6919,6 +6922,18 @@ static void reset_buffer_other_than_OLK(AV2Decoder *pbi) { } } + uint32_t ref_valid_mask = 0; + for (int ref_index = 0; ref_index < seq_params->ref_frames; ++ref_index) { + if (cm->ref_frame_map[ref_index] != NULL && + pbi->valid_for_referencing[ref_index]) { + ref_valid_mask |= (uint32_t)1 << ref_index; + } + } + if (pbi->decoder_model_verifier != NULL) { + av2_decoder_model_verifier_on_olk_reference_invalidation(pbi, + ref_valid_mask); + } + for (int layer = 0; layer <= seq_params->max_mlayer_id; layer++) { cm->olk_refresh_frame_flags[layer] = -1; cm->olk_co_vcl_refresh_frame_flags[layer] = -1; @@ -7648,6 +7663,10 @@ static void handle_sequence_header(AV2Decoder *pbi, OBU_TYPE obu_type, "Sequence Header changed at %s", avm_obu_type_to_string(obu_type)); } + if (pbi->decoder_model_verifier != NULL) { + av2_decoder_model_verifier_on_active_configuration(pbi, xlayer_id, + seq_header_id); + } return; } @@ -7748,6 +7767,10 @@ static void handle_sequence_header(AV2Decoder *pbi, OBU_TYPE obu_type, check_lcr_layer_map_conformance(pbi, xlayer_id); // check dependency map consistency for OPS check_ops_layer_map_conformance(pbi, xlayer_id); + if (pbi->decoder_model_verifier != NULL) { + av2_decoder_model_verifier_on_active_configuration(pbi, xlayer_id, + seq_header_id); + } } static int is_reference_mapping_consistent( @@ -9548,6 +9571,12 @@ int32_t av2_read_tilegroup_header( *first_tile_group_in_frame = is_first_tile_group; if (is_first_tile_group) { + if (pbi->decoder_model_verifier != NULL) { + cm->features.frame_symbol_count = 0; + for (int tile = 0; tile < pbi->allocated_tiles; ++tile) { + pbi->tile_data[tile].bit_reader.frame_symbol_count = 0; + } + } #if CONFIG_MISMATCH_DEBUG mismatch_move_frame_idx_r(1); #endif // CONFIG_MISMATCH_DEBUG @@ -9839,6 +9868,25 @@ void av2_decode_tg_tiles_and_wrapup(AV2Decoder *pbi, const uint8_t *data, return; } + if (pbi->decoder_model_verifier != NULL) { + uint64_t frame_symbol_count = 0; + for (int tile = 0; tile < tiles->rows * tiles->cols; ++tile) { + const uint64_t tile_symbol_count = + pbi->tile_data[tile].bit_reader.frame_symbol_count; + if (UINT64_MAX - frame_symbol_count < tile_symbol_count) { + av2_decoder_model_verifier_on_accounting_failure(pbi); + frame_symbol_count = UINT64_MAX; + break; + } + frame_symbol_count += tile_symbol_count; + } + cm->features.frame_symbol_count = frame_symbol_count; + } + + if (pbi->decoder_model_verifier != NULL) { + av2_decoder_model_verifier_on_frame_wrapup_start(pbi); + } + av2_alloc_cdef_buffers(cm, &pbi->cdef_worker, &pbi->cdef_sync, pbi->num_workers); av2_alloc_cdef_sync(cm, &pbi->cdef_sync, pbi->num_workers); diff --git a/av2/decoder/decoder.c b/av2/decoder/decoder.c index d60d319113..f6c2faba1b 100644 --- a/av2/decoder/decoder.c +++ b/av2/decoder/decoder.c @@ -40,6 +40,7 @@ #include "av2/decoder/decodeframe.h" #include "av2/decoder/decoder.h" +#include "av2/decoder/decoder_model.h" #include "av2/decoder/detokenize.h" #include "av2/decoder/obu.h" @@ -393,6 +394,8 @@ void av2_decoder_remove(AV2Decoder *pbi) { if (!pbi) return; + av2_decoder_model_verifier_destroy(pbi); + avm_get_worker_interface()->end(&pbi->lf_worker); avm_free(pbi->lf_worker.data1); @@ -582,14 +585,28 @@ static void release_current_frame(AV2Decoder *pbi) { cm->cur_frame = NULL; } +static void queue_output_frame(AV2Decoder *pbi, RefCntBuffer *frame, + int frame_to_show_map_idx, + Av2DmPresentationOwner presentation_owner) { + assign_output_frame_buffer_p(&pbi->output_frames[pbi->num_output_frames++], + frame); + frame->frame_output_done = 1; + if (pbi->decoder_model_verifier != NULL) { + av2_decoder_model_verifier_on_output(pbi, frame_to_show_map_idx, frame, + presentation_owner); + } +} + // This function flushes out the DPB, all the slots in the dpb is free to use. avm_codec_err_t flush_remaining_frames(struct AV2Decoder *pbi, int order_hint_limit) { avm_codec_err_t res = AVM_CODEC_OK; AV2_COMMON *const cm = &pbi->common; RefCntBuffer *output_candidate = NULL; + int output_candidate_ref_idx = -1; do { output_candidate = NULL; + output_candidate_ref_idx = -1; for (int i = 0; i < REF_FRAMES; i++) { RefCntBuffer *const buf = cm->ref_frame_map[i]; if (buf == NULL) continue; @@ -600,15 +617,15 @@ avm_codec_err_t flush_remaining_frames(struct AV2Decoder *pbi, derive_output_order_idx(cm, buf) <= derive_output_order_idx(cm, output_candidate))) { output_candidate = buf; + output_candidate_ref_idx = i; } } if (output_candidate != NULL) { if (pbi->num_output_frames >= (REF_FRAMES + 1) * AVM_MAX_NUM_STREAMS) { return AVM_CODEC_MEM_ERROR; } - assign_output_frame_buffer_p( - &pbi->output_frames[pbi->num_output_frames++], output_candidate); - output_candidate->frame_output_done = 1; + queue_output_frame(pbi, output_candidate, output_candidate_ref_idx, + AV2_DM_PRESENTATION_OWNER_IMPLICIT); } } while (output_candidate != NULL); return res; @@ -633,6 +650,7 @@ int av2_output_frame_buffers(AV2Decoder *pbi, int ref_idx) { AV2_COMMON *const cm = &pbi->common; RefCntBuffer *trigger_frame = NULL; RefCntBuffer *output_candidate = NULL; + int output_candidate_ref_idx = -1; int doh_error = 0; // Determine if the triggering frame is the current frame or a frame @@ -642,20 +660,21 @@ int av2_output_frame_buffers(AV2Decoder *pbi, int ref_idx) { // Add the previous frames into the output queue. do { output_candidate = trigger_frame; + output_candidate_ref_idx = ref_idx; for (int i = 0; i < cm->seq_params.ref_frames; i++) { if (is_frame_eligible_for_output(cm->ref_frame_map[i]) && derive_output_order_idx(cm, cm->ref_frame_map[i]) < derive_output_order_idx(cm, output_candidate)) { output_candidate = cm->ref_frame_map[i]; + output_candidate_ref_idx = i; } } if (output_candidate != trigger_frame) { if (cm->seq_params.monotonic_output_order_flag == 0) { doh_error |= check_and_update_output_doh(pbi, output_candidate); } - assign_output_frame_buffer_p( - &pbi->output_frames[pbi->num_output_frames++], output_candidate); - output_candidate->frame_output_done = 1; + queue_output_frame(pbi, output_candidate, output_candidate_ref_idx, + AV2_DM_PRESENTATION_OWNER_IMPLICIT); #if CONFIG_BITSTREAM_DEBUG avm_bitstream_queue_set_frame_read( derive_output_order_idx(cm, output_candidate) * 2 + 1); @@ -670,9 +689,12 @@ int av2_output_frame_buffers(AV2Decoder *pbi, int ref_idx) { // Add the output triggering frame into the output queue. doh_error |= check_and_update_output_doh(pbi, trigger_frame); } - assign_output_frame_buffer_p(&pbi->output_frames[pbi->num_output_frames++], - trigger_frame); - trigger_frame->frame_output_done = 1; + const int trigger_model_ref_idx = + ref_idx >= 0 ? ref_idx + : (cm->show_existing_frame ? cm->sef_ref_fb_idx : -1); + queue_output_frame(pbi, trigger_frame, trigger_model_ref_idx, + ref_idx < 0 ? AV2_DM_PRESENTATION_OWNER_CURRENT + : AV2_DM_PRESENTATION_OWNER_IMPLICIT); #if CONFIG_BITSTREAM_DEBUG if (trigger_frame->order_hint != cm->cur_frame->order_hint) { @@ -701,10 +723,8 @@ int av2_output_frame_buffers(AV2Decoder *pbi, int ref_idx) { if (cm->seq_params.monotonic_output_order_flag == 0) { doh_error |= check_and_update_output_doh(pbi, cm->ref_frame_map[i]); } - assign_output_frame_buffer_p( - &pbi->output_frames[pbi->num_output_frames++], - cm->ref_frame_map[i]); - cm->ref_frame_map[i]->frame_output_done = 1; + queue_output_frame(pbi, cm->ref_frame_map[i], i, + AV2_DM_PRESENTATION_OWNER_IMPLICIT); successive_output++; #if CONFIG_BITSTREAM_DEBUG avm_bitstream_queue_set_frame_read( @@ -772,6 +792,16 @@ static void update_frame_buffers(AV2Decoder *pbi, int frame_decoded) { } ++ref_index; } + uint32_t ref_valid_mask = 0; + for (int i = 0; i < cm->seq_params.ref_frames; ++i) { + if (cm->ref_frame_map[i] != NULL && pbi->valid_for_referencing[i]) { + ref_valid_mask |= (uint32_t)1 << i; + } + } + if (pbi->decoder_model_verifier != NULL) { + av2_decoder_model_verifier_after_reference_update( + pbi, (uint32_t)cm->current_frame.refresh_frame_flags, ref_valid_mask); + } update_subgop_stats(cm, &pbi->subgop_stats, cm->cur_frame->order_hint, pbi->enable_subgop_stats); if (((cm->immediate_output_picture && !cm->cur_frame->frame_output_done) || @@ -872,6 +902,14 @@ int av2_receive_compressed_data(AV2Decoder *pbi, size_t size, return 1; } + if (frame_decoded) { + // The suffix-OBU loop has completed, so CodedBits for this frame unit is + // now exact. Show-existing units leave the pending DFG open by definition. + if (pbi->decoder_model_verifier != NULL) { + av2_decoder_model_verifier_on_frame_unit_complete(pbi); + } + } + #if TXCOEFF_TIMER cm->cum_txcoeff_timer += cm->txcoeff_timer; fprintf(stderr, diff --git a/av2/decoder/decoder.h b/av2/decoder/decoder.h index 5672354c17..7efc240f2d 100644 --- a/av2/decoder/decoder.h +++ b/av2/decoder/decoder.h @@ -411,6 +411,10 @@ typedef struct AV2Decoder { DecOperatingPointParams dec_op_params; // Sub-bitstream extraction state (Annex F) SubBitstreamExtractionState sbe_state; + struct Av2DecoderModelVerifier *decoder_model_verifier; + int decoder_model_check_mode; + bool decoder_model_verifier_allocation_failed; + bool decoder_model_verifier_allocation_reported; int seen_frame_header; // The expected start_tile (tg_start syntax element) of the next tile group. int next_start_tile; diff --git a/av2/decoder/decoder_model.c b/av2/decoder/decoder_model.c new file mode 100644 index 0000000000..4f82ed630a --- /dev/null +++ b/av2/decoder/decoder_model.c @@ -0,0 +1,3654 @@ +/* + * Copyright (c) 2026, Alliance for Open Media. All rights reserved + * + * This source code is subject to the terms of the BSD 3-Clause Clear License + * and the Alliance for Open Media Patent License 1.0. If the BSD 3-Clause + * Clear License was not distributed with this source code in the LICENSE file, + * you can obtain it at aomedia.org/license/software-license/bsd-3-c-c/. If the + * Alliance for Open Media Patent License 1.0 was not distributed with this + * source code in the PATENTS file, you can obtain it at + * aomedia.org/license/patent-license/. + */ + +#include "av2/decoder/decoder_model.h" + +#include +#include +#include +#include +#include +#include + +#include "avm/avm_codec.h" +#include "avm/avmdx.h" +#include "avm_mem/avm_mem.h" +#include "av2/common/av2_common_int.h" +#include "av2/common/level.h" +#include "av2/decoder/annexF.h" +#include "av2/decoder/decoder.h" + +typedef enum Av2DmAdapterEventType { + AV2_DM_ADAPTER_RAW_OBU, + AV2_DM_ADAPTER_SEQUENCE_HEADER, + AV2_DM_ADAPTER_OPERATING_POINT_SET, + AV2_DM_ADAPTER_ACTIVE_CONFIGURATION, + AV2_DM_ADAPTER_BUFFER_REMOVAL_TIMING, + AV2_DM_ADAPTER_TEMPORAL_POINT, + AV2_DM_ADAPTER_FRAME_WRAPUP_START, + AV2_DM_ADAPTER_FRAME_UNIT_COMPLETE, + AV2_DM_ADAPTER_OLK_REFERENCE_INVALIDATION, + AV2_DM_ADAPTER_REFERENCE_UPDATE, + AV2_DM_ADAPTER_OUTPUT, + AV2_DM_ADAPTER_RECOVERY_RESET, + AV2_DM_ADAPTER_STREAM_CONFIGURATION_CHANGE, + AV2_DM_ADAPTER_FINISH +} Av2DmAdapterEventType; + +typedef enum Av2DmContextEventType { + AV2_DM_CONTEXT_FRAME, + AV2_DM_CONTEXT_OLK_REFERENCE_INVALIDATION, + AV2_DM_CONTEXT_REFERENCE_UPDATE, + AV2_DM_CONTEXT_OUTPUT, + AV2_DM_CONTEXT_RECOVERY_RESET +} Av2DmContextEventType; + +typedef enum Av2DmIndeterminateReason { + AV2_DM_REASON_NONE, + AV2_DM_REASON_MISSING_REQUIRED_INPUT, + AV2_DM_REASON_MISSING_ACTIVE_CONFIGURATION, + AV2_DM_REASON_INCOMPLETE_EXTRACTION, + AV2_DM_REASON_MISSING_FRAME_GENERATION, + AV2_DM_REASON_MISSING_PRESENTATION_PROVENANCE, + AV2_DM_REASON_MISSING_PRESENTATION_TIMING, + AV2_DM_REASON_INCOMPLETE_RAS_SEED, + AV2_DM_REASON_RECOVERY_RESET, + AV2_DM_REASON_INTERNAL_FAILURE +} Av2DmIndeterminateReason; + +typedef struct Av2DmRasSeedSnapshot { + uint32_t ref_index; + uint64_t generation; + int xlayer_id; + int mlayer_id; + int temporal_id; + bool generation_valid; +} Av2DmRasSeedSnapshot; + +typedef struct Av2DmPendingObu { + int obu_type; + int xlayer_id; + int mlayer_id; + int temporal_id; + uint64_t bits; + uint64_t frame_unit_index; + uint64_t temporal_unit_index; + uint64_t event_index; + bool decoder_retained; +} Av2DmPendingObu; + +typedef struct Av2DmAdapterEvent { + Av2DmAdapterEventType type; + uint64_t index; + uint64_t record_index; + uint64_t value; + bool decoder_retained; + Av2DmPendingObu raw_obu; +} Av2DmAdapterEvent; + +typedef struct Av2DmSequenceRecord { + int xlayer_id; + int sequence_header_id; + SequenceHeader sequence; +} Av2DmSequenceRecord; + +typedef struct Av2DmOpsRecord { + int xlayer_id; + int ops_id; + OperatingPointSet ops; +} Av2DmOpsRecord; + +typedef struct Av2DmBrtRecord { + int xlayer_id; + BufferRemovalTimingInfo brt; +} Av2DmBrtRecord; + +typedef struct Av2DmActiveConfigurationRecord { + int xlayer_id; + int sequence_header_id; + SequenceHeader sequence; + ContentInterpretation ci[MAX_NUM_MLAYERS]; +} Av2DmActiveConfigurationRecord; + +typedef struct Av2DmFrameSnapshot { + bool valid; + bool generation_valid; + uint64_t source_frame_unit_index; + uint64_t event_index; + uint64_t temporal_unit_index; + uint64_t parameter_generation; + uint64_t stream_generation; + uint64_t generation; + uint32_t ref_valid_mask; + int obu_type; + int xlayer_id; + int mlayer_id; + int temporal_id; + bool show_existing_frame; + bool implicit_output_frame; + bool leading_frame; + bool frame_is_intra; + bool allow_global_intrabc; + bool inloop_filtering_enabled; + uint32_t frame_width; + uint32_t frame_height; + uint64_t output_luma_samples; + uint32_t num_tiles; + uint32_t tile_columns; + uint64_t max_tile_width; + uint64_t max_tile_area; + bool non_rightmost_tile_width_valid; + uint64_t frame_symbol_count; + bool presentation_time_present; + uint64_t presentation_time_ticks; + bool ras_seed_complete; + uint32_t ras_seed_count; + Av2DmRasSeedSnapshot ras_seeds[AV2_DM_MAX_REF_FRAMES]; + bool multistream_decoder_mode; + MultistreamDecoderOperation msdo; + int multistream_even_allocation; + int multistream_large_picture_index; + int num_streams; + int stream_ids[AVM_MAX_NUM_STREAMS]; +} Av2DmFrameSnapshot; + +typedef struct Av2DmGenerationRecord { + const RefCntBuffer *buffer; + uint64_t generation; + uint64_t source_frame_unit_index; + uint64_t temporal_unit_index; + int xlayer_id; + int mlayer_id; + int temporal_id; + uint32_t width; + uint32_t height; + uint64_t output_luma_samples; + bool leading_frame; + bool random_access_point; + bool presentation_time_present; + uint64_t presentation_time_ticks; + bool presentation_timing_config_valid; + bool equal_picture_interval; + bool implicit_presentation_pending; +} Av2DmGenerationRecord; + +typedef struct Av2DmContextEvent { + Av2DmContextEventType type; + uint64_t event_index; + uint64_t source_frame_unit_index; + uint64_t presentation_frame_unit_index; + int presentation_xlayer_id; + int presentation_mlayer_id; + int presentation_tlayer_id; + uint64_t parameter_generation; + uint64_t stream_generation; + uint64_t generation; + int frame_obu_type; + bool leading_frame; + bool config_present; + Av2DmConfig config; + Av2DmFrameEvent frame; + Av2DmReferenceUpdateEvent reference_update; + bool set_initial_presentation_delay; + uint32_t ref_valid_mask; + Av2DmOutputEvent output; + uint32_t ras_seed_count; + Av2DmRasSeed ras_seeds[AV2_DM_MAX_REF_FRAMES]; + bool ras_seed_complete; + Av2DmIndeterminateReason indeterminate_reason; +} Av2DmContextEvent; + +typedef struct Av2DmContextKey { + int xlayer_id; + int ops_xlayer_id; + int ops_id; + int operating_point; + bool whole_xlayer; +} Av2DmContextKey; + +typedef struct Av2DmLiveRun Av2DmLiveRun; + +typedef struct Av2DmContext { + Av2DmContextKey key; + bool active; + SubBitstreamExtractionState membership; + uint64_t pending_dfg_bits; + bool pending_after_event_valid; + uint64_t pending_after_event; + uint64_t last_closed_dfg_bits; + uint64_t closed_dfgs; + uint64_t configuration_generation; + uint64_t active_sequence_record; + uint64_t active_ops_record; + uint64_t active_configuration_record; + Av2DmApplicability applicability; + bool incomplete_extraction; + bool recovery_reset_pending; + Av2DmLiveRun **runs; + size_t run_count; + size_t run_capacity; + Av2DmContextEvent *prefix_events; + size_t prefix_event_count; + size_t prefix_event_capacity; + bool last_config_present; + Av2DmConfig last_config; + uint64_t last_stream_generation; + bool last_ras_seed_complete; + uint32_t last_ras_seed_count; +} Av2DmContext; + +typedef struct Av2DmCvsAggregate { + bool open; + uint64_t number; + uint64_t violations; + uint64_t run_status_count[4]; + bool verification_complete; + Av2DmIndeterminateReason reason; +} Av2DmCvsAggregate; + +typedef enum Av2DmVerifierErrorCode { + AV2_DM_VERIFIER_ERROR_NONE, + AV2_DM_VERIFIER_ERROR_ALLOCATION, + AV2_DM_VERIFIER_ERROR_ARITHMETIC, + AV2_DM_VERIFIER_ERROR_INTERNAL_STATE, +} Av2DmVerifierErrorCode; + +struct Av2DecoderModelVerifier { + bool failed; + bool finished; + bool fatal_violation; + bool aggregate_incomplete; + bool error_emitted; + int check_mode; + Av2DmVerifierErrorCode error_code; + bool temporal_point_present; + bool temporal_unit_has_obu; + uint64_t temporal_point; + uint64_t raw_obus; + uint64_t raw_bits; + uint64_t temporal_unit_index; + uint64_t frame_unit_index; + uint64_t source_frame_unit_index; + bool source_frame_unit_started; + uint64_t closed_dfgs; + uint64_t temporal_points; + uint64_t parameter_generation; + uint64_t stream_generation; + uint64_t next_generation; + uint64_t frame_starts; + uint64_t reference_updates; + uint64_t olk_invalidations; + uint64_t outputs; + uint64_t last_frame_start_event; + uint64_t last_reference_update_event; + uint64_t last_olk_invalidation_event; + uint64_t last_output_event; + uint64_t last_output_callback_frame_unit; + uint64_t last_output_presentation_frame_unit; + uint64_t last_output_presentation_temporal_unit; + uint64_t last_output_generation; + int last_output_presentation_xlayer_id; + int last_output_presentation_mlayer_id; + int last_output_presentation_tlayer_id; + bool last_output_uses_current_presentation; + bool replay_previous_presentation_offset_valid; + Av2DmRational replay_previous_presentation_offset; + bool replay_last_presentation_offset_valid; + Av2DmRational replay_last_presentation_offset; + uint64_t finish_event; + uint64_t result_count; + uint64_t result_status_count[4]; + Av2DmFrameSnapshot pending_frame; + Av2DmFrameSnapshot last_completed_frame; + + Av2DmAdapterEvent last_event; + bool last_event_valid; + size_t event_count; + Av2DmPendingObu *current_tu_obus; + size_t current_tu_obu_count; + size_t current_tu_obu_capacity; + Av2DmSequenceRecord *sequence_records; + size_t sequence_record_count; + size_t sequence_record_capacity; + Av2DmOpsRecord *ops_records; + size_t ops_record_count; + size_t ops_record_capacity; + Av2DmBrtRecord *brt_records; + size_t brt_record_count; + size_t brt_record_capacity; + Av2DmActiveConfigurationRecord *active_records; + size_t active_record_count; + size_t active_record_capacity; + size_t rap_start_count; + bool last_rap_start_valid; + int last_rap_xlayer_id; + int last_rap_mlayer_id; + int last_rap_temporal_id; + uint64_t last_rap_frame_unit_index; + Av2DmContext *contexts; + size_t context_count; + size_t context_capacity; + Av2DmGenerationRecord *generations; + size_t generation_count; + size_t generation_capacity; + bool active_configuration_present[MAX_NUM_XLAYERS]; + uint64_t active_configuration_record[MAX_NUM_XLAYERS]; + uint64_t active_sequence_record[MAX_NUM_XLAYERS]; + bool current_brt_present[MAX_NUM_XLAYERS]; + uint64_t current_brt_record[MAX_NUM_XLAYERS]; + int multistream_even_allocation; + int multistream_large_picture_index; + bool current_source_frame_dispatched; + bool clk_boundary_seen[MAX_NUM_XLAYERS]; + uint64_t clk_boundary_temporal_unit[MAX_NUM_XLAYERS]; + Av2DmCvsAggregate cvs[MAX_NUM_XLAYERS]; + uint64_t bitstream_cvs; + uint64_t bitstream_status_count[4]; + bool first_non_conformant_valid; + int first_non_conformant_xlayer; + uint64_t first_non_conformant_cvs; + bool bitstream_result_emitted; +}; + +static void mark_failed(Av2DecoderModelVerifier *verifier); +static void mark_arithmetic_failed(Av2DecoderModelVerifier *verifier); +static void mark_allocation_failed(Av2DecoderModelVerifier *verifier); +static bool increment_u64(Av2DecoderModelVerifier *verifier, uint64_t *value); +static bool increment_size(Av2DecoderModelVerifier *verifier, size_t *value); +static bool obu_belongs_to_context(const Av2DmContext *context, + const Av2DmPendingObu *obu); +static void dispatch_context_event(Av2DecoderModelVerifier *verifier, + Av2DmContext *context, + const Av2DmContextEvent *event); +static void ensure_cvs_open(Av2DecoderModelVerifier *verifier, int xlayer_id); +static void finish_xlayer_cvs(Av2DecoderModelVerifier *verifier, int xlayer_id); +static void finish_all_cvs(Av2DecoderModelVerifier *verifier); +static void emit_bitstream_result(Av2DecoderModelVerifier *verifier, + bool complete); +static void destroy_context_runs(Av2DmContext *context); + +static void retire_xlayer_generations(Av2DecoderModelVerifier *verifier, + const AV2Decoder *pbi, int xlayer_id) { + size_t write_index = 0; + for (size_t i = 0; i < verifier->generation_count; ++i) { + Av2DmGenerationRecord *const generation = &verifier->generations[i]; + bool referenced = generation->implicit_presentation_pending; + for (int ref = 0; !referenced && ref < AV2_DM_MAX_REF_FRAMES; ++ref) { + referenced = pbi->common.ref_frame_map[ref] == generation->buffer && + pbi->valid_for_referencing[ref]; + } + if (generation->xlayer_id == xlayer_id && !referenced) continue; + if (write_index != i) { + verifier->generations[write_index] = *generation; + } + ++write_index; + } + verifier->generation_count = write_index; +} + +static bool reserve_array(Av2DecoderModelVerifier *verifier, void **array, + size_t *capacity, size_t needed, + size_t element_size) { + if (needed <= *capacity) return true; + size_t new_capacity = *capacity == 0 ? 8 : *capacity; + while (new_capacity < needed) { + if (new_capacity > SIZE_MAX / 2) { + new_capacity = needed; + break; + } + new_capacity *= 2; + } + if (new_capacity > SIZE_MAX / element_size) { + mark_arithmetic_failed(verifier); + return false; + } + void *const resized = avm_malloc(new_capacity * element_size); + if (resized == NULL) { + mark_allocation_failed(verifier); + return false; + } + if (*array != NULL) { + memcpy(resized, *array, *capacity * element_size); + avm_free(*array); + } + *array = resized; + *capacity = new_capacity; + return true; +} + +static void emit_generic_internal_failure_result(void) { + fprintf(stderr, + "AV2_DECODER_MODEL_RESULT status=INDETERMINATE xlayer=-1 ops=-1 " + "op=-1 rap=-1 mode=resource decoded=0 outputs=0 " + "reordered_outputs=0 violations=0 reason=internal_failure\n"); +} + +static void emit_generic_internal_failure_bitstream_result(void) { + fprintf(stderr, + "AV2_DECODER_MODEL_BITSTREAM_RESULT status=INDETERMINATE complete=0 " + "cvs=0 conformant_cvs=0 non_conformant_cvs=0 " + "indeterminate_cvs=0 not_applicable_cvs=0 " + "first_non_conformant_xlayer=-1 first_non_conformant_cvs=0\n"); +} + +static const char *verifier_error_name(Av2DmVerifierErrorCode code) { + switch (code) { + case AV2_DM_VERIFIER_ERROR_ALLOCATION: return "ALLOCATION_FAILURE"; + case AV2_DM_VERIFIER_ERROR_ARITHMETIC: return "ARITHMETIC_FAILURE"; + case AV2_DM_VERIFIER_ERROR_INTERNAL_STATE: + case AV2_DM_VERIFIER_ERROR_NONE: return "INTERNAL_STATE_FAILURE"; + } + return "INTERNAL_STATE_FAILURE"; +} + +static void emit_verifier_error(Av2DecoderModelVerifier *verifier, + Av2DmVerifierErrorCode code, int xlayer_id, + uint64_t cvs) { + if (verifier == NULL || verifier->error_emitted) return; + fprintf(stderr, "AV2_DECODER_MODEL_ERROR code=%s xlayer=%d cvs=%" PRIu64 "\n", + verifier_error_name(code), xlayer_id, cvs); + verifier->error_emitted = true; +} + +static bool add_u64(uint64_t left, uint64_t right, uint64_t *result) { + if (UINT64_MAX - left < right) return false; + *result = left + right; + return true; +} + +static uint32_t real_ref_valid_mask(const AV2Decoder *pbi) { + const AV2_COMMON *const cm = &pbi->common; + uint32_t mask = 0; + const int num_refs = cm->seq_params.ref_frames < AV2_DM_MAX_REF_FRAMES + ? cm->seq_params.ref_frames + : AV2_DM_MAX_REF_FRAMES; + for (int i = 0; i < num_refs; ++i) { + if (cm->ref_frame_map[i] != NULL && pbi->valid_for_referencing[i]) { + mask |= (uint32_t)1 << i; + } + } + return mask; +} + +static bool is_compressed_size_obu(int obu_type) { + switch (obu_type) { + case OBU_CLOSED_LOOP_KEY: + case OBU_OPEN_LOOP_KEY: + case OBU_LEADING_TILE_GROUP: + case OBU_REGULAR_TILE_GROUP: + case OBU_METADATA_SHORT: + case OBU_METADATA_GROUP: + case OBU_SWITCH: + case OBU_LEADING_SEF: + case OBU_REGULAR_SEF: + case OBU_LEADING_TIP: + case OBU_REGULAR_TIP: + case OBU_BRIDGE_FRAME: + case OBU_RAS_FRAME: return true; + default: return false; + } +} + +static Av2DmGenerationRecord *find_generation_by_buffer( + Av2DecoderModelVerifier *verifier, const RefCntBuffer *buffer) { + if (buffer == NULL) return NULL; + for (size_t i = verifier->generation_count; i > 0; --i) { + if (verifier->generations[i - 1].buffer == buffer) { + return &verifier->generations[i - 1]; + } + } + return NULL; +} + +static Av2DmGenerationRecord *find_generation_by_id( + Av2DecoderModelVerifier *verifier, uint64_t generation) { + if (generation == 0) return NULL; + for (size_t i = verifier->generation_count; i > 0; --i) { + if (verifier->generations[i - 1].generation == generation) { + return &verifier->generations[i - 1]; + } + } + return NULL; +} + +static Av2DmGenerationRecord *assign_generation( + Av2DecoderModelVerifier *verifier, const RefCntBuffer *buffer) { + if (buffer == NULL) return NULL; + if (verifier->next_generation == UINT64_MAX) { + mark_arithmetic_failed(verifier); + return NULL; + } + Av2DmGenerationRecord *record = find_generation_by_buffer(verifier, buffer); + if (record == NULL) { + if (verifier->generation_count == SIZE_MAX) { + mark_arithmetic_failed(verifier); + return NULL; + } + if (!reserve_array(verifier, (void **)&verifier->generations, + &verifier->generation_capacity, + verifier->generation_count + 1, + sizeof(*verifier->generations))) { + mark_failed(verifier); + return NULL; + } + record = &verifier->generations[verifier->generation_count]; + if (!increment_size(verifier, &verifier->generation_count)) return NULL; + } + memset(record, 0, sizeof(*record)); + record->buffer = buffer; + if (!increment_u64(verifier, &verifier->next_generation)) return NULL; + record->generation = verifier->next_generation; + return record; +} + +static void initialize_context_event(const Av2DecoderModelVerifier *verifier, + Av2DmContextEventType type, + Av2DmContextEvent *event) { + memset(event, 0, sizeof(*event)); + event->type = type; + event->event_index = verifier->event_count; + event->source_frame_unit_index = verifier->source_frame_unit_index; + event->parameter_generation = verifier->parameter_generation; + event->stream_generation = verifier->stream_generation; +} + +static bool frame_obu_matches(const Av2DmPendingObu *obu, + const Av2DmFrameSnapshot *frame) { + return obu->frame_unit_index == frame->source_frame_unit_index; +} + +static bool compressed_size_for_context(Av2DecoderModelVerifier *verifier, + const Av2DmContext *context, + const Av2DmFrameSnapshot *frame, + uint64_t *bytes) { + uint64_t bits = 0; + for (size_t i = 0; i < verifier->current_tu_obu_count; ++i) { + const Av2DmPendingObu *const obu = &verifier->current_tu_obus[i]; + if (!is_compressed_size_obu(obu->obu_type) || + !frame_obu_matches(obu, frame) || + !obu_belongs_to_context(context, obu)) { + continue; + } + if (!add_u64(bits, obu->bits, &bits)) { + mark_arithmetic_failed(verifier); + return false; + } + } + if ((bits & 7) != 0) return false; + *bytes = bits / 8; + return true; +} + +static void retire_completed_frame_obus(Av2DecoderModelVerifier *verifier, + uint64_t source_frame_unit_index) { + size_t write_index = 0; + for (size_t i = 0; i < verifier->current_tu_obu_count; ++i) { + Av2DmPendingObu *const obu = &verifier->current_tu_obus[i]; + if (is_compressed_size_obu(obu->obu_type) && + obu->frame_unit_index == source_frame_unit_index) { + continue; + } + if (write_index != i) verifier->current_tu_obus[write_index] = *obu; + ++write_index; + } + verifier->current_tu_obu_count = write_index; +} + +static void mark_failed(Av2DecoderModelVerifier *verifier) { + if (verifier == NULL) return; + verifier->failed = true; + if (verifier->error_code == AV2_DM_VERIFIER_ERROR_NONE) { + verifier->error_code = AV2_DM_VERIFIER_ERROR_INTERNAL_STATE; + } +} + +static void mark_failed_with_code(Av2DecoderModelVerifier *verifier, + Av2DmVerifierErrorCode code) { + if (verifier == NULL) return; + verifier->failed = true; + if (verifier->error_code == AV2_DM_VERIFIER_ERROR_NONE) { + verifier->error_code = code; + } +} + +static void mark_arithmetic_failed(Av2DecoderModelVerifier *verifier) { + mark_failed_with_code(verifier, AV2_DM_VERIFIER_ERROR_ARITHMETIC); +} + +static void mark_allocation_failed(Av2DecoderModelVerifier *verifier) { + mark_failed_with_code(verifier, AV2_DM_VERIFIER_ERROR_ALLOCATION); +} + +static bool verifier_accepts_events(const Av2DecoderModelVerifier *verifier) { + return verifier != NULL && !verifier->failed && !verifier->finished && + !verifier->fatal_violation; +} + +static bool increment_u64(Av2DecoderModelVerifier *verifier, uint64_t *value) { + if (*value == UINT64_MAX) { + mark_arithmetic_failed(verifier); + return false; + } + ++*value; + return true; +} + +static void add_u64_saturated(uint64_t *value, uint64_t addend) { + if (UINT64_MAX - *value < addend) { + *value = UINT64_MAX; + return; + } + *value += addend; +} + +static bool increment_size(Av2DecoderModelVerifier *verifier, size_t *value) { + if (*value == SIZE_MAX) { + mark_arithmetic_failed(verifier); + return false; + } + ++*value; + return true; +} + +static Av2DmAdapterEvent *append_event(Av2DecoderModelVerifier *verifier, + Av2DmAdapterEventType type) { + if (verifier == NULL || verifier->failed) { + mark_failed(verifier); + return NULL; + } + if (verifier->event_count == SIZE_MAX) { + mark_arithmetic_failed(verifier); + return NULL; + } + Av2DmAdapterEvent *const event = &verifier->last_event; + memset(event, 0, sizeof(*event)); + event->type = type; + event->index = verifier->event_count; + event->decoder_retained = true; + if (!increment_size(verifier, &verifier->event_count)) return NULL; + verifier->last_event_valid = true; + return event; +} + +static bool keys_equal(const Av2DmContextKey *left, + const Av2DmContextKey *right) { + return left->xlayer_id == right->xlayer_id && left->ops_id == right->ops_id && + left->ops_xlayer_id == right->ops_xlayer_id && + left->operating_point == right->operating_point && + left->whole_xlayer == right->whole_xlayer; +} + +static Av2DmContext *find_context(Av2DecoderModelVerifier *verifier, + const Av2DmContextKey *key) { + for (size_t i = 0; i < verifier->context_count; ++i) { + if (keys_equal(&verifier->contexts[i].key, key)) { + return &verifier->contexts[i]; + } + } + return NULL; +} + +static bool obu_belongs_to_context(const Av2DmContext *context, + const Av2DmPendingObu *obu) { + return av2_sbe_should_retain_obu(&context->membership, + (OBU_TYPE)obu->obu_type, obu->xlayer_id, + obu->mlayer_id, obu->temporal_id) != 0; +} + +static bool frame_belongs_to_context(const Av2DmContext *context, int xlayer_id, + int mlayer_id, int temporal_id) { + const Av2DmPendingObu frame_obu = { + OBU_REGULAR_TILE_GROUP, xlayer_id, mlayer_id, temporal_id, 0, 0, 0, 0, false + }; + return obu_belongs_to_context(context, &frame_obu); +} + +static const OperatingPoint *context_operating_point( + const Av2DecoderModelVerifier *verifier, const Av2DmContext *context, + const OperatingPointSet **ops_out) { + *ops_out = NULL; + if (context->key.whole_xlayer || + context->active_ops_record >= verifier->ops_record_count) { + return NULL; + } + const OperatingPointSet *const ops = + &verifier->ops_records[context->active_ops_record].ops; + if (context->key.operating_point < 0 || + context->key.operating_point >= ops->ops_cnt) { + return NULL; + } + *ops_out = ops; + return &ops->op[context->key.operating_point]; +} + +static bool build_context_config(const Av2DecoderModelVerifier *verifier, + const Av2DmContext *context, int mlayer_id, + const Av2DmFrameSnapshot *snapshot, + Av2DmConfig *config) { + memset(config, 0, sizeof(*config)); + config->scope.xlayer_id = context->key.xlayer_id; + config->scope.ops_xlayer_id = context->key.ops_xlayer_id; + config->scope.ops_id = context->key.ops_id; + config->scope.operating_point = context->key.operating_point; + config->scope.whole_xlayer = context->key.whole_xlayer; + config->applicability = AV2_DM_MISSING_REQUIRED_INPUT; + config->mode = AV2_DM_RESOURCE_AVAILABILITY_MODE; + config->stop_after_first_violation = + verifier->check_mode == AVM_DECODER_MODEL_CHECK_FATAL; + + if (context->active_configuration_record >= verifier->active_record_count) { + return true; + } + const Av2DmActiveConfigurationRecord *const active = + &verifier->active_records[context->active_configuration_record]; + const SequenceHeader *const sequence = &active->sequence; + if (mlayer_id < 0 || mlayer_id >= MAX_NUM_MLAYERS) return true; + const ContentInterpretation *const ci = &active->ci[mlayer_id]; + + config->applicability = sequence->seq_max_level_idx == 31 + ? AV2_DM_NOT_APPLICABLE + : AV2_DM_APPLICABLE; + config->level_idx = sequence->seq_max_level_idx; + config->tier = sequence->seq_tier; + config->profile = sequence->seq_profile_idc; + config->num_ref_frames = sequence->ref_frames; + config->explicit_num_ref_frames = true; + config->max_frame_width = sequence->max_frame_width; + config->max_frame_height = sequence->max_frame_height; + config->max_mlayer_id = sequence->max_mlayer_id; + config->still_picture = sequence->still_picture != 0; + config->timing_info_present = ci->ci_timing_info_present_flag != 0; + config->num_units_in_display_tick = ci->timing_info.num_units_in_display_tick; + config->time_scale = ci->timing_info.time_scale; + config->num_units_in_decoding_tick = + sequence->decoder_model_info.num_units_in_decoding_tick; + config->equal_picture_interval = + ci->timing_info.equal_elemental_interval != 0; + config->ticks_per_picture = ci->timing_info.num_ticks_per_elemental_duration; + config->initial_display_delay = + sequence->seq_max_display_model_info_present_flag + ? (uint32_t)sequence->seq_max_initial_display_delay_minus_1 + 1 + : (uint32_t)sequence->ref_frames + 2; + config->sequence_parameters_present = + sequence->seq_max_decoder_model_present_flag != 0; + config->sequence_decoder_buffer_delay = + sequence->seq_max_decoder_buffer_delay; + config->sequence_encoder_buffer_delay = + sequence->seq_max_encoder_buffer_delay; + config->sequence_low_delay_mode = sequence->seq_max_low_delay_mode_flag != 0; + + const OperatingPointSet *ops; + const OperatingPoint *const op = + context_operating_point(verifier, context, &ops); + if (!context->key.whole_xlayer) { + if (op == NULL || ops == NULL) { + config->applicability = AV2_DM_MISSING_REQUIRED_INPUT; + return true; + } + if (ops->ops_ptl_present_flag) { + const int xlayer_id = context->key.xlayer_id; + config->profile = op->ops_seq_profile_idc[xlayer_id]; + config->level_idx = op->ops_level_idx[xlayer_id]; + config->tier = op->ops_tier_flag[xlayer_id]; + if (config->level_idx == 31) { + config->applicability = AV2_DM_NOT_APPLICABLE; + } + } + config->operating_point_parameters_present = + op->ops_decoder_model_info_for_this_op_present_flag != 0; + config->operating_point_decoder_buffer_delay = + op->decoder_model_info.ops_decoder_buffer_delay; + config->operating_point_encoder_buffer_delay = + op->decoder_model_info.ops_encoder_buffer_delay; + config->operating_point_low_delay_mode = + op->decoder_model_info.ops_low_delay_mode_flag != 0; + if (op->ops_initial_display_delay_present_flag) { + config->initial_display_delay = op->ops_initial_display_delay; + } + } + + if (config->sequence_parameters_present || + config->operating_point_parameters_present) { + config->mode = AV2_DM_DECODING_SCHEDULE_MODE; + } + if (config->mode == AV2_DM_DECODING_SCHEDULE_MODE && + !sequence->decoder_model_info_present_flag) { + // DecCT is not present, even if a separately parsed parameter structure + // appears to request schedule mode. + config->applicability = AV2_DM_MISSING_REQUIRED_INPUT; + } + if (config->num_ref_frames == 0 || + config->num_ref_frames > AV2_DM_MAX_REF_FRAMES || + config->initial_display_delay == 0 || + config->initial_display_delay > AV2_DM_MAX_BUFFER_POOL_SIZE) { + config->applicability = AV2_DM_MISSING_REQUIRED_INPUT; + } + if (snapshot->multistream_decoder_mode && + config->applicability == AV2_DM_APPLICABLE) { + uint32_t scale_numerator = 4; + uint32_t scale_denominator = 1; + if (!snapshot->multistream_even_allocation) { + const int large = snapshot->multistream_large_picture_index; + if (large >= snapshot->num_streams || large >= AVM_MAX_NUM_STREAMS) { + config->applicability = AV2_DM_MISSING_REQUIRED_INPUT; + return true; + } + if (snapshot->stream_ids[large] == context->key.xlayer_id) { + scale_numerator = 3; + scale_denominator = 2; + } else { + scale_numerator = 9; + } + } + if (!av2_dm_get_level_limits(config->level_idx, config->tier, + config->profile, &config->level_limits) || + !av2_dm_apply_multistream_limits(snapshot->msdo.multistream_level_idx, + snapshot->msdo.multistream_tier_idx, + snapshot->msdo.multistream_profile_idc, + scale_numerator, scale_denominator, + &config->level_limits)) { + config->applicability = AV2_DM_MISSING_REQUIRED_INPUT; + } else { + config->level_limits_present = true; + } + } + return true; +} + +static bool find_buffer_removal_time(const Av2DecoderModelVerifier *verifier, + const Av2DmContext *context, + uint32_t *buffer_removal_time) { + const int xlayer_id = context->key.whole_xlayer ? context->key.xlayer_id + : context->key.ops_xlayer_id; + if (xlayer_id < 0 || xlayer_id >= MAX_NUM_XLAYERS || + !verifier->current_brt_present[xlayer_id] || + verifier->current_brt_record[xlayer_id] >= verifier->brt_record_count) { + return false; + } + const BufferRemovalTimingInfo *const brt = + &verifier->brt_records[verifier->current_brt_record[xlayer_id]].brt; + if (context->key.whole_xlayer) { + if (brt->br_ops_dependent_flag || brt->br_time < 0) return false; + *buffer_removal_time = (uint32_t)brt->br_time; + return true; + } + const int ops_id = context->key.ops_id; + const int op = context->key.operating_point; + if (!brt->br_ops_dependent_flag || brt->br_ops_id != ops_id || ops_id < 0 || + ops_id >= MAX_NUM_OPS_ID || op < 0 || op >= MAX_OPS_COUNT || + !brt->br_decoder_model_present_op_flag[ops_id][op] || + brt->br_time_op[ops_id][op] < 0) { + return false; + } + *buffer_removal_time = (uint32_t)brt->br_time_op[ops_id][op]; + return true; +} + +static bool recompute_pending_bits(Av2DecoderModelVerifier *verifier, + Av2DmContext *context) { + uint64_t bits = 0; + for (size_t i = 0; i < verifier->current_tu_obu_count; ++i) { + const Av2DmPendingObu *const obu = &verifier->current_tu_obus[i]; + if ((!context->pending_after_event_valid || + obu->event_index > context->pending_after_event) && + obu_belongs_to_context(context, obu) && + !add_u64(bits, obu->bits, &bits)) { + mark_arithmetic_failed(verifier); + return false; + } + } + context->pending_dfg_bits = bits; + return true; +} + +static void rebuild_incomplete_extraction( + const Av2DecoderModelVerifier *verifier, Av2DmContext *context) { + context->incomplete_extraction = false; + for (size_t i = 0; i < verifier->current_tu_obu_count; ++i) { + const Av2DmPendingObu *const obu = &verifier->current_tu_obus[i]; + if ((!context->pending_after_event_valid || + obu->event_index > context->pending_after_event) && + !obu->decoder_retained && obu_belongs_to_context(context, obu)) { + context->incomplete_extraction = true; + return; + } + } +} + +static bool reset_xlayer_cvs_obu_accounting(Av2DecoderModelVerifier *verifier, + int xlayer_id) { + for (size_t i = 0; i < verifier->context_count; ++i) { + Av2DmContext *const context = &verifier->contexts[i]; + if (context->key.xlayer_id != xlayer_id) continue; + context->pending_dfg_bits = 0; + context->pending_after_event_valid = false; + context->pending_after_event = 0; + context->last_closed_dfg_bits = 0; + if (!recompute_pending_bits(verifier, context)) return false; + rebuild_incomplete_extraction(verifier, context); + } + return true; +} + +static Av2DmContext *configure_context(Av2DecoderModelVerifier *verifier, + const Av2DmContextKey *key, + const OperatingPointSet *ops) { + Av2DmContext *context = find_context(verifier, key); + bool created = false; + if (context == NULL) { + if (verifier->context_count == SIZE_MAX) { + mark_arithmetic_failed(verifier); + return NULL; + } + if (!reserve_array(verifier, (void **)&verifier->contexts, + &verifier->context_capacity, verifier->context_count + 1, + sizeof(*verifier->contexts))) { + mark_failed(verifier); + return NULL; + } + context = &verifier->contexts[verifier->context_count]; + if (!increment_size(verifier, &verifier->context_count)) return NULL; + memset(context, 0, sizeof(*context)); + created = true; + context->key = *key; + context->active_sequence_record = UINT64_MAX; + context->active_ops_record = UINT64_MAX; + context->active_configuration_record = UINT64_MAX; + context->applicability = AV2_DM_MISSING_REQUIRED_INPUT; + } + + SubBitstreamExtractionState membership; + if (!av2_sbe_configure_decoder_model_scope(&membership, key->xlayer_id, ops, + key->operating_point, + key->whole_xlayer)) { + mark_failed(verifier); + return NULL; + } + const bool was_active = context->active; + context->membership = membership; + context->configuration_generation = verifier->parameter_generation; + context->active = true; + if (verifier->active_configuration_present[key->xlayer_id]) { + context->active_configuration_record = + verifier->active_configuration_record[key->xlayer_id]; + context->active_sequence_record = + verifier->active_sequence_record[key->xlayer_id]; + } else { + context->active_configuration_record = UINT64_MAX; + context->active_sequence_record = UINT64_MAX; + } + if ((created || !was_active) && !recompute_pending_bits(verifier, context)) { + mark_failed(verifier); + return NULL; + } + for (size_t i = 0; i < verifier->current_tu_obu_count; ++i) { + const Av2DmPendingObu *const obu = &verifier->current_tu_obus[i]; + if ((!context->pending_after_event_valid || + obu->event_index > context->pending_after_event) && + !obu->decoder_retained && obu_belongs_to_context(context, obu)) { + context->incomplete_extraction = true; + } + } + return context; +} + +static void append_rap_start(Av2DecoderModelVerifier *verifier, int obu_type, + int xlayer_id, int mlayer_id, int temporal_id, + uint64_t event_position) { + (void)event_position; + if (obu_type != OBU_CLOSED_LOOP_KEY && obu_type != OBU_OPEN_LOOP_KEY && + obu_type != OBU_RAS_FRAME) { + return; + } + if (verifier->last_rap_start_valid && + verifier->last_rap_frame_unit_index == + verifier->source_frame_unit_index && + verifier->last_rap_xlayer_id == xlayer_id && + verifier->last_rap_mlayer_id == mlayer_id && + verifier->last_rap_temporal_id == temporal_id) { + return; + } + if (verifier->rap_start_count == SIZE_MAX) { + mark_arithmetic_failed(verifier); + return; + } + ++verifier->rap_start_count; + verifier->last_rap_start_valid = true; + verifier->last_rap_xlayer_id = xlayer_id; + verifier->last_rap_mlayer_id = mlayer_id; + verifier->last_rap_temporal_id = temporal_id; + verifier->last_rap_frame_unit_index = verifier->source_frame_unit_index; +} + +void av2_decoder_model_verifier_init(AV2Decoder *pbi) { + if (pbi == NULL || pbi->decoder_model_verifier != NULL || + pbi->decoder_model_verifier_allocation_failed) { + return; + } + pbi->decoder_model_verifier_allocation_reported = false; + pbi->decoder_model_verifier = + (Av2DecoderModelVerifier *)avm_calloc(1, sizeof(Av2DecoderModelVerifier)); + pbi->decoder_model_verifier_allocation_failed = + pbi->decoder_model_verifier == NULL; + if (pbi->decoder_model_verifier != NULL) { + pbi->decoder_model_verifier->check_mode = pbi->decoder_model_check_mode; + } +} + +void av2_decoder_model_verifier_destroy(AV2Decoder *pbi) { + if (pbi == NULL) return; + if (pbi->decoder_model_verifier == NULL) { + pbi->decoder_model_verifier_allocation_failed = false; + pbi->decoder_model_verifier_allocation_reported = false; + return; + } + Av2DecoderModelVerifier *const verifier = pbi->decoder_model_verifier; + for (size_t i = 0; i < verifier->context_count; ++i) { + destroy_context_runs(&verifier->contexts[i]); + avm_free(verifier->contexts[i].runs); + avm_free(verifier->contexts[i].prefix_events); + } + avm_free(verifier->current_tu_obus); + avm_free(verifier->sequence_records); + avm_free(verifier->ops_records); + avm_free(verifier->brt_records); + avm_free(verifier->active_records); + avm_free(verifier->contexts); + avm_free(verifier->generations); + avm_free(verifier); + pbi->decoder_model_verifier = NULL; + pbi->decoder_model_verifier_allocation_failed = false; + pbi->decoder_model_verifier_allocation_reported = false; +} + +void av2_decoder_model_verifier_record_obu(AV2Decoder *pbi, int obu_type, + int xlayer_id, int mlayer_id, + int temporal_id, uint64_t obu_bits) { + if (pbi == NULL || pbi->decoder_model_verifier == NULL) return; + Av2DecoderModelVerifier *const verifier = pbi->decoder_model_verifier; + if (!verifier_accepts_events(verifier)) return; + + if (obu_type == OBU_TEMPORAL_DELIMITER && verifier->temporal_unit_has_obu) { + if (verifier->temporal_unit_index == UINT64_MAX) { + mark_arithmetic_failed(verifier); + return; + } + ++verifier->temporal_unit_index; + } + if (obu_type == OBU_TEMPORAL_DELIMITER) { + memset(verifier->current_brt_present, 0, + sizeof(verifier->current_brt_present)); + verifier->current_tu_obu_count = 0; + } + verifier->temporal_unit_has_obu = true; + + Av2DmAdapterEvent *const event = + append_event(verifier, AV2_DM_ADAPTER_RAW_OBU); + if (event == NULL) return; + + event->raw_obu.obu_type = obu_type; + event->raw_obu.xlayer_id = xlayer_id; + event->raw_obu.mlayer_id = mlayer_id; + event->raw_obu.temporal_id = temporal_id; + event->raw_obu.bits = obu_bits; + event->raw_obu.frame_unit_index = verifier->source_frame_unit_index; + event->raw_obu.temporal_unit_index = verifier->temporal_unit_index; + event->raw_obu.event_index = event->index; + event->raw_obu.decoder_retained = true; + + if (verifier->current_tu_obu_count == SIZE_MAX) { + mark_arithmetic_failed(verifier); + return; + } + if (!reserve_array(verifier, (void **)&verifier->current_tu_obus, + &verifier->current_tu_obu_capacity, + verifier->current_tu_obu_count + 1, + sizeof(*verifier->current_tu_obus))) { + mark_failed(verifier); + return; + } + verifier->current_tu_obus[verifier->current_tu_obu_count] = event->raw_obu; + if (!increment_size(verifier, &verifier->current_tu_obu_count)) return; + + if (!add_u64(verifier->raw_bits, obu_bits, &verifier->raw_bits) || + verifier->raw_obus == UINT64_MAX) { + mark_arithmetic_failed(verifier); + return; + } + ++verifier->raw_obus; + + for (size_t i = 0; i < verifier->context_count; ++i) { + Av2DmContext *const context = &verifier->contexts[i]; + if (context->active && obu_belongs_to_context(context, &event->raw_obu) && + !add_u64(context->pending_dfg_bits, obu_bits, + &context->pending_dfg_bits)) { + mark_arithmetic_failed(verifier); + return; + } + } + append_rap_start(verifier, obu_type, xlayer_id, mlayer_id, temporal_id, + event->index); +} + +void av2_decoder_model_verifier_on_source_frame_unit_start(AV2Decoder *pbi, + int xlayer_id, + int mlayer_id, + int temporal_id) { + (void)xlayer_id; + (void)mlayer_id; + (void)temporal_id; + if (pbi == NULL || pbi->decoder_model_verifier == NULL) return; + Av2DecoderModelVerifier *const verifier = pbi->decoder_model_verifier; + if (!verifier_accepts_events(verifier)) return; + if (verifier->source_frame_unit_started) { + if (verifier->source_frame_unit_index == UINT64_MAX) { + mark_arithmetic_failed(verifier); + return; + } + ++verifier->source_frame_unit_index; + } else { + verifier->source_frame_unit_started = true; + } + verifier->current_source_frame_dispatched = false; + for (size_t i = 0; i < verifier->context_count; ++i) { + verifier->contexts[i].prefix_event_count = 0; + } +} + +void av2_decoder_model_verifier_on_obu_filtered(AV2Decoder *pbi) { + if (pbi == NULL || pbi->decoder_model_verifier == NULL) return; + Av2DecoderModelVerifier *const verifier = pbi->decoder_model_verifier; + if (!verifier_accepts_events(verifier)) return; + if (!verifier->last_event_valid) return; + Av2DmAdapterEvent *const event = &verifier->last_event; + if (event->type == AV2_DM_ADAPTER_RAW_OBU) { + event->decoder_retained = false; + event->raw_obu.decoder_retained = false; + for (size_t i = verifier->current_tu_obu_count; i > 0; --i) { + Av2DmPendingObu *const obu = &verifier->current_tu_obus[i - 1]; + if (obu->event_index == event->raw_obu.event_index) { + obu->decoder_retained = false; + break; + } + } + for (size_t i = 0; i < verifier->context_count; ++i) { + Av2DmContext *const context = &verifier->contexts[i]; + if (context->active && obu_belongs_to_context(context, &event->raw_obu)) { + context->incomplete_extraction = true; + } + } + } +} + +void av2_decoder_model_verifier_on_accounting_failure(AV2Decoder *pbi) { + if (pbi != NULL && verifier_accepts_events(pbi->decoder_model_verifier)) { + mark_arithmetic_failed(pbi->decoder_model_verifier); + } +} + +void av2_decoder_model_verifier_on_internal_failure_for_testing( + AV2Decoder *pbi) { + if (pbi != NULL && verifier_accepts_events(pbi->decoder_model_verifier)) { + mark_failed(pbi->decoder_model_verifier); + } +} + +void av2_decoder_model_verifier_on_sequence_header(AV2Decoder *pbi, + int xlayer_id, + int sequence_header_id) { + if (pbi == NULL || pbi->decoder_model_verifier == NULL || xlayer_id < 0 || + xlayer_id >= MAX_NUM_XLAYERS || sequence_header_id < 0 || + sequence_header_id >= MAX_SEQ_NUM) { + return; + } + Av2DecoderModelVerifier *const verifier = pbi->decoder_model_verifier; + if (!verifier_accepts_events(verifier)) return; + size_t record_index = verifier->sequence_record_count; + for (size_t i = 0; i < verifier->sequence_record_count; ++i) { + if (verifier->sequence_records[i].xlayer_id == xlayer_id && + verifier->sequence_records[i].sequence_header_id == + sequence_header_id) { + record_index = i; + break; + } + } + if (record_index == verifier->sequence_record_count && + verifier->sequence_record_count == SIZE_MAX) { + mark_arithmetic_failed(verifier); + return; + } + if (verifier->failed || + (record_index == verifier->sequence_record_count && + !reserve_array(verifier, (void **)&verifier->sequence_records, + &verifier->sequence_record_capacity, + verifier->sequence_record_count + 1, + sizeof(*verifier->sequence_records)))) { + mark_failed(verifier); + return; + } + Av2DmSequenceRecord *const record = &verifier->sequence_records[record_index]; + record->xlayer_id = xlayer_id; + record->sequence_header_id = sequence_header_id; + record->sequence = pbi->seq_list[xlayer_id][sequence_header_id]; + Av2DmAdapterEvent *const event = + append_event(verifier, AV2_DM_ADAPTER_SEQUENCE_HEADER); + if (event == NULL) return; + event->record_index = record_index; + if (record_index == verifier->sequence_record_count) { + if (!increment_size(verifier, &verifier->sequence_record_count)) return; + } + if (!increment_u64(verifier, &verifier->parameter_generation)) return; + + const Av2DmContextKey key = { xlayer_id, -1, -1, -1, true }; + (void)configure_context(verifier, &key, NULL); +} + +static void deactivate_ops_contexts(Av2DecoderModelVerifier *verifier, + int ops_xlayer_id, int ops_id, + bool all_ops_sources) { + for (size_t i = 0; i < verifier->context_count; ++i) { + Av2DmContext *const context = &verifier->contexts[i]; + if (context->key.whole_xlayer) continue; + if (!all_ops_sources && context->key.ops_xlayer_id != ops_xlayer_id) { + continue; + } + if (ops_id >= 0 && context->key.ops_id != ops_id) continue; + context->active = false; + } +} + +static void configure_ops_contexts(Av2DecoderModelVerifier *verifier, + const OperatingPointSet *ops, + uint64_t ops_record) { + const int ops_xlayer_id = ops->obu_xlayer_id; + for (int op = 0; op < ops->ops_cnt; ++op) { + const int first_xlayer = + ops_xlayer_id == GLOBAL_XLAYER_ID ? 0 : ops_xlayer_id; + const int last_xlayer = ops_xlayer_id == GLOBAL_XLAYER_ID + ? GLOBAL_XLAYER_ID - 1 + : ops_xlayer_id; + for (int model_xlayer = first_xlayer; model_xlayer <= last_xlayer; + ++model_xlayer) { + if (ops_xlayer_id == GLOBAL_XLAYER_ID && + (ops->op[op].ops_xlayer_map & (1 << model_xlayer)) == 0) { + continue; + } + const Av2DmContextKey key = { model_xlayer, ops_xlayer_id, ops->ops_id, + op, false }; + Av2DmContext *const context = configure_context(verifier, &key, ops); + if (context != NULL) context->active_ops_record = ops_record; + } + } +} + +void av2_decoder_model_verifier_on_operating_point_set(AV2Decoder *pbi, + int xlayer_id, + int ops_id) { + if (pbi == NULL || pbi->decoder_model_verifier == NULL || xlayer_id < 0 || + xlayer_id >= MAX_NUM_XLAYERS || ops_id < 0 || ops_id >= MAX_NUM_OPS_ID) { + return; + } + Av2DecoderModelVerifier *const verifier = pbi->decoder_model_verifier; + if (!verifier_accepts_events(verifier)) return; + const OperatingPointSet *const ops = &pbi->ops_list[xlayer_id][ops_id]; + size_t record_index = verifier->ops_record_count; + for (size_t i = 0; i < verifier->ops_record_count; ++i) { + if (verifier->ops_records[i].xlayer_id == xlayer_id && + verifier->ops_records[i].ops_id == ops_id) { + record_index = i; + break; + } + } + if (record_index == verifier->ops_record_count && + verifier->ops_record_count == SIZE_MAX) { + mark_arithmetic_failed(verifier); + return; + } + if (verifier->failed || !ops->valid || + (record_index == verifier->ops_record_count && + !reserve_array(verifier, (void **)&verifier->ops_records, + &verifier->ops_record_capacity, + verifier->ops_record_count + 1, + sizeof(*verifier->ops_records)))) { + mark_failed(verifier); + return; + } + Av2DmOpsRecord *const record = &verifier->ops_records[record_index]; + record->xlayer_id = xlayer_id; + record->ops_id = ops_id; + record->ops = *ops; + Av2DmAdapterEvent *const event = + append_event(verifier, AV2_DM_ADAPTER_OPERATING_POINT_SET); + if (event == NULL) return; + event->record_index = record_index; + if (record_index == verifier->ops_record_count && + !increment_size(verifier, &verifier->ops_record_count)) { + return; + } + if (!increment_u64(verifier, &verifier->parameter_generation)) return; + + if (ops->ops_reset_flag) { + deactivate_ops_contexts(verifier, xlayer_id, -1, + xlayer_id == GLOBAL_XLAYER_ID); + } else if (ops->ops_cnt == 0) { + deactivate_ops_contexts(verifier, xlayer_id, ops_id, false); + } + + configure_ops_contexts(verifier, ops, event->record_index); + if (!ops->ops_reset_flag && ops->ops_cnt > 0) { + for (size_t i = 0; i < verifier->context_count; ++i) { + Av2DmContext *const context = &verifier->contexts[i]; + if (context->key.whole_xlayer || + context->key.ops_xlayer_id != xlayer_id || + context->key.ops_id != ops_id) { + continue; + } + const int op = context->key.operating_point; + if (op < 0 || op >= ops->ops_cnt || + (xlayer_id == GLOBAL_XLAYER_ID && + (ops->op[op].ops_xlayer_map & (1 << context->key.xlayer_id)) == 0)) { + context->active = false; + } + } + } +} + +void av2_decoder_model_verifier_on_active_configuration( + AV2Decoder *pbi, int xlayer_id, int sequence_header_id) { + if (pbi == NULL || pbi->decoder_model_verifier == NULL || xlayer_id < 0 || + xlayer_id >= MAX_NUM_XLAYERS || sequence_header_id < 0 || + sequence_header_id >= MAX_SEQ_NUM) { + return; + } + Av2DecoderModelVerifier *const verifier = pbi->decoder_model_verifier; + if (!verifier_accepts_events(verifier)) return; + // The CLK OBU and current-TU prefix have already been recorded, while the + // decoder has already flushed implicit outputs owned by the preceding CVS. + // Close only that xlayer and leave the retained prefix for the new CVS. + if (pbi->obu_type == OBU_CLOSED_LOOP_KEY && + (!verifier->clk_boundary_seen[xlayer_id] || + verifier->clk_boundary_temporal_unit[xlayer_id] != + verifier->temporal_unit_index)) { + finish_xlayer_cvs(verifier, xlayer_id); + if (verifier->fatal_violation || verifier->failed) return; + if (!reset_xlayer_cvs_obu_accounting(verifier, xlayer_id)) return; + retire_xlayer_generations(verifier, pbi, xlayer_id); + ensure_cvs_open(verifier, xlayer_id); + verifier->clk_boundary_seen[xlayer_id] = true; + verifier->clk_boundary_temporal_unit[xlayer_id] = + verifier->temporal_unit_index; + } + if (verifier->active_configuration_present[xlayer_id]) { + const Av2DmActiveConfigurationRecord *const previous = + &verifier + ->active_records[verifier->active_configuration_record[xlayer_id]]; + if (previous->sequence_header_id == sequence_header_id && + memcmp(&previous->sequence, &pbi->common.seq_params, + sizeof(previous->sequence)) == 0 && + memcmp(previous->ci, pbi->common.ci_params_per_layer, + sizeof(previous->ci)) == 0) { + return; + } + } + size_t record_index = verifier->active_record_count; + for (size_t i = 0; i < verifier->active_record_count; ++i) { + if (verifier->active_records[i].xlayer_id == xlayer_id) { + record_index = i; + break; + } + } + if (record_index == verifier->active_record_count && + verifier->active_record_count == SIZE_MAX) { + mark_arithmetic_failed(verifier); + return; + } + if (verifier->failed || + (record_index == verifier->active_record_count && + !reserve_array(verifier, (void **)&verifier->active_records, + &verifier->active_record_capacity, + verifier->active_record_count + 1, + sizeof(*verifier->active_records)))) { + mark_failed(verifier); + return; + } + Av2DmActiveConfigurationRecord *const record = + &verifier->active_records[record_index]; + record->xlayer_id = xlayer_id; + record->sequence_header_id = sequence_header_id; + record->sequence = pbi->common.seq_params; + memcpy(record->ci, pbi->common.ci_params_per_layer, sizeof(record->ci)); + Av2DmAdapterEvent *const event = + append_event(verifier, AV2_DM_ADAPTER_ACTIVE_CONFIGURATION); + if (event == NULL) return; + event->record_index = record_index; + if (record_index == verifier->active_record_count) { + if (!increment_size(verifier, &verifier->active_record_count)) return; + } + verifier->active_configuration_present[xlayer_id] = true; + verifier->active_configuration_record[xlayer_id] = event->record_index; + + uint64_t sequence_record = UINT64_MAX; + for (size_t i = verifier->sequence_record_count; i > 0; --i) { + const Av2DmSequenceRecord *const sequence = + &verifier->sequence_records[i - 1]; + if (sequence->xlayer_id == xlayer_id && + sequence->sequence_header_id == sequence_header_id) { + sequence_record = i - 1; + break; + } + } + verifier->active_sequence_record[xlayer_id] = sequence_record; + + const Av2DmContextKey whole_key = { xlayer_id, -1, -1, -1, true }; + (void)configure_context(verifier, &whole_key, NULL); + const int ops_sources[2] = { xlayer_id, GLOBAL_XLAYER_ID }; + for (int source_index = 0; source_index < 2; ++source_index) { + const int source = ops_sources[source_index]; + if (source_index == 1 && source == xlayer_id) continue; + for (int ops_id = 0; ops_id < MAX_NUM_OPS_ID; ++ops_id) { + const OperatingPointSet *const ops = &pbi->ops_list[source][ops_id]; + if (!ops->valid || ops->ops_cnt == 0) continue; + uint64_t ops_record = UINT64_MAX; + for (size_t i = verifier->ops_record_count; i > 0; --i) { + const Av2DmOpsRecord *const candidate = &verifier->ops_records[i - 1]; + if (candidate->xlayer_id == source && candidate->ops_id == ops_id) { + ops_record = i - 1; + break; + } + } + configure_ops_contexts(verifier, ops, ops_record); + } + } + for (size_t i = 0; i < verifier->context_count; ++i) { + Av2DmContext *const context = &verifier->contexts[i]; + if (context->key.xlayer_id != xlayer_id) continue; + context->active_configuration_record = event->record_index; + context->active_sequence_record = sequence_record; + } +} + +void av2_decoder_model_verifier_on_buffer_removal_timing(AV2Decoder *pbi, + int xlayer_id) { + if (pbi == NULL || pbi->decoder_model_verifier == NULL) return; + Av2DecoderModelVerifier *const verifier = pbi->decoder_model_verifier; + if (!verifier_accepts_events(verifier)) return; + size_t record_index = verifier->brt_record_count; + for (size_t i = 0; i < verifier->brt_record_count; ++i) { + if (verifier->brt_records[i].xlayer_id == xlayer_id) { + record_index = i; + break; + } + } + if (record_index == verifier->brt_record_count && + verifier->brt_record_count == SIZE_MAX) { + mark_arithmetic_failed(verifier); + return; + } + if (verifier->failed || + (record_index == verifier->brt_record_count && + !reserve_array(verifier, (void **)&verifier->brt_records, + &verifier->brt_record_capacity, + verifier->brt_record_count + 1, + sizeof(*verifier->brt_records)))) { + mark_failed(verifier); + return; + } + Av2DmBrtRecord *const record = &verifier->brt_records[record_index]; + record->xlayer_id = xlayer_id; + record->brt = pbi->common.brt_info; + Av2DmAdapterEvent *const event = + append_event(verifier, AV2_DM_ADAPTER_BUFFER_REMOVAL_TIMING); + if (event != NULL) { + event->record_index = record_index; + if (record_index == verifier->brt_record_count) { + if (!increment_size(verifier, &verifier->brt_record_count)) return; + } + if (xlayer_id >= 0 && xlayer_id < MAX_NUM_XLAYERS) { + verifier->current_brt_present[xlayer_id] = true; + verifier->current_brt_record[xlayer_id] = event->record_index; + } + } +} + +void av2_decoder_model_verifier_on_temporal_point(AV2Decoder *pbi, + uint64_t presentation_time) { + if (pbi == NULL || pbi->decoder_model_verifier == NULL) return; + Av2DecoderModelVerifier *const verifier = pbi->decoder_model_verifier; + if (!verifier_accepts_events(verifier)) return; + Av2DmAdapterEvent *const event = + append_event(verifier, AV2_DM_ADAPTER_TEMPORAL_POINT); + if (event == NULL) return; + event->value = presentation_time; + verifier->temporal_point_present = true; + verifier->temporal_point = presentation_time; + if (verifier->pending_frame.valid && + verifier->pending_frame.source_frame_unit_index == + verifier->source_frame_unit_index) { + verifier->pending_frame.presentation_time_present = true; + verifier->pending_frame.presentation_time_ticks = presentation_time; + } + if (verifier->temporal_points == UINT64_MAX) { + mark_arithmetic_failed(verifier); + } else { + ++verifier->temporal_points; + } +} + +void av2_decoder_model_verifier_on_multistream_configuration( + AV2Decoder *pbi, int even_allocation, int large_picture_index) { + if (pbi == NULL || pbi->decoder_model_verifier == NULL) return; + Av2DecoderModelVerifier *const verifier = pbi->decoder_model_verifier; + if (!verifier_accepts_events(verifier)) return; + verifier->multistream_even_allocation = even_allocation != 0; + verifier->multistream_large_picture_index = large_picture_index; +} + +static void capture_tile_statistics(const AV2_COMMON *cm, + Av2DmFrameSnapshot *snapshot) { + snapshot->num_tiles = (uint32_t)(cm->tiles.cols * cm->tiles.rows); + snapshot->tile_columns = (uint32_t)cm->tiles.cols; + snapshot->non_rightmost_tile_width_valid = true; + for (int row = 0; row < cm->tiles.rows; ++row) { + const int row_start = cm->tiles.row_start_sb[row] << cm->mib_size_log2; + int row_end = cm->tiles.row_start_sb[row + 1] << cm->mib_size_log2; + if (row_end > cm->mi_params.mi_rows) row_end = cm->mi_params.mi_rows; + const uint64_t tile_height = (uint64_t)(row_end - row_start) * MI_SIZE; + for (int col = 0; col < cm->tiles.cols; ++col) { + const int col_start = cm->tiles.col_start_sb[col] << cm->mib_size_log2; + int col_end = cm->tiles.col_start_sb[col + 1] << cm->mib_size_log2; + if (col_end > cm->mi_params.mi_cols) col_end = cm->mi_params.mi_cols; + const uint64_t tile_width = (uint64_t)(col_end - col_start) * MI_SIZE; + const uint64_t tile_area = tile_width * tile_height; + if (tile_width > snapshot->max_tile_width) { + snapshot->max_tile_width = tile_width; + } + if (tile_area > snapshot->max_tile_area) { + snapshot->max_tile_area = tile_area; + } + if (col + 1 != cm->tiles.cols && tile_width < 64) { + snapshot->non_rightmost_tile_width_valid = false; + } + } + } +} + +static void capture_ras_seed(Av2DecoderModelVerifier *verifier, + const AV2Decoder *pbi, + Av2DmFrameSnapshot *snapshot) { + snapshot->ras_seed_complete = true; + if (snapshot->obu_type != OBU_RAS_FRAME) return; + const AV2_COMMON *const cm = &pbi->common; + for (int i = 0; i < cm->seq_params.ref_frames; ++i) { + const RefCntBuffer *const frame = cm->ref_frame_map[i]; + if (frame == NULL || !pbi->valid_for_referencing[i] || + frame->long_term_id == -1) { + continue; + } + if (snapshot->ras_seed_count == AV2_DM_MAX_REF_FRAMES) { + snapshot->ras_seed_complete = false; + continue; + } + Av2DmRasSeedSnapshot *const seed = + &snapshot->ras_seeds[snapshot->ras_seed_count++]; + seed->ref_index = (uint32_t)i; + seed->xlayer_id = frame->xlayer_id; + seed->mlayer_id = frame->mlayer_id; + seed->temporal_id = frame->tlayer_id; + Av2DmGenerationRecord *const generation = + find_generation_by_buffer(verifier, frame); + if (generation != NULL) { + seed->generation_valid = true; + seed->generation = generation->generation; + } + } +} + +void av2_decoder_model_verifier_on_frame_wrapup_start(AV2Decoder *pbi) { + if (pbi == NULL || pbi->decoder_model_verifier == NULL) return; + Av2DecoderModelVerifier *const verifier = pbi->decoder_model_verifier; + if (!verifier_accepts_events(verifier)) return; + if (verifier->pending_frame.valid) { + if (verifier->pending_frame.source_frame_unit_index == + verifier->source_frame_unit_index) { + return; + } + mark_failed(verifier); + return; + } + Av2DmAdapterEvent *const adapter_event = + append_event(verifier, AV2_DM_ADAPTER_FRAME_WRAPUP_START); + if (adapter_event == NULL) return; + + AV2_COMMON *const cm = &pbi->common; + Av2DmFrameSnapshot *const snapshot = &verifier->pending_frame; + memset(snapshot, 0, sizeof(*snapshot)); + snapshot->valid = true; + snapshot->source_frame_unit_index = verifier->source_frame_unit_index; + snapshot->event_index = adapter_event->index; + verifier->last_frame_start_event = adapter_event->index; + snapshot->temporal_unit_index = verifier->temporal_unit_index; + snapshot->parameter_generation = verifier->parameter_generation; + snapshot->stream_generation = verifier->stream_generation; + snapshot->obu_type = pbi->obu_type; + snapshot->xlayer_id = cm->xlayer_id; + snapshot->mlayer_id = cm->mlayer_id; + snapshot->temporal_id = cm->tlayer_id; + snapshot->show_existing_frame = cm->show_existing_frame != 0; + snapshot->ref_valid_mask = real_ref_valid_mask(pbi); + snapshot->implicit_output_frame = cm->implicit_output_picture != 0; + snapshot->leading_frame = cm->is_leading_picture == 1; + snapshot->frame_is_intra = cm->current_frame.frame_type == KEY_FRAME || + cm->current_frame.frame_type == INTRA_ONLY_FRAME; + snapshot->allow_global_intrabc = cm->features.allow_global_intrabc != 0; + snapshot->inloop_filtering_enabled = + !snapshot->show_existing_frame && cm->cur_frame != NULL + ? is_filter_enabled_frame(cm) + : false; + snapshot->frame_width = cm->width; + snapshot->frame_height = cm->height; + const uint32_t output_width = snapshot->frame_is_intra + ? snapshot->frame_width + : (uint32_t)cm->seq_params.max_frame_width; + const uint32_t output_height = + snapshot->frame_is_intra ? snapshot->frame_height + : (uint32_t)cm->seq_params.max_frame_height; + snapshot->output_luma_samples = (uint64_t)output_width * output_height; + snapshot->frame_symbol_count = cm->features.frame_symbol_count; + snapshot->presentation_time_present = cm->temporal_point_info_present; + snapshot->presentation_time_ticks = + cm->temporal_point_info_metadata.mtpi_frame_presentation_time; + snapshot->multistream_decoder_mode = pbi->multistream_decoder_mode != 0; + snapshot->msdo = cm->msdo_params; + snapshot->multistream_even_allocation = verifier->multistream_even_allocation; + snapshot->multistream_large_picture_index = + verifier->multistream_large_picture_index; + snapshot->num_streams = cm->num_streams; + memcpy(snapshot->stream_ids, cm->stream_ids, sizeof(snapshot->stream_ids)); + if (!snapshot->show_existing_frame) capture_tile_statistics(cm, snapshot); + + capture_ras_seed(verifier, pbi, snapshot); + Av2DmGenerationRecord *generation = NULL; + if (snapshot->show_existing_frame) { + const int ref = cm->sef_ref_fb_idx; + if (ref >= 0 && ref < cm->seq_params.ref_frames) { + generation = find_generation_by_buffer(verifier, cm->ref_frame_map[ref]); + } + } else { + generation = assign_generation(verifier, cm->cur_frame); + } + if (generation != NULL) { + snapshot->generation_valid = true; + snapshot->generation = generation->generation; + if (!snapshot->show_existing_frame) { + generation->source_frame_unit_index = snapshot->source_frame_unit_index; + generation->temporal_unit_index = snapshot->temporal_unit_index; + generation->xlayer_id = snapshot->xlayer_id; + generation->mlayer_id = snapshot->mlayer_id; + generation->temporal_id = snapshot->temporal_id; + generation->width = snapshot->frame_width; + generation->height = snapshot->frame_height; + generation->output_luma_samples = snapshot->output_luma_samples; + generation->leading_frame = snapshot->leading_frame; + generation->random_access_point = + snapshot->obu_type == OBU_CLOSED_LOOP_KEY || + snapshot->obu_type == OBU_OPEN_LOOP_KEY || + snapshot->obu_type == OBU_RAS_FRAME; + generation->presentation_time_present = + snapshot->presentation_time_present; + generation->presentation_time_ticks = snapshot->presentation_time_ticks; + generation->implicit_presentation_pending = + snapshot->implicit_output_frame; + } + } + (void)increment_u64(verifier, &verifier->frame_starts); +} + +static void append_frame_to_context(Av2DecoderModelVerifier *verifier, + Av2DmContext *context, + const Av2DmFrameSnapshot *snapshot) { + Av2DmContextEvent storage; + initialize_context_event(verifier, AV2_DM_CONTEXT_FRAME, &storage); + Av2DmContextEvent *const model_event = &storage; + model_event->event_index = snapshot->event_index; + model_event->source_frame_unit_index = snapshot->source_frame_unit_index; + model_event->parameter_generation = snapshot->parameter_generation; + model_event->stream_generation = snapshot->stream_generation; + model_event->generation = snapshot->generation; + model_event->frame_obu_type = snapshot->obu_type; + model_event->leading_frame = snapshot->leading_frame; + model_event->config_present = build_context_config( + verifier, context, snapshot->mlayer_id, snapshot, &model_event->config); + Av2DmGenerationRecord *const generation = + find_generation_by_id(verifier, snapshot->generation); + if (generation != NULL && !snapshot->show_existing_frame && + model_event->config_present) { + generation->presentation_timing_config_valid = true; + generation->equal_picture_interval = + model_event->config.equal_picture_interval; + } + if (context->active_configuration_record >= verifier->active_record_count) { + model_event->indeterminate_reason = + AV2_DM_REASON_MISSING_ACTIVE_CONFIGURATION; + } else if (context->incomplete_extraction) { + model_event->indeterminate_reason = AV2_DM_REASON_INCOMPLETE_EXTRACTION; + } else if (!snapshot->generation_valid) { + model_event->indeterminate_reason = AV2_DM_REASON_MISSING_FRAME_GENERATION; + } else if (context->recovery_reset_pending) { + model_event->indeterminate_reason = AV2_DM_REASON_RECOVERY_RESET; + } + context->recovery_reset_pending = false; + if (model_event->indeterminate_reason != AV2_DM_REASON_NONE) { + model_event->config.applicability = AV2_DM_MISSING_REQUIRED_INPUT; + } + Av2DmFrameEvent *const frame = &model_event->frame; + frame->event_index = snapshot->event_index; + frame->temporal_unit_index = snapshot->temporal_unit_index; + frame->generation = snapshot->generation; + frame->ref_valid_mask = snapshot->ref_valid_mask; + frame->coded_bits = + snapshot->show_existing_frame ? 0 : context->last_closed_dfg_bits; + frame->show_existing_frame = snapshot->show_existing_frame; + frame->random_access_point = snapshot->obu_type == OBU_CLOSED_LOOP_KEY || + snapshot->obu_type == OBU_OPEN_LOOP_KEY || + snapshot->obu_type == OBU_RAS_FRAME; + frame->coded_as_closed_loop_key = snapshot->obu_type == OBU_CLOSED_LOOP_KEY; + frame->frame_is_intra = snapshot->frame_is_intra; + frame->allow_global_intrabc = snapshot->allow_global_intrabc; + frame->inloop_filtering_enabled = snapshot->inloop_filtering_enabled; + frame->frame_width = snapshot->frame_width; + frame->frame_height = snapshot->frame_height; + frame->num_tiles = snapshot->num_tiles; + frame->tile_columns = snapshot->tile_columns; + frame->max_tile_width = snapshot->max_tile_width; + frame->max_tile_area = snapshot->max_tile_area; + frame->non_rightmost_tile_width_valid = + snapshot->non_rightmost_tile_width_valid; + const bool parameters_changed = + !context->last_config_present || + context->last_stream_generation != model_event->stream_generation || + memcmp(&context->last_config, &model_event->config, + sizeof(model_event->config)) != 0; + frame->decoder_model_parameters_updated = + frame->random_access_point && parameters_changed; + frame->count_frame_header = true; + frame->frame_symbol_count = snapshot->frame_symbol_count; + if (!compressed_size_for_context(verifier, context, snapshot, + &frame->compressed_size_bytes)) { + mark_failed(verifier); + } + frame->buffer_removal_time_present = + find_buffer_removal_time(verifier, context, &frame->buffer_removal_time); + // AVM has no external TU output-time source here. The common model derives + // it from the first presentation-owner event in display order. + frame->temporal_unit_output_time_present = false; + model_event->ras_seed_complete = snapshot->ras_seed_complete; + for (uint32_t i = 0; i < snapshot->ras_seed_count; ++i) { + const Av2DmRasSeedSnapshot *const candidate = &snapshot->ras_seeds[i]; + if (!frame_belongs_to_context(context, candidate->xlayer_id, + candidate->mlayer_id, + candidate->temporal_id)) { + continue; + } + if (!candidate->generation_valid || + model_event->ras_seed_count == AV2_DM_MAX_REF_FRAMES) { + model_event->ras_seed_complete = false; + continue; + } + Av2DmRasSeed *const seed = + &model_event->ras_seeds[model_event->ras_seed_count++]; + seed->ref_index = candidate->ref_index; + seed->generation = candidate->generation; + } + if (snapshot->obu_type == OBU_RAS_FRAME && !model_event->ras_seed_complete && + model_event->indeterminate_reason == AV2_DM_REASON_NONE) { + model_event->indeterminate_reason = AV2_DM_REASON_INCOMPLETE_RAS_SEED; + model_event->config.applicability = AV2_DM_MISSING_REQUIRED_INPUT; + } + dispatch_context_event(verifier, context, model_event); +} + +void av2_decoder_model_verifier_on_frame_unit_complete(AV2Decoder *pbi) { + if (pbi == NULL || pbi->decoder_model_verifier == NULL) return; + Av2DecoderModelVerifier *const verifier = pbi->decoder_model_verifier; + if (!verifier_accepts_events(verifier)) return; + Av2DmAdapterEvent *const event = + append_event(verifier, AV2_DM_ADAPTER_FRAME_UNIT_COMPLETE); + if (event == NULL) return; + Av2DmFrameSnapshot snapshot = verifier->pending_frame; + const bool have_snapshot = + snapshot.valid && + snapshot.source_frame_unit_index == verifier->source_frame_unit_index; + if (!have_snapshot) { + memset(&snapshot, 0, sizeof(snapshot)); + snapshot.show_existing_frame = pbi->common.show_existing_frame != 0; + snapshot.xlayer_id = pbi->common.xlayer_id; + snapshot.mlayer_id = pbi->common.mlayer_id; + snapshot.temporal_id = pbi->common.tlayer_id; + } + event->value = snapshot.show_existing_frame; + event->raw_obu.xlayer_id = snapshot.xlayer_id; + event->raw_obu.mlayer_id = snapshot.mlayer_id; + event->raw_obu.temporal_id = snapshot.temporal_id; + if (!snapshot.show_existing_frame) { + for (size_t i = 0; i < verifier->context_count; ++i) { + Av2DmContext *const context = &verifier->contexts[i]; + if (!context->active || + !frame_belongs_to_context(context, snapshot.xlayer_id, + snapshot.mlayer_id, snapshot.temporal_id)) { + continue; + } + context->last_closed_dfg_bits = context->pending_dfg_bits; + context->pending_dfg_bits = 0; + context->pending_after_event_valid = true; + context->pending_after_event = event->index; + if (context->closed_dfgs == UINT64_MAX) { + mark_arithmetic_failed(verifier); + return; + } + ++context->closed_dfgs; + } + if (verifier->closed_dfgs == UINT64_MAX) { + mark_arithmetic_failed(verifier); + return; + } + ++verifier->closed_dfgs; + } + if (have_snapshot) { + Av2DmGenerationRecord *const generation = + find_generation_by_id(verifier, snapshot.generation); + if (generation != NULL && !snapshot.show_existing_frame) { + // Temporal-point metadata may be a suffix OBU parsed after the wrapup + // snapshot. Preserve its final frame-unit value with the pending owner. + generation->presentation_time_present = + snapshot.presentation_time_present; + generation->presentation_time_ticks = snapshot.presentation_time_ticks; + } + for (size_t i = 0; i < verifier->context_count; ++i) { + Av2DmContext *const context = &verifier->contexts[i]; + if (context->active && + frame_belongs_to_context(context, snapshot.xlayer_id, + snapshot.mlayer_id, snapshot.temporal_id)) { + append_frame_to_context(verifier, context, &snapshot); + } + } + verifier->last_completed_frame = snapshot; + if (!snapshot.show_existing_frame) { + retire_completed_frame_obus(verifier, snapshot.source_frame_unit_index); + } + } + verifier->current_source_frame_dispatched = true; + for (size_t i = 0; i < verifier->context_count; ++i) { + verifier->contexts[i].prefix_event_count = 0; + } + memset(&verifier->pending_frame, 0, sizeof(verifier->pending_frame)); + if (verifier->frame_unit_index == UINT64_MAX) { + mark_arithmetic_failed(verifier); + } else { + ++verifier->frame_unit_index; + } +} + +void av2_decoder_model_verifier_on_olk_reference_invalidation( + AV2Decoder *pbi, uint32_t ref_valid_mask) { + if (pbi == NULL || pbi->decoder_model_verifier == NULL) return; + Av2DecoderModelVerifier *const verifier = pbi->decoder_model_verifier; + if (!verifier_accepts_events(verifier)) return; + Av2DmAdapterEvent *const adapter_event = + append_event(verifier, AV2_DM_ADAPTER_OLK_REFERENCE_INVALIDATION); + if (adapter_event == NULL) return; + adapter_event->value = ref_valid_mask; + verifier->last_olk_invalidation_event = adapter_event->index; + for (size_t i = 0; i < verifier->context_count; ++i) { + Av2DmContext *const context = &verifier->contexts[i]; + if (!context->active || !frame_belongs_to_context( + context, pbi->common.xlayer_id, + pbi->common.mlayer_id, pbi->common.tlayer_id)) { + continue; + } + Av2DmContextEvent storage; + initialize_context_event( + verifier, AV2_DM_CONTEXT_OLK_REFERENCE_INVALIDATION, &storage); + Av2DmContextEvent *const event = &storage; + event->event_index = adapter_event->index; + event->source_frame_unit_index = verifier->source_frame_unit_index; + event->ref_valid_mask = ref_valid_mask; + event->leading_frame = pbi->common.is_leading_picture == 1; + dispatch_context_event(verifier, context, event); + } + (void)increment_u64(verifier, &verifier->olk_invalidations); +} + +void av2_decoder_model_verifier_after_reference_update( + AV2Decoder *pbi, uint32_t refresh_frame_flags, uint32_t ref_valid_mask) { + if (pbi == NULL || pbi->decoder_model_verifier == NULL) return; + Av2DecoderModelVerifier *const verifier = pbi->decoder_model_verifier; + if (!verifier_accepts_events(verifier)) return; + Av2DmAdapterEvent *const adapter_event = + append_event(verifier, AV2_DM_ADAPTER_REFERENCE_UPDATE); + if (adapter_event == NULL) return; + adapter_event->value = refresh_frame_flags; + verifier->last_reference_update_event = adapter_event->index; + Av2DmGenerationRecord *const generation = + find_generation_by_buffer(verifier, pbi->common.cur_frame); + for (size_t i = 0; i < verifier->context_count; ++i) { + Av2DmContext *const context = &verifier->contexts[i]; + if (!context->active || !frame_belongs_to_context( + context, pbi->common.xlayer_id, + pbi->common.mlayer_id, pbi->common.tlayer_id)) { + continue; + } + Av2DmContextEvent storage; + initialize_context_event(verifier, AV2_DM_CONTEXT_REFERENCE_UPDATE, + &storage); + Av2DmContextEvent *const event = &storage; + event->event_index = adapter_event->index; + event->source_frame_unit_index = verifier->source_frame_unit_index; + event->reference_update.refresh_frame_flags = refresh_frame_flags; + event->reference_update.ref_valid_mask = ref_valid_mask; + event->set_initial_presentation_delay = + pbi->common.show_existing_frame == 0; + if (generation != NULL) { + event->generation = generation->generation; + event->leading_frame = generation->leading_frame; + } + dispatch_context_event(verifier, context, event); + } + (void)increment_u64(verifier, &verifier->reference_updates); +} + +void av2_decoder_model_verifier_on_output(AV2Decoder *pbi, + int frame_to_show_map_idx, + const RefCntBuffer *frame, + Av2DmPresentationOwner owner) { + if (pbi == NULL || pbi->decoder_model_verifier == NULL || frame == NULL) { + return; + } + Av2DecoderModelVerifier *const verifier = pbi->decoder_model_verifier; + if (!verifier_accepts_events(verifier)) return; + Av2DmAdapterEvent *const adapter_event = + append_event(verifier, AV2_DM_ADAPTER_OUTPUT); + if (adapter_event == NULL) return; + adapter_event->value = + frame_to_show_map_idx < 0 ? UINT64_MAX : (uint64_t)frame_to_show_map_idx; + verifier->last_output_event = adapter_event->index; + const RefCntBuffer *generation_frame = frame; + if (frame_to_show_map_idx >= 0 && + frame_to_show_map_idx < pbi->common.seq_params.ref_frames && + pbi->common.ref_frame_map[frame_to_show_map_idx] != NULL) { + // A non-derived show-existing output can be queued through a copied + // current buffer. Annex E identifies it by the selected reference slot. + generation_frame = pbi->common.ref_frame_map[frame_to_show_map_idx]; + } + Av2DmGenerationRecord *const generation = + find_generation_by_buffer(verifier, generation_frame); + const bool current_presentation = owner == AV2_DM_PRESENTATION_OWNER_CURRENT; + const Av2DmFrameSnapshot *const current_owner = + current_presentation && verifier->last_completed_frame.valid && + verifier->last_completed_frame.source_frame_unit_index == + verifier->source_frame_unit_index + ? &verifier->last_completed_frame + : NULL; + const Av2DmGenerationRecord *const implicit_owner = + !current_presentation && generation != NULL && + generation->implicit_presentation_pending + ? generation + : NULL; + const bool owner_valid = current_owner != NULL || implicit_owner != NULL; + const uint64_t owner_frame_unit = + current_owner != NULL ? current_owner->source_frame_unit_index + : implicit_owner != NULL ? implicit_owner->source_frame_unit_index + : verifier->source_frame_unit_index; + const uint64_t owner_temporal_unit = + current_owner != NULL ? current_owner->temporal_unit_index + : implicit_owner != NULL ? implicit_owner->temporal_unit_index + : verifier->temporal_unit_index; + const int owner_xlayer = current_owner != NULL ? current_owner->xlayer_id + : implicit_owner != NULL ? implicit_owner->xlayer_id + : frame->xlayer_id; + const int owner_mlayer = current_owner != NULL ? current_owner->mlayer_id + : implicit_owner != NULL ? implicit_owner->mlayer_id + : frame->mlayer_id; + const int owner_tlayer = current_owner != NULL ? current_owner->temporal_id + : implicit_owner != NULL + ? implicit_owner->temporal_id + : frame->tlayer_id; + const bool owner_leading = + current_owner != NULL + ? current_owner->leading_frame + : implicit_owner != NULL && implicit_owner->leading_frame; + const bool owner_rap = + current_owner != NULL + ? current_owner->obu_type == OBU_CLOSED_LOOP_KEY || + current_owner->obu_type == OBU_OPEN_LOOP_KEY || + current_owner->obu_type == OBU_RAS_FRAME + : implicit_owner != NULL && implicit_owner->random_access_point; + const bool owner_presentation_time_present = + current_owner != NULL + ? current_owner->presentation_time_present + : implicit_owner != NULL && implicit_owner->presentation_time_present; + const uint64_t owner_presentation_time_ticks = + current_owner != NULL ? current_owner->presentation_time_ticks + : implicit_owner != NULL ? implicit_owner->presentation_time_ticks + : 0; + const uint64_t owner_luma_samples = + current_owner != NULL ? current_owner->output_luma_samples + : implicit_owner != NULL ? implicit_owner->output_luma_samples + : 0; + verifier->last_output_callback_frame_unit = verifier->source_frame_unit_index; + verifier->last_output_presentation_frame_unit = owner_frame_unit; + verifier->last_output_presentation_temporal_unit = owner_temporal_unit; + verifier->last_output_generation = + generation != NULL ? generation->generation : 0; + verifier->last_output_presentation_xlayer_id = owner_xlayer; + verifier->last_output_presentation_mlayer_id = owner_mlayer; + verifier->last_output_presentation_tlayer_id = owner_tlayer; + verifier->last_output_uses_current_presentation = current_presentation; + const uint32_t ref_valid_mask = real_ref_valid_mask(pbi); + for (size_t i = 0; i < verifier->context_count; ++i) { + Av2DmContext *const context = &verifier->contexts[i]; + if (!context->active || + !frame_belongs_to_context(context, owner_xlayer, owner_mlayer, + owner_tlayer)) { + continue; + } + Av2DmContextEvent storage; + initialize_context_event(verifier, AV2_DM_CONTEXT_OUTPUT, &storage); + Av2DmContextEvent *const event = &storage; + event->event_index = adapter_event->index; + event->source_frame_unit_index = verifier->source_frame_unit_index; + event->presentation_frame_unit_index = owner_frame_unit; + event->presentation_xlayer_id = owner_xlayer; + event->presentation_mlayer_id = owner_mlayer; + event->presentation_tlayer_id = owner_tlayer; + event->generation = generation != NULL ? generation->generation : 0; + event->leading_frame = owner_leading; + if (generation == NULL) { + event->indeterminate_reason = AV2_DM_REASON_MISSING_FRAME_GENERATION; + } else if (!owner_valid) { + event->indeterminate_reason = + AV2_DM_REASON_MISSING_PRESENTATION_PROVENANCE; + } + const bool owner_timing_config_valid = + current_owner != NULL + ? context->last_config_present + : implicit_owner != NULL && + implicit_owner->presentation_timing_config_valid; + const bool owner_equal_picture_interval = + current_owner != NULL + ? context->last_config.equal_picture_interval + : implicit_owner != NULL && implicit_owner->equal_picture_interval; + if (owner_valid && event->indeterminate_reason == AV2_DM_REASON_NONE && + owner_timing_config_valid && !owner_equal_picture_interval && + !owner_presentation_time_present) { + event->indeterminate_reason = AV2_DM_REASON_MISSING_PRESENTATION_TIMING; + } + Av2DmOutputEvent *const output = &event->output; + output->event_index = adapter_event->index; + output->temporal_unit_index = owner_temporal_unit; + output->generation = event->generation; + output->frame_to_show_map_idx = frame_to_show_map_idx; + output->ref_valid_mask = ref_valid_mask; + output->output_luma_samples = owner_luma_samples; + output->leading_frame = event->leading_frame; + output->presentation_uses_current_frame = current_presentation; + output->presentation_random_access_point = owner_rap; + output->presentation_time_present = owner_presentation_time_present; + output->presentation_time_ticks = owner_presentation_time_ticks; + dispatch_context_event(verifier, context, event); + } + if (!current_presentation && generation != NULL) { + generation->implicit_presentation_pending = false; + } + (void)increment_u64(verifier, &verifier->outputs); +} + +void av2_decoder_model_verifier_on_recovery_reset(AV2Decoder *pbi) { + if (pbi == NULL || pbi->decoder_model_verifier == NULL) return; + Av2DecoderModelVerifier *const verifier = pbi->decoder_model_verifier; + if (!verifier_accepts_events(verifier)) return; + Av2DmAdapterEvent *const adapter_event = + append_event(verifier, AV2_DM_ADAPTER_RECOVERY_RESET); + if (adapter_event == NULL) return; + for (size_t i = 0; i < verifier->context_count; ++i) { + Av2DmContext *const context = &verifier->contexts[i]; + if (!context->active) continue; + Av2DmContextEvent storage; + initialize_context_event(verifier, AV2_DM_CONTEXT_RECOVERY_RESET, &storage); + Av2DmContextEvent *const event = &storage; + event->event_index = adapter_event->index; + event->source_frame_unit_index = verifier->source_frame_unit_index; + event->indeterminate_reason = AV2_DM_REASON_RECOVERY_RESET; + context->recovery_reset_pending = true; + context->pending_dfg_bits = 0; + dispatch_context_event(verifier, context, event); + } + memset(&verifier->pending_frame, 0, sizeof(verifier->pending_frame)); +} + +void av2_decoder_model_verifier_on_stream_configuration_change( + AV2Decoder *pbi, bool preserve_current_tu_prefix) { + if (pbi == NULL || pbi->decoder_model_verifier == NULL) return; + Av2DecoderModelVerifier *const verifier = pbi->decoder_model_verifier; + if (!verifier_accepts_events(verifier)) return; + if (append_event(verifier, AV2_DM_ADAPTER_STREAM_CONFIGURATION_CHANGE) != + NULL) { + finish_all_cvs(verifier); + if (verifier->fatal_violation || verifier->failed) return; + verifier->generation_count = 0; + if (!preserve_current_tu_prefix) verifier->current_tu_obu_count = 0; + if (!increment_u64(verifier, &verifier->parameter_generation) || + !increment_u64(verifier, &verifier->stream_generation)) { + return; + } + for (size_t i = 0; i < verifier->context_count; ++i) { + verifier->contexts[i].active = false; + verifier->contexts[i].incomplete_extraction = false; + verifier->contexts[i].recovery_reset_pending = false; + verifier->contexts[i].pending_dfg_bits = 0; + verifier->contexts[i].pending_after_event_valid = false; + verifier->contexts[i].prefix_event_count = 0; + verifier->contexts[i].last_config_present = false; + } + memset(verifier->active_configuration_present, 0, + sizeof(verifier->active_configuration_present)); + } +} + +typedef struct Av2DmRunReport { + Av2DecoderModelVerifier *verifier; + Av2DmScope scope; + Av2DmMode mode; + int64_t rap; + uint64_t cvs; + uint32_t level_idx; + uint32_t tier; + uint64_t max_display_rate; + uint64_t max_decode_rate; + Av2DmContextEvent current_event; + bool current_event_valid; + uint64_t violations; +} Av2DmRunReport; + +struct Av2DmLiveRun { + Av2DecoderModel *model; + Av2DmRunReport report; + Av2DmConfig config; + Av2DmIndeterminateReason reason; + int64_t rap; + bool olk; + uint64_t start_source_frame_unit; +}; + +void av2_decoder_model_verifier_on_model_arithmetic_failure_for_testing( + AV2Decoder *pbi) { + if (pbi == NULL || !verifier_accepts_events(pbi->decoder_model_verifier)) { + return; + } + Av2DecoderModelVerifier *const verifier = pbi->decoder_model_verifier; + for (size_t i = 0; i < verifier->context_count; ++i) { + Av2DmContext *const context = &verifier->contexts[i]; + if (context->run_count != 0) { + av2_decoder_model_fail_arithmetic_for_testing(context->runs[0]->model); + return; + } + } +} + +static const char *indeterminate_reason_name(Av2DmIndeterminateReason reason) { + switch (reason) { + case AV2_DM_REASON_NONE: return "none"; + case AV2_DM_REASON_MISSING_REQUIRED_INPUT: return "missing_required_input"; + case AV2_DM_REASON_MISSING_ACTIVE_CONFIGURATION: + return "missing_active_configuration"; + case AV2_DM_REASON_INCOMPLETE_EXTRACTION: return "incomplete_extraction"; + case AV2_DM_REASON_MISSING_FRAME_GENERATION: + return "missing_frame_generation"; + case AV2_DM_REASON_MISSING_PRESENTATION_PROVENANCE: + return "missing_presentation_provenance"; + case AV2_DM_REASON_MISSING_PRESENTATION_TIMING: + return "missing_presentation_timing"; + case AV2_DM_REASON_INCOMPLETE_RAS_SEED: return "incomplete_ras_seed"; + case AV2_DM_REASON_RECOVERY_RESET: return "decoder_recovery_reset"; + case AV2_DM_REASON_INTERNAL_FAILURE: return "internal_failure"; + } + return "internal_failure"; +} + +static const char *mode_name(Av2DmMode mode) { + return mode == AV2_DM_DECODING_SCHEDULE_MODE ? "schedule" : "resource"; +} + +static const char *result_name(Av2DmResultStatus status) { + switch (status) { + case AV2_DM_RESULT_CONFORMANT: return "CONFORMANT"; + case AV2_DM_RESULT_NON_CONFORMANT: return "NON_CONFORMANT"; + case AV2_DM_RESULT_INDETERMINATE: return "INDETERMINATE"; + case AV2_DM_RESULT_NOT_APPLICABLE: return "NOT_APPLICABLE"; + } + return "INDETERMINATE"; +} + +static const char *scope_name(const Av2DmScope *scope) { + return scope->whole_xlayer ? "whole_xlayer" : "operating_point"; +} + +static const char *tier_name(uint32_t tier) { + return tier == 0 ? "main" : "high"; +} + +static const char *level_name(uint32_t level_idx) { + static const char *const names[] = { "2.0", "2.1", "3.0", "3.1", "4.0", "4.1", + "5.0", "5.1", "5.2", "5.3", "6.0", "6.1", + "6.2", "6.3", "7.0", "7.1", "7.2", "7.3", + "8.0", "8.1", "8.2", "8.3" }; + if (level_idx < sizeof(names) / sizeof(names[0])) return names[level_idx]; + if (level_idx == SEQ_LEVEL_MAX) return "maximum_parameters"; + return "reserved"; +} + +static bool wide_fits_u64(const Av2DmUnsignedWide *value) { + return value->limbs[1] == 0 && value->limbs[2] == 0 && value->limbs[3] == 0; +} + +static bool format_unsigned_wide(const Av2DmUnsignedWide *value, char *text, + size_t text_size) { + if (wide_fits_u64(value)) { + const int written = snprintf(text, text_size, "%" PRIu64, value->limbs[0]); + return written >= 0 && (size_t)written < text_size; + } + int highest_limb = 3; + while (highest_limb > 0 && value->limbs[highest_limb] == 0) --highest_limb; + int written = + snprintf(text, text_size, "0x%" PRIx64, value->limbs[highest_limb]); + if (written < 0 || (size_t)written >= text_size) return false; + size_t offset = (size_t)written; + for (int i = highest_limb - 1; i >= 0; --i) { + written = snprintf(text + offset, text_size - offset, "%016" PRIx64, + value->limbs[i]); + if (written < 0 || (size_t)written >= text_size - offset) return false; + offset += (size_t)written; + } + return true; +} + +static bool format_rational(const Av2DmRational *value, char *text, + size_t text_size) { + if (value == NULL) { + const int written = snprintf(text, text_size, "NA"); + return written >= 0 && (size_t)written < text_size; + } + char magnitude[67]; + char denominator[67]; + if (!format_unsigned_wide(&value->magnitude, magnitude, sizeof(magnitude)) || + !format_unsigned_wide(&value->denominator, denominator, + sizeof(denominator))) { + return false; + } + int written; + if (wide_fits_u64(&value->denominator) && value->denominator.limbs[0] == 1) { + written = snprintf(text, text_size, "%s%s", value->negative ? "-" : "", + magnitude); + } else { + written = snprintf(text, text_size, "%s%s/%s", value->negative ? "-" : "", + magnitude, denominator); + } + return written >= 0 && (size_t)written < text_size; +} + +typedef enum Av2DmMarginRule { + AV2_DM_MARGIN_NONE, + AV2_DM_MARGIN_OBSERVED_MINUS_LIMIT, + AV2_DM_MARGIN_LIMIT_MINUS_OBSERVED +} Av2DmMarginRule; + +typedef struct Av2DmViolationDescriptor { + const char *spec; + const char *condition; + const char *relation; + const char *observed_name; + const char *limit_name; + const char *unit; + const char *requirement; + const char *margin_name; + Av2DmMarginRule margin_rule; +} Av2DmViolationDescriptor; + +#define DM_DESCRIPTOR(spec, condition, relation, observed, limit, unit, \ + requirement, margin, margin_rule) \ + { spec, condition, relation, observed, limit, \ + unit, requirement, margin, margin_rule } + +static const Av2DmViolationDescriptor violation_descriptors[] = { + DM_DESCRIPTOR("annex_e.decoder_model_error_codes", + "free_decode_frame_buffer_available", "available", + "free_buffers", "required_free_buffers", "buffers", "available", + "availability", AV2_DM_MARGIN_NONE), + DM_DESCRIPTOR("annex_e.decoder_model_error_codes", + "show_existing_reference_buffer_available", "available", + "reference_buffer_state", "required_buffer_state", "buffers", + "available", "availability", AV2_DM_MARGIN_NONE), + DM_DESCRIPTOR("annex_e.decoder_model_error_codes", + "output_time_lte_presentation_time", "lte", "output_time", + "presentation_time", "seconds", "maximum", "lateness", + AV2_DM_MARGIN_OBSERVED_MINUS_LIMIT), + DM_DESCRIPTOR("annex_e.smoothing_buffer_underflow", + "scheduled_removal_gte_last_bit_arrival", "gte", + "scheduled_removal", "last_bit_arrival", "seconds", "minimum", + "lateness", AV2_DM_MARGIN_LIMIT_MINUS_OBSERVED), + DM_DESCRIPTOR("annex_e.smoothing_buffer_overflow", + "buffer_fullness_lte_buffer_size", "lte", + "buffer_fullness_bits", "buffer_size_bits", "bits", "maximum", + "excess_bits", AV2_DM_MARGIN_OBSERVED_MINUS_LIMIT), + DM_DESCRIPTOR("annex_e.bitstream_conformance.general", + "presentation_time_gte_previous_presentation_time", "gte", + "presentation_time", "previous_presentation_time", "seconds", + "minimum", "shortfall", AV2_DM_MARGIN_LIMIT_MINUS_OBSERVED), + DM_DESCRIPTOR("annex_e.bitstream_conformance.general", + "scheduled_removal_gte_resource_removal", "gte", + "scheduled_removal", "resource_removal", "seconds", "minimum", + "shortfall", AV2_DM_MARGIN_LIMIT_MINUS_OBSERVED), + DM_DESCRIPTOR("annex_e.decoder_buffer_delay_consistency", + "decoder_buffer_delay_lte_ceil_time_delta", "lte", + "time_delta_ticks", "decoder_buffer_delay_minus_one_ticks", + "ticks", "maximum", "decoder_buffer_delay_excess", + AV2_DM_MARGIN_NONE), + DM_DESCRIPTOR("annex_e.minimum_decode_time", + "available_decode_interval_gte_required_decode_interval", "gte", + "available_decode_interval", "required_decode_interval", + "seconds", "minimum", "shortfall", + AV2_DM_MARGIN_LIMIT_MINUS_OBSERVED), + DM_DESCRIPTOR("annex_e.minimum_presentation_interval", + "presentation_interval_gte_required_presentation_interval", + "gte", "presentation_interval", + "required_presentation_interval", "seconds", "minimum", + "shortfall", AV2_DM_MARGIN_LIMIT_MINUS_OBSERVED), + DM_DESCRIPTOR("annex_e.decode_deadline", + "decode_completion_time_lte_presentation_time", "lte", + "decode_completion_time", "presentation_time", "seconds", + "maximum", "lateness", AV2_DM_MARGIN_OBSERVED_MINUS_LIMIT), + DM_DESCRIPTOR("annex_e.level_imposed_constraints", + "decoder_buffer_delay_nonzero", "nonzero", + "decoder_buffer_delay", "zero", "seconds", "nonzero", + "difference", AV2_DM_MARGIN_NONE), + DM_DESCRIPTOR( + "annex_e.level_imposed_constraints", "decoder_buffer_delay_lte_maximum", + "lte", "decoder_buffer_delay", "maximum_decoder_buffer_delay", "seconds", + "maximum", "excess", AV2_DM_MARGIN_OBSERVED_MINUS_LIMIT), + DM_DESCRIPTOR("annex_a.levels", "frame_luma_samples_lte_max_picture_size", + "lte", "frame_luma_samples", "max_picture_size", "luma_samples", + "maximum", "excess", AV2_DM_MARGIN_OBSERVED_MINUS_LIMIT), + DM_DESCRIPTOR("annex_a.levels", "frame_width_lte_max_horizontal_size", "lte", + "frame_width", "max_horizontal_size", "luma_samples", "maximum", + "excess", AV2_DM_MARGIN_OBSERVED_MINUS_LIMIT), + DM_DESCRIPTOR("annex_a.levels", "frame_height_lte_max_vertical_size", "lte", + "frame_height", "max_vertical_size", "luma_samples", "maximum", + "excess", AV2_DM_MARGIN_OBSERVED_MINUS_LIMIT), + DM_DESCRIPTOR("annex_a.levels", "frame_width_gte_16", "gte", "frame_width", + "min_horizontal_size", "luma_samples", "minimum", "shortfall", + AV2_DM_MARGIN_LIMIT_MINUS_OBSERVED), + DM_DESCRIPTOR("annex_a.levels", "frame_height_gte_16", "gte", "frame_height", + "min_vertical_size", "luma_samples", "minimum", "shortfall", + AV2_DM_MARGIN_LIMIT_MINUS_OBSERVED), + DM_DESCRIPTOR("annex_a.levels", "num_tiles_lte_max_tiles", "lte", "num_tiles", + "max_tiles", "tiles", "maximum", "excess", + AV2_DM_MARGIN_OBSERVED_MINUS_LIMIT), + DM_DESCRIPTOR("annex_a.levels", "tile_columns_lte_max_tile_columns", "lte", + "tile_columns", "max_tile_columns", "tile_columns", "maximum", + "excess", AV2_DM_MARGIN_OBSERVED_MINUS_LIMIT), + DM_DESCRIPTOR("annex_a.levels", "tile_width_lte_max_tile_width", "lte", + "tile_width", "max_tile_width", "luma_samples", "maximum", + "excess", AV2_DM_MARGIN_OBSERVED_MINUS_LIMIT), + DM_DESCRIPTOR("annex_a.levels", "non_rightmost_tile_width_gte_64", "gte", + "offending_tile_width", "min_tile_width", "luma_samples", + "minimum", "shortfall", AV2_DM_MARGIN_NONE), + DM_DESCRIPTOR("annex_a.levels", "tile_area_lte_max_tile_area", "lte", + "tile_area", "max_tile_area", "luma_samples", "maximum", + "excess", AV2_DM_MARGIN_OBSERVED_MINUS_LIMIT), + DM_DESCRIPTOR("annex_a.levels", + "display_luma_samples_lte_output_interval_capacity", "lte", + "display_luma_samples", "display_capacity", + "luma_samples_per_interval", "maximum", "excess", + AV2_DM_MARGIN_OBSERVED_MINUS_LIMIT), + DM_DESCRIPTOR("annex_a.levels", "frame_headers_lte_max_header_rate", "lte", + "frame_headers_in_window", "max_frame_headers_in_window", + "frame_headers_per_second", "maximum", "excess", + AV2_DM_MARGIN_OBSERVED_MINUS_LIMIT), + DM_DESCRIPTOR("annex_a.levels", "num_ref_frames_lte_max_level_ref_frames", + "lte", "num_ref_frames", "max_level_ref_frames", + "reference_frames", "maximum", "excess", + AV2_DM_MARGIN_OBSERVED_MINUS_LIMIT), + DM_DESCRIPTOR("annex_a.levels", + "luma_sample_count_lte_frame_parsing_capacity", "lte", + "luma_sample_count", "frame_parsing_capacity", + "luma_samples_per_interval", "maximum", "excess", + AV2_DM_MARGIN_OBSERVED_MINUS_LIMIT), + DM_DESCRIPTOR("annex_a.levels", "num_tiles_lte_frame_parsing_tile_limit", + "lte", "num_tiles", "frame_parsing_tile_limit", + "tiles_per_interval", "maximum", "excess", + AV2_DM_MARGIN_OBSERVED_MINUS_LIMIT), + DM_DESCRIPTOR("annex_a.levels", "compressed_size_lte_derived_maximum", "lte", + "compressed_size", "maximum_compressed_size", "bytes", + "maximum", "excess", AV2_DM_MARGIN_OBSERVED_MINUS_LIMIT), + DM_DESCRIPTOR("annex_a.levels", "frame_symbol_count_lte_derived_maximum", + "lte", "frame_symbol_count", "maximum_frame_symbols", "symbols", + "maximum", "excess", AV2_DM_MARGIN_OBSERVED_MINUS_LIMIT), + DM_DESCRIPTOR( + "annex_a.levels", "max_tile_area_times_header_rate_lte_level_limit", + "lte", "tile_area_header_rate_product", + "max_tile_area_header_rate_product", "luma_samples_x_headers_per_second", + "maximum", "excess", AV2_DM_MARGIN_OBSERVED_MINUS_LIMIT), +}; + +#undef DM_DESCRIPTOR + +_Static_assert(sizeof(violation_descriptors) / + sizeof(violation_descriptors[0]) == + AV2_DM_VIOLATION_TILE_SIZE_HEADER_RATE + 1, + "Each decoder-model violation needs a descriptor"); + +static const Av2DmViolationDescriptor unknown_violation_descriptor = { + "unknown", "unknown_violation", "unknown", "observed_value", "limit_value", + "value", "unknown", "margin", AV2_DM_MARGIN_NONE +}; + +static const Av2DmViolationDescriptor *get_violation_descriptor( + Av2DmViolationCode code) { + if ((int)code < 0 || code > AV2_DM_VIOLATION_TILE_SIZE_HEADER_RATE) { + return &unknown_violation_descriptor; + } + return &violation_descriptors[code]; +} + +bool av2_decoder_model_violation_descriptor_is_complete( + Av2DmViolationCode code) { + if ((int)code < 0 || code > AV2_DM_VIOLATION_TILE_SIZE_HEADER_RATE) { + return false; + } + const Av2DmViolationDescriptor *const descriptor = + get_violation_descriptor(code); + return descriptor->spec != NULL && descriptor->spec[0] != '\0' && + descriptor->condition != NULL && descriptor->condition[0] != '\0' && + descriptor->relation != NULL && descriptor->relation[0] != '\0' && + descriptor->observed_name != NULL && + descriptor->observed_name[0] != '\0' && + descriptor->limit_name != NULL && descriptor->limit_name[0] != '\0' && + descriptor->unit != NULL && descriptor->unit[0] != '\0' && + descriptor->requirement != NULL && + descriptor->requirement[0] != '\0' && + descriptor->margin_name != NULL && descriptor->margin_name[0] != '\0'; +} + +typedef struct Av2DmTextBuilder { + char *text; + size_t size; + size_t length; + bool valid; +} Av2DmTextBuilder; + +static void append_detail(Av2DmTextBuilder *builder, const char *format, ...) { + if (!builder->valid) return; + va_list arguments; + va_start(arguments, format); + const int written = + vsnprintf(builder->text + builder->length, + builder->size - builder->length, format, arguments); + va_end(arguments); + if (written < 0 || (size_t)written >= builder->size - builder->length) { + builder->valid = false; + return; + } + builder->length += (size_t)written; +} + +static bool rational_to_long_double(const Av2DmRational *value, + long double *result) { + if (value == NULL || result == NULL) return false; + long double numerator = 0.0L; + long double denominator = 0.0L; + const long double limb_base = 18446744073709551616.0L; + for (int i = 3; i >= 0; --i) { + numerator = numerator * limb_base + (long double)value->magnitude.limbs[i]; + denominator = + denominator * limb_base + (long double)value->denominator.limbs[i]; + } + if (denominator == 0.0L) return false; + *result = numerator / denominator; + if (value->negative) *result = -*result; + return true; +} + +static bool append_decimal(Av2DmTextBuilder *builder, const char *name, + long double value, long double scale, + const char *suffix) { + bool negative = value < 0.0L; + if (negative) value = -value; + const long double scaled = value * scale * 1000.0L; + const long double rounded_value = scaled + 0.5L; + const long double uint64_limit = 18446744073709551616.0L; + if (!(rounded_value >= 0.0L) || rounded_value >= uint64_limit) { + return false; + } + const uint64_t rounded = (uint64_t)rounded_value; + append_detail(builder, " %s=%s%" PRIu64 ".%03" PRIu64 "%s", name, + negative ? "-" : "", rounded / 1000, rounded % 1000, suffix); + return builder->valid; +} + +static bool append_rational(Av2DmTextBuilder *builder, const char *name, + const Av2DmRational *value) { + char formatted[150]; + if (!format_rational(value, formatted, sizeof(formatted))) return false; + append_detail(builder, " %s=%s", name, formatted); + return builder->valid; +} + +static bool append_milliseconds(Av2DmTextBuilder *builder, const char *name, + const Av2DmRational *value) { + long double decimal; + char field[96]; + const int written = snprintf(field, sizeof(field), "%s_ms", name); + return written >= 0 && (size_t)written < sizeof(field) && + rational_to_long_double(value, &decimal) && + append_decimal(builder, field, decimal, 1000.0L, ""); +} + +static const char *affected_kind_name(Av2DmViolationAffectedKind kind) { + switch (kind) { + case AV2_DM_VIOLATION_AFFECTED_EVENT: return "event"; + case AV2_DM_VIOLATION_AFFECTED_DFG: return "dfg"; + case AV2_DM_VIOLATION_AFFECTED_OUTPUT: return "output"; + case AV2_DM_VIOLATION_AFFECTED_TEMPORAL_UNIT: return "temporal_unit"; + } + return "unknown"; +} + +static bool append_payload_details(Av2DmTextBuilder *builder, + const Av2DmViolation *violation) { + const Av2DmViolationDetail *const detail = &violation->detail; + switch (detail->kind) { + case AV2_DM_VIOLATION_DETAIL_NONE: return true; + case AV2_DM_VIOLATION_DETAIL_BUFFER_POOL: { + const Av2DmBufferPoolViolationDetail *const pool = + &detail->value.buffer_pool; + append_detail(builder, + " lane=%s pool_size=%u frames_in_use=%u free_buffers=%u " + "decoder_held_buffers=%u player_held_buffers=%u", + pool->resource_lane ? "resource" : "model", pool->pool_size, + pool->frames_in_use, pool->free_buffers, + pool->decoder_held_buffers, pool->player_held_buffers); + return builder->valid; + } + case AV2_DM_VIOLATION_DETAIL_REFERENCE_SLOT: { + const Av2DmReferenceSlotViolationDetail *const slot = + &detail->value.reference_slot; + append_detail(builder, " requested_reference_slot=%d slot_in_range=%d", + slot->requested_slot, slot->slot_in_range); + if (slot->slot_in_range) { + append_detail(builder, " ref_valid=%d vbi=%d", slot->reference_valid, + slot->buffer_index); + } else { + append_detail(builder, " ref_valid=NA vbi=NA"); + } + append_detail(builder, + " pool_size=%u frames_in_use=%u free_buffers=%u " + "decoder_held_buffers=%u player_held_buffers=%u", + slot->pool.pool_size, slot->pool.frames_in_use, + slot->pool.free_buffers, slot->pool.decoder_held_buffers, + slot->pool.player_held_buffers); + return builder->valid; + } + case AV2_DM_VIOLATION_DETAIL_DELAY_CONSISTENCY: + append_detail(builder, " decoder_buffer_delay_ticks=%u", + detail->value.delay_consistency.decoder_buffer_delay_ticks); + if (detail->value.delay_consistency.ceil_time_delta_present) { + Av2DmRational decoder_delay; + Av2DmRational excess; + if (!av2_dm_rational_make( + detail->value.delay_consistency.decoder_buffer_delay_ticks, 1, + &decoder_delay) || + !av2_dm_rational_subtract( + &decoder_delay, + &detail->value.delay_consistency.ceil_time_delta_ticks, + &excess) || + !append_rational( + builder, "ceil_time_delta_ticks", + &detail->value.delay_consistency.ceil_time_delta_ticks) || + !append_rational(builder, "decoder_buffer_delay_excess", &excess)) { + return false; + } + } else { + append_detail(builder, " ceil_time_delta_ticks=NA"); + } + return builder->valid; + case AV2_DM_VIOLATION_DETAIL_MINIMUM_DECODE_TIME: + return append_rational( + builder, "frame_decode_time", + &detail->value.minimum_decode_time.frame_decode_time) && + append_rational( + builder, "one_header_time", + &detail->value.minimum_decode_time.one_header_time) && + append_milliseconds( + builder, "frame_decode_time", + &detail->value.minimum_decode_time.frame_decode_time) && + append_milliseconds( + builder, "one_header_time", + &detail->value.minimum_decode_time.one_header_time); + case AV2_DM_VIOLATION_DETAIL_FRAME_INTERVAL: + return append_rational(builder, "frame_parsing_interval", + &detail->value.frame_interval) && + append_milliseconds(builder, "frame_parsing_interval", + &detail->value.frame_interval); + } + return false; +} + +bool av2_decoder_model_format_violation_details(const Av2DmViolation *violation, + uint64_t max_display_rate, + uint64_t max_decode_rate, + char *text, size_t text_size) { + if (violation == NULL || text == NULL || text_size == 0) return false; + text[0] = '\0'; + Av2DmTextBuilder builder = { text, text_size, 0, true }; + const Av2DmViolationDescriptor *const descriptor = + get_violation_descriptor(violation->code); + append_detail(&builder, "unit=%s requirement=%s relation=%s condition=%s", + descriptor->unit, descriptor->requirement, descriptor->relation, + descriptor->condition); + if (violation->affected_kind != AV2_DM_VIOLATION_AFFECTED_EVENT || + violation->affected_index != violation->event_index) { + append_detail(&builder, " affected=%s", + affected_kind_name(violation->affected_kind)); + if (violation->affected_kind == AV2_DM_VIOLATION_AFFECTED_TEMPORAL_UNIT) { + append_detail(&builder, " affected_temporal_unit=%" PRIu64, + violation->affected_index); + } else { + append_detail(&builder, " affected_event=%" PRIu64, + violation->affected_index); + } + } + if (violation->observed_present && + !append_rational(&builder, descriptor->observed_name, + &violation->observed)) { + return false; + } + if (violation->limit_present && + !append_rational(&builder, descriptor->limit_name, &violation->limit)) { + return false; + } + + Av2DmRational margin; + bool margin_present = false; + if (violation->observed_present && violation->limit_present && + descriptor->margin_rule != AV2_DM_MARGIN_NONE) { + margin_present = + descriptor->margin_rule == AV2_DM_MARGIN_OBSERVED_MINUS_LIMIT + ? av2_dm_rational_subtract(&violation->observed, &violation->limit, + &margin) + : av2_dm_rational_subtract(&violation->limit, &violation->observed, + &margin); + if (!margin_present || + !append_rational(&builder, descriptor->margin_name, &margin)) { + return false; + } + } + + if (!append_payload_details(&builder, violation)) return false; + if (strcmp(descriptor->unit, "seconds") == 0) { + if ((violation->observed_present && + !append_milliseconds(&builder, descriptor->observed_name, + &violation->observed)) || + (violation->limit_present && + !append_milliseconds(&builder, descriptor->limit_name, + &violation->limit)) || + (margin_present && + !append_milliseconds(&builder, descriptor->margin_name, &margin))) { + return false; + } + } + + if (violation->code == AV2_DM_VIOLATION_MIN_TILE_WIDTH) { + append_detail(&builder, + " non_rightmost_tile_width_valid=0 " + "offending_tile_width=NA min_tile_width=64"); + } + if (violation->code == AV2_DM_VIOLATION_MAX_DISPLAY_RATE && + violation->limit_present && max_display_rate != 0) { + Av2DmRational interval = violation->limit; + if (!av2_dm_rational_divide_u64(&interval, max_display_rate, &interval) || + !append_rational(&builder, "output_interval", &interval) || + !append_milliseconds(&builder, "output_interval", &interval)) { + return false; + } + long double samples; + long double seconds; + if (!violation->observed_present || + !rational_to_long_double(&violation->observed, &samples) || + !rational_to_long_double(&interval, &seconds) || seconds <= 0.0L || + !append_decimal(&builder, "observed_rate", samples / seconds, 0.000001L, + "Msamples/s") || + !append_decimal(&builder, "limit_rate", (long double)max_display_rate, + 0.000001L, "Msamples/s")) { + return false; + } + } else if (violation->code == AV2_DM_VIOLATION_FRAME_DECODE_RATE && + violation->detail.kind == AV2_DM_VIOLATION_DETAIL_FRAME_INTERVAL && + max_decode_rate != 0) { + long double samples; + long double seconds; + if (!violation->observed_present || + !rational_to_long_double(&violation->observed, &samples) || + !rational_to_long_double(&violation->detail.value.frame_interval, + &seconds) || + seconds <= 0.0L || + !append_decimal(&builder, "observed_rate", samples / seconds, 0.000001L, + "Msamples/s") || + !append_decimal(&builder, "limit_rate", (long double)max_decode_rate, + 0.000001L, "Msamples/s")) { + return false; + } + } else if (violation->code == AV2_DM_VIOLATION_FRAME_TILE_RATE && + violation->detail.kind == AV2_DM_VIOLATION_DETAIL_FRAME_INTERVAL) { + long double tiles; + long double tile_limit; + long double seconds; + if (!violation->observed_present || !violation->limit_present || + !rational_to_long_double(&violation->observed, &tiles) || + !rational_to_long_double(&violation->limit, &tile_limit) || + !rational_to_long_double(&violation->detail.value.frame_interval, + &seconds) || + seconds <= 0.0L || + !append_decimal(&builder, "observed_tile_rate", tiles / seconds, 1.0L, + "tiles/s") || + !append_decimal(&builder, "limit_tile_rate", tile_limit / seconds, 1.0L, + "tiles/s")) { + return false; + } + } + append_detail(&builder, " spec=%s", descriptor->spec); + return builder.valid; +} + +static const Av2DmContextEvent *find_context_event(const Av2DmRunReport *report, + uint64_t event_index) { + if (report->current_event_valid && + report->current_event.event_index == event_index) { + return &report->current_event; + } + return NULL; +} + +static const char *context_event_type_name(Av2DmContextEventType type) { + switch (type) { + case AV2_DM_CONTEXT_FRAME: return "frame"; + case AV2_DM_CONTEXT_OLK_REFERENCE_INVALIDATION: return "olk_invalidation"; + case AV2_DM_CONTEXT_REFERENCE_UPDATE: return "reference_update"; + case AV2_DM_CONTEXT_OUTPUT: return "output"; + case AV2_DM_CONTEXT_RECOVERY_RESET: return "recovery_reset"; + } + return "unknown"; +} + +static void print_event_location(const Av2DmRunReport *report, + uint64_t event_index) { + const Av2DmContextEvent *const event = + find_context_event(report, event_index); + if (event == NULL) return; + fprintf(stderr, " event_type=%s frame_unit=%" PRIu64, + context_event_type_name(event->type), event->source_frame_unit_index); + if (event->type == AV2_DM_CONTEXT_FRAME) { + fprintf(stderr, " temporal_unit=%" PRIu64, + event->frame.temporal_unit_index); + } else if (event->type == AV2_DM_CONTEXT_OUTPUT) { + fprintf(stderr, + " presentation_frame_unit=%" PRIu64 " temporal_unit=%" PRIu64, + event->presentation_frame_unit_index, + event->output.temporal_unit_index); + } +} + +static void print_violation_explanation(const Av2DmRunReport *report, + const Av2DmViolation *violation) { + char details[1024]; + if (av2_decoder_model_format_violation_details( + violation, report->max_display_rate, report->max_decode_rate, details, + sizeof(details))) { + fprintf(stderr, " %s", details); + } else { + fprintf(stderr, " details=unavailable"); + } +} + +static void report_decoder_model_violation(void *opaque, + const Av2DmViolation *violation) { + Av2DmRunReport *const report = (Av2DmRunReport *)opaque; + if (report->verifier->fatal_violation) return; + if (report->violations != UINT64_MAX) { + ++report->violations; + } + if (report->verifier->check_mode == AVM_DECODER_MODEL_CHECK_FATAL) { + report->verifier->fatal_violation = true; + } + char observed[150]; + char limit[150]; + if (!format_rational( + violation->observed_present ? &violation->observed : NULL, observed, + sizeof(observed))) { + snprintf(observed, sizeof(observed), "NA"); + } + if (!format_rational(violation->limit_present ? &violation->limit : NULL, + limit, sizeof(limit))) { + snprintf(limit, sizeof(limit), "NA"); + } + fprintf(stderr, + "AV2_DECODER_MODEL_WARNING status=NON_CONFORMANT code=%s " + "xlayer=%d ops=%d op=%d rap=%" PRId64 + " level=%u level_name=%s tier=%s scope=%s mode=%s event=%" PRIu64, + av2_dm_violation_code_name(violation->code), report->scope.xlayer_id, + report->scope.ops_id, report->scope.operating_point, report->rap, + report->level_idx, level_name(report->level_idx), + tier_name(report->tier), scope_name(&report->scope), + mode_name(report->mode), violation->event_index); + fprintf(stderr, " cvs=%" PRIu64, report->cvs); + print_event_location(report, violation->event_index); + fprintf(stderr, " observed=%s limit=%s", observed, limit); + print_violation_explanation(report, violation); + fprintf(stderr, "\n"); +} + +bool av2_decoder_model_report_violation_for_testing( + const Av2DmViolation *violation, bool fatal_mode, uint64_t *violation_count, + bool *fatal_violation) { + if (violation == NULL || violation_count == NULL || fatal_violation == NULL) { + return false; + } + Av2DecoderModelVerifier verifier; + memset(&verifier, 0, sizeof(verifier)); + verifier.check_mode = + fatal_mode ? AVM_DECODER_MODEL_CHECK_FATAL : AVM_DECODER_MODEL_CHECK_WARN; + Av2DmRunReport report; + memset(&report, 0, sizeof(report)); + report.verifier = &verifier; + report.scope.xlayer_id = 0; + report.scope.ops_xlayer_id = -1; + report.scope.ops_id = -1; + report.scope.operating_point = -1; + report.scope.whole_xlayer = true; + report.mode = AV2_DM_RESOURCE_AVAILABILITY_MODE; + report.cvs = 1; + report_decoder_model_violation(&report, violation); + *violation_count = report.violations; + *fatal_violation = verifier.fatal_violation; + return true; +} + +static Av2DmResultStatus aggregate_status(const uint64_t status_count[4]) { + if (status_count[AV2_DM_RESULT_NON_CONFORMANT] != 0) { + return AV2_DM_RESULT_NON_CONFORMANT; + } + if (status_count[AV2_DM_RESULT_INDETERMINATE] != 0) { + return AV2_DM_RESULT_INDETERMINATE; + } + if (status_count[AV2_DM_RESULT_CONFORMANT] != 0) { + return AV2_DM_RESULT_CONFORMANT; + } + return AV2_DM_RESULT_NOT_APPLICABLE; +} + +static void ensure_cvs_open(Av2DecoderModelVerifier *verifier, int xlayer_id) { + if (xlayer_id < 0 || xlayer_id >= MAX_NUM_XLAYERS) return; + Av2DmCvsAggregate *const cvs = &verifier->cvs[xlayer_id]; + if (cvs->open) return; + (void)increment_u64(verifier, &cvs->number); + cvs->open = true; + cvs->verification_complete = true; + cvs->reason = AV2_DM_REASON_NONE; + cvs->violations = 0; + memset(cvs->run_status_count, 0, sizeof(cvs->run_status_count)); +} + +static void emit_result(Av2DecoderModelVerifier *verifier, + const Av2DmResult *model_result, int64_t rap, + Av2DmIndeterminateReason reason, + const Av2DmRunReport *report) { + Av2DmResult result = *model_result; + if (result.arithmetic_failed) { + reason = AV2_DM_REASON_INTERNAL_FAILURE; + verifier->aggregate_incomplete = true; + if (verifier->error_code == AV2_DM_VERIFIER_ERROR_NONE) { + verifier->error_code = AV2_DM_VERIFIER_ERROR_ARITHMETIC; + } + emit_verifier_error(verifier, verifier->error_code, + report != NULL ? report->scope.xlayer_id : -1, + report != NULL ? report->cvs : 0); + } else if (result.missing_required_input && reason == AV2_DM_REASON_NONE) { + reason = AV2_DM_REASON_MISSING_REQUIRED_INPUT; + } + Av2DmCvsAggregate *cvs = NULL; + if (report != NULL && report->scope.xlayer_id >= 0 && + report->scope.xlayer_id < MAX_NUM_XLAYERS) { + cvs = &verifier->cvs[report->scope.xlayer_id]; + } + + if ((unsigned int)result.status >= 4) { + mark_failed(verifier); + result.status = AV2_DM_RESULT_INDETERMINATE; + } + if (verifier->failed) reason = AV2_DM_REASON_INTERNAL_FAILURE; + if (result.status != AV2_DM_RESULT_NON_CONFORMANT && + reason != AV2_DM_REASON_NONE) { + result.status = AV2_DM_RESULT_INDETERMINATE; + result.missing_required_input = true; + } + + bool accounting_overflow = + verifier->result_count == UINT64_MAX || + verifier->result_status_count[result.status] == UINT64_MAX; + if (cvs != NULL) { + accounting_overflow = accounting_overflow || + cvs->run_status_count[result.status] == UINT64_MAX; + } + if (accounting_overflow) { + mark_arithmetic_failed(verifier); + reason = AV2_DM_REASON_INTERNAL_FAILURE; + if (result.status != AV2_DM_RESULT_NON_CONFORMANT) { + result.status = AV2_DM_RESULT_INDETERMINATE; + result.missing_required_input = true; + } + } + // An accounting failure can change the destination from CONFORMANT to + // INDETERMINATE. Recheck that final target before any diagnostic is printed. + if (verifier->result_status_count[result.status] == UINT64_MAX || + (cvs != NULL && cvs->run_status_count[result.status] == UINT64_MAX)) { + mark_arithmetic_failed(verifier); + reason = AV2_DM_REASON_INTERNAL_FAILURE; + } + + (void)increment_u64(verifier, &verifier->result_count); + (void)increment_u64(verifier, &verifier->result_status_count[result.status]); + if (cvs != NULL) { + (void)increment_u64(verifier, &cvs->run_status_count[result.status]); + add_u64_saturated(&cvs->violations, result.violations); + if (result.status == AV2_DM_RESULT_INDETERMINATE || + reason != AV2_DM_REASON_NONE) { + cvs->verification_complete = false; + if (cvs->reason == AV2_DM_REASON_NONE || + reason == AV2_DM_REASON_INTERNAL_FAILURE) { + cvs->reason = reason; + } + } + } + + fprintf(stderr, + "AV2_DECODER_MODEL_RESULT status=%s xlayer=%d ops=%d op=%d " + "rap=%" PRId64 " mode=%s decoded=%" PRIu64 " outputs=%" PRIu64 + " reordered_outputs=%" PRIu64 " violations=%" PRIu64 " reason=%s", + result_name(result.status), result.scope.xlayer_id, + result.scope.ops_id, result.scope.operating_point, rap, + mode_name(result.mode), result.decoded_frames, result.output_frames, + result.reordered_outputs, result.violations, + indeterminate_reason_name(reason)); + if (report != NULL) { + fprintf(stderr, " level=%u level_name=%s tier=%s scope=%s", + report->level_idx, level_name(report->level_idx), + tier_name(report->tier), scope_name(&report->scope)); + } + fprintf(stderr, "\n"); +} + +static bool run_seed_contains_generation(const Av2DmLiveRun *run, + uint64_t generation) { + for (uint32_t i = 0; i < run->config.ras_seed_count; ++i) { + if (run->config.ras_seeds[i].generation == generation) return true; + } + return false; +} + +static void apply_event_to_run(Av2DecoderModelVerifier *verifier, + Av2DmLiveRun *run, + const Av2DmContextEvent *event) { + if (verifier->fatal_violation) return; + if (run->olk && + event->source_frame_unit_index > run->start_source_frame_unit && + event->leading_frame) { + return; + } + run->report.current_event = *event; + run->report.current_event_valid = true; + if (event->indeterminate_reason != AV2_DM_REASON_NONE && + run->reason == AV2_DM_REASON_NONE) { + run->reason = event->indeterminate_reason; + } + switch (event->type) { + case AV2_DM_CONTEXT_FRAME: + av2_decoder_model_start_frame(run->model, &event->frame); + break; + case AV2_DM_CONTEXT_OLK_REFERENCE_INVALIDATION: + av2_decoder_model_invalidate_olk_reference_buffers(run->model, + event->ref_valid_mask); + break; + case AV2_DM_CONTEXT_REFERENCE_UPDATE: + av2_decoder_model_update_reference_buffers(run->model, + &event->reference_update); + if (event->set_initial_presentation_delay) { + av2_decoder_model_set_initial_presentation_delay(run->model, + event->event_index); + } + break; + case AV2_DM_CONTEXT_OUTPUT: { + Av2DmOutputEvent output = event->output; + av2_decoder_model_output_frame(run->model, &output); + Av2DmState state; + if (av2_decoder_model_get_state(run->model, &state) && + state.last_presentation_offset_valid) { + if (verifier->replay_last_presentation_offset_valid) { + verifier->replay_previous_presentation_offset = + verifier->replay_last_presentation_offset; + verifier->replay_previous_presentation_offset_valid = true; + } + verifier->replay_last_presentation_offset = + state.last_presentation_offset; + verifier->replay_last_presentation_offset_valid = true; + } + break; + } + case AV2_DM_CONTEXT_RECOVERY_RESET: break; + } + run->report.current_event_valid = false; + memset(&run->report.current_event, 0, sizeof(run->report.current_event)); +} + +static Av2DmLiveRun *create_live_run(Av2DecoderModelVerifier *verifier, + Av2DmContext *context, + const Av2DmContextEvent *start_frame, + int64_t rap) { + if (context->run_count == SIZE_MAX) { + mark_arithmetic_failed(verifier); + return NULL; + } + if (!reserve_array(verifier, (void **)&context->runs, &context->run_capacity, + context->run_count + 1, sizeof(*context->runs))) { + mark_failed(verifier); + return NULL; + } + Av2DmLiveRun *const run = avm_calloc(1, sizeof(*run)); + if (run == NULL) { + mark_allocation_failed(verifier); + return NULL; + } + run->config = start_frame->config; + run->reason = start_frame->indeterminate_reason; + if (!start_frame->config_present || run->reason != AV2_DM_REASON_NONE) { + run->config.applicability = AV2_DM_MISSING_REQUIRED_INPUT; + } + if (start_frame->frame_obu_type == OBU_RAS_FRAME) { + run->config.ras_start = true; + run->config.ras_seed_complete = start_frame->ras_seed_complete; + run->config.ras_seed_count = start_frame->ras_seed_count; + memcpy(run->config.ras_seeds, start_frame->ras_seeds, + sizeof(run->config.ras_seeds)); + } + run->rap = rap; + run->olk = start_frame->frame_obu_type == OBU_OPEN_LOOP_KEY; + run->start_source_frame_unit = start_frame->source_frame_unit_index; + run->report.verifier = verifier; + run->report.scope = run->config.scope; + run->report.mode = run->config.mode; + run->report.rap = rap; + run->report.cvs = verifier->cvs[context->key.xlayer_id].number; + run->report.level_idx = run->config.level_idx; + run->report.tier = run->config.tier; + if (run->config.level_limits_present) { + run->report.max_display_rate = run->config.level_limits.max_display_rate; + run->report.max_decode_rate = run->config.level_limits.max_decode_rate; + } else { + Av2DmLevelLimits limits; + if (av2_dm_get_level_limits(run->config.level_idx, run->config.tier, + run->config.profile, &limits)) { + run->report.max_display_rate = limits.max_display_rate; + run->report.max_decode_rate = limits.max_decode_rate; + } + } + run->model = av2_decoder_model_create( + &run->config, report_decoder_model_violation, &run->report); + if (run->model == NULL) { + avm_free(run); + mark_allocation_failed(verifier); + return NULL; + } + context->runs[context->run_count] = run; + if (!increment_size(verifier, &context->run_count)) { + av2_decoder_model_destroy(run->model); + avm_free(run); + return NULL; + } + return run; +} + +static void finish_context_runs(Av2DecoderModelVerifier *verifier, + Av2DmContext *context) { + for (size_t i = 0; i < context->run_count; ++i) { + Av2DmLiveRun *const run = context->runs[i]; + if (!verifier->failed && !verifier->fatal_violation) { + av2_decoder_model_finish(run->model); + } + Av2DmResult result; + if (av2_decoder_model_get_result(run->model, &result)) { + Av2DmIndeterminateReason reason = run->reason; + if ((verifier->failed || verifier->fatal_violation) && + result.status != AV2_DM_RESULT_NON_CONFORMANT) { + av2_decoder_model_mark_incomplete(run->model); + (void)av2_decoder_model_get_result(run->model, &result); + if (reason == AV2_DM_REASON_NONE) { + reason = verifier->failed ? AV2_DM_REASON_INTERNAL_FAILURE + : AV2_DM_REASON_MISSING_REQUIRED_INPUT; + } + } + emit_result(verifier, &result, run->rap, reason, &run->report); + } else { + mark_failed(verifier); + } + av2_decoder_model_destroy(run->model); + avm_free(run); + } + context->run_count = 0; +} + +static void finish_partial_context_runs(Av2DecoderModelVerifier *verifier, + Av2DmContext *context) { + for (size_t i = 0; i < context->run_count; ++i) { + Av2DmLiveRun *const run = context->runs[i]; + Av2DmResult result; + if (av2_decoder_model_get_result(run->model, &result)) { + Av2DmIndeterminateReason reason = run->reason; + if (result.status != AV2_DM_RESULT_NON_CONFORMANT) { + av2_decoder_model_mark_incomplete(run->model); + (void)av2_decoder_model_get_result(run->model, &result); + if (reason == AV2_DM_REASON_NONE) { + reason = verifier->failed ? AV2_DM_REASON_INTERNAL_FAILURE + : AV2_DM_REASON_MISSING_REQUIRED_INPUT; + } + } + emit_result(verifier, &result, run->rap, reason, &run->report); + } else { + mark_failed(verifier); + } + av2_decoder_model_destroy(run->model); + avm_free(run); + } + context->run_count = 0; +} + +static void destroy_context_runs(Av2DmContext *context) { + for (size_t i = 0; i < context->run_count; ++i) { + av2_decoder_model_destroy(context->runs[i]->model); + avm_free(context->runs[i]); + } + context->run_count = 0; +} + +static bool prefix_event_applies(const Av2DmLiveRun *run, + const Av2DmContextEvent *event) { + if (run->olk) { + return event->type == AV2_DM_CONTEXT_OLK_REFERENCE_INVALIDATION; + } + if (run->config.ras_start) { + return event->type == AV2_DM_CONTEXT_OUTPUT && + run_seed_contains_generation(run, event->generation); + } + return true; +} + +static void update_live_run_parameters(Av2DmLiveRun *run, + const Av2DmContextEvent *event) { + run->report.current_event = *event; + run->report.current_event_valid = true; + const bool updated = av2_decoder_model_update_parameters( + run->model, &event->config, event->event_index); + run->report.current_event_valid = false; + memset(&run->report.current_event, 0, sizeof(run->report.current_event)); + if (!updated) { + if (run->reason == AV2_DM_REASON_NONE) { + run->reason = AV2_DM_REASON_MISSING_REQUIRED_INPUT; + } + return; + } + const bool ras_start = run->config.ras_start; + const bool ras_seed_complete = run->config.ras_seed_complete; + const uint32_t ras_seed_count = run->config.ras_seed_count; + Av2DmRasSeed ras_seeds[AV2_DM_MAX_REF_FRAMES]; + memcpy(ras_seeds, run->config.ras_seeds, sizeof(ras_seeds)); + const uint32_t initial_display_delay = run->config.initial_display_delay; + run->config = event->config; + run->config.initial_display_delay = initial_display_delay; + run->config.ras_start = ras_start; + run->config.ras_seed_complete = ras_seed_complete; + run->config.ras_seed_count = ras_seed_count; + memcpy(run->config.ras_seeds, ras_seeds, sizeof(run->config.ras_seeds)); + run->report.mode = run->config.mode; + run->report.level_idx = run->config.level_idx; + run->report.tier = run->config.tier; + run->report.max_display_rate = 0; + run->report.max_decode_rate = 0; + if (run->config.level_limits_present) { + run->report.max_display_rate = run->config.level_limits.max_display_rate; + run->report.max_decode_rate = run->config.level_limits.max_decode_rate; + } else { + Av2DmLevelLimits limits; + if (av2_dm_get_level_limits(run->config.level_idx, run->config.tier, + run->config.profile, &limits)) { + run->report.max_display_rate = limits.max_display_rate; + run->report.max_decode_rate = limits.max_decode_rate; + } + } +} + +static void mark_live_runs_incomplete(Av2DmContext *context) { + for (size_t i = 0; i < context->run_count; ++i) { + Av2DmLiveRun *const run = context->runs[i]; + av2_decoder_model_mark_incomplete(run->model); + if (run->reason == AV2_DM_REASON_NONE) { + run->reason = AV2_DM_REASON_MISSING_REQUIRED_INPUT; + } + } +} + +static void dispatch_context_event(Av2DecoderModelVerifier *verifier, + Av2DmContext *context, + const Av2DmContextEvent *event) { + if (verifier->fatal_violation) return; + if (event->type != AV2_DM_CONTEXT_FRAME) { + for (size_t i = 0; i < context->run_count && !verifier->fatal_violation; + ++i) { + apply_event_to_run(verifier, context->runs[i], event); + } + if (!verifier->current_source_frame_dispatched) { + if (context->prefix_event_count == SIZE_MAX) { + mark_arithmetic_failed(verifier); + return; + } + if (!reserve_array(verifier, (void **)&context->prefix_events, + &context->prefix_event_capacity, + context->prefix_event_count + 1, + sizeof(*context->prefix_events))) { + mark_failed(verifier); + return; + } + context->prefix_events[context->prefix_event_count] = *event; + if (!increment_size(verifier, &context->prefix_event_count)) return; + } + return; + } + + ensure_cvs_open(verifier, context->key.xlayer_id); + const bool config_changed = + context->last_config_present && + (context->last_stream_generation != event->stream_generation || + memcmp(&context->last_config, &event->config, sizeof(event->config)) != + 0); + if (config_changed) { + if (context->last_stream_generation == event->stream_generation && + event->frame.random_access_point && + event->frame.decoder_model_parameters_updated) { + for (size_t i = 0; i < context->run_count; ++i) { + update_live_run_parameters(context->runs[i], event); + } + } else { + // Annex E resets FirstBitArrival only when a new parameter set is + // received at a random-access point. A different transition cannot be + // verified by silently restarting the sequential model. + mark_live_runs_incomplete(context); + } + } + + bool created_segment = false; + if (context->run_count == 0) { + const int64_t rap = event->frame.random_access_point + ? (int64_t)event->source_frame_unit_index + : -1; + Av2DmLiveRun *const run = create_live_run(verifier, context, event, rap); + if (run == NULL) return; + for (size_t i = 0; i < context->prefix_event_count; ++i) { + if (prefix_event_applies(run, &context->prefix_events[i])) { + apply_event_to_run(verifier, run, &context->prefix_events[i]); + } + } + created_segment = true; + } + if (!created_segment && event->frame.random_access_point) { + Av2DmLiveRun *const run = create_live_run( + verifier, context, event, (int64_t)event->source_frame_unit_index); + if (run == NULL) return; + for (size_t i = 0; i < context->prefix_event_count; ++i) { + if (prefix_event_applies(run, &context->prefix_events[i])) { + apply_event_to_run(verifier, run, &context->prefix_events[i]); + } + } + } + for (size_t i = 0; i < context->run_count && !verifier->fatal_violation; + ++i) { + apply_event_to_run(verifier, context->runs[i], event); + } + context->last_config_present = true; + context->last_config = event->config; + context->last_stream_generation = event->stream_generation; + context->last_ras_seed_complete = event->ras_seed_complete; + context->last_ras_seed_count = event->ras_seed_count; +} + +static void finish_xlayer_cvs_internal(Av2DecoderModelVerifier *verifier, + int xlayer_id, bool partial) { + if (xlayer_id < 0 || xlayer_id >= MAX_NUM_XLAYERS || + !verifier->cvs[xlayer_id].open) { + return; + } + partial = partial || verifier->failed || verifier->fatal_violation; + for (size_t i = 0; i < verifier->context_count; ++i) { + Av2DmContext *const context = &verifier->contexts[i]; + if (context->key.xlayer_id != xlayer_id) continue; + if (partial) { + finish_partial_context_runs(verifier, context); + } else { + finish_context_runs(verifier, context); + } + if (verifier->failed || verifier->fatal_violation) partial = true; + context->prefix_event_count = 0; + context->last_config_present = false; + rebuild_incomplete_extraction(verifier, context); + } + Av2DmCvsAggregate *const cvs = &verifier->cvs[xlayer_id]; + if (partial) cvs->verification_complete = false; + if (verifier->failed) { + cvs->verification_complete = false; + cvs->reason = AV2_DM_REASON_INTERNAL_FAILURE; + if (cvs->run_status_count[AV2_DM_RESULT_NON_CONFORMANT] == 0) { + cvs->run_status_count[AV2_DM_RESULT_INDETERMINATE] = 1; + } + } + Av2DmResultStatus status = aggregate_status(cvs->run_status_count); + if (verifier->bitstream_cvs == UINT64_MAX || + verifier->bitstream_status_count[status] == UINT64_MAX) { + mark_arithmetic_failed(verifier); + cvs->verification_complete = false; + cvs->reason = AV2_DM_REASON_INTERNAL_FAILURE; + if (status != AV2_DM_RESULT_NON_CONFORMANT) { + cvs->run_status_count[AV2_DM_RESULT_INDETERMINATE] = 1; + status = AV2_DM_RESULT_INDETERMINATE; + } + } + // The failure above can redirect a conformant CVS to the indeterminate + // counter. Recheck and saturate that final target before reporting it. + if (verifier->bitstream_status_count[status] == UINT64_MAX) { + mark_arithmetic_failed(verifier); + cvs->verification_complete = false; + cvs->reason = AV2_DM_REASON_INTERNAL_FAILURE; + } + (void)increment_u64(verifier, &verifier->bitstream_cvs); + (void)increment_u64(verifier, &verifier->bitstream_status_count[status]); + if (status == AV2_DM_RESULT_NON_CONFORMANT && + !verifier->first_non_conformant_valid) { + verifier->first_non_conformant_valid = true; + verifier->first_non_conformant_xlayer = xlayer_id; + verifier->first_non_conformant_cvs = cvs->number; + } + fprintf(stderr, + "AV2_DECODER_MODEL_CVS_RESULT status=%s xlayer=%d cvs=%" PRIu64 + " violations=%" PRIu64 " verification_complete=%d reason=%s\n", + result_name(status), xlayer_id, cvs->number, cvs->violations, + cvs->verification_complete ? 1 : 0, + indeterminate_reason_name(cvs->reason)); + cvs->open = false; +} + +static void finish_xlayer_cvs(Av2DecoderModelVerifier *verifier, + int xlayer_id) { + finish_xlayer_cvs_internal(verifier, xlayer_id, false); +} + +static void finish_all_cvs(Av2DecoderModelVerifier *verifier) { + for (int xlayer_id = 0; xlayer_id < MAX_NUM_XLAYERS; ++xlayer_id) { + finish_xlayer_cvs(verifier, xlayer_id); + } +} + +static void finish_all_cvs_partial(Av2DecoderModelVerifier *verifier) { + for (int xlayer_id = 0; xlayer_id < MAX_NUM_XLAYERS; ++xlayer_id) { + finish_xlayer_cvs_internal(verifier, xlayer_id, true); + } +} + +static void emit_bitstream_result(Av2DecoderModelVerifier *verifier, + bool complete) { + if (verifier->bitstream_result_emitted) return; + Av2DmResultStatus status = aggregate_status(verifier->bitstream_status_count); + if (verifier->failed && status != AV2_DM_RESULT_NON_CONFORMANT) { + status = AV2_DM_RESULT_INDETERMINATE; + complete = false; + } + fprintf( + stderr, + "AV2_DECODER_MODEL_BITSTREAM_RESULT status=%s complete=%d cvs=%" PRIu64 + " conformant_cvs=%" PRIu64 " non_conformant_cvs=%" PRIu64 + " indeterminate_cvs=%" PRIu64 " not_applicable_cvs=%" PRIu64 + " first_non_conformant_xlayer=%d first_non_conformant_cvs=%" PRIu64 "\n", + result_name(status), complete ? 1 : 0, verifier->bitstream_cvs, + verifier->bitstream_status_count[AV2_DM_RESULT_CONFORMANT], + verifier->bitstream_status_count[AV2_DM_RESULT_NON_CONFORMANT], + verifier->bitstream_status_count[AV2_DM_RESULT_INDETERMINATE], + verifier->bitstream_status_count[AV2_DM_RESULT_NOT_APPLICABLE], + verifier->first_non_conformant_valid + ? verifier->first_non_conformant_xlayer + : -1, + verifier->first_non_conformant_valid ? verifier->first_non_conformant_cvs + : 0); + verifier->bitstream_result_emitted = true; +} + +static void find_error_location(const Av2DecoderModelVerifier *verifier, + int *xlayer_id, uint64_t *cvs) { + *xlayer_id = -1; + *cvs = 0; + for (int i = 0; i < MAX_NUM_XLAYERS; ++i) { + if (verifier->cvs[i].open) { + *xlayer_id = i; + *cvs = verifier->cvs[i].number; + return; + } + } +} + +void av2_decoder_model_verifier_finish(AV2Decoder *pbi) { + if (pbi == NULL) return; + if (pbi->decoder_model_verifier == NULL) { + if (pbi->decoder_model_verifier_allocation_failed && + !pbi->decoder_model_verifier_allocation_reported) { + fprintf(stderr, + "AV2_DECODER_MODEL_ERROR code=ALLOCATION_FAILURE xlayer=-1 " + "cvs=0\n"); + emit_generic_internal_failure_result(); + emit_generic_internal_failure_bitstream_result(); + pbi->decoder_model_verifier_allocation_reported = true; + } + return; + } + Av2DecoderModelVerifier *const verifier = pbi->decoder_model_verifier; + if (verifier->finished) return; + if (verifier->failed) { + int xlayer_id; + uint64_t cvs; + find_error_location(verifier, &xlayer_id, &cvs); + emit_verifier_error(verifier, verifier->error_code, xlayer_id, cvs); + } + if (!verifier->failed) { + Av2DmAdapterEvent *const finish = + append_event(verifier, AV2_DM_ADAPTER_FINISH); + if (finish != NULL) verifier->finish_event = finish->index; + } + verifier->finished = true; + if (verifier->fatal_violation) { + finish_all_cvs_partial(verifier); + } else { + finish_all_cvs(verifier); + } + if (verifier->failed && verifier->result_count == 0) { + emit_generic_internal_failure_result(); + (void)increment_u64(verifier, &verifier->result_count); + (void)increment_u64( + verifier, &verifier->result_status_count[AV2_DM_RESULT_INDETERMINATE]); + } + emit_bitstream_result(verifier, !verifier->failed && + !verifier->fatal_violation && + !verifier->aggregate_incomplete); +} + +bool av2_decoder_model_verifier_should_stop(const AV2Decoder *pbi) { + return pbi != NULL && pbi->decoder_model_verifier != NULL && + pbi->decoder_model_verifier->fatal_violation; +} + +static uint32_t saturate_size_to_u32(size_t value) { + return value > UINT32_MAX ? UINT32_MAX : (uint32_t)value; +} + +static void add_size_to_saturated_u32(uint32_t *total, size_t value) { + const uint32_t addend = saturate_size_to_u32(value); + if (UINT32_MAX - *total < addend) { + *total = UINT32_MAX; + } else { + *total += addend; + } +} + +bool av2_decoder_model_verifier_get_stats(const AV2Decoder *pbi, + Av2DmVerifierStats *stats) { + if (stats == NULL) return false; + memset(stats, 0, sizeof(*stats)); + if (pbi == NULL) return false; + if (pbi->decoder_model_verifier == NULL) { + if (!pbi->decoder_model_verifier_allocation_failed) return false; + stats->failed = true; + stats->result_count = + pbi->decoder_model_verifier_allocation_reported ? 1 : 0; + stats->indeterminate_results = stats->result_count; + return true; + } + const Av2DecoderModelVerifier *const verifier = pbi->decoder_model_verifier; + stats->available = true; + stats->failed = verifier->failed; + stats->raw_obus = verifier->raw_obus; + stats->raw_bits = verifier->raw_bits; + stats->event_count = verifier->event_count; + stats->temporal_unit_index = verifier->temporal_unit_index; + stats->frame_unit_index = verifier->frame_unit_index; + stats->closed_dfgs = verifier->closed_dfgs; + stats->rap_starts = verifier->rap_start_count; + stats->temporal_points = verifier->temporal_points; + stats->temporal_point_present = verifier->temporal_point_present; + stats->temporal_point = verifier->temporal_point; + stats->contexts = saturate_size_to_u32(verifier->context_count); + stats->frame_starts = verifier->frame_starts; + stats->reference_updates = verifier->reference_updates; + stats->olk_invalidations = verifier->olk_invalidations; + stats->outputs = verifier->outputs; + stats->last_frame_start_event = verifier->last_frame_start_event; + stats->last_reference_update_event = verifier->last_reference_update_event; + stats->last_olk_invalidation_event = verifier->last_olk_invalidation_event; + stats->last_output_event = verifier->last_output_event; + stats->last_output_callback_frame_unit = + verifier->last_output_callback_frame_unit; + stats->last_output_presentation_frame_unit = + verifier->last_output_presentation_frame_unit; + stats->last_output_presentation_temporal_unit = + verifier->last_output_presentation_temporal_unit; + stats->last_output_generation = verifier->last_output_generation; + stats->last_output_presentation_xlayer_id = + verifier->last_output_presentation_xlayer_id; + stats->last_output_presentation_mlayer_id = + verifier->last_output_presentation_mlayer_id; + stats->last_output_presentation_tlayer_id = + verifier->last_output_presentation_tlayer_id; + stats->last_output_uses_current_presentation = + verifier->last_output_uses_current_presentation; + stats->replay_previous_presentation_offset_valid = + verifier->replay_previous_presentation_offset_valid; + stats->replay_previous_presentation_offset = + verifier->replay_previous_presentation_offset; + stats->replay_last_presentation_offset_valid = + verifier->replay_last_presentation_offset_valid; + stats->replay_last_presentation_offset = + verifier->replay_last_presentation_offset; + stats->finish_event = verifier->finish_event; + stats->result_count = verifier->result_count; + stats->conformant_results = + verifier->result_status_count[AV2_DM_RESULT_CONFORMANT]; + stats->non_conformant_results = + verifier->result_status_count[AV2_DM_RESULT_NON_CONFORMANT]; + stats->indeterminate_results = + verifier->result_status_count[AV2_DM_RESULT_INDETERMINATE]; + stats->not_applicable_results = + verifier->result_status_count[AV2_DM_RESULT_NOT_APPLICABLE]; + for (size_t i = 0; i < verifier->context_count; ++i) { + add_size_to_saturated_u32(&stats->live_runs, + verifier->contexts[i].run_count); + } + stats->live_generations = saturate_size_to_u32(verifier->generation_count); + add_size_to_saturated_u32(&stats->parameter_records, + verifier->sequence_record_count); + add_size_to_saturated_u32(&stats->parameter_records, + verifier->ops_record_count); + add_size_to_saturated_u32(&stats->parameter_records, + verifier->brt_record_count); + add_size_to_saturated_u32(&stats->parameter_records, + verifier->active_record_count); + return true; +} + +bool av2_decoder_model_verifier_get_context_stats(const AV2Decoder *pbi, + uint32_t context_index, + Av2DmContextStats *stats) { + if (stats == NULL || pbi == NULL || pbi->decoder_model_verifier == NULL || + context_index >= pbi->decoder_model_verifier->context_count) { + return false; + } + const Av2DmContext *const context = + &pbi->decoder_model_verifier->contexts[context_index]; + memset(stats, 0, sizeof(*stats)); + stats->scope.xlayer_id = context->key.xlayer_id; + stats->scope.ops_xlayer_id = context->key.ops_xlayer_id; + stats->scope.ops_id = context->key.ops_id; + stats->scope.operating_point = context->key.operating_point; + stats->scope.whole_xlayer = context->key.whole_xlayer; + stats->active = context->active; + stats->active_configuration_present = + context->active_configuration_record != UINT64_MAX; + stats->active_sequence_header_id = -1; + if (context->active_sequence_record < + pbi->decoder_model_verifier->sequence_record_count) { + stats->active_sequence_header_id = + pbi->decoder_model_verifier + ->sequence_records[context->active_sequence_record] + .sequence_header_id; + } + stats->pending_dfg_bits = context->pending_dfg_bits; + stats->last_closed_dfg_bits = context->last_closed_dfg_bits; + stats->closed_dfgs = context->closed_dfgs; + stats->configuration_generation = context->configuration_generation; + stats->resolved_config_present = context->last_config_present; + if (context->last_config_present) { + stats->resolved_applicability = context->last_config.applicability; + stats->resolved_mode = context->last_config.mode; + stats->resolved_initial_display_delay = + context->last_config.initial_display_delay; + } + stats->last_ras_seed_complete = context->last_ras_seed_complete; + stats->last_ras_seed_count = context->last_ras_seed_count; + return true; +} diff --git a/av2/decoder/decoder_model.h b/av2/decoder/decoder_model.h new file mode 100644 index 0000000000..e7a598a2e3 --- /dev/null +++ b/av2/decoder/decoder_model.h @@ -0,0 +1,167 @@ +/* + * Copyright (c) 2026, Alliance for Open Media. All rights reserved + * + * This source code is subject to the terms of the BSD 3-Clause Clear License + * and the Alliance for Open Media Patent License 1.0. If the BSD 3-Clause + * Clear License was not distributed with this source code in the LICENSE file, + * you can obtain it at aomedia.org/license/software-license/bsd-3-c-c/. If the + * Alliance for Open Media Patent License 1.0 was not distributed with this + * source code in the PATENTS file, you can obtain it at + * aomedia.org/license/patent-license/. + */ + +#ifndef AVM_AV2_DECODER_DECODER_MODEL_H_ +#define AVM_AV2_DECODER_DECODER_MODEL_H_ + +#include +#include +#include + +#include "av2/common/decoder_model.h" + +#ifdef __cplusplus +extern "C" { +#endif + +struct AV2Decoder; +struct RefCntBuffer; +typedef struct Av2DecoderModelVerifier Av2DecoderModelVerifier; + +typedef enum Av2DmPresentationOwner { + AV2_DM_PRESENTATION_OWNER_CURRENT, + AV2_DM_PRESENTATION_OWNER_IMPLICIT +} Av2DmPresentationOwner; + +typedef struct Av2DmVerifierStats { + bool available; + bool failed; + uint64_t raw_obus; + uint64_t raw_bits; + uint64_t event_count; + uint64_t temporal_unit_index; + uint64_t frame_unit_index; + uint64_t closed_dfgs; + uint64_t rap_starts; + uint64_t temporal_points; + bool temporal_point_present; + uint64_t temporal_point; + uint32_t contexts; + uint64_t frame_starts; + uint64_t reference_updates; + uint64_t olk_invalidations; + uint64_t outputs; + uint64_t last_frame_start_event; + uint64_t last_reference_update_event; + uint64_t last_olk_invalidation_event; + uint64_t last_output_event; + uint64_t last_output_callback_frame_unit; + uint64_t last_output_presentation_frame_unit; + uint64_t last_output_presentation_temporal_unit; + uint64_t last_output_generation; + int last_output_presentation_xlayer_id; + int last_output_presentation_mlayer_id; + int last_output_presentation_tlayer_id; + bool last_output_uses_current_presentation; + bool replay_previous_presentation_offset_valid; + Av2DmRational replay_previous_presentation_offset; + bool replay_last_presentation_offset_valid; + Av2DmRational replay_last_presentation_offset; + uint64_t finish_event; + uint64_t result_count; + uint64_t conformant_results; + uint64_t non_conformant_results; + uint64_t indeterminate_results; + uint64_t not_applicable_results; + uint32_t live_runs; + uint32_t live_generations; + uint32_t parameter_records; +} Av2DmVerifierStats; + +typedef struct Av2DmContextStats { + Av2DmScope scope; + bool active; + bool active_configuration_present; + int active_sequence_header_id; + uint64_t pending_dfg_bits; + uint64_t last_closed_dfg_bits; + uint64_t closed_dfgs; + uint64_t configuration_generation; + bool resolved_config_present; + Av2DmApplicability resolved_applicability; + Av2DmMode resolved_mode; + uint32_t resolved_initial_display_delay; + bool last_ras_seed_complete; + uint32_t last_ras_seed_count; +} Av2DmContextStats; + +void av2_decoder_model_verifier_init(struct AV2Decoder *pbi); +void av2_decoder_model_verifier_destroy(struct AV2Decoder *pbi); + +void av2_decoder_model_verifier_on_sequence_header(struct AV2Decoder *pbi, + int xlayer_id, + int sequence_header_id); +void av2_decoder_model_verifier_on_operating_point_set(struct AV2Decoder *pbi, + int xlayer_id, + int ops_id); +void av2_decoder_model_verifier_on_active_configuration(struct AV2Decoder *pbi, + int xlayer_id, + int sequence_header_id); +void av2_decoder_model_verifier_on_buffer_removal_timing(struct AV2Decoder *pbi, + int xlayer_id); + +void av2_decoder_model_verifier_record_obu(struct AV2Decoder *pbi, int obu_type, + int xlayer_id, int mlayer_id, + int temporal_id, uint64_t obu_bits); +void av2_decoder_model_verifier_on_source_frame_unit_start( + struct AV2Decoder *pbi, int xlayer_id, int mlayer_id, int temporal_id); +void av2_decoder_model_verifier_on_obu_filtered(struct AV2Decoder *pbi); +void av2_decoder_model_verifier_on_accounting_failure(struct AV2Decoder *pbi); +void av2_decoder_model_verifier_on_internal_failure_for_testing( + struct AV2Decoder *pbi); +void av2_decoder_model_verifier_on_model_arithmetic_failure_for_testing( + struct AV2Decoder *pbi); + +void av2_decoder_model_verifier_on_temporal_point(struct AV2Decoder *pbi, + uint64_t presentation_time); +void av2_decoder_model_verifier_on_multistream_configuration( + struct AV2Decoder *pbi, int even_allocation, int large_picture_index); + +void av2_decoder_model_verifier_on_frame_wrapup_start(struct AV2Decoder *pbi); +void av2_decoder_model_verifier_on_frame_unit_complete(struct AV2Decoder *pbi); +void av2_decoder_model_verifier_on_olk_reference_invalidation( + struct AV2Decoder *pbi, uint32_t ref_valid_mask); +void av2_decoder_model_verifier_after_reference_update( + struct AV2Decoder *pbi, uint32_t refresh_frame_flags, + uint32_t ref_valid_mask); +void av2_decoder_model_verifier_on_output(struct AV2Decoder *pbi, + int frame_to_show_map_idx, + const struct RefCntBuffer *frame, + Av2DmPresentationOwner owner); +void av2_decoder_model_verifier_on_recovery_reset(struct AV2Decoder *pbi); +void av2_decoder_model_verifier_on_stream_configuration_change( + struct AV2Decoder *pbi, bool preserve_current_tu_prefix); +void av2_decoder_model_verifier_finish(struct AV2Decoder *pbi); +bool av2_decoder_model_verifier_should_stop(const struct AV2Decoder *pbi); + +bool av2_decoder_model_verifier_get_stats(const struct AV2Decoder *pbi, + Av2DmVerifierStats *stats); +bool av2_decoder_model_verifier_get_context_stats(const struct AV2Decoder *pbi, + uint32_t context_index, + Av2DmContextStats *stats); + +// Internal test support for the machine-readable violation diagnostics. +bool av2_decoder_model_violation_descriptor_is_complete( + Av2DmViolationCode code); +bool av2_decoder_model_format_violation_details(const Av2DmViolation *violation, + uint64_t max_display_rate, + uint64_t max_decode_rate, + char *text, size_t text_size); +bool av2_decoder_model_report_violation_for_testing( + const Av2DmViolation *violation, bool fatal_mode, uint64_t *violation_count, + bool *fatal_violation); + +#ifdef __cplusplus +} // extern "C" +#endif + +#endif // AVM_AV2_DECODER_DECODER_MODEL_H_ diff --git a/av2/decoder/obu.c b/av2/decoder/obu.c index 7f2f0c2808..118a42367f 100644 --- a/av2/decoder/obu.c +++ b/av2/decoder/obu.c @@ -31,9 +31,28 @@ #include "av2/common/enums.h" #include "av2/common/annexA.h" #include "av2/decoder/annexF.h" +#include "av2/decoder/decoder_model.h" static uint32_t read_temporal_delimiter_obu() { return 0; } +static void decoder_model_record_obu(AV2Decoder *pbi, + const ObuHeader *obu_header, + size_t header_bytes, + size_t payload_bytes) { + if (pbi->decoder_model_verifier == NULL) return; + const uint64_t max_obu_bytes = UINT64_MAX / 8; + if (header_bytes > max_obu_bytes || + payload_bytes > max_obu_bytes - header_bytes) { + av2_decoder_model_verifier_on_accounting_failure(pbi); + return; + } + const uint64_t obu_bits = + ((uint64_t)header_bytes + (uint64_t)payload_bytes) * 8; + av2_decoder_model_verifier_record_obu( + pbi, obu_header->type, obu_header->obu_xlayer_id, + obu_header->obu_mlayer_id, obu_header->obu_tlayer_id, obu_bits); +} + // Returns a boolean that indicates success. static int read_bitstream_level(AV2_LEVEL *seq_level_idx, struct avm_read_bit_buffer *rb) { @@ -458,11 +477,15 @@ static uint32_t read_multi_stream_decoder_operation_obu( const int multistream_even_allocation_flag = avm_rb_read_bit(rb); // read multistream_even_allocation_flag + int multistream_large_picture_idc = 0; if (!multistream_even_allocation_flag) { - const int multistream_large_picture_idc = + multistream_large_picture_idc = avm_rb_read_literal(rb, 3); // read multistream_large_picture_idc - (void)multistream_large_picture_idc; + } + if (pbi->decoder_model_verifier != NULL) { + av2_decoder_model_verifier_on_multistream_configuration( + pbi, multistream_even_allocation_flag, multistream_large_picture_idc); } for (int i = 0; i < num_streams; i++) { @@ -491,6 +514,11 @@ static uint32_t read_multi_stream_decoder_operation_obu( // Flush remaining frames from all active streams before switching config if (pbi->stream_info != NULL && config_changed) { flush_all_xlayer_frames(pbi, cm, true); + if (pbi->decoder_model_verifier != NULL) { + // The temporal delimiter and new MSDO have already been recorded and + // belong to the first DFG of the replacement configuration. + av2_decoder_model_verifier_on_stream_configuration_change(pbi, true); + } avm_free(pbi->stream_info); pbi->stream_info = NULL; pbi->glcr_stream_info_num_allocated = 0; @@ -718,6 +746,10 @@ static uint32_t read_sequence_header_obu(AV2Decoder *pbi, int xlayer_id, // cm->error.error_code is already set. return 0; } + if (pbi->decoder_model_verifier != NULL) { + av2_decoder_model_verifier_on_sequence_header(pbi, xlayer_id, + (int)seq_header_id); + } return ((rb->bit_offset - saved_bit_offset + 7) >> 3); } @@ -789,6 +821,9 @@ static uint32_t read_tilegroup_obu(AV2Decoder *pbi, // cm->error.error_code is already set. return 0; } + if (pbi->decoder_model_verifier != NULL) { + av2_decoder_model_verifier_on_frame_wrapup_start(pbi); + } header_size = (int32_t)avm_rb_bytes_read(rb); } else { if (av2_check_byte_alignment(cm, rb)) return 0; @@ -1083,6 +1118,11 @@ static void read_metadata_temporal_point_info(AV2Decoder *const pbi, AV2_COMMON *const cm = &pbi->common; cm->temporal_point_info_metadata.mtpi_frame_presentation_time = avm_rb_read_uleb(rb); + cm->temporal_point_info_present = true; + if (pbi->decoder_model_verifier != NULL) { + av2_decoder_model_verifier_on_temporal_point( + pbi, cm->temporal_point_info_metadata.mtpi_frame_presentation_time); + } uint8_t payload[1]; payload[0] = (cm->temporal_point_info_metadata.mtpi_frame_presentation_time & 0XFF); @@ -2366,6 +2406,7 @@ int avm_decode_frame_from_obus(struct AV2Decoder *pbi, const uint8_t *data, int frame_decoding_finished = 0; ObuHeader obu_header; memset(&obu_header, 0, sizeof(obu_header)); + cm->temporal_point_info_present = false; // Enable is_multistream if multiple extended layers are present. // Enable multistream_decoder_mode only when an MSDO OBU is present. @@ -2396,6 +2437,12 @@ int avm_decode_frame_from_obus(struct AV2Decoder *pbi, const uint8_t *data, // Flush and reset like a config change if (pbi->stream_info != NULL) { flush_all_xlayer_frames(pbi, cm, true); + if (pbi->decoder_model_verifier != NULL) { + // This transition is detected before the current frame unit's + // OBUs are parsed, so any retained OBU working set is stale. + av2_decoder_model_verifier_on_stream_configuration_change(pbi, + false); + } avm_free(pbi->stream_info); pbi->stream_info = NULL; pbi->glcr_stream_info_num_allocated = 0; @@ -2518,6 +2565,8 @@ int avm_decode_frame_from_obus(struct AV2Decoder *pbi, const uint8_t *data, return -1; } + decoder_model_record_obu(pbi, &obu_header, bytes_read, payload_size); + // Annex F: Sub-bitstream extraction. // When extraction is enabled, trigger retention map construction when // transitioning from structural OBUs to non-structural OBUs, then @@ -2532,6 +2581,9 @@ int avm_decode_frame_from_obus(struct AV2Decoder *pbi, const uint8_t *data, if (!av2_sbe_should_retain_obu( &pbi->sbe_state, obu_header.type, obu_header.obu_xlayer_id, obu_header.obu_mlayer_id, obu_header.obu_tlayer_id)) { + if (pbi->decoder_model_verifier != NULL) { + av2_decoder_model_verifier_on_obu_filtered(pbi); + } pbi->sbe_state.obus_removed++; data += payload_size; continue; @@ -3104,6 +3156,8 @@ int avm_decode_frame_from_obus(struct AV2Decoder *pbi, const uint8_t *data, return -1; } + decoder_model_record_obu(pbi, &obu_header, bytes_read, payload_size); + if (obu_header.type == OBU_PADDING) { decoded_payload_size = read_padding(cm, data, payload_size); obu_info *const curr_obu_info = &obu_list[count_obus_with_frame_unit]; diff --git a/av2/decoder/obu_buf.c b/av2/decoder/obu_buf.c index 2f3652fe1d..aa811cc3c1 100644 --- a/av2/decoder/obu_buf.c +++ b/av2/decoder/obu_buf.c @@ -24,6 +24,7 @@ #include "av2/common/timing.h" #include "av2/decoder/decoder.h" #include "av2/decoder/decodeframe.h" +#include "av2/decoder/decoder_model.h" #include "av2/decoder/obu.h" uint32_t av2_read_buffer_removal_timing_obu(struct AV2Decoder *pbi, @@ -86,5 +87,8 @@ uint32_t av2_read_buffer_removal_timing_obu(struct AV2Decoder *pbi, // cm->error.error_code is already set. return 0; } + if (pbi->decoder_model_verifier != NULL) { + av2_decoder_model_verifier_on_buffer_removal_timing(pbi, xlayer_id); + } return ((rb->bit_offset - saved_bit_offset + 7) >> 3); } diff --git a/av2/decoder/obu_ops.c b/av2/decoder/obu_ops.c index d98ba4a570..6bcde44c28 100644 --- a/av2/decoder/obu_ops.c +++ b/av2/decoder/obu_ops.c @@ -19,6 +19,7 @@ #include "av2/decoder/decoder.h" #include "av2/decoder/decodeframe.h" #include "av2/decoder/obu.h" +#include "av2/decoder/decoder_model.h" static void read_ops_mlayer_info(int xLId, struct OpsMLayerInfo *ops_mlayer_info, @@ -170,8 +171,8 @@ uint32_t av2_read_operating_point_set_obu(struct AV2Decoder *pbi, if (op->ops_decoder_model_info_for_this_op_present_flag) { read_ops_decoder_model_info(&op->decoder_model_info, rb); } - int ops_initial_display_delay_present_flag = avm_rb_read_bit(rb); - if (ops_initial_display_delay_present_flag) { + op->ops_initial_display_delay_present_flag = avm_rb_read_bit(rb); + if (op->ops_initial_display_delay_present_flag) { int ops_initial_display_delay_minus_1 = avm_rb_read_literal(rb, 4); op->ops_initial_display_delay = ops_initial_display_delay_minus_1 + 1; } else { @@ -291,5 +292,9 @@ uint32_t av2_read_operating_point_set_obu(struct AV2Decoder *pbi, return 0; } ops->valid = 1; + if (pbi->decoder_model_verifier != NULL) { + av2_decoder_model_verifier_on_operating_point_set(pbi, obu_xlayer_id, + ops_id); + } return ((rb->bit_offset - saved_bit_offset + 7) >> 3); } diff --git a/avm/avmdx.h b/avm/avmdx.h index e83a34305f..0f263ffbaa 100644 --- a/avm/avmdx.h +++ b/avm/avmdx.h @@ -173,6 +173,15 @@ typedef struct av2_ext_ref_frame { int num; } av2_ext_ref_frame_t; +/*!\enum avm_decoder_model_check_mode + * \brief Decoder-model conformance verification mode. + */ +typedef enum avm_decoder_model_check_mode { + AVM_DECODER_MODEL_CHECK_OFF = 0, + AVM_DECODER_MODEL_CHECK_FATAL, + AVM_DECODER_MODEL_CHECK_WARN, +} avm_decoder_model_check_mode_t; + /*!\enum avm_dec_control_id * \brief AVM decoder control functions * @@ -399,6 +408,14 @@ enum avm_dec_control_id { /*!\brief Codec control function to advance output_frames_offset by given step */ AVMD_INCR_OUTPUT_FRAMES_OFFSET, + + /*!\brief Codec control function to set decoder-model conformance checking, + * avm_decoder_model_check_mode_t parameter. + * + * The default is AVM_DECODER_MODEL_CHECK_OFF. The mode must be set before + * compressed input is submitted. + */ + AV2D_SET_DECODER_MODEL_CHECK_MODE, }; /*!\cond */ @@ -510,6 +527,10 @@ AVM_CTRL_USE_TYPE(AV2D_SET_SELECTED_LOCAL_OPS, int *) AVM_CTRL_USE_TYPE(AV2D_SET_OUTPUT_ALL_LAYERS, int) #define AVM_CTRL_AV2D_SET_OUTPUT_ALL_LAYERS +AVM_CTRL_USE_TYPE(AV2D_SET_DECODER_MODEL_CHECK_MODE, + avm_decoder_model_check_mode_t) +#define AVM_CTRL_AV2D_SET_DECODER_MODEL_CHECK_MODE + AVM_CTRL_USE_TYPE(AV2_SET_INSPECTION_CALLBACK, avm_inspect_init *) #define AVM_CTRL_AV2_SET_INSPECTION_CALLBACK /*!\endcond */ diff --git a/avm_dsp/bitreader.c b/avm_dsp/bitreader.c index ef46182ec9..222141a931 100644 --- a/avm_dsp/bitreader.c +++ b/avm_dsp/bitreader.c @@ -18,6 +18,8 @@ int avm_reader_init(avm_reader *r, const uint8_t *buffer, size_t size) { } r->buffer_end = buffer + size; r->buffer = buffer; + r->count_frame_symbols = 1; + r->frame_symbol_count = 0; avm_od_ec_dec_init(&r->ec, buffer, (uint32_t)size); #if CONFIG_ACCOUNTING r->accounting = NULL; diff --git a/avm_dsp/bitreader.h b/avm_dsp/bitreader.h index 3be50a72db..08c5971f36 100644 --- a/avm_dsp/bitreader.h +++ b/avm_dsp/bitreader.h @@ -82,6 +82,8 @@ struct avm_reader { Accounting *accounting; #endif uint8_t allow_update_cdf; + uint8_t count_frame_symbols; + uint64_t frame_symbol_count; }; typedef struct avm_reader avm_reader; @@ -270,6 +272,7 @@ static INLINE int avm_read_literal_(avm_reader *r, int bits ACCT_INFO_PARAM) { literal += od_ec_decode_literal_bypass(&r->ec, n); n_bits -= n; } + if (r->count_frame_symbols) r->frame_symbol_count += (uint64_t)bits; #if CONFIG_BITSTREAM_DEBUG bitstream_queue_pop_literal(literal, bits); #endif // CONFIG_BITSTREAM_DEBUG @@ -368,6 +371,7 @@ static INLINE int avm_read_symbol_(avm_reader *r, avm_cdf_prob *cdf, int nsymbs ACCT_INFO_PARAM) { int ret; ret = avm_read_cdf(r, cdf, nsymbs, ACCT_INFO_NAME); + if (r->count_frame_symbols) ++r->frame_symbol_count; if (r->allow_update_cdf) update_cdf(cdf, ret, nsymbs); return ret; } @@ -389,6 +393,7 @@ static INLINE int avm_read_symbol_probdata(avm_reader *r, avm_cdf_prob *cdf, ProbModelInfo prob_info) { FILE *filedata = prob_info.fDataCollect; const int symLength = prob_info.num_symb; + if (r->count_frame_symbols) ++r->frame_symbol_count; // Estimated probability and counter information const int counter_engine = (int)cdf[symLength]; for (int i = 0; i < prob_info.num_dim; i++) { diff --git a/test/brt_test.cc b/test/brt_test.cc index d604b2a811..f016c5c814 100644 --- a/test/brt_test.cc +++ b/test/brt_test.cc @@ -16,6 +16,7 @@ #include "av2/encoder/brt_syntax.h" #include "av2/decoder/decoder.h" +#include "av2/decoder/decoder_model.h" #include "av2/decoder/decodeframe.h" extern "C" { #include "av2/decoder/obu.h" @@ -59,6 +60,8 @@ class BrtTest : public ::testing::Test { }; TEST_F(BrtTest, NonOpsDependent) { + av2_decoder_model_verifier_init(pbi_); + ASSERT_NE(pbi_->decoder_model_verifier, nullptr); BufferRemovalTimingInfo src; memset(&src, 0, sizeof(src)); src.br_ops_dependent_flag = 0; @@ -74,6 +77,11 @@ TEST_F(BrtTest, NonOpsDependent) { EXPECT_EQ(pbi_->common.brt_info.br_ops_dependent_flag, 0); EXPECT_EQ(pbi_->common.brt_info.br_time, 42); + + Av2DmVerifierStats stats; + ASSERT_TRUE(av2_decoder_model_verifier_get_stats(pbi_, &stats)); + EXPECT_EQ(stats.event_count, 1u); + av2_decoder_model_verifier_destroy(pbi_); } TEST_F(BrtTest, OpsDependentWithModel) { diff --git a/test/decoder_model_integration_test.cc b/test/decoder_model_integration_test.cc new file mode 100644 index 0000000000..a48dd2a5c1 --- /dev/null +++ b/test/decoder_model_integration_test.cc @@ -0,0 +1,2305 @@ +/* + * Copyright (c) 2026, Alliance for Open Media. All rights reserved + * + * This source code is subject to the terms of the BSD 3-Clause Clear License + * and the Alliance for Open Media Patent License 1.0. If the BSD 3-Clause + * Clear License was not distributed with this source code in the LICENSE file, + * you can obtain it at aomedia.org/license/software-license/bsd-3-c-c/. If the + * Alliance for Open Media Patent License 1.0 was not distributed with this + * source code in the PATENTS file, you can obtain it at + * aomedia.org/license/patent-license/. + */ + +#include +#include +#include +#include +#include + +#include "third_party/googletest/src/googletest/include/gtest/gtest.h" + +#include "avm/avm_encoder.h" +#include "avm/avmcx.h" +#include "avm_dsp/bitreader_buffer.h" +#include "avm_mem/avm_mem.h" +#include "av2/common/decoder_model.h" +#include "av2/common/obu_util.h" +#include "av2/decoder/decoder.h" +#include "av2/decoder/decoder_model.h" +#include "test/codec_factory.h" +#include "test/encode_test_driver.h" +#include "test/i420_video_source.h" + +namespace { + +class DecoderModelAdapterTestBase : public ::testing::Test { + protected: + void SetUp() override { + pbi_ = static_cast(avm_memalign(32, sizeof(*pbi_))); + ASSERT_NE(pbi_, nullptr); + memset(pbi_, 0, sizeof(*pbi_)); + memset(&frame_, 0, sizeof(frame_)); + memset(&second_frame_, 0, sizeof(second_frame_)); + av2_decoder_model_verifier_init(pbi_); + ASSERT_NE(pbi_->decoder_model_verifier, nullptr); + Configure(64, 64); + } + + void TearDown() override { + av2_decoder_model_verifier_destroy(pbi_); + avm_free(pbi_); + } + + void Configure(int width, int height) { + SequenceHeader *const sequence = &pbi_->seq_list[0][0]; + memset(sequence, 0, sizeof(*sequence)); + sequence->seq_header_id = 0; + sequence->seq_max_level_idx = SEQ_LEVEL_2_0; + sequence->seq_tier = 0; + sequence->seq_profile_idc = MAIN_420_10_IP0; + sequence->ref_frames = 8; + sequence->max_frame_width = width; + sequence->max_frame_height = height; + sequence->seq_max_mlayer_cnt = 1; + sequence->seq_max_display_model_info_present_flag = 1; + sequence->seq_max_initial_display_delay_minus_1 = 0; + sequence->decoder_model_info.num_units_in_decoding_tick = 1; + sequence->still_picture = 1; + pbi_->common.seq_params = *sequence; + ContentInterpretation *const ci = &pbi_->common.ci_params_per_layer[0]; + ci->ci_timing_info_present_flag = 1; + ci->timing_info.num_units_in_display_tick = 1; + ci->timing_info.time_scale = 30; + ci->timing_info.equal_elemental_interval = 1; + ci->timing_info.num_ticks_per_elemental_duration = 1; + av2_decoder_model_verifier_on_sequence_header(pbi_, 0, 0); + av2_decoder_model_verifier_on_active_configuration(pbi_, 0, 0); + } + + void StartFrame(int obu_type, int width = 64, int height = 64, + RefCntBuffer *frame = nullptr, int mlayer_id = 0, + FRAME_TYPE frame_type = KEY_FRAME, + bool implicit_output = false, bool complete = true, + int xlayer_id = 0, int temporal_id = 0, + bool activate_configuration = false, + bool filter_obu = false) { + if (frame == nullptr) frame = &frame_; + av2_decoder_model_verifier_on_source_frame_unit_start( + pbi_, xlayer_id, mlayer_id, temporal_id); + av2_decoder_model_verifier_record_obu(pbi_, obu_type, xlayer_id, mlayer_id, + temporal_id, 800); + if (filter_obu) av2_decoder_model_verifier_on_obu_filtered(pbi_); + pbi_->obu_type = static_cast(obu_type); + AV2_COMMON *const cm = &pbi_->common; + cm->xlayer_id = xlayer_id; + cm->mlayer_id = mlayer_id; + cm->tlayer_id = temporal_id; + cm->is_leading_picture = 0; + cm->show_existing_frame = 0; + cm->implicit_output_picture = implicit_output; + cm->cur_frame = frame; + cm->width = width; + cm->height = height; + cm->mi_params.mi_cols = (width + MI_SIZE - 1) / MI_SIZE; + cm->mi_params.mi_rows = (height + MI_SIZE - 1) / MI_SIZE; + cm->mib_size_log2 = 0; + cm->tiles.cols = 1; + cm->tiles.rows = 1; + cm->tiles.col_start_sb[0] = 0; + cm->tiles.col_start_sb[1] = cm->mi_params.mi_cols; + cm->tiles.row_start_sb[0] = 0; + cm->tiles.row_start_sb[1] = cm->mi_params.mi_rows; + cm->current_frame.frame_type = frame_type; + cm->current_frame.refresh_frame_flags = 1; + frame->xlayer_id = xlayer_id; + frame->mlayer_id = mlayer_id; + frame->tlayer_id = temporal_id; + frame->width = width; + frame->height = height; + frame->implicit_output_picture = implicit_output; + if (activate_configuration) { + av2_decoder_model_verifier_on_active_configuration(pbi_, xlayer_id, 0); + } + av2_decoder_model_verifier_on_frame_wrapup_start(pbi_); + if (complete) { + av2_decoder_model_verifier_record_obu(pbi_, OBU_METADATA_SHORT, xlayer_id, + mlayer_id, temporal_id, 80); + av2_decoder_model_verifier_on_frame_unit_complete(pbi_); + } + } + + void UpdateAndOutput() { + pbi_->common.ref_frame_map[0] = &frame_; + pbi_->valid_for_referencing[0] = 1; + av2_decoder_model_verifier_after_reference_update(pbi_, 1, 1); + av2_decoder_model_verifier_on_output(pbi_, -1, &frame_, + AV2_DM_PRESENTATION_OWNER_CURRENT); + } + + AV2Decoder *pbi_ = nullptr; + RefCntBuffer frame_; + RefCntBuffer second_frame_; +}; + +class DecoderModelHookOrderTest : public DecoderModelAdapterTestBase {}; +class DecoderModelResultTest : public DecoderModelAdapterTestBase {}; + +size_t CountOccurrences(const std::string &text, const std::string &pattern) { + size_t count = 0; + for (size_t position = 0; + (position = text.find(pattern, position)) != std::string::npos; + position += pattern.size()) { + ++count; + } + return count; +} + +void ExpectAdapterRational(const Av2DmRational &actual, uint64_t numerator, + uint64_t denominator) { + Av2DmRational expected; + ASSERT_TRUE(av2_dm_rational_make(numerator, denominator, &expected)); + int comparison = 1; + ASSERT_TRUE(av2_dm_rational_compare(&actual, &expected, &comparison)); + EXPECT_EQ(comparison, 0); +} + +TEST(DecoderModelDiagnosticTest, DescriptorsAreExhaustiveAndRangeSafe) { + struct ExpectedDescriptor { + const char *spec; + const char *condition; + const char *relation; + const char *observed; + const char *limit; + const char *unit; + const char *requirement; + const char *margin; + bool lower_bound; + }; +#define EXPECTED_DESCRIPTOR(spec, condition, relation, observed, limit, unit, \ + requirement, margin, lower_bound) \ + { \ + spec, condition, relation, observed, limit, unit, requirement, margin, \ + lower_bound \ + } + static const ExpectedDescriptor expected[] = { + EXPECTED_DESCRIPTOR("annex_e.decoder_model_error_codes", + "free_decode_frame_buffer_available", "available", + "free_buffers", "required_free_buffers", "buffers", + "available", nullptr, false), + EXPECTED_DESCRIPTOR("annex_e.decoder_model_error_codes", + "show_existing_reference_buffer_available", "available", + "reference_buffer_state", "required_buffer_state", + "buffers", "available", nullptr, false), + EXPECTED_DESCRIPTOR("annex_e.decoder_model_error_codes", + "output_time_lte_presentation_time", "lte", + "output_time", "presentation_time", "seconds", + "maximum", "lateness", false), + EXPECTED_DESCRIPTOR("annex_e.smoothing_buffer_underflow", + "scheduled_removal_gte_last_bit_arrival", "gte", + "scheduled_removal", "last_bit_arrival", "seconds", + "minimum", "lateness", true), + EXPECTED_DESCRIPTOR("annex_e.smoothing_buffer_overflow", + "buffer_fullness_lte_buffer_size", "lte", + "buffer_fullness_bits", "buffer_size_bits", "bits", + "maximum", "excess_bits", false), + EXPECTED_DESCRIPTOR("annex_e.bitstream_conformance.general", + "presentation_time_gte_previous_presentation_time", + "gte", "presentation_time", + "previous_presentation_time", "seconds", "minimum", + "shortfall", true), + EXPECTED_DESCRIPTOR("annex_e.bitstream_conformance.general", + "scheduled_removal_gte_resource_removal", "gte", + "scheduled_removal", "resource_removal", "seconds", + "minimum", "shortfall", true), + EXPECTED_DESCRIPTOR("annex_e.decoder_buffer_delay_consistency", + "decoder_buffer_delay_lte_ceil_time_delta", "lte", + "time_delta_ticks", + "decoder_buffer_delay_minus_one_ticks", "ticks", + "maximum", nullptr, false), + EXPECTED_DESCRIPTOR( + "annex_e.minimum_decode_time", + "available_decode_interval_gte_required_decode_interval", "gte", + "available_decode_interval", "required_decode_interval", "seconds", + "minimum", "shortfall", true), + EXPECTED_DESCRIPTOR( + "annex_e.minimum_presentation_interval", + "presentation_interval_gte_required_presentation_interval", "gte", + "presentation_interval", "required_presentation_interval", "seconds", + "minimum", "shortfall", true), + EXPECTED_DESCRIPTOR("annex_e.decode_deadline", + "decode_completion_time_lte_presentation_time", "lte", + "decode_completion_time", "presentation_time", + "seconds", "maximum", "lateness", false), + EXPECTED_DESCRIPTOR("annex_e.level_imposed_constraints", + "decoder_buffer_delay_nonzero", "nonzero", + "decoder_buffer_delay", "zero", "seconds", "nonzero", + nullptr, false), + EXPECTED_DESCRIPTOR("annex_e.level_imposed_constraints", + "decoder_buffer_delay_lte_maximum", "lte", + "decoder_buffer_delay", "maximum_decoder_buffer_delay", + "seconds", "maximum", "excess", false), + EXPECTED_DESCRIPTOR("annex_a.levels", + "frame_luma_samples_lte_max_picture_size", "lte", + "frame_luma_samples", "max_picture_size", + "luma_samples", "maximum", "excess", false), + EXPECTED_DESCRIPTOR("annex_a.levels", "frame_width_lte_max_horizontal_size", + "lte", "frame_width", "max_horizontal_size", + "luma_samples", "maximum", "excess", false), + EXPECTED_DESCRIPTOR("annex_a.levels", "frame_height_lte_max_vertical_size", + "lte", "frame_height", "max_vertical_size", + "luma_samples", "maximum", "excess", false), + EXPECTED_DESCRIPTOR("annex_a.levels", "frame_width_gte_16", "gte", + "frame_width", "min_horizontal_size", "luma_samples", + "minimum", "shortfall", true), + EXPECTED_DESCRIPTOR("annex_a.levels", "frame_height_gte_16", "gte", + "frame_height", "min_vertical_size", "luma_samples", + "minimum", "shortfall", true), + EXPECTED_DESCRIPTOR("annex_a.levels", "num_tiles_lte_max_tiles", "lte", + "num_tiles", "max_tiles", "tiles", "maximum", "excess", + false), + EXPECTED_DESCRIPTOR("annex_a.levels", "tile_columns_lte_max_tile_columns", + "lte", "tile_columns", "max_tile_columns", + "tile_columns", "maximum", "excess", false), + EXPECTED_DESCRIPTOR("annex_a.levels", "tile_width_lte_max_tile_width", + "lte", "tile_width", "max_tile_width", "luma_samples", + "maximum", "excess", false), + EXPECTED_DESCRIPTOR("annex_a.levels", "non_rightmost_tile_width_gte_64", + "gte", "offending_tile_width", "min_tile_width", + "luma_samples", "minimum", nullptr, true), + EXPECTED_DESCRIPTOR("annex_a.levels", "tile_area_lte_max_tile_area", "lte", + "tile_area", "max_tile_area", "luma_samples", "maximum", + "excess", false), + EXPECTED_DESCRIPTOR( + "annex_a.levels", "display_luma_samples_lte_output_interval_capacity", + "lte", "display_luma_samples", "display_capacity", + "luma_samples_per_interval", "maximum", "excess", false), + EXPECTED_DESCRIPTOR("annex_a.levels", "frame_headers_lte_max_header_rate", + "lte", "frame_headers_in_window", + "max_frame_headers_in_window", + "frame_headers_per_second", "maximum", "excess", false), + EXPECTED_DESCRIPTOR("annex_a.levels", + "num_ref_frames_lte_max_level_ref_frames", "lte", + "num_ref_frames", "max_level_ref_frames", + "reference_frames", "maximum", "excess", false), + EXPECTED_DESCRIPTOR( + "annex_a.levels", "luma_sample_count_lte_frame_parsing_capacity", "lte", + "luma_sample_count", "frame_parsing_capacity", + "luma_samples_per_interval", "maximum", "excess", false), + EXPECTED_DESCRIPTOR("annex_a.levels", + "num_tiles_lte_frame_parsing_tile_limit", "lte", + "num_tiles", "frame_parsing_tile_limit", + "tiles_per_interval", "maximum", "excess", false), + EXPECTED_DESCRIPTOR("annex_a.levels", "compressed_size_lte_derived_maximum", + "lte", "compressed_size", "maximum_compressed_size", + "bytes", "maximum", "excess", false), + EXPECTED_DESCRIPTOR("annex_a.levels", + "frame_symbol_count_lte_derived_maximum", "lte", + "frame_symbol_count", "maximum_frame_symbols", + "symbols", "maximum", "excess", false), + EXPECTED_DESCRIPTOR( + "annex_a.levels", "max_tile_area_times_header_rate_lte_level_limit", + "lte", "tile_area_header_rate_product", + "max_tile_area_header_rate_product", + "luma_samples_x_headers_per_second", "maximum", "excess", false), + }; +#undef EXPECTED_DESCRIPTOR + static_assert(sizeof(expected) / sizeof(expected[0]) == + AV2_DM_VIOLATION_TILE_SIZE_HEADER_RATE + 1, + "Update this test when a violation code is added"); + + for (int code = AV2_DM_VIOLATION_DECODE_FRAME_BUFFER_UNAVAILABLE; + code <= AV2_DM_VIOLATION_TILE_SIZE_HEADER_RATE; ++code) { + const auto violation_code = static_cast(code); + EXPECT_TRUE( + av2_decoder_model_violation_descriptor_is_complete(violation_code)); + Av2DmViolation violation{}; + violation.code = violation_code; + violation.observed_present = true; + violation.limit_present = true; + const uint64_t observed = expected[code].lower_bound ? 9 : 11; + ASSERT_TRUE(av2_dm_rational_make(observed, 1, &violation.observed)); + ASSERT_TRUE(av2_dm_rational_make(10, 1, &violation.limit)); + char details[1024]; + ASSERT_TRUE(av2_decoder_model_format_violation_details( + &violation, 1000000, 1000000, details, sizeof(details))); + const std::string formatted(details); + const ExpectedDescriptor &descriptor = expected[code]; + EXPECT_NE(formatted.find(std::string("unit=") + descriptor.unit + + " requirement=" + descriptor.requirement + + " relation=" + descriptor.relation + + " condition=" + descriptor.condition), + std::string::npos) + << code << ": " << formatted; + EXPECT_NE(formatted.find(std::string(" ") + descriptor.observed + "=" + + std::to_string(observed)), + std::string::npos) + << code << ": " << formatted; + EXPECT_NE(formatted.find(std::string(" ") + descriptor.limit + "=10"), + std::string::npos) + << code << ": " << formatted; + if (descriptor.margin != nullptr) { + EXPECT_NE(formatted.find(std::string(" ") + descriptor.margin + "=1"), + std::string::npos) + << code << ": " << formatted; + } + EXPECT_NE(formatted.find(std::string(" spec=") + descriptor.spec), + std::string::npos) + << code << ": " << formatted; + EXPECT_LT(formatted.size(), sizeof(details)) << code; + } + + const auto unknown = static_cast(999); + EXPECT_FALSE(av2_decoder_model_violation_descriptor_is_complete(unknown)); + Av2DmViolation violation{}; + violation.code = unknown; + char details[128]; + ASSERT_TRUE(av2_decoder_model_format_violation_details( + &violation, 0, 0, details, sizeof(details))); + EXPECT_STREQ(details, + "unit=value requirement=unknown relation=unknown " + "condition=unknown_violation spec=unknown"); +} + +TEST(DecoderModelDiagnosticTest, UnderflowAndOverflowUseExactNamedOperands) { + Av2DmViolation underflow{}; + underflow.code = AV2_DM_VIOLATION_SMOOTHING_BUFFER_UNDERFLOW; + underflow.event_index = 108; + underflow.affected_index = 108; + underflow.observed_present = true; + underflow.limit_present = true; + ASSERT_TRUE(av2_dm_rational_make(28906, 27225, &underflow.observed)); + ASSERT_TRUE(av2_dm_rational_make(460253, 375000, &underflow.limit)); + char details[1024]; + ASSERT_TRUE(av2_decoder_model_format_violation_details( + &underflow, 0, 0, details, sizeof(details))); + EXPECT_STREQ(details, + "unit=seconds requirement=minimum relation=gte " + "condition=scheduled_removal_gte_last_bit_arrival " + "scheduled_removal=28906/27225 last_bit_arrival=460253/375000 " + "lateness=22541839/136125000 scheduled_removal_ms=1061.745 " + "last_bit_arrival_ms=1227.341 lateness_ms=165.597 " + "spec=annex_e.smoothing_buffer_underflow"); + + Av2DmViolation overflow{}; + overflow.code = AV2_DM_VIOLATION_SMOOTHING_BUFFER_OVERFLOW; + overflow.observed_present = true; + overflow.limit_present = true; + ASSERT_TRUE(av2_dm_rational_make(101, 1, &overflow.observed)); + ASSERT_TRUE(av2_dm_rational_make(100, 1, &overflow.limit)); + ASSERT_TRUE(av2_decoder_model_format_violation_details( + &overflow, 0, 0, details, sizeof(details))); + EXPECT_STREQ(details, + "unit=bits requirement=maximum relation=lte " + "condition=buffer_fullness_lte_buffer_size " + "buffer_fullness_bits=101 buffer_size_bits=100 excess_bits=1 " + "spec=annex_e.smoothing_buffer_overflow"); + EXPECT_EQ(std::string(details).find("underflow"), std::string::npos); +} + +TEST(DecoderModelDiagnosticTest, TypedAvailabilityAndDelayDetailsAreExact) { + Av2DmViolation unavailable{}; + unavailable.code = AV2_DM_VIOLATION_DECODE_FRAME_BUFFER_UNAVAILABLE; + unavailable.detail.kind = AV2_DM_VIOLATION_DETAIL_BUFFER_POOL; + unavailable.detail.value.buffer_pool = { false, 10, 10, 0, 8, 2 }; + char details[1024]; + ASSERT_TRUE(av2_decoder_model_format_violation_details( + &unavailable, 0, 0, details, sizeof(details))); + EXPECT_NE(std::string(details).find( + "lane=model pool_size=10 frames_in_use=10 free_buffers=0 " + "decoder_held_buffers=8 player_held_buffers=2"), + std::string::npos); + + Av2DmViolation empty{}; + empty.code = AV2_DM_VIOLATION_DECODE_EXISTING_FRAME_BUFFER_EMPTY; + empty.detail.kind = AV2_DM_VIOLATION_DETAIL_REFERENCE_SLOT; + empty.detail.value.reference_slot.requested_slot = 3; + empty.detail.value.reference_slot.slot_in_range = true; + empty.detail.value.reference_slot.reference_valid = true; + empty.detail.value.reference_slot.buffer_index = -1; + empty.detail.value.reference_slot.pool = { false, 10, 4, 6, 4, 1 }; + ASSERT_TRUE(av2_decoder_model_format_violation_details(&empty, 0, 0, details, + sizeof(details))); + EXPECT_NE(std::string(details).find( + "requested_reference_slot=3 slot_in_range=1 ref_valid=1 " + "vbi=-1 pool_size=10 frames_in_use=4 free_buffers=6"), + std::string::npos); + + empty.detail.value.reference_slot.requested_slot = 8; + empty.detail.value.reference_slot.slot_in_range = false; + empty.detail.value.reference_slot.reference_valid = false; + empty.detail.value.reference_slot.buffer_index = -1; + ASSERT_TRUE(av2_decoder_model_format_violation_details(&empty, 0, 0, details, + sizeof(details))); + EXPECT_NE(std::string(details).find( + "requested_reference_slot=8 slot_in_range=0 ref_valid=NA " + "vbi=NA pool_size=10 frames_in_use=4 free_buffers=6"), + std::string::npos); + + Av2DmViolation delay{}; + delay.code = AV2_DM_VIOLATION_DECODER_BUFFER_DELAY_INCONSISTENT; + delay.observed_present = true; + delay.limit_present = true; + ASSERT_TRUE(av2_dm_rational_make(4, 1, &delay.observed)); + ASSERT_TRUE(av2_dm_rational_make(4, 1, &delay.limit)); + delay.detail.kind = AV2_DM_VIOLATION_DETAIL_DELAY_CONSISTENCY; + delay.detail.value.delay_consistency.decoder_buffer_delay_ticks = 5; + delay.detail.value.delay_consistency.ceil_time_delta_present = true; + ASSERT_TRUE(av2_dm_rational_make( + 4, 1, &delay.detail.value.delay_consistency.ceil_time_delta_ticks)); + ASSERT_TRUE(av2_decoder_model_format_violation_details(&delay, 0, 0, details, + sizeof(details))); + EXPECT_NE(std::string(details).find( + "unit=ticks requirement=maximum relation=lte " + "condition=decoder_buffer_delay_lte_ceil_time_delta " + "time_delta_ticks=4 decoder_buffer_delay_minus_one_ticks=4 " + "decoder_buffer_delay_ticks=5 " + "ceil_time_delta_ticks=4 decoder_buffer_delay_excess=1"), + std::string::npos); +} + +TEST(DecoderModelDiagnosticTest, DerivedIntervalsAndRatesRemainExplicit) { + Av2DmViolation minimum_decode{}; + minimum_decode.code = AV2_DM_VIOLATION_MINIMUM_DECODE_TIME; + minimum_decode.observed_present = true; + minimum_decode.limit_present = true; + ASSERT_TRUE(av2_dm_rational_make(1, 200, &minimum_decode.observed)); + ASSERT_TRUE(av2_dm_rational_make(1, 100, &minimum_decode.limit)); + minimum_decode.detail.kind = AV2_DM_VIOLATION_DETAIL_MINIMUM_DECODE_TIME; + ASSERT_TRUE(av2_dm_rational_make( + 1, 100, + &minimum_decode.detail.value.minimum_decode_time.frame_decode_time)); + ASSERT_TRUE(av2_dm_rational_make( + 1, 120, + &minimum_decode.detail.value.minimum_decode_time.one_header_time)); + char details[1024]; + ASSERT_TRUE(av2_decoder_model_format_violation_details( + &minimum_decode, 0, 0, details, sizeof(details))); + EXPECT_NE(std::string(details).find( + "required_decode_interval=1/100 shortfall=1/200 " + "frame_decode_time=1/100 one_header_time=1/120"), + std::string::npos); + + Av2DmViolation tile_rate{}; + tile_rate.code = AV2_DM_VIOLATION_FRAME_TILE_RATE; + tile_rate.observed_present = true; + tile_rate.limit_present = true; + ASSERT_TRUE(av2_dm_rational_make(3, 1, &tile_rate.observed)); + ASSERT_TRUE(av2_dm_rational_make(2, 1, &tile_rate.limit)); + tile_rate.detail.kind = AV2_DM_VIOLATION_DETAIL_FRAME_INTERVAL; + ASSERT_TRUE( + av2_dm_rational_make(1, 60, &tile_rate.detail.value.frame_interval)); + ASSERT_TRUE(av2_decoder_model_format_violation_details( + &tile_rate, 0, 0, details, sizeof(details))); + EXPECT_NE(std::string(details).find("frame_parsing_interval=1/60 " + "frame_parsing_interval_ms=16.667 " + "observed_tile_rate=180.000tiles/s " + "limit_tile_rate=120.000tiles/s"), + std::string::npos); +} + +TEST(DecoderModelDiagnosticTest, FormattingIsBoundedForMaximumWidthValues) { + Av2DmViolation violation{}; + violation.code = AV2_DM_VIOLATION_MAX_FRAME_SYMBOLS; + violation.observed_present = true; + violation.observed.magnitude = { { UINT64_MAX, UINT64_MAX, UINT64_MAX, + UINT64_MAX } }; + violation.observed.denominator = { { 1, 0, 0, 0 } }; + char details[1024]; + ASSERT_TRUE(av2_decoder_model_format_violation_details( + &violation, 0, 0, details, sizeof(details))); + EXPECT_NE(std::string(details).find("frame_symbol_count=0xffffffffffffffff"), + std::string::npos); + char too_small[8]; + EXPECT_FALSE(av2_decoder_model_format_violation_details( + &violation, 0, 0, too_small, sizeof(too_small))); + + // A huge optional decimal cannot be represented by the bounded formatter. + violation.code = AV2_DM_VIOLATION_DECODER_BUFFER_DELAY_ZERO; + violation.observed.magnitude = { { 0, 1, 0, 0 } }; + EXPECT_FALSE(av2_decoder_model_format_violation_details( + &violation, 0, 0, details, sizeof(details))); + + uint64_t violation_count = 0; + bool fatal_violation = true; + testing::internal::CaptureStderr(); + ASSERT_TRUE(av2_decoder_model_report_violation_for_testing( + &violation, false, &violation_count, &fatal_violation)); + const std::string warning = testing::internal::GetCapturedStderr(); + EXPECT_NE(warning.find("AV2_DECODER_MODEL_WARNING status=NON_CONFORMANT"), + std::string::npos); + EXPECT_NE(warning.find(" details=unavailable\n"), std::string::npos); + EXPECT_EQ(violation_count, 1u); + EXPECT_FALSE(fatal_violation); + + violation_count = 0; + testing::internal::CaptureStderr(); + ASSERT_TRUE(av2_decoder_model_report_violation_for_testing( + &violation, true, &violation_count, &fatal_violation)); + const std::string fatal_warning = testing::internal::GetCapturedStderr(); + EXPECT_NE( + fatal_warning.find("AV2_DECODER_MODEL_WARNING status=NON_CONFORMANT"), + std::string::npos); + EXPECT_NE(fatal_warning.find(" details=unavailable\n"), std::string::npos); + EXPECT_EQ(violation_count, 1u); + EXPECT_TRUE(fatal_violation); +} + +TEST(DecoderModelDiagnosticTest, FormattingIsIndependentOfNumericLocale) { + Av2DmViolation timing{}; + timing.code = AV2_DM_VIOLATION_DISPLAY_FRAME_LATE; + timing.observed_present = true; + timing.limit_present = true; + ASSERT_TRUE(av2_dm_rational_make(1, 3, &timing.observed)); + ASSERT_TRUE(av2_dm_rational_make(1, 4, &timing.limit)); + const char *const previous_locale = std::setlocale(LC_NUMERIC, nullptr); + const std::string saved_locale = + previous_locale == nullptr ? "C" : previous_locale; + const char *selected_locale = std::setlocale(LC_NUMERIC, "fr_FR.UTF-8"); + if (selected_locale == nullptr) { + selected_locale = std::setlocale(LC_NUMERIC, "de_DE.UTF-8"); + } + char details[1024]; + const bool formatted = selected_locale != nullptr && + av2_decoder_model_format_violation_details( + &timing, 0, 0, details, sizeof(details)); + const std::string result = formatted ? details : ""; + (void)std::setlocale(LC_NUMERIC, saved_locale.c_str()); + if (selected_locale == nullptr) GTEST_SKIP() << "No comma-decimal locale"; + ASSERT_TRUE(formatted); + EXPECT_NE(result.find("output_time_ms=333.333"), std::string::npos); + EXPECT_EQ(result.find(','), std::string::npos); +} + +TEST_F(DecoderModelHookOrderTest, + HookOrderMatchesWrapupReferenceOutputAndFinish) { + StartFrame(OBU_CLOSED_LOOP_KEY); + UpdateAndOutput(); + av2_decoder_model_verifier_finish(pbi_); + + Av2DmVerifierStats stats; + ASSERT_TRUE(av2_decoder_model_verifier_get_stats(pbi_, &stats)); + EXPECT_EQ(stats.frame_starts, 1u); + EXPECT_EQ(stats.reference_updates, 1u); + EXPECT_EQ(stats.outputs, 1u); + EXPECT_LT(stats.last_frame_start_event, stats.last_reference_update_event); + EXPECT_LT(stats.last_reference_update_event, stats.last_output_event); + EXPECT_LT(stats.last_output_event, stats.finish_event); +} + +TEST_F(DecoderModelHookOrderTest, OlkInvalidationPrecedesOlkFrameStart) { + StartFrame(OBU_CLOSED_LOOP_KEY); + UpdateAndOutput(); + + av2_decoder_model_verifier_on_source_frame_unit_start(pbi_, 0, 0, 0); + pbi_->obu_type = OBU_OPEN_LOOP_KEY; + pbi_->common.ref_frame_map[0] = nullptr; + pbi_->valid_for_referencing[0] = 0; + av2_decoder_model_verifier_on_olk_reference_invalidation(pbi_, 0); + Av2DmVerifierStats before_start; + ASSERT_TRUE(av2_decoder_model_verifier_get_stats(pbi_, &before_start)); + av2_decoder_model_verifier_record_obu(pbi_, OBU_OPEN_LOOP_KEY, 0, 0, 0, 800); + pbi_->common.cur_frame = &second_frame_; + second_frame_.xlayer_id = 0; + second_frame_.mlayer_id = 0; + second_frame_.tlayer_id = 0; + second_frame_.width = 64; + second_frame_.height = 64; + av2_decoder_model_verifier_on_frame_wrapup_start(pbi_); + + Av2DmVerifierStats after_start; + ASSERT_TRUE(av2_decoder_model_verifier_get_stats(pbi_, &after_start)); + EXPECT_EQ(after_start.olk_invalidations, 1u); + EXPECT_EQ(after_start.frame_starts, 2u); + EXPECT_EQ(before_start.last_olk_invalidation_event, + after_start.last_olk_invalidation_event); + EXPECT_LT(after_start.last_olk_invalidation_event, + after_start.last_frame_start_event); + + av2_decoder_model_verifier_record_obu(pbi_, OBU_METADATA_SHORT, 0, 0, 0, 80); + av2_decoder_model_verifier_on_frame_unit_complete(pbi_); + pbi_->common.ref_frame_map[0] = &second_frame_; + pbi_->valid_for_referencing[0] = 1; + av2_decoder_model_verifier_after_reference_update(pbi_, 1, 1); + av2_decoder_model_verifier_on_output(pbi_, -1, &second_frame_, + AV2_DM_PRESENTATION_OWNER_CURRENT); + + Av2DmVerifierStats live; + ASSERT_TRUE(av2_decoder_model_verifier_get_stats(pbi_, &live)); + EXPECT_EQ(live.live_runs, 2u); + EXPECT_EQ(live.result_count, 0u); + testing::internal::CaptureStderr(); + av2_decoder_model_verifier_finish(pbi_); + const std::string diagnostics = testing::internal::GetCapturedStderr(); + ASSERT_TRUE(av2_decoder_model_verifier_get_stats(pbi_, &live)); + EXPECT_EQ(live.result_count, 2u); + EXPECT_EQ(live.live_runs, 0u); + EXPECT_EQ(CountOccurrences(diagnostics, "AV2_DECODER_MODEL_CVS_RESULT "), 1u); + EXPECT_NE(diagnostics.find("rap=0 mode=resource decoded=2"), + std::string::npos); + EXPECT_NE(diagnostics.find("rap=1 mode=resource decoded=1"), + std::string::npos); +} + +TEST_F(DecoderModelHookOrderTest, + DelayedImplicitAndCurrentOutputsKeepSeparateProvenance) { + pbi_->seq_list[1][0] = pbi_->seq_list[0][0]; + pbi_->seq_list[1][0].max_mlayer_id = 1; + pbi_->seq_list[1][0].seq_max_mlayer_cnt = 2; + pbi_->common.seq_params = pbi_->seq_list[1][0]; + pbi_->common.ci_params_per_layer[1] = pbi_->common.ci_params_per_layer[0]; + av2_decoder_model_verifier_on_sequence_header(pbi_, 1, 0); + av2_decoder_model_verifier_on_active_configuration(pbi_, 1, 0); + + StartFrame(OBU_CLOSED_LOOP_KEY, 64, 64, &frame_, 1, KEY_FRAME, true, true, 1, + 1); + pbi_->common.ref_frame_map[0] = &frame_; + pbi_->valid_for_referencing[0] = 1; + av2_decoder_model_verifier_after_reference_update(pbi_, 1, 1); + + Av2DmVerifierStats before_output; + ASSERT_TRUE(av2_decoder_model_verifier_get_stats(pbi_, &before_output)); + EXPECT_EQ(before_output.outputs, 0u); + + av2_decoder_model_verifier_record_obu(pbi_, OBU_TEMPORAL_DELIMITER, 0, 0, 0, + 8); + pbi_->common.seq_params = pbi_->seq_list[0][0]; + StartFrame(OBU_CLOSED_LOOP_KEY, 64, 64, &second_frame_, 0, KEY_FRAME); + pbi_->common.ref_frame_map[1] = &second_frame_; + pbi_->valid_for_referencing[1] = 1; + av2_decoder_model_verifier_after_reference_update(pbi_, 2, 3); + + av2_decoder_model_verifier_on_output(pbi_, 0, &frame_, + AV2_DM_PRESENTATION_OWNER_IMPLICIT); + Av2DmVerifierStats implicit_stats; + ASSERT_TRUE(av2_decoder_model_verifier_get_stats(pbi_, &implicit_stats)); + EXPECT_EQ(implicit_stats.last_output_callback_frame_unit, 1u); + EXPECT_EQ(implicit_stats.last_output_presentation_frame_unit, 0u); + EXPECT_EQ(implicit_stats.last_output_presentation_temporal_unit, 0u); + EXPECT_EQ(implicit_stats.last_output_generation, 1u); + EXPECT_EQ(implicit_stats.last_output_presentation_xlayer_id, 1); + EXPECT_EQ(implicit_stats.last_output_presentation_mlayer_id, 1); + EXPECT_EQ(implicit_stats.last_output_presentation_tlayer_id, 1); + EXPECT_FALSE(implicit_stats.last_output_uses_current_presentation); + + av2_decoder_model_verifier_on_output(pbi_, -1, &second_frame_, + AV2_DM_PRESENTATION_OWNER_CURRENT); + Av2DmVerifierStats current_stats; + ASSERT_TRUE(av2_decoder_model_verifier_get_stats(pbi_, ¤t_stats)); + EXPECT_EQ(current_stats.last_output_callback_frame_unit, 1u); + EXPECT_EQ(current_stats.last_output_presentation_frame_unit, 1u); + EXPECT_EQ(current_stats.last_output_presentation_temporal_unit, 1u); + EXPECT_EQ(current_stats.last_output_generation, 2u); + EXPECT_EQ(current_stats.last_output_presentation_xlayer_id, 0); + EXPECT_EQ(current_stats.last_output_presentation_mlayer_id, 0); + EXPECT_EQ(current_stats.last_output_presentation_tlayer_id, 0); + EXPECT_TRUE(current_stats.last_output_uses_current_presentation); + + testing::internal::CaptureStderr(); + av2_decoder_model_verifier_finish(pbi_); + const std::string diagnostics = testing::internal::GetCapturedStderr(); + ASSERT_TRUE(av2_decoder_model_verifier_get_stats(pbi_, ¤t_stats)); + EXPECT_EQ(current_stats.result_count, 2u); + EXPECT_EQ(current_stats.conformant_results, 2u) << diagnostics; +} + +TEST_F(DecoderModelHookOrderTest, + OutputFrameBuffersQueuesImplicitBeforeCurrentWithoutRetiming) { + StartFrame(OBU_CLOSED_LOOP_KEY, 64, 64, &frame_, 0, KEY_FRAME, true); + frame_.display_order_hint = 4; + pbi_->common.ref_frame_map[0] = &frame_; + pbi_->valid_for_referencing[0] = 1; + av2_decoder_model_verifier_after_reference_update(pbi_, 1, 1); + + av2_decoder_model_verifier_record_obu(pbi_, OBU_TEMPORAL_DELIMITER, 0, 0, 0, + 8); + StartFrame(OBU_REGULAR_TILE_GROUP, 64, 64, &second_frame_, 0, INTER_FRAME); + second_frame_.display_order_hint = 5; + pbi_->common.ref_frame_map[1] = &second_frame_; + pbi_->valid_for_referencing[1] = 1; + av2_decoder_model_verifier_after_reference_update(pbi_, 2, 3); + pbi_->last_output_doh[0][0] = -1; + + ASSERT_EQ(av2_output_frame_buffers(pbi_, -1), 0); + ASSERT_EQ(pbi_->num_output_frames, 2u); + EXPECT_EQ(pbi_->output_frames[0], &frame_); + EXPECT_EQ(pbi_->output_frames[1], &second_frame_); + EXPECT_TRUE(frame_.frame_output_done); + EXPECT_TRUE(second_frame_.frame_output_done); + + Av2DmVerifierStats stats; + ASSERT_TRUE(av2_decoder_model_verifier_get_stats(pbi_, &stats)); + EXPECT_EQ(stats.outputs, 2u); + EXPECT_EQ(stats.last_output_callback_frame_unit, 1u); + EXPECT_EQ(stats.last_output_presentation_frame_unit, 1u); + EXPECT_EQ(stats.last_output_presentation_temporal_unit, 1u); + EXPECT_EQ(stats.last_output_generation, 2u); + EXPECT_TRUE(stats.last_output_uses_current_presentation); + + testing::internal::CaptureStderr(); + av2_decoder_model_verifier_finish(pbi_); + testing::internal::GetCapturedStderr(); + ASSERT_TRUE(av2_decoder_model_verifier_get_stats(pbi_, &stats)); + ASSERT_TRUE(stats.replay_previous_presentation_offset_valid); + ASSERT_TRUE(stats.replay_last_presentation_offset_valid); + ExpectAdapterRational(stats.replay_previous_presentation_offset, 0, 1); + ExpectAdapterRational(stats.replay_last_presentation_offset, 1, 30); +} + +TEST_F(DecoderModelHookOrderTest, + OutputFrameBuffersQueuesSuccessiveImplicitWithItsOwner) { + StartFrame(OBU_CLOSED_LOOP_KEY, 64, 64, &frame_, 0, KEY_FRAME, true); + frame_.display_order_hint = 6; + pbi_->common.ref_frame_map[0] = &frame_; + pbi_->valid_for_referencing[0] = 1; + av2_decoder_model_verifier_after_reference_update(pbi_, 1, 1); + + av2_decoder_model_verifier_record_obu(pbi_, OBU_TEMPORAL_DELIMITER, 0, 0, 0, + 8); + StartFrame(OBU_REGULAR_TILE_GROUP, 64, 64, &second_frame_, 0, INTER_FRAME); + second_frame_.display_order_hint = 5; + pbi_->common.ref_frame_map[1] = &second_frame_; + pbi_->valid_for_referencing[1] = 1; + av2_decoder_model_verifier_after_reference_update(pbi_, 2, 3); + pbi_->last_output_doh[0][0] = -1; + + ASSERT_EQ(av2_output_frame_buffers(pbi_, -1), 0); + ASSERT_EQ(pbi_->num_output_frames, 2u); + EXPECT_EQ(pbi_->output_frames[0], &second_frame_); + EXPECT_EQ(pbi_->output_frames[1], &frame_); + + Av2DmVerifierStats stats; + ASSERT_TRUE(av2_decoder_model_verifier_get_stats(pbi_, &stats)); + EXPECT_EQ(stats.outputs, 2u); + EXPECT_EQ(stats.last_output_callback_frame_unit, 1u); + EXPECT_EQ(stats.last_output_presentation_frame_unit, 0u); + EXPECT_EQ(stats.last_output_presentation_temporal_unit, 0u); + EXPECT_EQ(stats.last_output_generation, 1u); + EXPECT_FALSE(stats.last_output_uses_current_presentation); +} + +TEST_F(DecoderModelHookOrderTest, + DisplacedReferenceOutputUsesPendingImplicitOwner) { + StartFrame(OBU_CLOSED_LOOP_KEY, 64, 64, &frame_, 0, KEY_FRAME, true); + frame_.display_order_hint = 4; + pbi_->common.ref_frame_map[0] = &frame_; + pbi_->valid_for_referencing[0] = 1; + av2_decoder_model_verifier_after_reference_update(pbi_, 1, 1); + + av2_decoder_model_verifier_record_obu(pbi_, OBU_TEMPORAL_DELIMITER, 0, 0, 0, + 8); + StartFrame(OBU_REGULAR_TILE_GROUP, 64, 64, &second_frame_, 0, INTER_FRAME); + second_frame_.display_order_hint = 5; + pbi_->last_output_doh[0][0] = -1; + + ASSERT_EQ(av2_output_frame_buffers(pbi_, 0), 0); + ASSERT_EQ(pbi_->num_output_frames, 1u); + EXPECT_EQ(pbi_->output_frames[0], &frame_); + Av2DmVerifierStats stats; + ASSERT_TRUE(av2_decoder_model_verifier_get_stats(pbi_, &stats)); + EXPECT_EQ(stats.last_output_callback_frame_unit, 1u); + EXPECT_EQ(stats.last_output_presentation_frame_unit, 0u); + EXPECT_EQ(stats.last_output_presentation_temporal_unit, 0u); + EXPECT_EQ(stats.last_output_generation, 1u); + EXPECT_FALSE(stats.last_output_uses_current_presentation); +} + +TEST_F(DecoderModelHookOrderTest, FlushUsesPendingImplicitOwner) { + StartFrame(OBU_CLOSED_LOOP_KEY, 64, 64, &frame_, 0, KEY_FRAME, true); + frame_.display_order_hint = 4; + pbi_->common.ref_frame_map[0] = &frame_; + pbi_->valid_for_referencing[0] = 1; + av2_decoder_model_verifier_after_reference_update(pbi_, 1, 1); + + ASSERT_EQ(flush_remaining_frames(pbi_, 100), AVM_CODEC_OK); + ASSERT_EQ(pbi_->num_output_frames, 1u); + EXPECT_EQ(pbi_->output_frames[0], &frame_); + Av2DmVerifierStats stats; + ASSERT_TRUE(av2_decoder_model_verifier_get_stats(pbi_, &stats)); + EXPECT_EQ(stats.last_output_callback_frame_unit, 0u); + EXPECT_EQ(stats.last_output_presentation_frame_unit, 0u); + EXPECT_EQ(stats.last_output_presentation_temporal_unit, 0u); + EXPECT_EQ(stats.last_output_generation, 1u); + EXPECT_FALSE(stats.last_output_uses_current_presentation); +} + +TEST_F(DecoderModelHookOrderTest, + ClkBoundaryKeepsPrefixAfterOldImplicitOutput) { + StartFrame(OBU_CLOSED_LOOP_KEY, 64, 64, &frame_, 0, KEY_FRAME, true); + pbi_->common.ref_frame_map[0] = &frame_; + pbi_->valid_for_referencing[0] = 1; + av2_decoder_model_verifier_after_reference_update(pbi_, 1, 1); + + av2_decoder_model_verifier_on_source_frame_unit_start(pbi_, 0, 0, 0); + av2_decoder_model_verifier_record_obu(pbi_, OBU_CLOSED_LOOP_KEY, 0, 0, 0, + 800); + pbi_->obu_type = OBU_CLOSED_LOOP_KEY; + testing::internal::CaptureStderr(); + av2_decoder_model_verifier_on_output(pbi_, 0, &frame_, + AV2_DM_PRESENTATION_OWNER_IMPLICIT); + av2_decoder_model_verifier_on_active_configuration(pbi_, 0, 0); + const std::string diagnostics = testing::internal::GetCapturedStderr(); + + EXPECT_NE(diagnostics.find("AV2_DECODER_MODEL_RESULT status=CONFORMANT " + "xlayer=0 ops=-1 op=-1 rap=0 mode=resource " + "decoded=1 outputs=1"), + std::string::npos); + EXPECT_NE(diagnostics.find("AV2_DECODER_MODEL_CVS_RESULT status=CONFORMANT " + "xlayer=0 cvs=1"), + std::string::npos); + Av2DmContextStats context; + ASSERT_TRUE(av2_decoder_model_verifier_get_context_stats(pbi_, 0, &context)); + EXPECT_EQ(context.pending_dfg_bits, 800u); +} + +TEST_F(DecoderModelHookOrderTest, ClkBoundaryDropsPriorCvsPendingDfgBits) { + StartFrame(OBU_CLOSED_LOOP_KEY, 64, 64, &frame_, 0, KEY_FRAME, true); + pbi_->common.ref_frame_map[0] = &frame_; + pbi_->valid_for_referencing[0] = 1; + av2_decoder_model_verifier_after_reference_update(pbi_, 1, 1); + + av2_decoder_model_verifier_record_obu(pbi_, OBU_TEMPORAL_DELIMITER, 0, 0, 0, + 8); + av2_decoder_model_verifier_on_source_frame_unit_start(pbi_, 0, 0, 0); + av2_decoder_model_verifier_record_obu(pbi_, OBU_REGULAR_SEF, 0, 0, 0, 400); + AV2_COMMON *const cm = &pbi_->common; + pbi_->obu_type = OBU_REGULAR_SEF; + cm->show_existing_frame = 1; + cm->sef_ref_fb_idx = 0; + cm->cur_frame = &second_frame_; + av2_decoder_model_verifier_on_frame_wrapup_start(pbi_); + av2_decoder_model_verifier_on_frame_unit_complete(pbi_); + av2_decoder_model_verifier_after_reference_update(pbi_, 0, 1); + av2_decoder_model_verifier_on_output(pbi_, 0, &second_frame_, + AV2_DM_PRESENTATION_OWNER_CURRENT); + + av2_decoder_model_verifier_record_obu(pbi_, OBU_TEMPORAL_DELIMITER, 0, 0, 0, + 8); + av2_decoder_model_verifier_on_source_frame_unit_start(pbi_, 0, 0, 0); + av2_decoder_model_verifier_record_obu(pbi_, OBU_CLOSED_LOOP_KEY, 0, 0, 0, + 800); + pbi_->obu_type = OBU_CLOSED_LOOP_KEY; + testing::internal::CaptureStderr(); + av2_decoder_model_verifier_on_active_configuration(pbi_, 0, 0); + (void)testing::internal::GetCapturedStderr(); + + Av2DmContextStats context; + ASSERT_TRUE(av2_decoder_model_verifier_get_context_stats(pbi_, 0, &context)); + EXPECT_EQ(context.pending_dfg_bits, 808u); +} + +TEST_F(DecoderModelHookOrderTest, ClkBoundaryOnlyClosesItsXlayer) { + pbi_->seq_list[1][0] = pbi_->seq_list[0][0]; + pbi_->common.seq_params = pbi_->seq_list[1][0]; + pbi_->obu_type = OBU_SEQUENCE_HEADER; + av2_decoder_model_verifier_on_sequence_header(pbi_, 1, 0); + av2_decoder_model_verifier_on_active_configuration(pbi_, 1, 0); + + pbi_->common.seq_params = pbi_->seq_list[0][0]; + StartFrame(OBU_CLOSED_LOOP_KEY, 64, 64, &frame_, 0, KEY_FRAME, false, true, + 0); + UpdateAndOutput(); + pbi_->common.seq_params = pbi_->seq_list[1][0]; + StartFrame(OBU_CLOSED_LOOP_KEY, 64, 64, &second_frame_, 0, KEY_FRAME, false, + true, 1); + pbi_->common.ref_frame_map[1] = &second_frame_; + pbi_->valid_for_referencing[1] = 1; + av2_decoder_model_verifier_after_reference_update(pbi_, 2, 3); + av2_decoder_model_verifier_on_output(pbi_, -1, &second_frame_, + AV2_DM_PRESENTATION_OWNER_CURRENT); + + av2_decoder_model_verifier_on_source_frame_unit_start(pbi_, 0, 0, 0); + av2_decoder_model_verifier_record_obu(pbi_, OBU_CLOSED_LOOP_KEY, 0, 0, 0, + 800); + pbi_->common.seq_params = pbi_->seq_list[0][0]; + pbi_->obu_type = OBU_CLOSED_LOOP_KEY; + testing::internal::CaptureStderr(); + av2_decoder_model_verifier_on_active_configuration(pbi_, 0, 0); + const std::string diagnostics = testing::internal::GetCapturedStderr(); + EXPECT_NE(diagnostics.find("AV2_DECODER_MODEL_CVS_RESULT status=CONFORMANT " + "xlayer=0 cvs=1"), + std::string::npos); + EXPECT_EQ(diagnostics.find("xlayer=1 cvs=1"), std::string::npos); + + Av2DmVerifierStats stats; + ASSERT_TRUE(av2_decoder_model_verifier_get_stats(pbi_, &stats)); + EXPECT_EQ(stats.live_runs, 1u); +} + +TEST_F(DecoderModelHookOrderTest, + MultipleClkFrameUnitsInSameTemporalUnitShareCvs) { + testing::internal::CaptureStderr(); + StartFrame(OBU_CLOSED_LOOP_KEY, 64, 64, &frame_, 0, KEY_FRAME, false, true, 0, + 0, true); + UpdateAndOutput(); + + StartFrame(OBU_CLOSED_LOOP_KEY, 64, 64, &second_frame_, 0, KEY_FRAME, false, + true, 0, 0, true); + pbi_->common.ref_frame_map[1] = &second_frame_; + pbi_->valid_for_referencing[1] = 1; + av2_decoder_model_verifier_after_reference_update(pbi_, 2, 3); + av2_decoder_model_verifier_on_output(pbi_, -1, &second_frame_, + AV2_DM_PRESENTATION_OWNER_CURRENT); + av2_decoder_model_verifier_finish(pbi_); + const std::string diagnostics = testing::internal::GetCapturedStderr(); + + EXPECT_EQ(CountOccurrences(diagnostics, "AV2_DECODER_MODEL_CVS_RESULT "), 1u); + EXPECT_NE(diagnostics.find("xlayer=0 cvs=1"), std::string::npos); + EXPECT_NE(diagnostics.find("AV2_DECODER_MODEL_BITSTREAM_RESULT "), + std::string::npos); + EXPECT_NE(diagnostics.find("cvs=1 "), std::string::npos); +} + +TEST_F(DecoderModelResultTest, ConformantResultAndFinishAreIdempotent) { + StartFrame(OBU_CLOSED_LOOP_KEY); + UpdateAndOutput(); + av2_decoder_model_verifier_finish(pbi_); + av2_decoder_model_verifier_finish(pbi_); + + Av2DmVerifierStats stats; + ASSERT_TRUE(av2_decoder_model_verifier_get_stats(pbi_, &stats)); + EXPECT_EQ(stats.result_count, 1u); + EXPECT_EQ(stats.conformant_results, 1u); + EXPECT_EQ(stats.non_conformant_results, 0u); + EXPECT_EQ(stats.indeterminate_results, 0u); +} + +TEST_F(DecoderModelResultTest, + VerifierAllocationFailureIsIndeterminateAndIdempotent) { + av2_decoder_model_verifier_destroy(pbi_); + ASSERT_EQ(pbi_->decoder_model_verifier, nullptr); + pbi_->decoder_model_verifier_allocation_failed = true; + + testing::internal::CaptureStderr(); + av2_decoder_model_verifier_finish(pbi_); + av2_decoder_model_verifier_finish(pbi_); + const std::string diagnostics = testing::internal::GetCapturedStderr(); + + EXPECT_EQ(CountOccurrences(diagnostics, "AV2_DECODER_MODEL_ERROR "), 1u); + EXPECT_NE(diagnostics.find("code=ALLOCATION_FAILURE"), std::string::npos); + EXPECT_EQ(CountOccurrences(diagnostics, "AV2_DECODER_MODEL_RESULT "), 1u); + EXPECT_EQ( + CountOccurrences(diagnostics, "AV2_DECODER_MODEL_BITSTREAM_RESULT "), 1u); + EXPECT_NE( + diagnostics.find("status=INDETERMINATE xlayer=-1 ops=-1 op=-1 rap=-1 " + "mode=resource decoded=0 outputs=0 reordered_outputs=0 " + "violations=0 reason=internal_failure"), + std::string::npos); + + Av2DmVerifierStats stats; + ASSERT_TRUE(av2_decoder_model_verifier_get_stats(pbi_, &stats)); + EXPECT_FALSE(stats.available); + EXPECT_TRUE(stats.failed); + EXPECT_EQ(stats.result_count, 1u); + EXPECT_EQ(stats.indeterminate_results, 1u); +} + +TEST_F(DecoderModelResultTest, + EarlyVerifierFailureIsIndeterminateAndIdempotent) { + av2_decoder_model_verifier_destroy(pbi_); + av2_decoder_model_verifier_init(pbi_); + ASSERT_NE(pbi_->decoder_model_verifier, nullptr); + av2_decoder_model_verifier_on_accounting_failure(pbi_); + + testing::internal::CaptureStderr(); + av2_decoder_model_verifier_finish(pbi_); + av2_decoder_model_verifier_finish(pbi_); + const std::string diagnostics = testing::internal::GetCapturedStderr(); + + EXPECT_EQ(CountOccurrences(diagnostics, "AV2_DECODER_MODEL_ERROR "), 1u); + EXPECT_NE(diagnostics.find("code=ARITHMETIC_FAILURE"), std::string::npos); + EXPECT_EQ(CountOccurrences(diagnostics, "AV2_DECODER_MODEL_RESULT "), 1u); + EXPECT_NE( + diagnostics.find("status=INDETERMINATE xlayer=-1 ops=-1 op=-1 rap=-1 " + "mode=resource decoded=0 outputs=0 reordered_outputs=0 " + "violations=0 reason=internal_failure"), + std::string::npos); + + Av2DmVerifierStats stats; + ASSERT_TRUE(av2_decoder_model_verifier_get_stats(pbi_, &stats)); + EXPECT_TRUE(stats.available); + EXPECT_TRUE(stats.failed); + EXPECT_EQ(stats.result_count, 1u); + EXPECT_EQ(stats.indeterminate_results, 1u); +} + +TEST_F(DecoderModelResultTest, + InternalFailureBeforeRunResultMakesOpenCvsIndeterminate) { + av2_decoder_model_verifier_on_source_frame_unit_start(pbi_, 0, 0, 0); + av2_decoder_model_verifier_record_obu(pbi_, OBU_CLOSED_LOOP_KEY, 0, 0, 0, + 800); + pbi_->obu_type = OBU_CLOSED_LOOP_KEY; + av2_decoder_model_verifier_on_active_configuration(pbi_, 0, 0); + av2_decoder_model_verifier_on_internal_failure_for_testing(pbi_); + + testing::internal::CaptureStderr(); + av2_decoder_model_verifier_finish(pbi_); + const std::string diagnostics = testing::internal::GetCapturedStderr(); + EXPECT_EQ(CountOccurrences(diagnostics, "AV2_DECODER_MODEL_ERROR "), 1u); + EXPECT_NE(diagnostics.find("code=INTERNAL_STATE_FAILURE"), std::string::npos); + EXPECT_EQ(diagnostics.find("AV2_DECODER_MODEL_WARNING "), std::string::npos); + EXPECT_NE(diagnostics.find("AV2_DECODER_MODEL_CVS_RESULT " + "status=INDETERMINATE xlayer=0 cvs=1 " + "violations=0 verification_complete=0 " + "reason=internal_failure"), + std::string::npos); + EXPECT_NE(diagnostics.find("AV2_DECODER_MODEL_BITSTREAM_RESULT " + "status=INDETERMINATE complete=0 cvs=1"), + std::string::npos); +} + +TEST_F(DecoderModelResultTest, + InternalFailureAfterViolationPreservesNonConformantCvs) { + testing::internal::CaptureStderr(); + StartFrame(OBU_CLOSED_LOOP_KEY, 4096, 64); + av2_decoder_model_verifier_on_internal_failure_for_testing(pbi_); + av2_decoder_model_verifier_finish(pbi_); + const std::string diagnostics = testing::internal::GetCapturedStderr(); + EXPECT_EQ(CountOccurrences(diagnostics, "AV2_DECODER_MODEL_ERROR "), 1u); + EXPECT_NE(diagnostics.find("code=INTERNAL_STATE_FAILURE"), std::string::npos); + EXPECT_EQ(CountOccurrences(diagnostics, "AV2_DECODER_MODEL_WARNING "), 2u); + EXPECT_NE(diagnostics.find("AV2_DECODER_MODEL_CVS_RESULT " + "status=NON_CONFORMANT xlayer=0 cvs=1"), + std::string::npos); + EXPECT_NE(diagnostics.find("verification_complete=0 " + "reason=internal_failure"), + std::string::npos); + EXPECT_NE(diagnostics.find("AV2_DECODER_MODEL_BITSTREAM_RESULT " + "status=NON_CONFORMANT complete=0 cvs=1"), + std::string::npos); +} + +TEST_F(DecoderModelResultTest, FatalModeDoesNotStopForInternalVerifierFailure) { + av2_decoder_model_verifier_destroy(pbi_); + pbi_->decoder_model_check_mode = AVM_DECODER_MODEL_CHECK_FATAL; + av2_decoder_model_verifier_init(pbi_); + ASSERT_NE(pbi_->decoder_model_verifier, nullptr); + Configure(64, 64); + + av2_decoder_model_verifier_on_internal_failure_for_testing(pbi_); + EXPECT_FALSE(av2_decoder_model_verifier_should_stop(pbi_)); + + testing::internal::CaptureStderr(); + av2_decoder_model_verifier_finish(pbi_); + const std::string diagnostics = testing::internal::GetCapturedStderr(); + EXPECT_EQ(CountOccurrences(diagnostics, "AV2_DECODER_MODEL_ERROR "), 1u); + EXPECT_NE(diagnostics.find("code=INTERNAL_STATE_FAILURE"), std::string::npos); + EXPECT_EQ(diagnostics.find("AV2_DECODER_MODEL_WARNING "), std::string::npos); + EXPECT_NE(diagnostics.find("status=INDETERMINATE complete=0"), + std::string::npos); +} + +TEST_F(DecoderModelResultTest, ModelArithmeticFailureDoesNotSuppressLaterCvs) { + testing::internal::CaptureStderr(); + StartFrame(OBU_CLOSED_LOOP_KEY, 64, 64, &frame_, 0, KEY_FRAME, false, true, 0, + 0, true); + UpdateAndOutput(); + av2_decoder_model_verifier_on_model_arithmetic_failure_for_testing(pbi_); + + av2_decoder_model_verifier_record_obu(pbi_, OBU_TEMPORAL_DELIMITER, 0, 0, 0, + 8); + StartFrame(OBU_CLOSED_LOOP_KEY, 64, 64, &frame_, 0, KEY_FRAME, false, true, 0, + 0, true); + UpdateAndOutput(); + av2_decoder_model_verifier_finish(pbi_); + const std::string diagnostics = testing::internal::GetCapturedStderr(); + + EXPECT_EQ(CountOccurrences(diagnostics, "AV2_DECODER_MODEL_ERROR "), 1u); + EXPECT_NE(diagnostics.find("code=ARITHMETIC_FAILURE"), std::string::npos); + EXPECT_NE(diagnostics.find("status=INDETERMINATE xlayer=0 cvs=1"), + std::string::npos); + EXPECT_NE(diagnostics.find("status=CONFORMANT xlayer=0 cvs=2"), + std::string::npos); + EXPECT_NE(diagnostics.find("status=INDETERMINATE complete=0 cvs=2"), + std::string::npos); + EXPECT_EQ(diagnostics.find("AV2_DECODER_MODEL_WARNING "), std::string::npos); +} + +TEST_F(DecoderModelResultTest, StaticLevelViolationIsNonConformant) { + pbi_->common.seq_params.max_frame_width = 4096; + pbi_->seq_list[0][0].max_frame_width = 4096; + av2_decoder_model_verifier_on_active_configuration(pbi_, 0, 0); + StartFrame(OBU_CLOSED_LOOP_KEY, 4096, 64); + UpdateAndOutput(); + av2_decoder_model_verifier_finish(pbi_); + + Av2DmVerifierStats stats; + ASSERT_TRUE(av2_decoder_model_verifier_get_stats(pbi_, &stats)); + EXPECT_EQ(stats.result_count, 1u); + EXPECT_EQ(stats.non_conformant_results, 1u); +} + +TEST_F(DecoderModelResultTest, WarningIsReportedWhenViolationOccurs) { + testing::internal::CaptureStderr(); + StartFrame(OBU_CLOSED_LOOP_KEY, 4096, 64); + const std::string immediate = testing::internal::GetCapturedStderr(); + EXPECT_EQ(CountOccurrences(immediate, "AV2_DECODER_MODEL_WARNING "), 2u); + EXPECT_NE(immediate.find("code=MAX_PICTURE_SIZE "), std::string::npos); + EXPECT_NE(immediate.find("code=MAX_HORIZONTAL_SIZE "), std::string::npos); + + testing::internal::CaptureStderr(); + UpdateAndOutput(); + Av2DmVerifierStats live; + ASSERT_TRUE(av2_decoder_model_verifier_get_stats(pbi_, &live)); + EXPECT_EQ(live.outputs, 1u); + av2_decoder_model_verifier_finish(pbi_); + const std::string final = testing::internal::GetCapturedStderr(); + EXPECT_EQ(final.find("AV2_DECODER_MODEL_WARNING "), std::string::npos); + EXPECT_NE(final.find("AV2_DECODER_MODEL_CVS_RESULT status=NON_CONFORMANT"), + std::string::npos); + EXPECT_NE( + final.find("AV2_DECODER_MODEL_BITSTREAM_RESULT status=NON_CONFORMANT"), + std::string::npos); + Av2DmVerifierStats finished; + ASSERT_TRUE(av2_decoder_model_verifier_get_stats(pbi_, &finished)); + EXPECT_EQ(finished.live_runs, 0u); +} + +TEST_F(DecoderModelResultTest, FatalModeStopsBeforeOpeningThirdCvs) { + av2_decoder_model_verifier_destroy(pbi_); + pbi_->decoder_model_check_mode = AVM_DECODER_MODEL_CHECK_FATAL; + av2_decoder_model_verifier_init(pbi_); + ASSERT_NE(pbi_->decoder_model_verifier, nullptr); + Configure(64, 64); + + testing::internal::CaptureStderr(); + StartFrame(OBU_CLOSED_LOOP_KEY, 64, 64, &frame_, 0, KEY_FRAME, false, true, 0, + 0, true); + UpdateAndOutput(); + + av2_decoder_model_verifier_record_obu(pbi_, OBU_TEMPORAL_DELIMITER, 0, 0, 0, + 8); + StartFrame(OBU_CLOSED_LOOP_KEY, 4096, 64, &frame_, 0, KEY_FRAME, false, true, + 0, 0, true); + EXPECT_TRUE(av2_decoder_model_verifier_should_stop(pbi_)); + + av2_decoder_model_verifier_record_obu(pbi_, OBU_TEMPORAL_DELIMITER, 0, 0, 0, + 8); + StartFrame(OBU_CLOSED_LOOP_KEY, 64, 64, &frame_, 0, KEY_FRAME, false, true, 0, + 0, true); + av2_decoder_model_verifier_finish(pbi_); + const std::string diagnostics = testing::internal::GetCapturedStderr(); + + EXPECT_EQ(CountOccurrences(diagnostics, "AV2_DECODER_MODEL_WARNING "), 1u); + EXPECT_NE(diagnostics.find("status=CONFORMANT xlayer=0 cvs=1"), + std::string::npos); + EXPECT_NE(diagnostics.find("status=NON_CONFORMANT xlayer=0 cvs=2"), + std::string::npos); + EXPECT_NE(diagnostics.find("verification_complete=0"), std::string::npos); + EXPECT_EQ(diagnostics.find("xlayer=0 cvs=3"), std::string::npos); + EXPECT_NE(diagnostics.find("status=NON_CONFORMANT complete=0 cvs=2"), + std::string::npos); +} + +TEST_F(DecoderModelResultTest, + ThreeCvsResultsPreserveNonConformantBitstreamVerdict) { + testing::internal::CaptureStderr(); + StartFrame(OBU_CLOSED_LOOP_KEY, 64, 64, &frame_, 0, KEY_FRAME, false, true, 0, + 0, true); + UpdateAndOutput(); + + av2_decoder_model_verifier_record_obu(pbi_, OBU_TEMPORAL_DELIMITER, 0, 0, 0, + 8); + StartFrame(OBU_CLOSED_LOOP_KEY, 4096, 64, &frame_, 0, KEY_FRAME, false, true, + 0, 0, true); + UpdateAndOutput(); + + av2_decoder_model_verifier_record_obu(pbi_, OBU_TEMPORAL_DELIMITER, 0, 0, 0, + 8); + StartFrame(OBU_CLOSED_LOOP_KEY, 64, 64, &frame_, 0, KEY_FRAME, false, true, 0, + 0, true); + UpdateAndOutput(); + av2_decoder_model_verifier_finish(pbi_); + const std::string diagnostics = testing::internal::GetCapturedStderr(); + + EXPECT_NE(diagnostics.find("AV2_DECODER_MODEL_CVS_RESULT status=CONFORMANT " + "xlayer=0 cvs=1"), + std::string::npos); + EXPECT_NE( + diagnostics.find("AV2_DECODER_MODEL_CVS_RESULT status=NON_CONFORMANT " + "xlayer=0 cvs=2"), + std::string::npos); + EXPECT_NE(diagnostics.find("AV2_DECODER_MODEL_CVS_RESULT status=CONFORMANT " + "xlayer=0 cvs=3"), + std::string::npos); + EXPECT_NE(diagnostics.find("AV2_DECODER_MODEL_BITSTREAM_RESULT " + "status=NON_CONFORMANT complete=1 cvs=3"), + std::string::npos); + EXPECT_NE(diagnostics.find("first_non_conformant_xlayer=0 " + "first_non_conformant_cvs=2"), + std::string::npos); +} + +TEST_F(DecoderModelResultTest, + ClkBoundaryRebuildsIncompleteExtractionFromRetainedPrefix) { + testing::internal::CaptureStderr(); + StartFrame(OBU_CLOSED_LOOP_KEY, 64, 64, &frame_, 0, KEY_FRAME, false, true, 0, + 0, true, true); + UpdateAndOutput(); + + av2_decoder_model_verifier_record_obu(pbi_, OBU_TEMPORAL_DELIMITER, 0, 0, 0, + 8); + StartFrame(OBU_CLOSED_LOOP_KEY, 64, 64, &frame_, 0, KEY_FRAME, false, true, 0, + 0, true); + UpdateAndOutput(); + + av2_decoder_model_verifier_record_obu(pbi_, OBU_TEMPORAL_DELIMITER, 0, 0, 0, + 8); + StartFrame(OBU_CLOSED_LOOP_KEY, 64, 64, &frame_, 0, KEY_FRAME, false, true, 0, + 0, true, true); + UpdateAndOutput(); + av2_decoder_model_verifier_finish(pbi_); + const std::string diagnostics = testing::internal::GetCapturedStderr(); + + EXPECT_NE(diagnostics.find("AV2_DECODER_MODEL_CVS_RESULT " + "status=INDETERMINATE xlayer=0 cvs=1"), + std::string::npos); + EXPECT_NE(diagnostics.find("AV2_DECODER_MODEL_CVS_RESULT status=CONFORMANT " + "xlayer=0 cvs=2"), + std::string::npos); + EXPECT_NE(diagnostics.find("AV2_DECODER_MODEL_CVS_RESULT " + "status=INDETERMINATE xlayer=0 cvs=3"), + std::string::npos); + EXPECT_EQ(CountOccurrences(diagnostics, "reason=incomplete_extraction"), 4u); +} + +TEST_F(DecoderModelResultTest, CompletedCvsReleasesLiveStorage) { + testing::internal::CaptureStderr(); + for (int cvs = 0; cvs < 32; ++cvs) { + av2_decoder_model_verifier_record_obu(pbi_, OBU_TEMPORAL_DELIMITER, 0, 0, 0, + 8); + StartFrame(OBU_CLOSED_LOOP_KEY, 64, 64, &frame_, 0, KEY_FRAME, false, true, + 0, 0, true); + UpdateAndOutput(); + Av2DmVerifierStats live; + ASSERT_TRUE(av2_decoder_model_verifier_get_stats(pbi_, &live)); + EXPECT_EQ(live.live_runs, 1u); + EXPECT_LE(live.live_generations, 1u); + EXPECT_LE(live.parameter_records, 2u); + } + av2_decoder_model_verifier_finish(pbi_); + (void)testing::internal::GetCapturedStderr(); + + Av2DmVerifierStats finished; + ASSERT_TRUE(av2_decoder_model_verifier_get_stats(pbi_, &finished)); + EXPECT_EQ(finished.live_runs, 0u); + EXPECT_LE(finished.live_generations, 1u); + EXPECT_LE(finished.parameter_records, 2u); +} + +TEST_F(DecoderModelResultTest, ExplicitOperatingPointLevelOverridesSequence) { + SequenceHeader *const sequence = &pbi_->seq_list[0][0]; + sequence->seq_max_level_idx = SEQ_LEVEL_3_0; + sequence->max_frame_width = 640; + sequence->max_frame_height = 480; + pbi_->common.seq_params = *sequence; + av2_decoder_model_verifier_on_active_configuration(pbi_, 0, 0); + + OperatingPointSet *const ops = &pbi_->ops_list[0][1]; + memset(ops, 0, sizeof(*ops)); + ops->valid = 1; + ops->obu_xlayer_id = 0; + ops->ops_id = 1; + ops->ops_cnt = 1; + ops->ops_ptl_present_flag = 1; + ops->op[0].ops_seq_profile_idc[0] = MAIN_420_10_IP0; + ops->op[0].ops_level_idx[0] = SEQ_LEVEL_2_1; + ops->op[0].ops_mlayer_count[0] = 1; + ops->op[0].mlayer_info.ops_mlayer_map[0] = 1; + ops->op[0].mlayer_info.ops_tlayer_map[0][0] = 1; + av2_decoder_model_verifier_on_operating_point_set(pbi_, 0, 1); + + testing::internal::CaptureStderr(); + StartFrame(OBU_CLOSED_LOOP_KEY, 640, 480); + UpdateAndOutput(); + av2_decoder_model_verifier_finish(pbi_); + const std::string diagnostics = testing::internal::GetCapturedStderr(); + + Av2DmVerifierStats stats; + ASSERT_TRUE(av2_decoder_model_verifier_get_stats(pbi_, &stats)); + EXPECT_EQ(stats.result_count, 2u); + EXPECT_EQ(stats.conformant_results, 1u); + EXPECT_EQ(stats.non_conformant_results, 1u); + EXPECT_EQ(CountOccurrences(diagnostics, "AV2_DECODER_MODEL_WARNING "), 1u); + EXPECT_NE(diagnostics.find("code=MAX_PICTURE_SIZE xlayer=0 ops=1 op=0"), + std::string::npos); + EXPECT_NE(diagnostics.find("level=1 level_name=2.1 tier=main " + "scope=operating_point mode=resource"), + std::string::npos); + EXPECT_NE(diagnostics.find("event_type=frame frame_unit=0 temporal_unit=0"), + std::string::npos); + EXPECT_NE(diagnostics.find("observed=307200 limit=278784 " + "unit=luma_samples requirement=maximum"), + std::string::npos); + EXPECT_EQ(diagnostics.find("0x0000000000000000"), std::string::npos); + EXPECT_NE(diagnostics.find("status=CONFORMANT xlayer=0 ops=-1 op=-1"), + std::string::npos); +} + +TEST_F(DecoderModelResultTest, + OlkParameterUpdatePreservesContinuousRunAndStartsFreshRun) { + SequenceHeader *const sequence = &pbi_->seq_list[0][0]; + sequence->seq_max_level_idx = SEQ_LEVEL_3_0; + sequence->max_frame_width = 640; + sequence->max_frame_height = 480; + pbi_->common.seq_params = *sequence; + av2_decoder_model_verifier_on_active_configuration(pbi_, 0, 0); + + OperatingPointSet *const ops = &pbi_->ops_list[0][1]; + memset(ops, 0, sizeof(*ops)); + ops->valid = 1; + ops->obu_xlayer_id = 0; + ops->ops_id = 1; + ops->ops_cnt = 1; + ops->ops_ptl_present_flag = 1; + ops->op[0].ops_seq_profile_idc[0] = MAIN_420_10_IP0; + ops->op[0].ops_level_idx[0] = SEQ_LEVEL_3_0; + ops->op[0].ops_mlayer_count[0] = 1; + ops->op[0].mlayer_info.ops_mlayer_map[0] = 1; + ops->op[0].mlayer_info.ops_tlayer_map[0][0] = 1; + av2_decoder_model_verifier_on_operating_point_set(pbi_, 0, 1); + + testing::internal::CaptureStderr(); + StartFrame(OBU_CLOSED_LOOP_KEY, 64, 64, &frame_); + UpdateAndOutput(); + + ops->op[0].ops_level_idx[0] = SEQ_LEVEL_2_0; + av2_decoder_model_verifier_on_operating_point_set(pbi_, 0, 1); + av2_decoder_model_verifier_record_obu(pbi_, OBU_TEMPORAL_DELIMITER, 0, 0, 0, + 8); + StartFrame(OBU_OPEN_LOOP_KEY, 640, 480, &second_frame_); + pbi_->common.ref_frame_map[1] = &second_frame_; + pbi_->valid_for_referencing[1] = 1; + av2_decoder_model_verifier_after_reference_update(pbi_, 2, 3); + av2_decoder_model_verifier_on_output(pbi_, -1, &second_frame_, + AV2_DM_PRESENTATION_OWNER_CURRENT); + av2_decoder_model_verifier_finish(pbi_); + const std::string diagnostics = testing::internal::GetCapturedStderr(); + + EXPECT_EQ(CountOccurrences(diagnostics, "AV2_DECODER_MODEL_CVS_RESULT "), 1u); + EXPECT_NE(diagnostics.find("status=NON_CONFORMANT xlayer=0 ops=1 op=0 " + "rap=0 mode=resource decoded=2"), + std::string::npos); + EXPECT_NE(diagnostics.find("status=NON_CONFORMANT xlayer=0 ops=1 op=0 " + "rap=1 mode=resource decoded=1"), + std::string::npos); + EXPECT_EQ(CountOccurrences(diagnostics, + "code=MAX_PICTURE_SIZE xlayer=0 ops=1 op=0"), + 2u); +} + +TEST_F(DecoderModelResultTest, + NonRapOperatingPointChangeIsIndeterminateWithoutRestart) { + OperatingPointSet *const ops = &pbi_->ops_list[0][1]; + memset(ops, 0, sizeof(*ops)); + ops->valid = 1; + ops->obu_xlayer_id = 0; + ops->ops_id = 1; + ops->ops_cnt = 1; + ops->ops_ptl_present_flag = 1; + ops->op[0].ops_seq_profile_idc[0] = MAIN_420_10_IP0; + ops->op[0].ops_level_idx[0] = SEQ_LEVEL_3_0; + ops->op[0].ops_mlayer_count[0] = 1; + ops->op[0].mlayer_info.ops_mlayer_map[0] = 1; + ops->op[0].mlayer_info.ops_tlayer_map[0][0] = 1; + av2_decoder_model_verifier_on_operating_point_set(pbi_, 0, 1); + + StartFrame(OBU_CLOSED_LOOP_KEY, 64, 64, &frame_); + UpdateAndOutput(); + ops->op[0].ops_level_idx[0] = SEQ_LEVEL_2_0; + av2_decoder_model_verifier_on_operating_point_set(pbi_, 0, 1); + av2_decoder_model_verifier_record_obu(pbi_, OBU_TEMPORAL_DELIMITER, 0, 0, 0, + 8); + StartFrame(OBU_REGULAR_TILE_GROUP, 64, 64, &second_frame_, 0, INTER_FRAME); + + testing::internal::CaptureStderr(); + av2_decoder_model_verifier_finish(pbi_); + const std::string diagnostics = testing::internal::GetCapturedStderr(); + EXPECT_NE(diagnostics.find("status=INDETERMINATE xlayer=0 ops=1 op=0 " + "rap=0 mode=resource decoded=1"), + std::string::npos); + EXPECT_NE(diagnostics.find("reason=missing_required_input"), + std::string::npos); + EXPECT_EQ(diagnostics.find("xlayer=0 ops=1 op=0 rap=1"), std::string::npos); +} + +TEST_F(DecoderModelResultTest, + MaximumLayerIdLowersReferenceLimitForWholeAndOperatingPointScopes) { + SequenceHeader *const sequence = &pbi_->seq_list[0][0]; + sequence->seq_max_level_idx = SEQ_LEVEL_2_0; + sequence->max_frame_width = 512; + sequence->max_frame_height = 288; + sequence->max_mlayer_id = 1; + sequence->seq_max_mlayer_cnt = 1; + sequence->still_picture = 0; + pbi_->common.seq_params = *sequence; + av2_decoder_model_verifier_on_active_configuration(pbi_, 0, 0); + + OperatingPointSet *const ops = &pbi_->ops_list[0][1]; + memset(ops, 0, sizeof(*ops)); + ops->valid = 1; + ops->obu_xlayer_id = 0; + ops->ops_id = 1; + ops->ops_cnt = 1; + ops->ops_ptl_present_flag = 1; + ops->op[0].ops_seq_profile_idc[0] = MAIN_420_10_IP0; + ops->op[0].ops_level_idx[0] = SEQ_LEVEL_2_0; + ops->op[0].ops_mlayer_count[0] = 1; + ops->op[0].mlayer_info.ops_mlayer_map[0] = 1; + ops->op[0].mlayer_info.ops_tlayer_map[0][0] = 1; + av2_decoder_model_verifier_on_operating_point_set(pbi_, 0, 1); + + pbi_->common.features.allow_global_intrabc = 1; + pbi_->common.lf.apply_deblocking_filter[0] = 1; + testing::internal::CaptureStderr(); + StartFrame(OBU_CLOSED_LOOP_KEY, 512, 288); + UpdateAndOutput(); + av2_decoder_model_verifier_finish(pbi_); + const std::string diagnostics = testing::internal::GetCapturedStderr(); + + Av2DmVerifierStats stats; + ASSERT_TRUE(av2_decoder_model_verifier_get_stats(pbi_, &stats)); + EXPECT_EQ(stats.result_count, 2u); + EXPECT_EQ(stats.non_conformant_results, 2u); + EXPECT_EQ(CountOccurrences(diagnostics, "code=MAX_REFERENCE_FRAMES "), 2u); + EXPECT_NE(diagnostics.find("code=MAX_REFERENCE_FRAMES xlayer=0 ops=-1"), + std::string::npos); + EXPECT_NE(diagnostics.find("code=MAX_REFERENCE_FRAMES xlayer=0 ops=1 op=0"), + std::string::npos); +} + +TEST_F(DecoderModelResultTest, + InterOutputUsesSequenceMaximumForDisplaySampleRate) { + SequenceHeader *const sequence = &pbi_->seq_list[0][0]; + sequence->seq_max_level_idx = SEQ_LEVEL_2_0; + sequence->max_frame_width = 640; + sequence->max_frame_height = 480; + sequence->ref_frames = 3; + sequence->still_picture = 0; + pbi_->common.seq_params = *sequence; + av2_decoder_model_verifier_on_active_configuration(pbi_, 0, 0); + + testing::internal::CaptureStderr(); + StartFrame(OBU_CLOSED_LOOP_KEY, 320, 240, &frame_); + UpdateAndOutput(); + + av2_decoder_model_verifier_record_obu(pbi_, OBU_TEMPORAL_DELIMITER, 0, 0, 0, + 8); + StartFrame(OBU_REGULAR_TILE_GROUP, 320, 240, &second_frame_, 0, INTER_FRAME); + pbi_->common.ref_frame_map[0] = &second_frame_; + pbi_->valid_for_referencing[0] = 1; + av2_decoder_model_verifier_after_reference_update(pbi_, 1, 1); + av2_decoder_model_verifier_on_output(pbi_, -1, &second_frame_, + AV2_DM_PRESENTATION_OWNER_CURRENT); + av2_decoder_model_verifier_finish(pbi_); + const std::string diagnostics = testing::internal::GetCapturedStderr(); + + Av2DmVerifierStats stats; + ASSERT_TRUE(av2_decoder_model_verifier_get_stats(pbi_, &stats)); + EXPECT_EQ(stats.result_count, 1u); + EXPECT_EQ(stats.non_conformant_results, 1u); + EXPECT_EQ(CountOccurrences(diagnostics, "code=MAX_DISPLAY_RATE "), 1u); + EXPECT_EQ( + CountOccurrences(diagnostics, "code=MINIMUM_PRESENTATION_INTERVAL "), 1u); + EXPECT_NE(diagnostics.find("level=0 level_name=2.0 tier=main " + "scope=whole_xlayer mode=resource"), + std::string::npos); + EXPECT_NE(diagnostics.find("event_type=output"), std::string::npos); + EXPECT_NE(diagnostics.find("unit=luma_samples_per_interval " + "requirement=maximum relation=lte " + "condition=display_luma_samples_lte_" + "output_interval_capacity"), + std::string::npos); + EXPECT_NE(diagnostics.find("observed_rate="), std::string::npos); + EXPECT_NE(diagnostics.find("Msamples/s limit_rate="), std::string::npos); + EXPECT_NE(diagnostics.find("output_interval_ms="), std::string::npos); + EXPECT_NE(diagnostics.find("unit=seconds requirement=minimum relation=gte " + "condition=presentation_interval_gte_required_" + "presentation_interval"), + std::string::npos); + EXPECT_NE(diagnostics.find("presentation_interval_ms=33.333"), + std::string::npos); + EXPECT_EQ(diagnostics.find("0x0000000000000000"), std::string::npos); +} + +TEST_F(DecoderModelResultTest, UndefinedLowMultistreamLevelIsIndeterminate) { + SequenceHeader *const sequence = &pbi_->seq_list[0][0]; + sequence->seq_max_level_idx = SEQ_LEVEL_4_0; + sequence->still_picture = 0; + pbi_->common.seq_params = *sequence; + av2_decoder_model_verifier_on_active_configuration(pbi_, 0, 0); + + pbi_->multistream_decoder_mode = 1; + pbi_->common.num_streams = 2; + pbi_->common.stream_ids[0] = 0; + pbi_->common.stream_ids[1] = 1; + pbi_->common.msdo_params.multistream_profile_idc = MAIN_420_10_IP0; + pbi_->common.msdo_params.multistream_level_idx = SEQ_LEVEL_3_1; + pbi_->common.msdo_params.multistream_tier_idx = 0; + av2_decoder_model_verifier_on_multistream_configuration(pbi_, 1, 0); + + testing::internal::CaptureStderr(); + StartFrame(OBU_CLOSED_LOOP_KEY); + UpdateAndOutput(); + av2_decoder_model_verifier_finish(pbi_); + const std::string diagnostics = testing::internal::GetCapturedStderr(); + + Av2DmVerifierStats stats; + ASSERT_TRUE(av2_decoder_model_verifier_get_stats(pbi_, &stats)); + EXPECT_EQ(stats.result_count, 1u); + EXPECT_EQ(stats.indeterminate_results, 1u); + EXPECT_EQ(CountOccurrences(diagnostics, "AV2_DECODER_MODEL_WARNING "), 0u); + EXPECT_NE(diagnostics.find("status=INDETERMINATE "), std::string::npos); + EXPECT_NE(diagnostics.find("reason=missing_required_input"), + std::string::npos); +} + +TEST_F(DecoderModelResultTest, MissingActiveConfigurationIsIndeterminate) { + av2_decoder_model_verifier_on_stream_configuration_change(pbi_, false); + av2_decoder_model_verifier_on_sequence_header(pbi_, 0, 0); + StartFrame(OBU_CLOSED_LOOP_KEY); + UpdateAndOutput(); + av2_decoder_model_verifier_finish(pbi_); + + Av2DmVerifierStats stats; + ASSERT_TRUE(av2_decoder_model_verifier_get_stats(pbi_, &stats)); + EXPECT_EQ(stats.result_count, 1u); + EXPECT_EQ(stats.indeterminate_results, 1u); +} + +TEST_F(DecoderModelResultTest, + MissingVariablePresentationTimingHasExactReason) { + pbi_->common.ci_params_per_layer[0].timing_info.equal_elemental_interval = 0; + pbi_->common.seq_params.still_picture = 0; + pbi_->seq_list[0][0] = pbi_->common.seq_params; + av2_decoder_model_verifier_on_active_configuration(pbi_, 0, 0); + + StartFrame(OBU_CLOSED_LOOP_KEY); + pbi_->common.ref_frame_map[0] = &frame_; + pbi_->valid_for_referencing[0] = 1; + av2_decoder_model_verifier_after_reference_update(pbi_, 1, 1); + av2_decoder_model_verifier_on_output(pbi_, -1, &frame_, + AV2_DM_PRESENTATION_OWNER_CURRENT); + + testing::internal::CaptureStderr(); + av2_decoder_model_verifier_finish(pbi_); + const std::string diagnostics = testing::internal::GetCapturedStderr(); + Av2DmVerifierStats stats; + ASSERT_TRUE(av2_decoder_model_verifier_get_stats(pbi_, &stats)); + EXPECT_EQ(stats.indeterminate_results, 1u); + EXPECT_EQ(stats.non_conformant_results, 0u); + EXPECT_NE(diagnostics.find("reason=missing_presentation_timing"), + std::string::npos); +} + +TEST_F(DecoderModelResultTest, + MissingImplicitOwnerHasPresentationProvenanceReason) { + pbi_->common.ci_params_per_layer[0].timing_info.equal_elemental_interval = 0; + pbi_->common.seq_params.still_picture = 0; + pbi_->seq_list[0][0] = pbi_->common.seq_params; + av2_decoder_model_verifier_on_active_configuration(pbi_, 0, 0); + + StartFrame(OBU_CLOSED_LOOP_KEY); + pbi_->common.ref_frame_map[0] = &frame_; + pbi_->valid_for_referencing[0] = 1; + av2_decoder_model_verifier_after_reference_update(pbi_, 1, 1); + av2_decoder_model_verifier_on_output(pbi_, 0, &frame_, + AV2_DM_PRESENTATION_OWNER_IMPLICIT); + + testing::internal::CaptureStderr(); + av2_decoder_model_verifier_finish(pbi_); + const std::string diagnostics = testing::internal::GetCapturedStderr(); + Av2DmVerifierStats stats; + ASSERT_TRUE(av2_decoder_model_verifier_get_stats(pbi_, &stats)); + EXPECT_EQ(stats.indeterminate_results, 1u); + EXPECT_EQ(stats.non_conformant_results, 0u); + EXPECT_NE(diagnostics.find("reason=missing_presentation_provenance"), + std::string::npos); + EXPECT_EQ(diagnostics.find("reason=missing_presentation_timing"), + std::string::npos); +} + +TEST_F(DecoderModelResultTest, + SuffixTemporalPointRemainsWithDelayedImplicitOwner) { + pbi_->common.ci_params_per_layer[0].timing_info.equal_elemental_interval = 0; + pbi_->common.seq_params.still_picture = 0; + pbi_->seq_list[0][0] = pbi_->common.seq_params; + av2_decoder_model_verifier_on_active_configuration(pbi_, 0, 0); + + StartFrame(OBU_CLOSED_LOOP_KEY, 64, 64, &frame_, 0, KEY_FRAME, true, false); + av2_decoder_model_verifier_on_temporal_point(pbi_, 7); + av2_decoder_model_verifier_record_obu(pbi_, OBU_METADATA_SHORT, 0, 0, 0, 80); + av2_decoder_model_verifier_on_frame_unit_complete(pbi_); + pbi_->common.ref_frame_map[0] = &frame_; + pbi_->valid_for_referencing[0] = 1; + av2_decoder_model_verifier_after_reference_update(pbi_, 1, 1); + + av2_decoder_model_verifier_record_obu(pbi_, OBU_TEMPORAL_DELIMITER, 0, 0, 0, + 8); + StartFrame(OBU_REGULAR_TILE_GROUP, 64, 64, &second_frame_, 0, INTER_FRAME); + pbi_->common.ref_frame_map[1] = &second_frame_; + pbi_->valid_for_referencing[1] = 1; + av2_decoder_model_verifier_after_reference_update(pbi_, 2, 3); + av2_decoder_model_verifier_on_output(pbi_, 0, &frame_, + AV2_DM_PRESENTATION_OWNER_IMPLICIT); + + Av2DmVerifierStats output_stats; + ASSERT_TRUE(av2_decoder_model_verifier_get_stats(pbi_, &output_stats)); + EXPECT_EQ(output_stats.last_output_callback_frame_unit, 1u); + EXPECT_EQ(output_stats.last_output_presentation_frame_unit, 0u); + EXPECT_EQ(output_stats.last_output_presentation_temporal_unit, 0u); + EXPECT_FALSE(output_stats.last_output_uses_current_presentation); + + testing::internal::CaptureStderr(); + av2_decoder_model_verifier_finish(pbi_); + const std::string diagnostics = testing::internal::GetCapturedStderr(); + EXPECT_EQ(diagnostics.find("reason=missing_presentation_timing"), + std::string::npos); +} + +TEST_F(DecoderModelResultTest, MissingOutputGenerationIsIndeterminate) { + StartFrame(OBU_CLOSED_LOOP_KEY); + pbi_->common.ref_frame_map[0] = &frame_; + pbi_->valid_for_referencing[0] = 1; + av2_decoder_model_verifier_after_reference_update(pbi_, 1, 1); + second_frame_.xlayer_id = 0; + second_frame_.mlayer_id = 0; + second_frame_.tlayer_id = 0; + second_frame_.width = 64; + second_frame_.height = 64; + + testing::internal::CaptureStderr(); + av2_decoder_model_verifier_on_output(pbi_, -1, &second_frame_, + AV2_DM_PRESENTATION_OWNER_CURRENT); + av2_decoder_model_verifier_finish(pbi_); + const std::string diagnostics = testing::internal::GetCapturedStderr(); + + Av2DmVerifierStats stats; + ASSERT_TRUE(av2_decoder_model_verifier_get_stats(pbi_, &stats)); + EXPECT_EQ(stats.indeterminate_results, 1u); + EXPECT_NE(diagnostics.find("reason=missing_frame_generation"), + std::string::npos); +} + +TEST_F(DecoderModelResultTest, + CopiedShowExistingOutputUsesSourceReferenceGeneration) { + StartFrame(OBU_CLOSED_LOOP_KEY); + UpdateAndOutput(); + + av2_decoder_model_verifier_record_obu(pbi_, OBU_TEMPORAL_DELIMITER, 0, 0, 0, + 8); + av2_decoder_model_verifier_on_source_frame_unit_start(pbi_, 0, 0, 0); + av2_decoder_model_verifier_record_obu(pbi_, OBU_REGULAR_SEF, 0, 0, 0, 80); + AV2_COMMON *const cm = &pbi_->common; + pbi_->obu_type = OBU_REGULAR_SEF; + cm->show_existing_frame = 1; + cm->sef_ref_fb_idx = 0; + cm->cur_frame = &second_frame_; + second_frame_.xlayer_id = 0; + second_frame_.mlayer_id = 0; + second_frame_.tlayer_id = 0; + second_frame_.width = 64; + second_frame_.height = 64; + av2_decoder_model_verifier_on_frame_wrapup_start(pbi_); + av2_decoder_model_verifier_on_frame_unit_complete(pbi_); + av2_decoder_model_verifier_after_reference_update(pbi_, 0, 1); + + testing::internal::CaptureStderr(); + av2_decoder_model_verifier_on_output(pbi_, 0, &second_frame_, + AV2_DM_PRESENTATION_OWNER_CURRENT); + Av2DmVerifierStats output_stats; + ASSERT_TRUE(av2_decoder_model_verifier_get_stats(pbi_, &output_stats)); + EXPECT_EQ(output_stats.last_output_callback_frame_unit, 1u); + EXPECT_EQ(output_stats.last_output_presentation_frame_unit, 1u); + EXPECT_EQ(output_stats.last_output_presentation_temporal_unit, 1u); + EXPECT_EQ(output_stats.last_output_generation, 1u); + EXPECT_TRUE(output_stats.last_output_uses_current_presentation); + av2_decoder_model_verifier_finish(pbi_); + const std::string diagnostics = testing::internal::GetCapturedStderr(); + + EXPECT_NE(diagnostics.find("status=CONFORMANT"), std::string::npos); + EXPECT_NE(diagnostics.find("decoded=1 outputs=2"), std::string::npos); + EXPECT_EQ(diagnostics.find("reason=missing_frame_generation"), + std::string::npos); +} + +TEST_F(DecoderModelResultTest, + AliasedShowExistingUsesCurrentOwnerWithoutConsumingImplicitOwner) { + StartFrame(OBU_CLOSED_LOOP_KEY, 64, 64, &frame_, 0, KEY_FRAME, true); + pbi_->common.ref_frame_map[0] = &frame_; + pbi_->valid_for_referencing[0] = 1; + av2_decoder_model_verifier_after_reference_update(pbi_, 1, 1); + + av2_decoder_model_verifier_record_obu(pbi_, OBU_TEMPORAL_DELIMITER, 0, 0, 0, + 8); + av2_decoder_model_verifier_on_source_frame_unit_start(pbi_, 0, 0, 0); + av2_decoder_model_verifier_record_obu(pbi_, OBU_REGULAR_SEF, 0, 0, 0, 80); + AV2_COMMON *const cm = &pbi_->common; + pbi_->obu_type = OBU_REGULAR_SEF; + cm->show_existing_frame = 1; + cm->sef_ref_fb_idx = 0; + cm->cur_frame = &frame_; + av2_decoder_model_verifier_on_frame_wrapup_start(pbi_); + av2_decoder_model_verifier_on_frame_unit_complete(pbi_); + av2_decoder_model_verifier_after_reference_update(pbi_, 0, 1); + + av2_decoder_model_verifier_on_output(pbi_, 0, &frame_, + AV2_DM_PRESENTATION_OWNER_CURRENT); + Av2DmVerifierStats current_stats; + ASSERT_TRUE(av2_decoder_model_verifier_get_stats(pbi_, ¤t_stats)); + EXPECT_EQ(current_stats.last_output_callback_frame_unit, 1u); + EXPECT_EQ(current_stats.last_output_presentation_frame_unit, 1u); + EXPECT_EQ(current_stats.last_output_presentation_temporal_unit, 1u); + EXPECT_EQ(current_stats.last_output_generation, 1u); + EXPECT_TRUE(current_stats.last_output_uses_current_presentation); + + av2_decoder_model_verifier_on_output(pbi_, 0, &frame_, + AV2_DM_PRESENTATION_OWNER_IMPLICIT); + Av2DmVerifierStats implicit_stats; + ASSERT_TRUE(av2_decoder_model_verifier_get_stats(pbi_, &implicit_stats)); + EXPECT_EQ(implicit_stats.last_output_callback_frame_unit, 1u); + EXPECT_EQ(implicit_stats.last_output_presentation_frame_unit, 0u); + EXPECT_EQ(implicit_stats.last_output_presentation_temporal_unit, 0u); + EXPECT_EQ(implicit_stats.last_output_generation, 1u); + EXPECT_FALSE(implicit_stats.last_output_uses_current_presentation); +} + +TEST_F(DecoderModelResultTest, IncompleteRasSeedHasExactReason) { + RefCntBuffer external_long_term; + memset(&external_long_term, 0, sizeof(external_long_term)); + external_long_term.long_term_id = 7; + pbi_->common.ref_frame_map[0] = &external_long_term; + pbi_->valid_for_referencing[0] = 1; + + testing::internal::CaptureStderr(); + StartFrame(OBU_RAS_FRAME, 64, 64, &second_frame_); + pbi_->common.ref_frame_map[1] = &second_frame_; + pbi_->valid_for_referencing[1] = 1; + av2_decoder_model_verifier_after_reference_update(pbi_, 2, 3); + av2_decoder_model_verifier_finish(pbi_); + const std::string diagnostics = testing::internal::GetCapturedStderr(); + + Av2DmVerifierStats stats; + ASSERT_TRUE(av2_decoder_model_verifier_get_stats(pbi_, &stats)); + EXPECT_EQ(stats.indeterminate_results, 1u); + EXPECT_NE(diagnostics.find("status=INDETERMINATE"), std::string::npos); + EXPECT_NE(diagnostics.find("reason=incomplete_ras_seed"), std::string::npos); +} + +TEST_F(DecoderModelResultTest, + DecoderRecoveryResetMakesOverlappingRunsIndeterminate) { + StartFrame(OBU_CLOSED_LOOP_KEY); + UpdateAndOutput(); + + av2_decoder_model_verifier_on_recovery_reset(pbi_); + StartFrame(OBU_CLOSED_LOOP_KEY, 64, 64, &second_frame_); + pbi_->common.ref_frame_map[0] = &second_frame_; + pbi_->valid_for_referencing[0] = 1; + av2_decoder_model_verifier_after_reference_update(pbi_, 1, 1); + av2_decoder_model_verifier_on_output(pbi_, -1, &second_frame_, + AV2_DM_PRESENTATION_OWNER_CURRENT); + + testing::internal::CaptureStderr(); + av2_decoder_model_verifier_finish(pbi_); + const std::string diagnostics = testing::internal::GetCapturedStderr(); + + Av2DmVerifierStats stats; + ASSERT_TRUE(av2_decoder_model_verifier_get_stats(pbi_, &stats)); + EXPECT_EQ(stats.result_count, 2u); + EXPECT_EQ(stats.indeterminate_results, 2u); + EXPECT_EQ(stats.conformant_results, 0u); + EXPECT_NE(diagnostics.find("reason=decoder_recovery_reset"), + std::string::npos); +} + +TEST_F(DecoderModelResultTest, + UnknownFailedObuAffectsEveryDisjointOperatingPoint) { + pbi_->seq_list[0][0].seq_max_mlayer_cnt = 2; + pbi_->common.seq_params = pbi_->seq_list[0][0]; + av2_decoder_model_verifier_on_active_configuration(pbi_, 0, 0); + OperatingPointSet *const ops = &pbi_->ops_list[0][7]; + memset(ops, 0, sizeof(*ops)); + ops->valid = 1; + ops->obu_xlayer_id = 0; + ops->ops_id = 7; + ops->ops_cnt = 2; + ops->ops_mlayer_info_idc = 1; + ops->op[0].mlayer_info.ops_mlayer_map[0] = 1; + ops->op[0].mlayer_info.ops_tlayer_map[0][0] = 1; + ops->op[1].mlayer_info.ops_mlayer_map[0] = 2; + ops->op[1].mlayer_info.ops_tlayer_map[0][1] = 1; + av2_decoder_model_verifier_on_operating_point_set(pbi_, 0, 7); + + StartFrame(OBU_CLOSED_LOOP_KEY, 64, 64, &frame_, 1); + UpdateAndOutput(); + + // The next source frame unit fails before its OBU header/payload has been + // recorded, so its Annex F membership cannot be inferred from the prior + // layer-1 OBU. + av2_decoder_model_verifier_on_source_frame_unit_start(pbi_, 0, 0, 0); + av2_decoder_model_verifier_on_recovery_reset(pbi_); + StartFrame(OBU_CLOSED_LOOP_KEY, 64, 64, &second_frame_, 0); + pbi_->common.ref_frame_map[0] = &second_frame_; + pbi_->valid_for_referencing[0] = 1; + av2_decoder_model_verifier_after_reference_update(pbi_, 1, 1); + av2_decoder_model_verifier_on_output(pbi_, -1, &second_frame_, + AV2_DM_PRESENTATION_OWNER_CURRENT); + + testing::internal::CaptureStderr(); + av2_decoder_model_verifier_finish(pbi_); + const std::string diagnostics = testing::internal::GetCapturedStderr(); + + EXPECT_NE(diagnostics.find("status=INDETERMINATE xlayer=0 ops=7 op=0"), + std::string::npos); + EXPECT_EQ(diagnostics.find("status=CONFORMANT xlayer=0 ops=7 op=0"), + std::string::npos); +} + +TEST_F(DecoderModelResultTest, CompleteRasSeedIsReplayedFromFreshModel) { + frame_.long_term_id = 5; + StartFrame(OBU_CLOSED_LOOP_KEY, 64, 64, &frame_, 0, KEY_FRAME, true); + pbi_->common.ref_frame_map[0] = &frame_; + pbi_->valid_for_referencing[0] = 1; + av2_decoder_model_verifier_after_reference_update(pbi_, 1, 1); + av2_decoder_model_verifier_on_output(pbi_, -1, &frame_, + AV2_DM_PRESENTATION_OWNER_CURRENT); + av2_decoder_model_verifier_record_obu(pbi_, OBU_TEMPORAL_DELIMITER, 0, 0, 0, + 8); + + StartFrame(OBU_RAS_FRAME, 64, 64, &second_frame_); + pbi_->common.current_frame.refresh_frame_flags = 2; + pbi_->common.ref_frame_map[1] = &second_frame_; + pbi_->valid_for_referencing[1] = 1; + av2_decoder_model_verifier_after_reference_update(pbi_, 2, 3); + testing::internal::CaptureStderr(); + av2_decoder_model_verifier_on_output(pbi_, 0, &frame_, + AV2_DM_PRESENTATION_OWNER_IMPLICIT); + av2_decoder_model_verifier_finish(pbi_); + const std::string diagnostics = testing::internal::GetCapturedStderr(); + + Av2DmVerifierStats stats; + ASSERT_TRUE(av2_decoder_model_verifier_get_stats(pbi_, &stats)); + EXPECT_EQ(stats.result_count, 2u); + // In the continuous CLK run the TU-0 frame is not output until the RAS + // callback and is therefore late. The independently initialized RAS run is + // conformant using the seeded long-term generation. + EXPECT_EQ(stats.conformant_results, 1u); + EXPECT_EQ(stats.non_conformant_results, 1u); + EXPECT_EQ(stats.indeterminate_results, 0u); + EXPECT_EQ(CountOccurrences(diagnostics, "code=DISPLAY_FRAME_LATE "), 1u); +} + +TEST_F(DecoderModelResultTest, + AbsentSequenceDelayUsesReferenceCountBasedInference) { + pbi_->seq_list[0][0].seq_max_display_model_info_present_flag = 0; + pbi_->seq_list[0][0].seq_max_initial_display_delay_minus_1 = + BUFFER_POOL_MAX_SIZE - 1; + pbi_->common.seq_params = pbi_->seq_list[0][0]; + av2_decoder_model_verifier_on_active_configuration(pbi_, 0, 0); + + StartFrame(OBU_CLOSED_LOOP_KEY); + Av2DmContextStats stats; + ASSERT_TRUE(av2_decoder_model_verifier_get_context_stats(pbi_, 0, &stats)); + EXPECT_TRUE(stats.resolved_config_present); + EXPECT_EQ(stats.resolved_initial_display_delay, 10u); +} + +TEST_F(DecoderModelResultTest, ScheduleModeRequiresSignalledDecodingClock) { + pbi_->seq_list[0][0].seq_max_decoder_model_present_flag = 1; + pbi_->seq_list[0][0].decoder_model_info_present_flag = 0; + pbi_->common.seq_params = pbi_->seq_list[0][0]; + av2_decoder_model_verifier_on_active_configuration(pbi_, 0, 0); + + StartFrame(OBU_CLOSED_LOOP_KEY); + Av2DmContextStats stats; + ASSERT_TRUE(av2_decoder_model_verifier_get_context_stats(pbi_, 0, &stats)); + EXPECT_EQ(stats.resolved_mode, AV2_DM_DECODING_SCHEDULE_MODE); + EXPECT_EQ(stats.resolved_applicability, AV2_DM_MISSING_REQUIRED_INPUT); +} + +static_assert(AVM_DECODER_CTRL_ID_MAX == 279, + "Existing decoder control IDs must not change"); +static_assert(AVMD_INCR_OUTPUT_FRAMES_OFFSET == 291, + "Existing decoder control IDs must not change"); +static_assert(AV2D_SET_DECODER_MODEL_CHECK_MODE == 292, + "The decoder-model control must be appended"); + +TEST(DecoderModelControlTest, RejectsInvalidAndLateModeChanges) { + avm_codec_dec_cfg_t config = {}; + libavm_test::AV2Decoder decoder(config); + decoder.Control(AV2D_SET_DECODER_MODEL_CHECK_MODE, + AVM_DECODER_MODEL_CHECK_OFF); + decoder.Control(AV2D_SET_DECODER_MODEL_CHECK_MODE, + AVM_DECODER_MODEL_CHECK_WARN); + decoder.Control(AV2D_SET_DECODER_MODEL_CHECK_MODE, + AVM_DECODER_MODEL_CHECK_FATAL); + decoder.Control(AV2D_SET_DECODER_MODEL_CHECK_MODE, 3, + AVM_CODEC_INVALID_PARAM); + + const uint8_t invalid_input = 0; + EXPECT_NE(decoder.DecodeFrame(&invalid_input, 1), AVM_CODEC_OK); + decoder.Control(AV2D_SET_DECODER_MODEL_CHECK_MODE, + AVM_DECODER_MODEL_CHECK_WARN, AVM_CODEC_INVALID_PARAM); +} + +#if CONFIG_AV2_ENCODER && CONFIG_AV2_DECODER + +struct DecoderModelEncodedPacket { + std::vector bytes; + avm_codec_pts_t pts; +}; + +struct DecoderModelDecodedFrame { + avm_img_fmt_t format; + unsigned int width; + unsigned int height; + unsigned int render_width; + unsigned int render_height; + unsigned int bit_depth; + unsigned int x_chroma_shift; + unsigned int y_chroma_shift; + int monochrome; + int color_primaries; + int transfer_characteristics; + int matrix_coefficients; + int color_range; + int tlayer_id; + int mlayer_id; + int xlayer_id; + uintptr_t timestamp; + std::vector pixels; + + bool operator==(const DecoderModelDecodedFrame &other) const { + return format == other.format && width == other.width && + height == other.height && render_width == other.render_width && + render_height == other.render_height && + bit_depth == other.bit_depth && + x_chroma_shift == other.x_chroma_shift && + y_chroma_shift == other.y_chroma_shift && + monochrome == other.monochrome && + color_primaries == other.color_primaries && + transfer_characteristics == other.transfer_characteristics && + matrix_coefficients == other.matrix_coefficients && + color_range == other.color_range && tlayer_id == other.tlayer_id && + mlayer_id == other.mlayer_id && xlayer_id == other.xlayer_id && + timestamp == other.timestamp && pixels == other.pixels; + } +}; + +struct DecoderModelDecodeOutput { + std::vector frames; + std::vector statuses; + std::string diagnostics; +}; + +static bool ParseSequenceLevel(uint8_t *payload, size_t payload_size, + uint32_t *level_bit_offset, uint32_t *level) { + if (payload == nullptr || payload_size == 0 || level_bit_offset == nullptr || + level == nullptr) { + return false; + } + avm_read_bit_buffer reader = { payload, payload + payload_size, 0, nullptr, + nullptr }; + const uint32_t sequence_header_id = avm_rb_read_uvlc(&reader); + const uint32_t profile = avm_rb_read_literal(&reader, PROFILE_BITS); + (void)avm_rb_read_bit(&reader); + if (sequence_header_id >= MAX_SEQ_NUM || profile >= MAX_PROFILES || + reader.bit_offset > payload_size * 8 || + payload_size * 8 - reader.bit_offset < LEVEL_BITS) { + return false; + } + *level_bit_offset = reader.bit_offset; + *level = avm_rb_read_literal(&reader, LEVEL_BITS); + return is_valid_seq_level_idx(static_cast(*level)); +} + +static void WriteBits(uint8_t *data, uint32_t bit_offset, uint32_t bit_count, + uint32_t value) { + for (uint32_t bit = 0; bit < bit_count; ++bit) { + const uint32_t position = bit_offset + bit; + const uint8_t mask = static_cast(1u << (7 - position % 8)); + if ((value >> (bit_count - bit - 1)) & 1) { + data[position / 8] |= mask; + } else { + data[position / 8] &= static_cast(~mask); + } + } +} + +static bool RewriteSequenceLevels(std::vector *data, + AV2_LEVEL expected_level, + AV2_LEVEL replacement_level, + size_t *rewritten_headers) { + if (data == nullptr || rewritten_headers == nullptr || + expected_level >= SEQ_LEVEL_4_0 || replacement_level >= SEQ_LEVEL_4_0) { + return false; + } + *rewritten_headers = 0; + for (DecoderModelEncodedPacket &packet : *data) { + size_t offset = 0; + while (offset < packet.bytes.size()) { + ObuHeader header; + size_t payload_size = 0; + size_t bytes_read = 0; + const size_t remaining = packet.bytes.size() - offset; + if (avm_read_obu_header_and_size(packet.bytes.data() + offset, remaining, + &header, &payload_size, + &bytes_read) != AVM_CODEC_OK || + bytes_read > remaining || payload_size > remaining - bytes_read) { + return false; + } + if (header.type == OBU_SEQUENCE_HEADER) { + uint8_t *const payload = packet.bytes.data() + offset + bytes_read; + uint32_t level_offset = 0; + uint32_t level = 0; + if (!ParseSequenceLevel(payload, payload_size, &level_offset, &level) || + level != static_cast(expected_level)) { + return false; + } + WriteBits(payload, level_offset, LEVEL_BITS, + static_cast(replacement_level)); + uint32_t reparsed_offset = 0; + if (!ParseSequenceLevel(payload, payload_size, &reparsed_offset, + &level) || + reparsed_offset != level_offset || + level != static_cast(replacement_level)) { + return false; + } + ++*rewritten_headers; + } + offset += bytes_read + payload_size; + } + } + return *rewritten_headers != 0; +} + +static DecoderModelDecodedFrame CopyDecodedFrame(const avm_image_t &image) { + DecoderModelDecodedFrame frame; + frame.format = image.fmt; + frame.width = image.d_w; + frame.height = image.d_h; + frame.render_width = image.r_w; + frame.render_height = image.r_h; + frame.bit_depth = image.bit_depth; + frame.x_chroma_shift = image.x_chroma_shift; + frame.y_chroma_shift = image.y_chroma_shift; + frame.monochrome = image.monochrome; + frame.color_primaries = image.cp; + frame.transfer_characteristics = image.tc; + frame.matrix_coefficients = image.mc; + frame.color_range = image.range; + frame.tlayer_id = image.tlayer_id; + frame.mlayer_id = image.mlayer_id; + frame.xlayer_id = image.xlayer_id; + frame.timestamp = reinterpret_cast(image.user_priv); + const int plane_count = image.monochrome ? 1 : 3; + const int bytes_per_sample = + (image.fmt & AVM_IMG_FMT_HIGHBITDEPTH) != 0 ? 2 : 1; + for (int plane = 0; plane < plane_count; ++plane) { + const int plane_width = avm_img_plane_width(&image, plane); + const int plane_height = avm_img_plane_height(&image, plane); + for (int row = 0; row < plane_height; ++row) { + const uint8_t *const row_start = + image.planes[plane] + row * image.stride[plane]; + frame.pixels.insert(frame.pixels.end(), row_start, + row_start + plane_width * bytes_per_sample); + } + } + return frame; +} + +static void AppendDecodedFrames(libavm_test::AV2Decoder *decoder, + DecoderModelDecodeOutput *output) { + libavm_test::DxDataIterator iterator = decoder->GetDxData(); + const avm_image_t *image = nullptr; + while ((image = iterator.Next()) != nullptr) { + output->frames.push_back(CopyDecodedFrame(*image)); + } +} + +static DecoderModelDecodeOutput DecodePackets( + const std::vector &packets, + avm_decoder_model_check_mode_t mode = AVM_DECODER_MODEL_CHECK_WARN, + bool set_mode = true) { + DecoderModelDecodeOutput output; + avm_codec_dec_cfg_t config = {}; + config.threads = 1; + libavm_test::AV2Decoder decoder(config); + if (set_mode) decoder.Control(AV2D_SET_DECODER_MODEL_CHECK_MODE, mode); + testing::internal::CaptureStderr(); + for (const DecoderModelEncodedPacket &packet : packets) { + void *const timestamp = reinterpret_cast( + static_cast(packet.pts) + static_cast(1)); + const avm_codec_err_t status = decoder.DecodeFrame( + packet.bytes.data(), packet.bytes.size(), timestamp); + output.statuses.push_back(status); + if (status != AVM_CODEC_OK) break; + AppendDecodedFrames(&decoder, &output); + } + const avm_codec_err_t flush_status = decoder.DecodeFrame(nullptr, 0); + output.statuses.push_back(flush_status); + if (flush_status == AVM_CODEC_OK) AppendDecodedFrames(&decoder, &output); + output.diagnostics = testing::internal::GetCapturedStderr(); + return output; +} + +class DecoderModelEncodedStreamTest : public ::testing::Test, + public libavm_test::EncoderTest { + protected: + DecoderModelEncodedStreamTest() + : EncoderTest(&libavm_test::kAV2), controls_set_(false), + target_level_(SEQ_LEVEL_3_0) {} + + void SetUp() override { + InitializeConfig(); + SetMode(libavm_test::kOnePassGood); + cfg_.g_threads = 1; + cfg_.g_lag_in_frames = 0; + cfg_.kf_min_dist = 9999; + cfg_.kf_max_dist = 9999; + cfg_.rc_end_usage = AVM_Q; + } + + void PreEncodeFrameHook(libavm_test::VideoSource *video, + libavm_test::Encoder *encoder) override { + if (controls_set_ || video->frame() != 0) return; + encoder->Control(AVME_SET_CPUUSED, 5); + encoder->Control(AVME_SET_QP, 235); + encoder->Control(AV2E_SET_TARGET_SEQ_LEVEL_IDX, target_level_); + encoder->Control(AV2E_SET_TIMING_INFO_TYPE, AVM_TIMING_EQUAL); + encoder->Control(AV2E_SET_ENABLE_INTRABC, 0); + encoder->Control(AV2E_SET_MAX_REFERENCE_FRAMES, 3); + encoder->SetOption("enable-intrabc-ext", "0"); + encoder->SetOption("dpb-size", "4"); + controls_set_ = true; + } + + bool DoDecode() const override { return false; } + + void FramePktHook(const avm_codec_cx_pkt_t *packet, + libavm_test::DxDataIterator *) override { + if (packet->kind != AVM_CODEC_CX_FRAME_PKT) return; + DecoderModelEncodedPacket encoded; + const uint8_t *const begin = + static_cast(packet->data.frame.buf); + encoded.bytes.assign(begin, begin + packet->data.frame.sz); + encoded.pts = packet->data.frame.pts; + packets_.push_back(encoded); + } + + bool controls_set_; + AV2_LEVEL target_level_; + std::vector packets_; +}; + +TEST_F(DecoderModelEncodedStreamTest, Level20Control) { + Av2DmLevelLimits limits; + ASSERT_TRUE( + av2_dm_get_level_limits(SEQ_LEVEL_2_0, 0, MAIN_420_10_IP0, &limits)); + constexpr uint64_t kPictureSize = 352 * 288; + ASSERT_LE(kPictureSize, limits.max_picture_size); + ASSERT_LE(352u, limits.max_horizontal_size); + ASSERT_LE(288u, limits.max_vertical_size); + ASSERT_LE(kPictureSize * 10, limits.max_display_rate); + ASSERT_LE(kPictureSize * 10, limits.max_decode_rate); + + target_level_ = SEQ_LEVEL_2_0; + libavm_test::I420VideoSource video("hantro_collage_w352h288.yuv", 352, 288, + 10, 1, 0, 12); + ASSERT_NO_FATAL_FAILURE(RunLoop(&video)); + ASSERT_FALSE(packets_.empty()); + const DecoderModelDecodeOutput output = DecodePackets(packets_); + const DecoderModelDecodeOutput explicit_off = + DecodePackets(packets_, AVM_DECODER_MODEL_CHECK_OFF); + const DecoderModelDecodeOutput default_off = + DecodePackets(packets_, AVM_DECODER_MODEL_CHECK_OFF, false); + for (const avm_codec_err_t status : output.statuses) { + ASSERT_EQ(status, AVM_CODEC_OK); + } + EXPECT_EQ(output.frames.size(), 12u); + EXPECT_EQ(CountOccurrences(output.diagnostics, "AV2_DECODER_MODEL_RESULT "), + 1u); + EXPECT_EQ(CountOccurrences(output.diagnostics, "AV2_DECODER_MODEL_WARNING "), + 0u); + EXPECT_NE(output.diagnostics.find("status=CONFORMANT"), std::string::npos); + EXPECT_NE(output.diagnostics.find("violations=0 reason=none"), + std::string::npos); + EXPECT_EQ(explicit_off.statuses, default_off.statuses); + EXPECT_EQ(explicit_off.frames, default_off.frames); + EXPECT_EQ(explicit_off.diagnostics.find("AV2_DECODER_MODEL_"), + std::string::npos); + EXPECT_EQ(default_off.diagnostics.find("AV2_DECODER_MODEL_"), + std::string::npos); +} + +TEST_F(DecoderModelEncodedStreamTest, Level30AndIncorrectLevel21) { + Av2DmLevelLimits level_2_1; + Av2DmLevelLimits level_3_0; + ASSERT_TRUE( + av2_dm_get_level_limits(SEQ_LEVEL_2_1, 0, MAIN_420_10_IP0, &level_2_1)); + ASSERT_TRUE( + av2_dm_get_level_limits(SEQ_LEVEL_3_0, 0, MAIN_420_10_IP0, &level_3_0)); + constexpr uint64_t kPictureSize = 640 * 480; + ASSERT_GT(kPictureSize, level_2_1.max_picture_size); + ASSERT_LE(kPictureSize, level_3_0.max_picture_size); + ASSERT_LE(640u, level_2_1.max_horizontal_size); + ASSERT_LE(480u, level_2_1.max_vertical_size); + ASSERT_LE(kPictureSize * 10, level_2_1.max_display_rate); + ASSERT_LE(kPictureSize * 10, level_2_1.max_decode_rate); + + libavm_test::I420VideoSource video("niklas_640_480_30.yuv", 640, 480, 10, 1, + 0, 12); + ASSERT_NO_FATAL_FAILURE(RunLoop(&video)); + ASSERT_FALSE(packets_.empty()); + std::vector incorrect_level = packets_; + size_t rewritten_headers = 0; + ASSERT_TRUE(RewriteSequenceLevels(&incorrect_level, SEQ_LEVEL_3_0, + SEQ_LEVEL_2_1, &rewritten_headers)); + ASSERT_GT(rewritten_headers, 0u); + + const DecoderModelDecodeOutput positive = DecodePackets(packets_); + const DecoderModelDecodeOutput negative = DecodePackets(incorrect_level); + const DecoderModelDecodeOutput fatal = + DecodePackets(incorrect_level, AVM_DECODER_MODEL_CHECK_FATAL); + ASSERT_EQ(positive.statuses, negative.statuses); + for (const avm_codec_err_t status : positive.statuses) { + ASSERT_EQ(status, AVM_CODEC_OK); + } + ASSERT_EQ(positive.frames, negative.frames); + ASSERT_EQ(positive.frames.size(), 12u); + + EXPECT_EQ(CountOccurrences(positive.diagnostics, "AV2_DECODER_MODEL_RESULT "), + 1u); + EXPECT_EQ( + CountOccurrences(positive.diagnostics, "AV2_DECODER_MODEL_WARNING "), 0u); + EXPECT_NE(positive.diagnostics.find("status=CONFORMANT"), std::string::npos); + EXPECT_NE(positive.diagnostics.find("violations=0 reason=none"), + std::string::npos); + + EXPECT_EQ(CountOccurrences(negative.diagnostics, "AV2_DECODER_MODEL_RESULT "), + 1u); + EXPECT_EQ( + CountOccurrences(negative.diagnostics, "AV2_DECODER_MODEL_WARNING "), + positive.frames.size()); + EXPECT_EQ(CountOccurrences(negative.diagnostics, "code=MAX_PICTURE_SIZE "), + positive.frames.size()); + EXPECT_NE(negative.diagnostics.find("status=NON_CONFORMANT"), + std::string::npos); + EXPECT_NE(negative.diagnostics.find("violations=12 reason=none"), + std::string::npos); + + ASSERT_FALSE(fatal.statuses.empty()); + EXPECT_EQ(fatal.statuses.front(), AVM_CODEC_UNSUP_BITSTREAM); + EXPECT_EQ(CountOccurrences(fatal.diagnostics, "AV2_DECODER_MODEL_WARNING "), + 1u); + EXPECT_EQ( + CountOccurrences(fatal.diagnostics, "AV2_DECODER_MODEL_CVS_RESULT "), 1u); + EXPECT_EQ(CountOccurrences(fatal.diagnostics, + "AV2_DECODER_MODEL_BITSTREAM_RESULT "), + 1u); + EXPECT_NE(fatal.diagnostics.find("status=NON_CONFORMANT"), std::string::npos); + EXPECT_NE(fatal.diagnostics.find("complete=0"), std::string::npos); +} + +#endif // CONFIG_AV2_ENCODER && CONFIG_AV2_DECODER + +} // namespace diff --git a/test/decoder_model_parser_test.cc b/test/decoder_model_parser_test.cc new file mode 100644 index 0000000000..87f815c69e --- /dev/null +++ b/test/decoder_model_parser_test.cc @@ -0,0 +1,762 @@ +/* + * Copyright (c) 2026, Alliance for Open Media. All rights reserved + * + * This source code is subject to the terms of the BSD 3-Clause Clear License + * and the Alliance for Open Media Patent License 1.0. If the BSD 3-Clause + * Clear License was not distributed with this source code in the LICENSE file, + * you can obtain it at aomedia.org/license/software-license/bsd-3-c-c/. If the + * Alliance for Open Media Patent License 1.0 was not distributed with this + * source code in the PATENTS file, you can obtain it at + * aomedia.org/license/patent-license/. + */ + +#include +#include +#include + +#include "third_party/googletest/src/googletest/include/gtest/gtest.h" + +#include "avm_dsp/bitreader.h" +#include "avm_dsp/bitwriter.h" +#include "avm_mem/avm_mem.h" +#include "av2/decoder/annexF.h" +#include "av2/decoder/decoder.h" +#include "av2/decoder/decoder_model.h" + +namespace { + +class DecoderModelParserTest : public ::testing::Test { + protected: + void SetUp() override { + pbi_ = static_cast(avm_memalign(32, sizeof(*pbi_))); + ASSERT_NE(pbi_, nullptr); + memset(pbi_, 0, sizeof(*pbi_)); + av2_decoder_model_verifier_init(pbi_); + ASSERT_NE(pbi_->decoder_model_verifier, nullptr); + } + + void TearDown() override { + av2_decoder_model_verifier_destroy(pbi_); + avm_free(pbi_); + } + + void AddWholeXlayerContext(int xlayer_id, int sequence_header_id) { + pbi_->seq_list[xlayer_id][sequence_header_id].seq_header_id = + sequence_header_id; + av2_decoder_model_verifier_on_sequence_header(pbi_, xlayer_id, + sequence_header_id); + } + + AV2Decoder *pbi_ = nullptr; +}; + +TEST_F(DecoderModelParserTest, LifecycleStartsWithAvailableEmptyState) { + Av2DmVerifierStats stats; + ASSERT_TRUE(av2_decoder_model_verifier_get_stats(pbi_, &stats)); + EXPECT_TRUE(stats.available); + EXPECT_FALSE(stats.failed); + EXPECT_EQ(stats.raw_obus, 0u); + EXPECT_EQ(stats.contexts, 0u); +} + +TEST_F(DecoderModelParserTest, ReplaysPrefixAndClosesCompleteDfg) { + av2_decoder_model_verifier_record_obu(pbi_, OBU_TEMPORAL_DELIMITER, 0, 0, 0, + 16); + av2_decoder_model_verifier_record_obu(pbi_, OBU_SEQUENCE_HEADER, 0, 0, 0, 80); + AddWholeXlayerContext(0, 0); + av2_decoder_model_verifier_record_obu(pbi_, OBU_REGULAR_TILE_GROUP, 0, 0, 0, + 120); + av2_decoder_model_verifier_record_obu(pbi_, OBU_METADATA_SHORT, 0, 0, 0, 40); + + Av2DmContextStats context; + ASSERT_TRUE(av2_decoder_model_verifier_get_context_stats(pbi_, 0, &context)); + EXPECT_EQ(context.pending_dfg_bits, 256u); + + pbi_->common.show_existing_frame = 0; + av2_decoder_model_verifier_on_frame_unit_complete(pbi_); + ASSERT_TRUE(av2_decoder_model_verifier_get_context_stats(pbi_, 0, &context)); + EXPECT_EQ(context.pending_dfg_bits, 0u); + EXPECT_EQ(context.last_closed_dfg_bits, 256u); + EXPECT_EQ(context.closed_dfgs, 1u); +} + +TEST_F(DecoderModelParserTest, ShowExistingDoesNotConsumePendingDfgBits) { + AddWholeXlayerContext(0, 0); + av2_decoder_model_verifier_record_obu(pbi_, OBU_SEQUENCE_HEADER, 0, 0, 0, 24); + pbi_->common.show_existing_frame = 1; + av2_decoder_model_verifier_on_frame_unit_complete(pbi_); + + Av2DmContextStats context; + ASSERT_TRUE(av2_decoder_model_verifier_get_context_stats(pbi_, 0, &context)); + EXPECT_EQ(context.pending_dfg_bits, 24u); + EXPECT_EQ(context.closed_dfgs, 0u); + + av2_decoder_model_verifier_record_obu(pbi_, OBU_METADATA_SHORT, 0, 0, 0, 8); + pbi_->common.show_existing_frame = 0; + av2_decoder_model_verifier_on_frame_unit_complete(pbi_); + ASSERT_TRUE(av2_decoder_model_verifier_get_context_stats(pbi_, 0, &context)); + EXPECT_EQ(context.last_closed_dfg_bits, 32u); + EXPECT_EQ(context.closed_dfgs, 1u); +} + +TEST_F(DecoderModelParserTest, OperatingPointUsesAnnexFMembership) { + av2_decoder_model_verifier_record_obu(pbi_, OBU_OPERATING_POINT_SET, 0, 0, 0, + 32); + OperatingPointSet *const ops = &pbi_->ops_list[0][3]; + memset(ops, 0, sizeof(*ops)); + ops->valid = 1; + ops->obu_xlayer_id = 0; + ops->ops_id = 3; + ops->ops_cnt = 1; + ops->ops_mlayer_info_idc = 1; + ops->op[0].mlayer_info.ops_mlayer_map[0] = 1 << 1; + ops->op[0].mlayer_info.ops_tlayer_map[0][1] = 1 << 2; + av2_decoder_model_verifier_on_operating_point_set(pbi_, 0, 3); + + av2_decoder_model_verifier_record_obu(pbi_, OBU_REGULAR_TILE_GROUP, 0, 1, 2, + 100); + av2_decoder_model_verifier_record_obu(pbi_, OBU_REGULAR_TILE_GROUP, 0, 0, 1, + 200); + av2_decoder_model_verifier_record_obu(pbi_, OBU_SEQUENCE_HEADER, 0, 0, 0, 16); + av2_decoder_model_verifier_record_obu( + pbi_, OBU_MULTI_STREAM_DECODER_OPERATION, GLOBAL_XLAYER_ID, 0, 0, 24); + + Av2DmContextStats context; + ASSERT_TRUE(av2_decoder_model_verifier_get_context_stats(pbi_, 0, &context)); + EXPECT_FALSE(context.scope.whole_xlayer); + EXPECT_EQ(context.scope.ops_xlayer_id, 0); + EXPECT_EQ(context.scope.ops_id, 3); + // OPS prefix + selected frame + preserved sequence/global structural OBUs. + EXPECT_EQ(context.pending_dfg_bits, 172u); +} + +TEST_F(DecoderModelParserTest, RasSeedsAreFilteredPerOperatingPoint) { + SequenceHeader *const sequence = &pbi_->seq_list[0][0]; + sequence->seq_header_id = 0; + sequence->seq_max_level_idx = SEQ_LEVEL_2_0; + sequence->seq_profile_idc = MAIN_420_10_IP0; + sequence->ref_frames = 8; + sequence->max_frame_width = 64; + sequence->max_frame_height = 64; + sequence->seq_max_mlayer_cnt = 2; + sequence->still_picture = 1; + pbi_->common.seq_params = *sequence; + ContentInterpretation *const ci = &pbi_->common.ci_params_per_layer[0]; + ci->ci_timing_info_present_flag = 1; + ci->timing_info.num_units_in_display_tick = 1; + ci->timing_info.time_scale = 30; + ci->timing_info.equal_elemental_interval = 1; + ci->timing_info.num_ticks_per_elemental_duration = 1; + av2_decoder_model_verifier_on_sequence_header(pbi_, 0, 0); + + OperatingPointSet *const ops = &pbi_->ops_list[0][3]; + memset(ops, 0, sizeof(*ops)); + ops->valid = 1; + ops->obu_xlayer_id = 0; + ops->ops_id = 3; + ops->ops_cnt = 2; + ops->ops_mlayer_info_idc = 1; + ops->op[0].mlayer_info.ops_mlayer_map[0] = 1; + ops->op[0].mlayer_info.ops_tlayer_map[0][0] = 1; + ops->op[1].mlayer_info.ops_mlayer_map[0] = 3; + ops->op[1].mlayer_info.ops_tlayer_map[0][0] = 1; + ops->op[1].mlayer_info.ops_tlayer_map[0][1] = 1; + av2_decoder_model_verifier_on_operating_point_set(pbi_, 0, 3); + av2_decoder_model_verifier_on_active_configuration(pbi_, 0, 0); + + RefCntBuffer long_term; + RefCntBuffer untracked_long_term; + RefCntBuffer ras_frame; + memset(&long_term, 0, sizeof(long_term)); + memset(&untracked_long_term, 0, sizeof(untracked_long_term)); + memset(&ras_frame, 0, sizeof(ras_frame)); + long_term.xlayer_id = 0; + long_term.mlayer_id = 1; + long_term.tlayer_id = 0; + long_term.long_term_id = 7; + long_term.width = 64; + long_term.height = 64; + untracked_long_term.xlayer_id = 0; + untracked_long_term.mlayer_id = 1; + untracked_long_term.tlayer_id = 0; + untracked_long_term.long_term_id = 8; + + const auto snapshot_frame = [this](int obu_type, int mlayer_id, + RefCntBuffer *frame) { + av2_decoder_model_verifier_on_source_frame_unit_start(pbi_, 0, mlayer_id, + 0); + av2_decoder_model_verifier_record_obu(pbi_, obu_type, 0, mlayer_id, 0, 800); + AV2_COMMON *const cm = &pbi_->common; + pbi_->obu_type = static_cast(obu_type); + cm->xlayer_id = 0; + cm->mlayer_id = mlayer_id; + cm->tlayer_id = 0; + cm->show_existing_frame = 0; + cm->cur_frame = frame; + cm->width = 64; + cm->height = 64; + cm->mi_params.mi_cols = 16; + cm->mi_params.mi_rows = 16; + cm->mib_size_log2 = 0; + cm->tiles.cols = 1; + cm->tiles.rows = 1; + cm->tiles.col_start_sb[0] = 0; + cm->tiles.col_start_sb[1] = 16; + cm->tiles.row_start_sb[0] = 0; + cm->tiles.row_start_sb[1] = 16; + cm->current_frame.frame_type = KEY_FRAME; + frame->xlayer_id = 0; + frame->mlayer_id = mlayer_id; + frame->tlayer_id = 0; + frame->width = 64; + frame->height = 64; + av2_decoder_model_verifier_on_frame_wrapup_start(pbi_); + av2_decoder_model_verifier_on_frame_unit_complete(pbi_); + }; + + snapshot_frame(OBU_REGULAR_TILE_GROUP, 1, &long_term); + pbi_->common.ref_frame_map[0] = &long_term; + pbi_->common.ref_frame_map[1] = &untracked_long_term; + pbi_->valid_for_referencing[0] = 1; + pbi_->valid_for_referencing[1] = 1; + snapshot_frame(OBU_RAS_FRAME, 0, &ras_frame); + + bool found_excluding_op = false; + bool found_including_op = false; + Av2DmVerifierStats verifier_stats; + ASSERT_TRUE(av2_decoder_model_verifier_get_stats(pbi_, &verifier_stats)); + for (uint32_t i = 0; i < verifier_stats.contexts; ++i) { + Av2DmContextStats context; + ASSERT_TRUE( + av2_decoder_model_verifier_get_context_stats(pbi_, i, &context)); + if (context.scope.ops_id != 3) continue; + if (context.scope.operating_point == 0) { + found_excluding_op = true; + EXPECT_TRUE(context.last_ras_seed_complete); + EXPECT_EQ(context.last_ras_seed_count, 0u); + } else if (context.scope.operating_point == 1) { + found_including_op = true; + EXPECT_FALSE(context.last_ras_seed_complete); + EXPECT_EQ(context.last_ras_seed_count, 1u); + } + } + EXPECT_TRUE(found_excluding_op); + EXPECT_TRUE(found_including_op); +} + +TEST_F(DecoderModelParserTest, GlobalOperatingPointCreatesPerXlayerContexts) { + OperatingPointSet *const ops = &pbi_->ops_list[GLOBAL_XLAYER_ID][2]; + memset(ops, 0, sizeof(*ops)); + ops->valid = 1; + ops->obu_xlayer_id = GLOBAL_XLAYER_ID; + ops->ops_id = 2; + ops->ops_cnt = 1; + ops->ops_mlayer_info_idc = 1; + ops->op[0].ops_xlayer_map = (1 << 1) | (1 << 3); + ops->op[0].mlayer_info.ops_mlayer_map[1] = 1; + ops->op[0].mlayer_info.ops_tlayer_map[1][0] = 1; + ops->op[0].mlayer_info.ops_mlayer_map[3] = 1 << 2; + ops->op[0].mlayer_info.ops_tlayer_map[3][2] = 1 << 1; + av2_decoder_model_verifier_on_operating_point_set(pbi_, GLOBAL_XLAYER_ID, 2); + + Av2DmVerifierStats stats; + ASSERT_TRUE(av2_decoder_model_verifier_get_stats(pbi_, &stats)); + ASSERT_EQ(stats.contexts, 2u); + Av2DmContextStats first; + Av2DmContextStats second; + ASSERT_TRUE(av2_decoder_model_verifier_get_context_stats(pbi_, 0, &first)); + ASSERT_TRUE(av2_decoder_model_verifier_get_context_stats(pbi_, 1, &second)); + EXPECT_EQ(first.scope.xlayer_id, 1); + EXPECT_EQ(first.scope.ops_xlayer_id, GLOBAL_XLAYER_ID); + EXPECT_EQ(second.scope.xlayer_id, 3); + EXPECT_EQ(second.scope.ops_xlayer_id, GLOBAL_XLAYER_ID); + + av2_decoder_model_verifier_record_obu(pbi_, OBU_REGULAR_TILE_GROUP, 1, 0, 0, + 40); + av2_decoder_model_verifier_record_obu(pbi_, OBU_REGULAR_TILE_GROUP, 3, 2, 1, + 80); + ASSERT_TRUE(av2_decoder_model_verifier_get_context_stats(pbi_, 0, &first)); + ASSERT_TRUE(av2_decoder_model_verifier_get_context_stats(pbi_, 1, &second)); + EXPECT_EQ(first.pending_dfg_bits, 40u); + EXPECT_EQ(second.pending_dfg_bits, 80u); +} + +TEST_F(DecoderModelParserTest, UnselectedFrameDoesNotCloseOperatingPointDfg) { + OperatingPointSet *const ops = &pbi_->ops_list[0][1]; + memset(ops, 0, sizeof(*ops)); + ops->valid = 1; + ops->obu_xlayer_id = 0; + ops->ops_id = 1; + ops->ops_cnt = 1; + ops->ops_mlayer_info_idc = 1; + ops->op[0].mlayer_info.ops_mlayer_map[0] = 1 << 1; + ops->op[0].mlayer_info.ops_tlayer_map[0][1] = 1 << 2; + av2_decoder_model_verifier_on_operating_point_set(pbi_, 0, 1); + + av2_decoder_model_verifier_record_obu(pbi_, OBU_SEQUENCE_HEADER, 0, 0, 0, 24); + av2_decoder_model_verifier_record_obu(pbi_, OBU_REGULAR_TILE_GROUP, 0, 0, 0, + 40); + pbi_->common.xlayer_id = 0; + pbi_->common.mlayer_id = 0; + pbi_->common.tlayer_id = 0; + pbi_->common.show_existing_frame = 0; + av2_decoder_model_verifier_on_frame_unit_complete(pbi_); + + Av2DmContextStats context; + ASSERT_TRUE(av2_decoder_model_verifier_get_context_stats(pbi_, 0, &context)); + EXPECT_EQ(context.pending_dfg_bits, 24u); + EXPECT_EQ(context.closed_dfgs, 0u); + + av2_decoder_model_verifier_record_obu(pbi_, OBU_REGULAR_TILE_GROUP, 0, 1, 2, + 80); + pbi_->common.mlayer_id = 1; + pbi_->common.tlayer_id = 2; + av2_decoder_model_verifier_on_frame_unit_complete(pbi_); + ASSERT_TRUE(av2_decoder_model_verifier_get_context_stats(pbi_, 0, &context)); + EXPECT_EQ(context.last_closed_dfg_bits, 104u); + EXPECT_EQ(context.closed_dfgs, 1u); +} + +TEST_F(DecoderModelParserTest, OtherXlayerDoesNotCloseWholeXlayerDfg) { + AddWholeXlayerContext(2, 0); + av2_decoder_model_verifier_record_obu(pbi_, OBU_SEQUENCE_HEADER, 2, 0, 0, 16); + pbi_->common.xlayer_id = 4; + pbi_->common.mlayer_id = 0; + pbi_->common.tlayer_id = 0; + pbi_->common.show_existing_frame = 0; + av2_decoder_model_verifier_on_frame_unit_complete(pbi_); + + Av2DmContextStats context; + ASSERT_TRUE(av2_decoder_model_verifier_get_context_stats(pbi_, 0, &context)); + EXPECT_EQ(context.pending_dfg_bits, 16u); + EXPECT_EQ(context.closed_dfgs, 0u); +} + +TEST_F(DecoderModelParserTest, RedefinedOpsDoesNotRewriteOldDfgMembership) { + OperatingPointSet *const ops = &pbi_->ops_list[0][4]; + memset(ops, 0, sizeof(*ops)); + ops->valid = 1; + ops->obu_xlayer_id = 0; + ops->ops_id = 4; + ops->ops_cnt = 1; + ops->ops_mlayer_info_idc = 1; + ops->op[0].mlayer_info.ops_mlayer_map[0] = 1; + ops->op[0].mlayer_info.ops_tlayer_map[0][0] = 1; + av2_decoder_model_verifier_on_operating_point_set(pbi_, 0, 4); + + av2_decoder_model_verifier_record_obu(pbi_, OBU_REGULAR_TILE_GROUP, 0, 1, 0, + 40); + pbi_->common.xlayer_id = 0; + pbi_->common.mlayer_id = 1; + pbi_->common.tlayer_id = 0; + pbi_->common.show_existing_frame = 0; + av2_decoder_model_verifier_on_frame_unit_complete(pbi_); + + av2_decoder_model_verifier_record_obu(pbi_, OBU_OPERATING_POINT_SET, 0, 0, 0, + 20); + ops->op[0].mlayer_info.ops_mlayer_map[0] = 1 << 1; + ops->op[0].mlayer_info.ops_tlayer_map[0][1] = 1; + av2_decoder_model_verifier_on_operating_point_set(pbi_, 0, 4); + + Av2DmContextStats context; + ASSERT_TRUE(av2_decoder_model_verifier_get_context_stats(pbi_, 0, &context)); + EXPECT_EQ(context.pending_dfg_bits, 20u); + av2_decoder_model_verifier_record_obu(pbi_, OBU_REGULAR_TILE_GROUP, 0, 1, 0, + 80); + av2_decoder_model_verifier_on_frame_unit_complete(pbi_); + ASSERT_TRUE(av2_decoder_model_verifier_get_context_stats(pbi_, 0, &context)); + EXPECT_EQ(context.last_closed_dfg_bits, 100u); + EXPECT_EQ(context.closed_dfgs, 1u); +} + +TEST_F(DecoderModelParserTest, OpsResetDeactivatesPriorScope) { + OperatingPointSet *const ops = &pbi_->ops_list[0][5]; + memset(ops, 0, sizeof(*ops)); + ops->valid = 1; + ops->obu_xlayer_id = 0; + ops->ops_id = 5; + ops->ops_cnt = 1; + ops->op[0].mlayer_info.ops_mlayer_map[0] = 1; + ops->op[0].mlayer_info.ops_tlayer_map[0][0] = 1; + av2_decoder_model_verifier_on_operating_point_set(pbi_, 0, 5); + + ops->ops_cnt = 0; + ops->ops_reset_flag = 0; + av2_decoder_model_verifier_on_operating_point_set(pbi_, 0, 5); + Av2DmContextStats context; + ASSERT_TRUE(av2_decoder_model_verifier_get_context_stats(pbi_, 0, &context)); + EXPECT_FALSE(context.active); + + av2_decoder_model_verifier_record_obu(pbi_, OBU_REGULAR_TILE_GROUP, 0, 0, 0, + 64); + pbi_->common.xlayer_id = 0; + pbi_->common.mlayer_id = 0; + pbi_->common.tlayer_id = 0; + pbi_->common.show_existing_frame = 0; + av2_decoder_model_verifier_on_frame_unit_complete(pbi_); + ASSERT_TRUE(av2_decoder_model_verifier_get_context_stats(pbi_, 0, &context)); + EXPECT_EQ(context.pending_dfg_bits, 0u); + EXPECT_EQ(context.closed_dfgs, 0u); +} + +TEST_F(DecoderModelParserTest, ActiveConfigurationUsesActivatedSequence) { + pbi_->seq_list[0][1].seq_header_id = 1; + pbi_->seq_list[0][2].seq_header_id = 2; + av2_decoder_model_verifier_on_sequence_header(pbi_, 0, 1); + av2_decoder_model_verifier_on_sequence_header(pbi_, 0, 2); + pbi_->common.seq_params = pbi_->seq_list[0][1]; + av2_decoder_model_verifier_on_active_configuration(pbi_, 0, 1); + + Av2DmContextStats context; + ASSERT_TRUE(av2_decoder_model_verifier_get_context_stats(pbi_, 0, &context)); + EXPECT_TRUE(context.active_configuration_present); + EXPECT_EQ(context.active_sequence_header_id, 1); + + OperatingPointSet *const ops = &pbi_->ops_list[0][6]; + memset(ops, 0, sizeof(*ops)); + ops->valid = 1; + ops->obu_xlayer_id = 0; + ops->ops_id = 6; + ops->ops_cnt = 1; + av2_decoder_model_verifier_on_operating_point_set(pbi_, 0, 6); + ASSERT_TRUE(av2_decoder_model_verifier_get_context_stats(pbi_, 1, &context)); + EXPECT_TRUE(context.active_configuration_present); + EXPECT_EQ(context.active_sequence_header_id, 1); +} + +TEST_F(DecoderModelParserTest, LaterXlayerKeepsCurrentTuGlobalPrefix) { + av2_decoder_model_verifier_record_obu(pbi_, OBU_TEMPORAL_DELIMITER, 0, 0, 0, + 8); + av2_decoder_model_verifier_record_obu( + pbi_, OBU_MULTI_STREAM_DECODER_OPERATION, GLOBAL_XLAYER_ID, 0, 0, 24); + av2_decoder_model_verifier_record_obu(pbi_, OBU_REGULAR_TILE_GROUP, 0, 0, 0, + 40); + pbi_->common.xlayer_id = 0; + pbi_->common.mlayer_id = 0; + pbi_->common.tlayer_id = 0; + pbi_->common.show_existing_frame = 0; + av2_decoder_model_verifier_on_frame_unit_complete(pbi_); + + av2_decoder_model_verifier_record_obu(pbi_, OBU_SEQUENCE_HEADER, 1, 0, 0, 16); + AddWholeXlayerContext(1, 0); + Av2DmContextStats context; + ASSERT_TRUE(av2_decoder_model_verifier_get_context_stats(pbi_, 0, &context)); + EXPECT_EQ(context.pending_dfg_bits, 48u); + + av2_decoder_model_verifier_record_obu(pbi_, OBU_REGULAR_TILE_GROUP, 1, 0, 0, + 80); + pbi_->common.xlayer_id = 1; + av2_decoder_model_verifier_on_frame_unit_complete(pbi_); + ASSERT_TRUE(av2_decoder_model_verifier_get_context_stats(pbi_, 0, &context)); + EXPECT_EQ(context.last_closed_dfg_bits, 128u); +} + +TEST_F(DecoderModelParserTest, RedundantSequencePreservesOpenSefDfg) { + AddWholeXlayerContext(0, 0); + av2_decoder_model_verifier_record_obu(pbi_, OBU_TEMPORAL_DELIMITER, 0, 0, 0, + 8); + av2_decoder_model_verifier_record_obu(pbi_, OBU_REGULAR_SEF, 0, 0, 0, 16); + pbi_->common.xlayer_id = 0; + pbi_->common.mlayer_id = 0; + pbi_->common.tlayer_id = 0; + pbi_->common.show_existing_frame = 1; + av2_decoder_model_verifier_on_frame_unit_complete(pbi_); + av2_decoder_model_verifier_record_obu(pbi_, OBU_TEMPORAL_DELIMITER, 0, 0, 0, + 8); + av2_decoder_model_verifier_record_obu(pbi_, OBU_SEQUENCE_HEADER, 0, 0, 0, 24); + av2_decoder_model_verifier_on_sequence_header(pbi_, 0, 0); + av2_decoder_model_verifier_record_obu(pbi_, OBU_REGULAR_TILE_GROUP, 0, 0, 0, + 40); + pbi_->common.show_existing_frame = 0; + av2_decoder_model_verifier_on_frame_unit_complete(pbi_); + + Av2DmContextStats context; + ASSERT_TRUE(av2_decoder_model_verifier_get_context_stats(pbi_, 0, &context)); + EXPECT_EQ(context.last_closed_dfg_bits, 96u); +} + +TEST_F(DecoderModelParserTest, RedundantOpsPreservesOpenSefDfg) { + OperatingPointSet *const ops = &pbi_->ops_list[0][4]; + memset(ops, 0, sizeof(*ops)); + ops->valid = 1; + ops->obu_xlayer_id = 0; + ops->ops_id = 4; + ops->ops_cnt = 1; + ops->ops_mlayer_info_idc = 1; + ops->op[0].mlayer_info.ops_mlayer_map[0] = 1; + ops->op[0].mlayer_info.ops_tlayer_map[0][0] = 1; + av2_decoder_model_verifier_on_operating_point_set(pbi_, 0, 4); + av2_decoder_model_verifier_record_obu(pbi_, OBU_TEMPORAL_DELIMITER, 0, 0, 0, + 8); + av2_decoder_model_verifier_record_obu(pbi_, OBU_REGULAR_SEF, 0, 0, 0, 16); + pbi_->common.xlayer_id = 0; + pbi_->common.mlayer_id = 0; + pbi_->common.tlayer_id = 0; + pbi_->common.show_existing_frame = 1; + av2_decoder_model_verifier_on_frame_unit_complete(pbi_); + + av2_decoder_model_verifier_record_obu(pbi_, OBU_TEMPORAL_DELIMITER, 0, 0, 0, + 8); + av2_decoder_model_verifier_record_obu(pbi_, OBU_OPERATING_POINT_SET, 0, 0, 0, + 20); + av2_decoder_model_verifier_on_operating_point_set(pbi_, 0, 4); + av2_decoder_model_verifier_record_obu(pbi_, OBU_REGULAR_TILE_GROUP, 0, 0, 0, + 40); + pbi_->common.show_existing_frame = 0; + av2_decoder_model_verifier_on_frame_unit_complete(pbi_); + + Av2DmContextStats context; + ASSERT_TRUE(av2_decoder_model_verifier_get_context_stats(pbi_, 0, &context)); + EXPECT_EQ(context.last_closed_dfg_bits, 92u); +} + +TEST_F(DecoderModelParserTest, StreamBoundaryRelinksIdenticalConfiguration) { + AddWholeXlayerContext(0, 0); + pbi_->common.seq_params = pbi_->seq_list[0][0]; + av2_decoder_model_verifier_on_active_configuration(pbi_, 0, 0); + Av2DmVerifierStats before; + ASSERT_TRUE(av2_decoder_model_verifier_get_stats(pbi_, &before)); + av2_decoder_model_verifier_on_stream_configuration_change(pbi_, false); + av2_decoder_model_verifier_on_active_configuration(pbi_, 0, 0); + Av2DmVerifierStats after; + ASSERT_TRUE(av2_decoder_model_verifier_get_stats(pbi_, &after)); + EXPECT_EQ(after.event_count, before.event_count + 2); + Av2DmContextStats context; + ASSERT_TRUE(av2_decoder_model_verifier_get_context_stats(pbi_, 0, &context)); + EXPECT_TRUE(context.active); + EXPECT_TRUE(context.active_configuration_present); +} + +TEST_F(DecoderModelParserTest, StreamBoundaryKeepsNewTemporalUnitPrefix) { + AddWholeXlayerContext(0, 0); + pbi_->common.seq_params = pbi_->seq_list[0][0]; + av2_decoder_model_verifier_on_active_configuration(pbi_, 0, 0); + av2_decoder_model_verifier_record_obu(pbi_, OBU_TEMPORAL_DELIMITER, + GLOBAL_XLAYER_ID, 0, 0, 8); + av2_decoder_model_verifier_record_obu( + pbi_, OBU_MULTI_STREAM_DECODER_OPERATION, GLOBAL_XLAYER_ID, 0, 0, 80); + + av2_decoder_model_verifier_on_stream_configuration_change(pbi_, true); + av2_decoder_model_verifier_on_sequence_header(pbi_, 0, 0); + av2_decoder_model_verifier_on_active_configuration(pbi_, 0, 0); + av2_decoder_model_verifier_record_obu(pbi_, OBU_CLOSED_LOOP_KEY, 0, 0, 0, + 800); + pbi_->common.show_existing_frame = 0; + av2_decoder_model_verifier_on_frame_unit_complete(pbi_); + + Av2DmContextStats context; + ASSERT_TRUE(av2_decoder_model_verifier_get_context_stats(pbi_, 0, &context)); + EXPECT_EQ(context.last_closed_dfg_bits, 888u); +} + +TEST_F(DecoderModelParserTest, StreamBoundaryDropsStaleTemporalUnitPrefix) { + AddWholeXlayerContext(0, 0); + pbi_->common.seq_params = pbi_->seq_list[0][0]; + av2_decoder_model_verifier_on_active_configuration(pbi_, 0, 0); + av2_decoder_model_verifier_record_obu(pbi_, OBU_TEMPORAL_DELIMITER, 0, 0, 0, + 8); + av2_decoder_model_verifier_record_obu(pbi_, OBU_REGULAR_SEF, 0, 0, 0, 80); + + av2_decoder_model_verifier_on_stream_configuration_change(pbi_, false); + av2_decoder_model_verifier_on_sequence_header(pbi_, 0, 0); + av2_decoder_model_verifier_on_active_configuration(pbi_, 0, 0); + av2_decoder_model_verifier_record_obu(pbi_, OBU_CLOSED_LOOP_KEY, 0, 0, 0, + 800); + pbi_->common.show_existing_frame = 0; + av2_decoder_model_verifier_on_frame_unit_complete(pbi_); + + Av2DmContextStats context; + ASSERT_TRUE(av2_decoder_model_verifier_get_context_stats(pbi_, 0, &context)); + EXPECT_EQ(context.last_closed_dfg_bits, 800u); +} + +TEST_F(DecoderModelParserTest, ReactivationDoesNotReusePriorConfiguration) { + AddWholeXlayerContext(0, 0); + pbi_->common.seq_params = pbi_->seq_list[0][0]; + av2_decoder_model_verifier_on_active_configuration(pbi_, 0, 0); + av2_decoder_model_verifier_on_stream_configuration_change(pbi_, false); + + pbi_->seq_list[0][1].seq_header_id = 1; + av2_decoder_model_verifier_on_sequence_header(pbi_, 0, 1); + Av2DmContextStats context; + ASSERT_TRUE(av2_decoder_model_verifier_get_context_stats(pbi_, 0, &context)); + EXPECT_TRUE(context.active); + EXPECT_FALSE(context.active_configuration_present); + EXPECT_EQ(context.active_sequence_header_id, -1); +} + +TEST_F(DecoderModelParserTest, GlobalResetDeactivatesLocalAndGlobalOps) { + OperatingPointSet *const local = &pbi_->ops_list[1][0]; + memset(local, 0, sizeof(*local)); + local->valid = 1; + local->obu_xlayer_id = 1; + local->ops_id = 0; + local->ops_cnt = 1; + av2_decoder_model_verifier_on_operating_point_set(pbi_, 1, 0); + OperatingPointSet *const global = &pbi_->ops_list[GLOBAL_XLAYER_ID][0]; + memset(global, 0, sizeof(*global)); + global->valid = 1; + global->obu_xlayer_id = GLOBAL_XLAYER_ID; + global->ops_id = 0; + global->ops_cnt = 1; + global->op[0].ops_xlayer_map = 1 << 2; + av2_decoder_model_verifier_on_operating_point_set(pbi_, GLOBAL_XLAYER_ID, 0); + + global->ops_reset_flag = 1; + global->ops_cnt = 0; + av2_decoder_model_verifier_on_operating_point_set(pbi_, GLOBAL_XLAYER_ID, 0); + Av2DmContextStats context; + ASSERT_TRUE(av2_decoder_model_verifier_get_context_stats(pbi_, 0, &context)); + EXPECT_FALSE(context.active); + ASSERT_TRUE(av2_decoder_model_verifier_get_context_stats(pbi_, 1, &context)); + EXPECT_FALSE(context.active); +} + +TEST_F(DecoderModelParserTest, FilteredRapDoesNotSuppressOtherXlayerRap) { + av2_decoder_model_verifier_record_obu(pbi_, OBU_CLOSED_LOOP_KEY, 1, 0, 0, 40); + av2_decoder_model_verifier_on_obu_filtered(pbi_); + av2_decoder_model_verifier_record_obu(pbi_, OBU_CLOSED_LOOP_KEY, 2, 0, 0, 40); + Av2DmVerifierStats stats; + ASSERT_TRUE(av2_decoder_model_verifier_get_stats(pbi_, &stats)); + EXPECT_EQ(stats.rap_starts, 2u); +} + +TEST_F(DecoderModelParserTest, FilteredRapDoesNotSuppressNextSourceRap) { + av2_decoder_model_verifier_on_source_frame_unit_start(pbi_, 1, 0, 0); + av2_decoder_model_verifier_record_obu(pbi_, OBU_CLOSED_LOOP_KEY, 1, 0, 0, 40); + av2_decoder_model_verifier_on_obu_filtered(pbi_); + av2_decoder_model_verifier_on_source_frame_unit_start(pbi_, 1, 0, 0); + av2_decoder_model_verifier_record_obu(pbi_, OBU_CLOSED_LOOP_KEY, 1, 0, 0, 40); + av2_decoder_model_verifier_record_obu(pbi_, OBU_CLOSED_LOOP_KEY, 1, 0, 0, 40); + Av2DmVerifierStats stats; + ASSERT_TRUE(av2_decoder_model_verifier_get_stats(pbi_, &stats)); + EXPECT_EQ(stats.rap_starts, 2u); +} + +TEST_F(DecoderModelParserTest, TemporalPointRetainsFullUlebValueAndPresence) { + constexpr uint64_t kPresentationTime = 0xfedcba98u; + av2_decoder_model_verifier_on_temporal_point(pbi_, kPresentationTime); + Av2DmVerifierStats stats; + ASSERT_TRUE(av2_decoder_model_verifier_get_stats(pbi_, &stats)); + EXPECT_TRUE(stats.temporal_point_present); + EXPECT_EQ(stats.temporal_point, kPresentationTime); + EXPECT_EQ(stats.temporal_points, 1u); +} + +TEST_F(DecoderModelParserTest, ConfigurationBoundaryIsImmutableEvent) { + av2_decoder_model_verifier_on_stream_configuration_change(pbi_, false); + Av2DmVerifierStats stats; + ASSERT_TRUE(av2_decoder_model_verifier_get_stats(pbi_, &stats)); + EXPECT_EQ(stats.event_count, 1u); +} + +TEST(DecoderModelAnnexFTest, WholeXlayerAndStructuralMembership) { + SubBitstreamExtractionState scope; + ASSERT_TRUE(av2_sbe_configure_decoder_model_scope(&scope, 2, nullptr, -1, 1)); + EXPECT_TRUE( + av2_sbe_should_retain_obu(&scope, OBU_REGULAR_TILE_GROUP, 2, 7, 3)); + EXPECT_FALSE( + av2_sbe_should_retain_obu(&scope, OBU_REGULAR_TILE_GROUP, 3, 0, 0)); + EXPECT_TRUE( + av2_sbe_should_retain_obu(&scope, OBU_TEMPORAL_DELIMITER, 3, 0, 0)); + EXPECT_TRUE(av2_sbe_should_retain_obu( + &scope, OBU_MULTI_STREAM_DECODER_OPERATION, GLOBAL_XLAYER_ID, 0, 0)); +} + +TEST(DecoderModelAnnexFTest, OperatingPointPreservesBaseConfiguration) { + OperatingPointSet ops; + memset(&ops, 0, sizeof(ops)); + ops.valid = 1; + ops.obu_xlayer_id = 4; + ops.ops_cnt = 1; + ops.ops_mlayer_info_idc = 1; + ops.op[0].mlayer_info.ops_mlayer_map[4] = 1 << 1; + ops.op[0].mlayer_info.ops_tlayer_map[4][1] = 1 << 2; + + SubBitstreamExtractionState scope; + ASSERT_TRUE(av2_sbe_configure_decoder_model_scope(&scope, 4, &ops, 0, 0)); + EXPECT_TRUE( + av2_sbe_should_retain_obu(&scope, OBU_REGULAR_TILE_GROUP, 4, 1, 2)); + EXPECT_FALSE( + av2_sbe_should_retain_obu(&scope, OBU_REGULAR_TILE_GROUP, 4, 0, 1)); + EXPECT_TRUE(av2_sbe_should_retain_obu(&scope, OBU_SEQUENCE_HEADER, 4, 0, 0)); +} + +TEST(DecoderModelAnnexFTest, GlobalOperatingPointIsScopedPerXlayer) { + OperatingPointSet ops; + memset(&ops, 0, sizeof(ops)); + ops.valid = 1; + ops.obu_xlayer_id = GLOBAL_XLAYER_ID; + ops.ops_cnt = 1; + ops.ops_mlayer_info_idc = 1; + ops.op[0].ops_xlayer_map = (1 << 1) | (1 << 3); + ops.op[0].mlayer_info.ops_mlayer_map[1] = 1; + ops.op[0].mlayer_info.ops_tlayer_map[1][0] = 1; + ops.op[0].mlayer_info.ops_mlayer_map[3] = 1 << 2; + ops.op[0].mlayer_info.ops_tlayer_map[3][2] = 1 << 1; + + SubBitstreamExtractionState scope; + ASSERT_TRUE(av2_sbe_configure_decoder_model_scope(&scope, 3, &ops, 0, 0)); + EXPECT_TRUE( + av2_sbe_should_retain_obu(&scope, OBU_REGULAR_TILE_GROUP, 3, 2, 1)); + EXPECT_FALSE( + av2_sbe_should_retain_obu(&scope, OBU_REGULAR_TILE_GROUP, 1, 0, 0)); +} + +static bool DecodeCountedSymbols() { + uint8_t buffer[64] = { 0 }; + avm_cdf_prob write_cdf[3] = { AVM_CDF2(16384) }; + avm_writer writer; + memset(&writer, 0, sizeof(writer)); + avm_start_encode(&writer, buffer); + avm_write_literal(&writer, 21, 5); + avm_write_symbol(&writer, 1, write_cdf, 2); + avm_stop_encode(&writer); + + avm_cdf_prob read_cdf[3] = { AVM_CDF2(16384) }; + avm_reader reader; + if (avm_reader_init(&reader, buffer, writer.pos) != 0) return false; + reader.allow_update_cdf = 0; + if (avm_read_literal(&reader, 5, {}) != 21) return false; + if (avm_read_symbol(&reader, read_cdf, 2, {}) != 1) return false; + return reader.frame_symbol_count == 6; +} + +TEST(DecoderModelSymbolCountTest, MirrorsEncoderLiteralAndSymbolCount) { + EXPECT_TRUE(DecodeCountedSymbols()); +} + +TEST(DecoderModelSymbolCountTest, DirectCdfAndBitAreNotFrameSymbols) { + uint8_t buffer[64] = { 0 }; + avm_cdf_prob cdf[3] = { AVM_CDF2(16384) }; + avm_writer writer; + memset(&writer, 0, sizeof(writer)); + avm_start_encode(&writer, buffer); + avm_write_bit(&writer, 1); + avm_write_cdf(&writer, 0, cdf, 2); + avm_stop_encode(&writer); + + avm_reader reader; + ASSERT_EQ(avm_reader_init(&reader, buffer, writer.pos), 0); + EXPECT_EQ(avm_read_bit(&reader, {}), 1); + EXPECT_EQ(avm_read_cdf(&reader, cdf, 2, {}), 0); + EXPECT_EQ(reader.frame_symbol_count, 0u); +} + +TEST(DecoderModelSymbolCountTest, IndependentReadersAreThreadLocal) { + constexpr int kThreads = 8; + constexpr int kIterations = 100; + std::vector results(kThreads, 0); + std::vector threads; + for (int i = 0; i < kThreads; ++i) { + threads.emplace_back([i, &results]() { + bool result = true; + for (int j = 0; j < kIterations; ++j) result &= DecodeCountedSymbols(); + results[i] = result ? 1 : 0; + }); + } + for (std::thread &thread : threads) thread.join(); + for (int result : results) EXPECT_EQ(result, 1); +} + +} // namespace diff --git a/test/decoder_model_test.cc b/test/decoder_model_test.cc new file mode 100644 index 0000000000..501d24d46c --- /dev/null +++ b/test/decoder_model_test.cc @@ -0,0 +1,2828 @@ +/* + * Copyright (c) 2026, Alliance for Open Media. All rights reserved + * + * This source code is subject to the terms of the BSD 3-Clause Clear License + * and the Alliance for Open Media Patent License 1.0. If the BSD 3-Clause Clear + * License was not distributed with this source code in the LICENSE file, you + * can obtain it at aomedia.org/license/software-license/bsd-3-c-c/. If the + * Alliance for Open Media Patent License 1.0 was not distributed with this + * source code in the PATENTS file, you can obtain it at + * aomedia.org/license/patent-license/. + */ + +#include +#include +#include +#include + +#include "av2/common/decoder_model.h" +#include "third_party/googletest/src/googletest/include/gtest/gtest.h" + +namespace { + +Av2DmUnsignedWide MakeWide(uint64_t limb3, uint64_t limb2, uint64_t limb1, + uint64_t limb0) { + return { { limb0, limb1, limb2, limb3 } }; +} + +void ExpectRational(const Av2DmRational &value, uint64_t limb3, uint64_t limb2, + uint64_t limb1, uint64_t limb0, uint64_t denominator, + bool negative = false) { + EXPECT_EQ(value.magnitude.limbs[3], limb3); + EXPECT_EQ(value.magnitude.limbs[2], limb2); + EXPECT_EQ(value.magnitude.limbs[1], limb1); + EXPECT_EQ(value.magnitude.limbs[0], limb0); + EXPECT_EQ(value.denominator.limbs[3], 0u); + EXPECT_EQ(value.denominator.limbs[2], 0u); + EXPECT_EQ(value.denominator.limbs[1], 0u); + EXPECT_EQ(value.denominator.limbs[0], denominator); + EXPECT_EQ(value.negative, negative); +} + +TEST(DecoderModelRationalTest, RejectsZeroDenominatorAndCanonicalizesZero) { + Av2DmRational value; + EXPECT_FALSE(av2_dm_rational_make(1, 0, &value)); + ASSERT_TRUE( + av2_dm_rational_make_wide(MakeWide(0, 0, 0, 0), 123, true, &value)); + ExpectRational(value, 0, 0, 0, 0, 1); +} + +TEST(DecoderModelRationalTest, ReducesGoldenVectors) { + Av2DmRational value; + ASSERT_TRUE(av2_dm_rational_make(1667000, 1000000, &value)); + ExpectRational(value, 0, 0, 0, 1667, 1000); + + ASSERT_TRUE( + av2_dm_rational_make_wide(MakeWide(0, 0, 1, 0), 16, false, &value)); + ExpectRational(value, 0, 0, 0, UINT64_C(1) << 60, 1); +} + +TEST(DecoderModelRationalTest, AddsAndSubtractsExactly) { + Av2DmRational one_third; + Av2DmRational one_sixth; + Av2DmRational result; + ASSERT_TRUE(av2_dm_rational_make(1, 3, &one_third)); + ASSERT_TRUE(av2_dm_rational_make(1, 6, &one_sixth)); + ASSERT_TRUE(av2_dm_rational_add(&one_third, &one_sixth, &result)); + ExpectRational(result, 0, 0, 0, 1, 2); + ASSERT_TRUE(av2_dm_rational_subtract(&one_sixth, &one_third, &result)); + ExpectRational(result, 0, 0, 0, 1, 6, true); + ASSERT_TRUE(av2_dm_rational_add(&one_third, &result, &result)); + ExpectRational(result, 0, 0, 0, 1, 6); +} + +TEST(DecoderModelRationalTest, MultipliesAndDividesWithCrossCancellation) { + Av2DmRational value; + Av2DmRational result; + ASSERT_TRUE(av2_dm_rational_make(UINT64_MAX, UINT64_MAX - 1, &value)); + ASSERT_TRUE(av2_dm_rational_multiply_u64(&value, UINT64_MAX - 1, &result)); + ExpectRational(result, 0, 0, 0, UINT64_MAX, 1); + + ASSERT_TRUE(av2_dm_rational_make(UINT64_MAX, 3, &value)); + ASSERT_TRUE(av2_dm_rational_divide_u64(&value, UINT64_MAX, &result)); + ExpectRational(result, 0, 0, 0, 1, 3); + + ASSERT_TRUE(av2_dm_rational_make(UINT64_MAX, 1, &value)); + ASSERT_TRUE(av2_dm_rational_multiply_u64(&value, UINT64_MAX, &result)); + ExpectRational(result, 0, 0, UINT64_MAX - 1, 1, 1); +} + +TEST(DecoderModelRationalTest, RetainsDenominatorsWiderThan64Bits) { + Av2DmRational left; + Av2DmRational right; + Av2DmRational result; + ASSERT_TRUE(av2_dm_rational_make(1, UINT64_MAX, &left)); + ASSERT_TRUE(av2_dm_rational_make(1, UINT64_MAX - 1, &right)); + // Golden result generated with Python fractions.Fraction: + // Fraction(1, 2**64 - 1) + Fraction(1, 2**64 - 2). + ASSERT_TRUE(av2_dm_rational_add(&left, &right, &result)); + + EXPECT_EQ(result.magnitude.limbs[0], UINT64_MAX - 2); + EXPECT_EQ(result.magnitude.limbs[1], 1u); + EXPECT_EQ(result.magnitude.limbs[2], 0u); + EXPECT_EQ(result.magnitude.limbs[3], 0u); + EXPECT_EQ(result.denominator.limbs[0], 2u); + EXPECT_EQ(result.denominator.limbs[1], UINT64_MAX - 2); + EXPECT_EQ(result.denominator.limbs[2], 0u); + EXPECT_EQ(result.denominator.limbs[3], 0u); + + int comparison; + ASSERT_TRUE(av2_dm_rational_compare(&result, &left, &comparison)); + EXPECT_EQ(comparison, 1); +} + +TEST(DecoderModelRationalTest, ComparesMaximumWideValuesAtExactBoundary) { + const Av2DmUnsignedWide maximum = + MakeWide(UINT64_MAX, UINT64_MAX, UINT64_MAX, UINT64_MAX); + const Av2DmUnsignedWide one_less = + MakeWide(UINT64_MAX, UINT64_MAX, UINT64_MAX, UINT64_MAX - 1); + Av2DmRational left; + Av2DmRational equal; + Av2DmRational lower; + ASSERT_TRUE(av2_dm_rational_make_wide(maximum, UINT64_MAX, false, &left)); + ASSERT_TRUE(av2_dm_rational_make_wide(maximum, UINT64_MAX, false, &equal)); + ASSERT_TRUE(av2_dm_rational_make_wide(one_less, UINT64_MAX, false, &lower)); + int comparison = 7; + ASSERT_TRUE(av2_dm_rational_compare(&left, &equal, &comparison)); + EXPECT_EQ(comparison, 0); + ASSERT_TRUE(av2_dm_rational_compare(&lower, &left, &comparison)); + EXPECT_EQ(comparison, -1); + ASSERT_TRUE(av2_dm_rational_compare(&left, &lower, &comparison)); + EXPECT_EQ(comparison, 1); +} + +TEST(DecoderModelRationalTest, DetectsMagnitudeAndDenominatorOverflow) { + Av2DmRational maximum; + Av2DmRational one; + Av2DmRational result; + ASSERT_TRUE(av2_dm_rational_make_wide( + MakeWide(UINT64_MAX, UINT64_MAX, UINT64_MAX, UINT64_MAX), 1, false, + &maximum)); + ASSERT_TRUE(av2_dm_rational_make(1, 1, &one)); + EXPECT_FALSE(av2_dm_rational_add(&maximum, &one, &result)); + EXPECT_FALSE(av2_dm_rational_multiply_u64(&maximum, 2, &result)); + + ASSERT_TRUE(av2_dm_rational_make(1, 1, &maximum)); + maximum.denominator = + MakeWide(UINT64_MAX, UINT64_MAX, UINT64_MAX, UINT64_MAX); + EXPECT_FALSE(av2_dm_rational_divide_u64(&maximum, 2, &result)); +} + +TEST(DecoderModelRationalTest, RebasesLongRunningTimelineExactly) { + Av2DmRational values[3]; + Av2DmRational origin; + const Av2DmUnsignedWide long_time = MakeWide(0, 0, UINT64_C(1) << 20, 12345); + ASSERT_TRUE(av2_dm_rational_make_wide(long_time, 90000, false, &origin)); + values[0] = origin; + Av2DmRational increment; + ASSERT_TRUE(av2_dm_rational_make(1, 60000, &increment)); + ASSERT_TRUE(av2_dm_rational_add(&origin, &increment, &values[1])); + ASSERT_TRUE(av2_dm_rational_add(&values[1], &increment, &values[2])); + ASSERT_TRUE(av2_dm_rational_rebase(values, 3, &origin)); + ExpectRational(values[0], 0, 0, 0, 0, 1); + ExpectRational(values[1], 0, 0, 0, 1, 60000); + ExpectRational(values[2], 0, 0, 0, 1, 30000); +} + +TEST(DecoderModelRationalTest, RebaseCopiesAliasedOrigin) { + Av2DmRational values[2]; + ASSERT_TRUE(av2_dm_rational_make(10, 1, &values[0])); + ASSERT_TRUE(av2_dm_rational_make(11, 1, &values[1])); + ASSERT_TRUE(av2_dm_rational_rebase(values, 2, &values[0])); + ExpectRational(values[0], 0, 0, 0, 0, 1); + ExpectRational(values[1], 0, 0, 0, 1, 1); +} + +TEST(DecoderModelRationalTest, RebaseFailureIsAtomic) { + Av2DmRational values[2]; + ASSERT_TRUE(av2_dm_rational_make(1, 1, &values[0])); + ASSERT_TRUE(av2_dm_rational_make_wide( + MakeWide(UINT64_MAX, UINT64_MAX, UINT64_MAX, UINT64_MAX), 1, false, + &values[1])); + Av2DmRational origin; + ASSERT_TRUE( + av2_dm_rational_make_wide(MakeWide(0, 0, 0, 1), 1, true, &origin)); + const Av2DmRational original_values[2] = { values[0], values[1] }; + + EXPECT_FALSE(av2_dm_rational_rebase(values, 2, &origin)); + EXPECT_EQ(memcmp(values, original_values, sizeof(values)), 0); +} + +TEST(DecoderModelBufferPoolTest, InitializesEightAndSixteenReferencePools) { + Av2DmBufferPool pool; + ASSERT_TRUE(av2_dm_buffer_pool_initialize(&pool, 8)); + EXPECT_EQ(pool.pool_size, 10u); + EXPECT_EQ(av2_dm_buffer_pool_get_free_buffer(&pool), 0); + EXPECT_EQ(av2_dm_buffer_pool_frames_in_use(&pool), 0u); + for (uint32_t i = 0; i < 8; ++i) EXPECT_EQ(pool.vbi[i], -1); + + ASSERT_TRUE(av2_dm_buffer_pool_initialize(&pool, 16)); + EXPECT_EQ(pool.pool_size, 18u); + for (uint32_t i = 0; i < 16; ++i) EXPECT_EQ(pool.vbi[i], -1); + EXPECT_FALSE(av2_dm_buffer_pool_initialize(&pool, 0)); + EXPECT_FALSE(av2_dm_buffer_pool_initialize(&pool, 17)); +} + +TEST(DecoderModelBufferPoolTest, ReferenceSlotsMaintainExactCounts) { + Av2DmBufferPool pool; + ASSERT_TRUE(av2_dm_buffer_pool_initialize(&pool, 8)); + ASSERT_TRUE(av2_dm_buffer_pool_set_vbi(&pool, 0, 3)); + ASSERT_TRUE(av2_dm_buffer_pool_set_vbi(&pool, 1, 3)); + EXPECT_EQ(pool.buffers[3].decoder_ref_count, 2u); + EXPECT_EQ(av2_dm_buffer_pool_frames_in_use(&pool), 1u); + ASSERT_TRUE(av2_dm_buffer_pool_set_vbi(&pool, 0, 4)); + EXPECT_EQ(pool.buffers[3].decoder_ref_count, 1u); + EXPECT_EQ(pool.buffers[4].decoder_ref_count, 1u); + ASSERT_TRUE(av2_dm_buffer_pool_set_vbi(&pool, 1, -1)); + EXPECT_EQ(pool.buffers[3].decoder_ref_count, 0u); + EXPECT_FALSE(pool.buffers[3].generation_valid); +} + +TEST(DecoderModelBufferPoolTest, DetectsFullPoolAndCountUnderflow) { + Av2DmBufferPool pool; + ASSERT_TRUE(av2_dm_buffer_pool_initialize(&pool, 16)); + for (uint32_t i = 0; i < pool.pool_size; ++i) { + ASSERT_TRUE(av2_dm_buffer_pool_add_player_ref(&pool, i)); + } + EXPECT_EQ(av2_dm_buffer_pool_get_free_buffer(&pool), -1); + EXPECT_FALSE(av2_dm_buffer_pool_remove_decoder_ref(&pool, 0)); + EXPECT_FALSE(av2_dm_buffer_pool_release(&pool, 0)); + ASSERT_TRUE(av2_dm_buffer_pool_remove_player_ref(&pool, 17)); + EXPECT_EQ(av2_dm_buffer_pool_get_free_buffer(&pool), 17); +} + +TEST(DecoderModelBufferPoolTest, RejectsInvalidIndicesWithoutMutation) { + Av2DmBufferPool pool; + ASSERT_TRUE(av2_dm_buffer_pool_initialize(&pool, 8)); + EXPECT_FALSE(av2_dm_buffer_pool_set_vbi(&pool, 8, 0)); + EXPECT_FALSE(av2_dm_buffer_pool_set_vbi(&pool, 0, 10)); + EXPECT_FALSE(av2_dm_buffer_pool_add_decoder_ref(&pool, 10)); + EXPECT_EQ(av2_dm_buffer_pool_frames_in_use(&pool), 0u); +} + +struct ViolationCollector { + std::vector violations; +}; + +void CollectViolation(void *opaque, const Av2DmViolation *violation) { + static_cast(opaque)->violations.push_back(*violation); +} + +bool HasViolation(const ViolationCollector &collector, + Av2DmViolationCode code) { + for (const Av2DmViolation &violation : collector.violations) { + if (violation.code == code) return true; + } + return false; +} + +size_t CountViolations(const ViolationCollector &collector, + Av2DmViolationCode code) { + size_t count = 0; + for (const Av2DmViolation &violation : collector.violations) { + if (violation.code == code) ++count; + } + return count; +} + +const Av2DmViolation *FindViolation(const ViolationCollector &collector, + Av2DmViolationCode code) { + for (const Av2DmViolation &violation : collector.violations) { + if (violation.code == code) return &violation; + } + return nullptr; +} + +bool EqualRational(const Av2DmRational &left, const Av2DmRational &right) { + int comparison; + return av2_dm_rational_compare(&left, &right, &comparison) && comparison == 0; +} + +void ExpectSameViolationMultiset(const ViolationCollector &online, + const ViolationCollector &deferred) { + ASSERT_EQ(online.violations.size(), deferred.violations.size()); + std::vector matched(deferred.violations.size(), false); + for (const Av2DmViolation &expected : online.violations) { + bool found = false; + for (size_t i = 0; i < deferred.violations.size(); ++i) { + const Av2DmViolation &candidate = deferred.violations[i]; + if (matched[i] || candidate.code != expected.code || + candidate.observed_present != expected.observed_present || + candidate.limit_present != expected.limit_present || + (expected.observed_present && + !EqualRational(candidate.observed, expected.observed)) || + (expected.limit_present && + !EqualRational(candidate.limit, expected.limit))) { + continue; + } + matched[i] = true; + found = true; + break; + } + EXPECT_TRUE(found) << "Missing deferred violation for code " + << expected.code; + } +} + +void ExpectSameResult(const Av2DecoderModel *online, + const Av2DecoderModel *deferred) { + Av2DmResult online_result; + Av2DmResult deferred_result; + ASSERT_TRUE(av2_decoder_model_get_result(online, &online_result)); + ASSERT_TRUE(av2_decoder_model_get_result(deferred, &deferred_result)); + EXPECT_EQ(online_result.applicability, deferred_result.applicability); + EXPECT_EQ(online_result.status, deferred_result.status); + EXPECT_EQ(online_result.violations, deferred_result.violations); +} + +Av2DmConfig MakeModelConfig(Av2DmMode mode) { + Av2DmConfig config = {}; + config.mode = mode; + config.applicability = AV2_DM_APPLICABLE; + config.level_idx = 2; + config.profile = 0; + config.num_ref_frames = 8; + config.max_frame_width = 64; + config.max_frame_height = 64; + config.explicit_num_ref_frames = false; + config.timing_info_present = true; + config.num_units_in_display_tick = 1; + config.time_scale = 90000; + config.num_units_in_decoding_tick = 1; + config.equal_picture_interval = true; + config.ticks_per_picture = 3000; + config.initial_display_delay = 2; + config.sequence_parameters_present = true; + config.sequence_decoder_buffer_delay = 9000; + config.sequence_encoder_buffer_delay = 9000; + config.level_limits_present = true; + config.level_limits.max_picture_size = 1000000; + config.level_limits.max_horizontal_size = 2000; + config.level_limits.max_vertical_size = 2000; + config.level_limits.max_display_rate = 1000000000; + config.level_limits.max_decode_rate = 1000000; + config.level_limits.max_header_rate = 1000; + config.level_limits.max_tiles = 512; + config.level_limits.max_tile_columns = 64; + config.level_limits.max_tile_width = 16384; + config.level_limits.max_tile_area = 100000000; + config.level_limits.max_tile_size_header_rate_product = UINT64_MAX; + config.level_limits.picture_size_profile_factor = 15; + config.level_limits.min_compression_basis = 2; + EXPECT_TRUE(av2_dm_rational_make(1000000, 1, &config.level_limits.bit_rate)); + EXPECT_TRUE( + av2_dm_rational_make(1000000, 1, &config.level_limits.buffer_size)); + return config; +} + +Av2DmFrameEvent MakeFrame(uint64_t index, uint64_t generation, + uint32_t removal_ticks = 0) { + Av2DmFrameEvent event = {}; + event.event_index = index; + event.temporal_unit_index = index; + event.generation = generation; + event.ref_valid_mask = UINT32_MAX; + event.coded_bits = 1000; + event.random_access_point = index == 0; + event.coded_as_closed_loop_key = index == 0; + event.frame_is_intra = index == 0; + event.frame_width = 64; + event.frame_height = 64; + event.num_tiles = 1; + event.tile_columns = 1; + event.max_tile_width = 64; + event.max_tile_area = 4096; + event.non_rightmost_tile_width_valid = true; + event.buffer_removal_time_present = true; + event.buffer_removal_time = removal_ticks; + event.count_frame_header = true; + event.compressed_size_bytes = 128; + return event; +} + +Av2DmReferenceUpdateEvent Refresh(uint32_t flags, uint32_t valid) { + Av2DmReferenceUpdateEvent event = {}; + event.refresh_frame_flags = flags; + event.ref_valid_mask = valid; + return event; +} + +Av2DmOutputEvent Output(uint64_t index, uint64_t generation, int map_index) { + Av2DmOutputEvent event = {}; + event.event_index = index; + event.temporal_unit_index = index; + event.generation = generation; + event.frame_to_show_map_idx = map_index; + event.ref_valid_mask = UINT32_MAX; + event.output_luma_samples = 4096; + return event; +} + +void ExpectEqualRational(const Av2DmRational &actual, uint64_t numerator, + uint64_t denominator) { + Av2DmRational expected; + ASSERT_TRUE(av2_dm_rational_make(numerator, denominator, &expected)); + int comparison; + ASSERT_TRUE(av2_dm_rational_compare(&actual, &expected, &comparison)); + EXPECT_EQ(comparison, 0); +} + +TEST(DecoderModelProcessTest, ResourceModeUsesDefaultInitialRemoval) { + Av2DmConfig config = MakeModelConfig(AV2_DM_RESOURCE_AVAILABILITY_MODE); + Av2DecoderModel *model = av2_decoder_model_create(&config, nullptr, nullptr); + ASSERT_NE(model, nullptr); + const Av2DmFrameEvent frame = MakeFrame(0, 10); + av2_decoder_model_start_frame(model, &frame); + Av2DmState state; + ASSERT_TRUE(av2_decoder_model_get_state(model, &state)); + ExpectEqualRational(state.scheduled_removal, 7, 9); + ExpectEqualRational(state.time_to_decode, 64 * 64, 1000000); + Av2DmRational expected_completion; + ASSERT_TRUE(av2_dm_rational_add(&state.scheduled_removal, + &state.time_to_decode, &expected_completion)); + int comparison; + ASSERT_TRUE( + av2_dm_rational_compare(&state.time, &expected_completion, &comparison)); + EXPECT_EQ(comparison, 0); + av2_decoder_model_destroy(model); +} + +TEST(DecoderModelProcessTest, ResourceModeDoesNotRequireDecodingClock) { + Av2DmConfig config = MakeModelConfig(AV2_DM_RESOURCE_AVAILABILITY_MODE); + config.num_units_in_decoding_tick = 0; + Av2DecoderModel *model = av2_decoder_model_create(&config, nullptr, nullptr); + ASSERT_NE(model, nullptr); + Av2DmResult result; + ASSERT_TRUE(av2_decoder_model_get_result(model, &result)); + EXPECT_FALSE(result.missing_required_input); + av2_decoder_model_destroy(model); +} + +TEST(DecoderModelProcessTest, ScheduleModeFallsBackToSequenceParameters) { + Av2DmConfig config = MakeModelConfig(AV2_DM_DECODING_SCHEDULE_MODE); + config.scope.whole_xlayer = false; + config.operating_point_parameters_present = false; + config.sequence_decoder_buffer_delay = 9000; + Av2DecoderModel *model = av2_decoder_model_create(&config, nullptr, nullptr); + ASSERT_NE(model, nullptr); + const Av2DmFrameEvent frame = MakeFrame(0, 1); + av2_decoder_model_start_frame(model, &frame); + Av2DmState state; + ASSERT_TRUE(av2_decoder_model_get_state(model, &state)); + ExpectEqualRational(state.scheduled_removal, 1, 10); + Av2DmResult result; + ASSERT_TRUE(av2_decoder_model_get_result(model, &result)); + EXPECT_FALSE(result.missing_required_input); + av2_decoder_model_destroy(model); +} + +TEST(DecoderModelProcessTest, ExplicitOperatingPointParametersTakePrecedence) { + Av2DmConfig config = MakeModelConfig(AV2_DM_DECODING_SCHEDULE_MODE); + config.scope.whole_xlayer = false; + config.operating_point_parameters_present = true; + config.operating_point_decoder_buffer_delay = 18000; + config.operating_point_encoder_buffer_delay = 9000; + Av2DecoderModel *model = av2_decoder_model_create(&config, nullptr, nullptr); + ASSERT_NE(model, nullptr); + const Av2DmFrameEvent frame = MakeFrame(0, 1); + av2_decoder_model_start_frame(model, &frame); + Av2DmState state; + ASSERT_TRUE(av2_decoder_model_get_state(model, &state)); + ExpectEqualRational(state.scheduled_removal, 1, 5); + av2_decoder_model_destroy(model); + + config.scope.whole_xlayer = true; + model = av2_decoder_model_create(&config, nullptr, nullptr); + ASSERT_NE(model, nullptr); + av2_decoder_model_start_frame(model, &frame); + ASSERT_TRUE(av2_decoder_model_get_state(model, &state)); + ExpectEqualRational(state.scheduled_removal, 1, 10); + av2_decoder_model_destroy(model); +} + +TEST(DecoderModelProcessTest, LowDelayDefersWithoutUnderflowViolation) { + for (const bool low_delay : { false, true }) { + Av2DmConfig config = MakeModelConfig(AV2_DM_DECODING_SCHEDULE_MODE); + config.time_scale = 10; + config.num_units_in_decoding_tick = 1; + config.sequence_low_delay_mode = low_delay; + ASSERT_TRUE(av2_dm_rational_make(1000, 1, &config.level_limits.bit_rate)); + ASSERT_TRUE( + av2_dm_rational_make(1000, 1, &config.level_limits.buffer_size)); + ViolationCollector collector; + Av2DecoderModel *model = + av2_decoder_model_create(&config, CollectViolation, &collector); + ASSERT_NE(model, nullptr); + Av2DmFrameEvent frame = MakeFrame(0, 1); + frame.coded_bits = 150; + av2_decoder_model_start_frame(model, &frame); + EXPECT_EQ( + HasViolation(collector, AV2_DM_VIOLATION_SMOOTHING_BUFFER_UNDERFLOW), + !low_delay); + if (low_delay) { + Av2DmState state; + ASSERT_TRUE(av2_decoder_model_get_state(model, &state)); + ExpectEqualRational(state.removal, 1, 5); + } + av2_decoder_model_destroy(model); + } +} + +TEST(DecoderModelProcessTest, OlkInvalidationMirrorsAllInvalidSlots) { + Av2DmConfig config = MakeModelConfig(AV2_DM_RESOURCE_AVAILABILITY_MODE); + Av2DecoderModel *model = av2_decoder_model_create(&config, nullptr, nullptr); + ASSERT_NE(model, nullptr); + Av2DmFrameEvent first = MakeFrame(0, 10); + av2_decoder_model_start_frame(model, &first); + const Av2DmReferenceUpdateEvent refresh01 = Refresh(3, 3); + av2_decoder_model_update_reference_buffers(model, &refresh01); + Av2DmState state; + ASSERT_TRUE(av2_decoder_model_get_state(model, &state)); + ASSERT_EQ(state.buffer_pool.vbi[0], state.buffer_pool.vbi[1]); + const int old_buffer = state.buffer_pool.vbi[0]; + ASSERT_EQ(state.buffer_pool.buffers[old_buffer].decoder_ref_count, 2u); + av2_decoder_model_invalidate_olk_reference_buffers(model, 1); + ASSERT_TRUE(av2_decoder_model_get_state(model, &state)); + EXPECT_EQ(state.buffer_pool.vbi[0], old_buffer); + EXPECT_EQ(state.buffer_pool.vbi[1], -1); + EXPECT_EQ(state.buffer_pool.buffers[old_buffer].decoder_ref_count, 1u); + av2_decoder_model_destroy(model); +} + +TEST(DecoderModelProcessTest, + FrameStartSynchronizesNonOlkInvalidationBeforeBufferAllocation) { + Av2DmConfig config = MakeModelConfig(AV2_DM_RESOURCE_AVAILABILITY_MODE); + ViolationCollector collector; + Av2DecoderModel *model = + av2_decoder_model_create(&config, CollectViolation, &collector); + ASSERT_NE(model, nullptr); + + uint32_t valid_mask = 0; + for (uint32_t i = 0; i < 8; ++i) { + Av2DmFrameEvent frame = MakeFrame(i, i + 1); + av2_decoder_model_start_frame(model, &frame); + valid_mask |= 1u << i; + const Av2DmReferenceUpdateEvent refresh = Refresh(1u << i, valid_mask); + av2_decoder_model_update_reference_buffers(model, &refresh); + } + + Av2DmFrameEvent ninth = MakeFrame(8, 9); + av2_decoder_model_start_frame(model, &ninth); + Av2DmOutputEvent first_old_output = Output(100, 1, 0); + av2_decoder_model_output_frame(model, &first_old_output); + Av2DmReferenceUpdateEvent ninth_refresh = Refresh(1, valid_mask); + av2_decoder_model_update_reference_buffers(model, &ninth_refresh); + + Av2DmFrameEvent tenth = MakeFrame(9, 10); + av2_decoder_model_start_frame(model, &tenth); + Av2DmOutputEvent second_old_output = Output(101, 2, 1); + av2_decoder_model_output_frame(model, &second_old_output); + Av2DmReferenceUpdateEvent tenth_refresh = Refresh(2, valid_mask); + av2_decoder_model_update_reference_buffers(model, &tenth_refresh); + + Av2DmState before; + ASSERT_TRUE(av2_decoder_model_get_state(model, &before)); + ASSERT_EQ(av2_dm_buffer_pool_frames_in_use(&before.buffer_pool), 10u); + const int32_t invalidated_buffer = before.buffer_pool.vbi[7]; + ASSERT_GE(invalidated_buffer, 0); + + Av2DmFrameEvent eleventh = MakeFrame(10, 11); + eleventh.ref_valid_mask = valid_mask & ~(1u << 7); + av2_decoder_model_start_frame(model, &eleventh); + + Av2DmState after; + ASSERT_TRUE(av2_decoder_model_get_state(model, &after)); + EXPECT_EQ(after.buffer_pool.vbi[7], -1); + EXPECT_EQ(after.current_buffer_index, invalidated_buffer); + EXPECT_FALSE(HasViolation(collector, + AV2_DM_VIOLATION_DECODE_FRAME_BUFFER_UNAVAILABLE)); + av2_decoder_model_destroy(model); +} + +TEST(DecoderModelProcessTest, + RasSeedsSharedLongTermGenerationOrIsIndeterminate) { + Av2DmConfig config = MakeModelConfig(AV2_DM_RESOURCE_AVAILABILITY_MODE); + config.ras_start = true; + config.ras_seed_complete = true; + config.ras_seed_count = 2; + config.ras_seeds[0] = { 0, 77 }; + config.ras_seeds[1] = { 3, 77 }; + Av2DecoderModel *model = av2_decoder_model_create(&config, nullptr, nullptr); + ASSERT_NE(model, nullptr); + Av2DmState state; + ASSERT_TRUE(av2_decoder_model_get_state(model, &state)); + EXPECT_EQ(state.buffer_pool.vbi[0], state.buffer_pool.vbi[3]); + EXPECT_EQ(av2_dm_buffer_pool_frames_in_use(&state.buffer_pool), 1u); + const int buffer = state.buffer_pool.vbi[0]; + ASSERT_GE(buffer, 0); + EXPECT_EQ(state.buffer_pool.buffers[buffer].decoder_ref_count, 2u); + av2_decoder_model_destroy(model); + + config.ras_seed_complete = false; + model = av2_decoder_model_create(&config, nullptr, nullptr); + ASSERT_NE(model, nullptr); + Av2DmResult result; + ASSERT_TRUE(av2_decoder_model_get_result(model, &result)); + EXPECT_EQ(result.status, AV2_DM_RESULT_INDETERMINATE); + av2_decoder_model_destroy(model); +} + +TEST(DecoderModelProcessTest, InitialDelayRebasesHistoricalPresentation) { + Av2DmConfig config = MakeModelConfig(AV2_DM_RESOURCE_AVAILABILITY_MODE); + ViolationCollector collector; + Av2DecoderModel *model = + av2_decoder_model_create(&config, CollectViolation, &collector); + ASSERT_NE(model, nullptr); + Av2DmFrameEvent first = MakeFrame(0, 10); + av2_decoder_model_start_frame(model, &first); + const Av2DmReferenceUpdateEvent refresh0 = Refresh(1, 1); + av2_decoder_model_update_reference_buffers(model, &refresh0); + av2_decoder_model_set_initial_presentation_delay(model, 0); + Av2DmOutputEvent first_output = Output(0, 10, -1); + av2_decoder_model_output_frame(model, &first_output); + Av2DmState state; + ASSERT_TRUE(av2_decoder_model_get_state(model, &state)); + EXPECT_FALSE(state.last_presentation_valid); + + Av2DmFrameEvent second = MakeFrame(1, 11); + av2_decoder_model_start_frame(model, &second); + const Av2DmReferenceUpdateEvent refresh1 = Refresh(2, 3); + av2_decoder_model_update_reference_buffers(model, &refresh1); + av2_decoder_model_set_initial_presentation_delay(model, 0); + ASSERT_TRUE(av2_decoder_model_get_state(model, &state)); + ASSERT_TRUE(state.initial_presentation_delay_known); + ASSERT_TRUE(state.last_presentation_valid); + int comparison; + ASSERT_TRUE(av2_dm_rational_compare(&state.last_presentation, + &state.initial_presentation_delay, + &comparison)); + EXPECT_EQ(comparison, 0); + EXPECT_FALSE(HasViolation(collector, AV2_DM_VIOLATION_DISPLAY_FRAME_LATE)); + EXPECT_FALSE(HasViolation(collector, AV2_DM_VIOLATION_DECODE_DEADLINE)); + av2_decoder_model_destroy(model); +} + +TEST(DecoderModelProcessTest, + InitialDelayReportsConsolidatedWorstOutputAtReferenceUpdateEvent) { + Av2DmConfig config = MakeModelConfig(AV2_DM_DECODING_SCHEDULE_MODE); + config.initial_display_delay = 2; + ViolationCollector collector; + Av2DecoderModel *model = + av2_decoder_model_create(&config, CollectViolation, &collector); + ASSERT_NE(model, nullptr); + + Av2DmFrameEvent first = MakeFrame(1, 1); + // Make the first generation take longer to decode than the generation which + // later establishes the initial presentation delay. + first.random_access_point = true; + first.coded_as_closed_loop_key = true; + first.frame_is_intra = true; + first.frame_width = 640; + first.frame_height = 640; + av2_decoder_model_start_frame(model, &first); + const Av2DmReferenceUpdateEvent first_refresh = Refresh(1, 1); + av2_decoder_model_update_reference_buffers(model, &first_refresh); + av2_decoder_model_set_initial_presentation_delay(model, 2); + Av2DmState state; + ASSERT_TRUE(av2_decoder_model_get_state(model, &state)); + ASSERT_FALSE(state.initial_presentation_delay_known); + + Av2DmOutputEvent first_output = Output(10, 1, 0); + first_output.temporal_unit_index = 0; + av2_decoder_model_output_frame(model, &first_output); + Av2DmOutputEvent second_output = Output(11, 1, 0); + second_output.temporal_unit_index = 0; + av2_decoder_model_output_frame(model, &second_output); + EXPECT_FALSE(HasViolation(collector, AV2_DM_VIOLATION_DISPLAY_FRAME_LATE)); + EXPECT_FALSE(HasViolation(collector, AV2_DM_VIOLATION_DECODE_DEADLINE)); + + // Continue after an intentionally non-conformant same-removal schedule. The + // resulting backward lane time makes the reference-update event the first + // point where both historical output occurrences can be proven late. + Av2DmFrameEvent second = MakeFrame(20, 2, 0); + av2_decoder_model_start_frame(model, &second); + const Av2DmReferenceUpdateEvent second_refresh = Refresh(2, 3); + av2_decoder_model_update_reference_buffers(model, &second_refresh); + av2_decoder_model_set_initial_presentation_delay(model, 99); + ASSERT_TRUE(av2_decoder_model_get_state(model, &state)); + ASSERT_TRUE(state.initial_presentation_delay_known); + + EXPECT_EQ(CountViolations(collector, AV2_DM_VIOLATION_DISPLAY_FRAME_LATE), + 1u); + EXPECT_EQ(CountViolations(collector, AV2_DM_VIOLATION_DECODE_DEADLINE), 1u); + for (const Av2DmViolation &violation : collector.violations) { + if (violation.code == AV2_DM_VIOLATION_DISPLAY_FRAME_LATE || + violation.code == AV2_DM_VIOLATION_DECODE_DEADLINE) { + EXPECT_EQ(violation.event_index, 99u); + } + } + av2_decoder_model_set_initial_presentation_delay(model, 100); + EXPECT_EQ(CountViolations(collector, AV2_DM_VIOLATION_DISPLAY_FRAME_LATE), + 1u); + EXPECT_EQ(CountViolations(collector, AV2_DM_VIOLATION_DECODE_DEADLINE), 1u); + av2_decoder_model_destroy(model); +} + +TEST(DecoderModelProcessTest, DeadlineUsesDecodedGenerationIdentity) { + Av2DmConfig config = MakeModelConfig(AV2_DM_DECODING_SCHEDULE_MODE); + config.initial_display_delay = 1; + ViolationCollector collector; + Av2DecoderModel *model = + av2_decoder_model_create(&config, CollectViolation, &collector); + ASSERT_NE(model, nullptr); + Av2DmFrameEvent first = MakeFrame(0, 100); + av2_decoder_model_start_frame(model, &first); + const Av2DmReferenceUpdateEvent refresh0 = Refresh(1, 1); + av2_decoder_model_update_reference_buffers(model, &refresh0); + av2_decoder_model_set_initial_presentation_delay(model, 0); + Av2DmFrameEvent second = MakeFrame(1, 200, 9000); + av2_decoder_model_start_frame(model, &second); + const Av2DmReferenceUpdateEvent refresh1 = Refresh(2, 3); + av2_decoder_model_update_reference_buffers(model, &refresh1); + Av2DmOutputEvent output = Output(2, 200, -1); + av2_decoder_model_output_frame(model, &output); + EXPECT_TRUE(HasViolation(collector, AV2_DM_VIOLATION_DECODE_DEADLINE)); + av2_decoder_model_destroy(model); +} + +TEST(DecoderModelProcessTest, EmptyShowExistingBufferIsReported) { + Av2DmConfig config = MakeModelConfig(AV2_DM_RESOURCE_AVAILABILITY_MODE); + ViolationCollector collector; + Av2DecoderModel *model = + av2_decoder_model_create(&config, CollectViolation, &collector); + ASSERT_NE(model, nullptr); + Av2DmOutputEvent output = Output(0, 1, 0); + output.ref_valid_mask = 1; + av2_decoder_model_output_frame(model, &output); + EXPECT_TRUE(HasViolation( + collector, AV2_DM_VIOLATION_DECODE_EXISTING_FRAME_BUFFER_EMPTY)); + const Av2DmViolation *const violation = FindViolation( + collector, AV2_DM_VIOLATION_DECODE_EXISTING_FRAME_BUFFER_EMPTY); + ASSERT_NE(violation, nullptr); + ASSERT_EQ(violation->detail.kind, AV2_DM_VIOLATION_DETAIL_REFERENCE_SLOT); + EXPECT_EQ(violation->affected_kind, AV2_DM_VIOLATION_AFFECTED_OUTPUT); + EXPECT_EQ(violation->detail.value.reference_slot.requested_slot, 0); + EXPECT_TRUE(violation->detail.value.reference_slot.slot_in_range); + EXPECT_TRUE(violation->detail.value.reference_slot.reference_valid); + EXPECT_EQ(violation->detail.value.reference_slot.buffer_index, -1); + EXPECT_EQ(violation->detail.value.reference_slot.pool.free_buffers, + violation->detail.value.reference_slot.pool.pool_size); + av2_decoder_model_destroy(model); +} + +TEST(DecoderModelProcessTest, PeriodicRebasePreservesExactTimeline) { + Av2DmConfig config = MakeModelConfig(AV2_DM_DECODING_SCHEDULE_MODE); + config.initial_display_delay = 1; + config.rebase_interval_events = 3; + Av2DecoderModel *model = av2_decoder_model_create(&config, nullptr, nullptr); + ASSERT_NE(model, nullptr); + Av2DmFrameEvent frame = MakeFrame(0, 1); + av2_decoder_model_start_frame(model, &frame); + const Av2DmReferenceUpdateEvent refresh = Refresh(1, 1); + av2_decoder_model_update_reference_buffers(model, &refresh); + av2_decoder_model_set_initial_presentation_delay(model, 0); + Av2DmState state; + ASSERT_TRUE(av2_decoder_model_get_state(model, &state)); + ExpectEqualRational(state.time, 0, 1); + ExpectEqualRational(state.decode_completion, 0, 1); + Av2DmOutputEvent output = Output(3, 1, 0); + av2_decoder_model_output_frame(model, &output); + ASSERT_TRUE(av2_decoder_model_get_state(model, &state)); + ASSERT_TRUE(state.last_presentation_valid); + ExpectEqualRational(state.last_presentation, 0, 1); + av2_decoder_model_destroy(model); +} + +TEST(DecoderModelProcessTest, RebaseKeepsScheduleAndResourceLaneOriginShared) { + for (const uint32_t removal_ticks : { 1799u, 1800u, 1801u }) { + Av2DmConfig config = MakeModelConfig(AV2_DM_DECODING_SCHEDULE_MODE); + config.initial_display_delay = 1; + config.rebase_interval_events = 4; + config.level_limits.max_decode_rate = 409600; + ViolationCollector collector; + Av2DecoderModel *model = + av2_decoder_model_create(&config, CollectViolation, &collector); + ASSERT_NE(model, nullptr); + + Av2DmFrameEvent first = MakeFrame(0, 1); + av2_decoder_model_start_frame(model, &first); + const Av2DmReferenceUpdateEvent refresh = Refresh(1, 1); + av2_decoder_model_update_reference_buffers(model, &refresh); + av2_decoder_model_set_initial_presentation_delay(model, 0); + + Av2DmFrameEvent delayed = MakeFrame(1, 2, 9000); + av2_decoder_model_start_frame(model, &delayed); + Av2DmFrameEvent boundary = MakeFrame(2, 3, removal_ticks); + av2_decoder_model_start_frame(model, &boundary); + + EXPECT_EQ(HasViolation(collector, + AV2_DM_VIOLATION_SCHEDULE_BEFORE_RESOURCE_REMOVAL), + removal_ticks < 1800); + av2_decoder_model_destroy(model); + } +} + +TEST(DecoderModelStorageTest, DirectWarningsDiscardPayloadAndStayBounded) { + Av2DmConfig config = MakeModelConfig(AV2_DM_DECODING_SCHEDULE_MODE); + ViolationCollector collector; + Av2DecoderModel *model = + av2_decoder_model_create(&config, CollectViolation, &collector); + ASSERT_NE(model, nullptr); + + for (uint64_t i = 0; i < 112; ++i) { + Av2DmFrameEvent frame = MakeFrame(i, i + 1, (uint32_t)(i * 3000)); + frame.temporal_unit_index = 0; + frame.frame_width = config.level_limits.max_horizontal_size + 1; + av2_decoder_model_start_frame(model, &frame); + } + EXPECT_EQ(CountViolations(collector, AV2_DM_VIOLATION_MAX_HORIZONTAL_SIZE), + 112u); + Av2DmResult result; + ASSERT_TRUE(av2_decoder_model_get_result(model, &result)); + EXPECT_GE(result.violations, 112u); + Av2DmStorageStats storage; + ASSERT_TRUE(av2_decoder_model_get_storage_stats(model, &storage)); + EXPECT_LE(storage.high_water_dfgs, 16u); + EXPECT_EQ(storage.high_water_outputs, 0u); + EXPECT_EQ(storage.high_water_tus, 1u); + av2_decoder_model_destroy(model); +} + +TEST(DecoderModelStorageTest, ProvenSmoothingOverflowReleasesFullnessHistory) { + Av2DmConfig config = MakeModelConfig(AV2_DM_DECODING_SCHEDULE_MODE); + config.sequence_low_delay_mode = true; + ASSERT_TRUE(av2_dm_rational_make(1000, 1, &config.level_limits.bit_rate)); + ASSERT_TRUE(av2_dm_rational_make(10, 1, &config.level_limits.buffer_size)); + ViolationCollector collector; + Av2DecoderModel *model = + av2_decoder_model_create(&config, CollectViolation, &collector); + ASSERT_NE(model, nullptr); + + for (uint32_t i = 0; i < 200; ++i) { + Av2DmFrameEvent frame = MakeFrame(i, (uint64_t)i + 1, 90000 - (i & 1)); + frame.coded_bits = 1; + if (i == 199) { + frame.frame_width = config.level_limits.max_horizontal_size + 1; + } + av2_decoder_model_start_frame(model, &frame); + } + ASSERT_TRUE( + HasViolation(collector, AV2_DM_VIOLATION_SMOOTHING_BUFFER_OVERFLOW)); + EXPECT_TRUE(HasViolation(collector, AV2_DM_VIOLATION_MAX_HORIZONTAL_SIZE)); + Av2DmStorageStats storage; + ASSERT_TRUE(av2_decoder_model_get_storage_stats(model, &storage)); + EXPECT_LE(storage.active_dfgs, 1u); + EXPECT_LE(storage.high_water_dfgs, 16u); + av2_decoder_model_destroy(model); +} + +TEST(DecoderModelStorageTest, NonIncreasingOutputTimesRestartRateHistory) { + Av2DmConfig config = MakeModelConfig(AV2_DM_RESOURCE_AVAILABILITY_MODE); + config.initial_display_delay = 1; + config.level_limits.max_header_rate = 1; + config.level_limits.max_tile_size_header_rate_product = 1; + ViolationCollector collector; + Av2DecoderModel *model = + av2_decoder_model_create(&config, CollectViolation, &collector); + ASSERT_NE(model, nullptr); + + for (uint32_t i = 0; i < 200; ++i) { + Av2DmFrameEvent frame = MakeFrame(i, (uint64_t)i + 1); + frame.temporal_unit_output_time_present = true; + ASSERT_TRUE(av2_dm_rational_make(0, 1, &frame.temporal_unit_output_time)); + av2_decoder_model_start_frame(model, &frame); + const Av2DmReferenceUpdateEvent refresh = Refresh(1, 1); + av2_decoder_model_update_reference_buffers(model, &refresh); + av2_decoder_model_set_initial_presentation_delay(model, i); + Av2DmOutputEvent output = Output(1000 + i, (uint64_t)i + 1, -1); + output.temporal_unit_index = i; + av2_decoder_model_output_frame(model, &output); + } + EXPECT_TRUE(HasViolation(collector, AV2_DM_VIOLATION_MAX_DISPLAY_RATE)); + EXPECT_TRUE(HasViolation(collector, AV2_DM_VIOLATION_MAX_HEADER_RATE)); + EXPECT_TRUE(HasViolation(collector, AV2_DM_VIOLATION_TILE_SIZE_HEADER_RATE)); + Av2DmStorageStats storage; + ASSERT_TRUE(av2_decoder_model_get_storage_stats(model, &storage)); + EXPECT_LE(storage.active_tus, 4u); + EXPECT_LE(storage.high_water_tus, 6u); + av2_decoder_model_destroy(model); +} + +TEST(DecoderModelStorageTest, UnoutputDeadGenerationsRetireUnresolvedTus) { + Av2DmConfig config = MakeModelConfig(AV2_DM_DECODING_SCHEDULE_MODE); + config.sequence_low_delay_mode = true; + Av2DecoderModel *model = av2_decoder_model_create(&config, nullptr, nullptr); + ASSERT_NE(model, nullptr); + + for (uint32_t i = 0; i < 200; ++i) { + Av2DmFrameEvent frame = MakeFrame(i, (uint64_t)i + 1, 70000 + i * 9000); + av2_decoder_model_start_frame(model, &frame); + } + Av2DmStorageStats storage; + ASSERT_TRUE(av2_decoder_model_get_storage_stats(model, &storage)); + EXPECT_LE(storage.active_tus, 2u); + EXPECT_LE(storage.high_water_tus, 2u); + + av2_decoder_model_finish(model); + Av2DmResult result; + ASSERT_TRUE(av2_decoder_model_get_result(model, &result)); + EXPECT_TRUE(result.missing_required_input); + av2_decoder_model_destroy(model); +} + +TEST(DecoderModelStorageTest, TimedTusRetireWithoutOutputCallbacks) { + Av2DmConfig config = MakeModelConfig(AV2_DM_RESOURCE_AVAILABILITY_MODE); + Av2DecoderModel *model = av2_decoder_model_create(&config, nullptr, nullptr); + ASSERT_NE(model, nullptr); + + for (uint32_t i = 0; i < 200; ++i) { + Av2DmFrameEvent frame = MakeFrame(i, (uint64_t)i + 1); + frame.temporal_unit_output_time_present = true; + ASSERT_TRUE(av2_dm_rational_make(i, 30, &frame.temporal_unit_output_time)); + av2_decoder_model_start_frame(model, &frame); + } + Av2DmStorageStats storage; + ASSERT_TRUE(av2_decoder_model_get_storage_stats(model, &storage)); + EXPECT_LE(storage.active_tus, 33u); + EXPECT_LE(storage.high_water_tus, 33u); + av2_decoder_model_destroy(model); +} + +TEST(DecoderModelStorageTest, EmptyTusDoNotRequireOutputTiming) { + Av2DmConfig config = MakeModelConfig(AV2_DM_RESOURCE_AVAILABILITY_MODE); + Av2DecoderModel *model = av2_decoder_model_create(&config, nullptr, nullptr); + ASSERT_NE(model, nullptr); + + for (uint32_t i = 0; i < 4; ++i) { + Av2DmFrameEvent frame = MakeFrame(i, (uint64_t)i + 1); + frame.count_frame_header = false; + av2_decoder_model_start_frame(model, &frame); + } + av2_decoder_model_finish(model); + Av2DmResult result; + ASSERT_TRUE(av2_decoder_model_get_result(model, &result)); + EXPECT_FALSE(result.missing_required_input); + av2_decoder_model_destroy(model); +} + +TEST(DecoderModelStorageTest, OneHourFixedRateTracesRemainBounded) { + struct Trace { + uint32_t frames; + uint32_t frames_per_second; + }; + for (const Trace trace : { Trace{ 108000, 30 }, Trace{ 216000, 60 } }) { + SCOPED_TRACE(trace.frames_per_second); + Av2DmConfig config = MakeModelConfig(AV2_DM_DECODING_SCHEDULE_MODE); + config.initial_display_delay = 1; + config.ticks_per_picture = 90000 / trace.frames_per_second; + ViolationCollector collector; + Av2DecoderModel *model = + av2_decoder_model_create(&config, CollectViolation, &collector); + ASSERT_NE(model, nullptr); + + for (uint32_t i = 0; i < trace.frames; ++i) { + Av2DmFrameEvent frame = + MakeFrame(i, (uint64_t)i + 1, i * config.ticks_per_picture); + av2_decoder_model_start_frame(model, &frame); + const Av2DmReferenceUpdateEvent refresh = Refresh(1, 1); + av2_decoder_model_update_reference_buffers(model, &refresh); + av2_decoder_model_set_initial_presentation_delay(model, i); + Av2DmOutputEvent output = + Output((uint64_t)trace.frames + i, (uint64_t)i + 1, -1); + output.temporal_unit_index = i; + av2_decoder_model_output_frame(model, &output); + ASSERT_TRUE(collector.violations.empty()) << "frame " << i; + } + + Av2DmStorageStats storage; + ASSERT_TRUE(av2_decoder_model_get_storage_stats(model, &storage)); + EXPECT_LE(storage.high_water_dfgs, 16u); + EXPECT_EQ(storage.high_water_outputs, 0u); + EXPECT_LE(storage.high_water_tus, trace.frames_per_second + 3); + EXPECT_LE(storage.high_water_generations, + (uint32_t)AV2_DM_MAX_BUFFER_POOL_SIZE); + EXPECT_EQ(storage.high_water_cvs, 1u); + EXPECT_EQ(storage.high_water_rap_runs, 1u); + + av2_decoder_model_finish(model); + ASSERT_TRUE(av2_decoder_model_get_storage_stats(model, &storage)); + EXPECT_EQ(storage.active_dfgs, 0u); + EXPECT_EQ(storage.active_outputs, 0u); + EXPECT_EQ(storage.active_tus, 0u); + EXPECT_EQ(storage.active_generations, 0u); + EXPECT_EQ(storage.active_cvs, 0u); + EXPECT_EQ(storage.active_rap_runs, 0u); + av2_decoder_model_destroy(model); + } +} + +TEST(DecoderModelProcessTest, ScheduleModeReportsUnavailableDecodeBuffer) { + Av2DmConfig config = MakeModelConfig(AV2_DM_DECODING_SCHEDULE_MODE); + config.equal_picture_interval = false; + config.initial_display_delay = 8; + ViolationCollector collector; + Av2DecoderModel *model = + av2_decoder_model_create(&config, CollectViolation, &collector); + ASSERT_NE(model, nullptr); + for (uint32_t i = 0; i < 8; ++i) { + Av2DmFrameEvent frame = MakeFrame(i, i + 1, i * 9000); + av2_decoder_model_start_frame(model, &frame); + const Av2DmReferenceUpdateEvent refresh = + Refresh(1u << i, (1u << (i + 1)) - 1); + av2_decoder_model_update_reference_buffers(model, &refresh); + av2_decoder_model_set_initial_presentation_delay(model, 0); + } + Av2DmOutputEvent rap_output = Output(99, 1, 0); + rap_output.presentation_time_present = true; + av2_decoder_model_output_frame(model, &rap_output); + for (uint32_t i = 8; i < 10; ++i) { + Av2DmFrameEvent frame = MakeFrame(i, i + 1, i * 9000); + av2_decoder_model_start_frame(model, &frame); + Av2DmOutputEvent output = Output(100 + i, i + 1, -1); + output.temporal_unit_index = i; + output.presentation_time_present = true; + output.presentation_time_ticks = 900000 + i; + av2_decoder_model_output_frame(model, &output); + } + Av2DmFrameEvent blocked = MakeFrame(10, 11, 90000); + av2_decoder_model_start_frame(model, &blocked); + EXPECT_TRUE(HasViolation(collector, + AV2_DM_VIOLATION_DECODE_FRAME_BUFFER_UNAVAILABLE)); + const Av2DmViolation *const violation = FindViolation( + collector, AV2_DM_VIOLATION_DECODE_FRAME_BUFFER_UNAVAILABLE); + ASSERT_NE(violation, nullptr); + ASSERT_EQ(violation->detail.kind, AV2_DM_VIOLATION_DETAIL_BUFFER_POOL); + EXPECT_EQ(violation->detail.value.buffer_pool.free_buffers, 0u); + EXPECT_EQ(violation->detail.value.buffer_pool.frames_in_use, + violation->detail.value.buffer_pool.pool_size); + av2_decoder_model_destroy(model); +} + +TEST(DecoderModelProcessTest, ReorderedOutputsAreCountedByGeneration) { + Av2DmConfig config = MakeModelConfig(AV2_DM_DECODING_SCHEDULE_MODE); + Av2DecoderModel *model = av2_decoder_model_create(&config, nullptr, nullptr); + ASSERT_NE(model, nullptr); + Av2DmFrameEvent first = MakeFrame(0, 10); + av2_decoder_model_start_frame(model, &first); + const Av2DmReferenceUpdateEvent refresh0 = Refresh(1, 1); + av2_decoder_model_update_reference_buffers(model, &refresh0); + Av2DmFrameEvent second = MakeFrame(1, 20, 9000); + av2_decoder_model_start_frame(model, &second); + const Av2DmReferenceUpdateEvent refresh1 = Refresh(2, 3); + av2_decoder_model_update_reference_buffers(model, &refresh1); + av2_decoder_model_set_initial_presentation_delay(model, 0); + Av2DmOutputEvent second_output = Output(2, 20, 1); + av2_decoder_model_output_frame(model, &second_output); + Av2DmOutputEvent first_output = Output(3, 10, 0); + av2_decoder_model_output_frame(model, &first_output); + Av2DmResult result; + ASSERT_TRUE(av2_decoder_model_get_result(model, &result)); + EXPECT_EQ(result.reordered_outputs, 1u); + av2_decoder_model_destroy(model); +} + +TEST(DecoderModelProcessTest, RetiredDfgGenerationMetadataSurvivesUntilOutput) { + Av2DmConfig config = MakeModelConfig(AV2_DM_DECODING_SCHEDULE_MODE); + config.initial_display_delay = 1; + Av2DecoderModel *model = av2_decoder_model_create(&config, nullptr, nullptr); + ASSERT_NE(model, nullptr); + + Av2DmFrameEvent first = MakeFrame(0, 1); + av2_decoder_model_start_frame(model, &first); + const Av2DmReferenceUpdateEvent refresh = Refresh(1, 1); + av2_decoder_model_update_reference_buffers(model, &refresh); + av2_decoder_model_set_initial_presentation_delay(model, 0); + for (uint32_t i = 1; i <= 20; ++i) { + Av2DmFrameEvent filler = MakeFrame(i, (uint64_t)i + 1, i * 9000); + av2_decoder_model_start_frame(model, &filler); + } + Av2DmStorageStats storage; + ASSERT_TRUE(av2_decoder_model_get_storage_stats(model, &storage)); + EXPECT_LT(storage.active_dfgs, 20u); + + Av2DmOutputEvent latest = Output(100, 21, -1); + latest.temporal_unit_index = 20; + av2_decoder_model_output_frame(model, &latest); + Av2DmOutputEvent oldest = Output(101, 1, 0); + oldest.temporal_unit_index = 0; + av2_decoder_model_output_frame(model, &oldest); + Av2DmResult result; + ASSERT_TRUE(av2_decoder_model_get_result(model, &result)); + EXPECT_EQ(result.reordered_outputs, 1u); + av2_decoder_model_destroy(model); +} + +TEST(DecoderModelProcessTest, + FixedRatePresentationFollowsReorderedOwnerTusExactly) { + Av2DmConfig config = MakeModelConfig(AV2_DM_DECODING_SCHEDULE_MODE); + config.initial_display_delay = 4; + Av2DecoderModel *model = av2_decoder_model_create(&config, nullptr, nullptr); + ASSERT_NE(model, nullptr); + + const uint64_t decode_tus[] = { 0, 5, 4, 6 }; + for (uint32_t i = 0; i < 4; ++i) { + Av2DmFrameEvent frame = MakeFrame(i, i + 1, i * 9000); + frame.temporal_unit_index = decode_tus[i]; + av2_decoder_model_start_frame(model, &frame); + const Av2DmReferenceUpdateEvent refresh = + Refresh(1u << i, (1u << (i + 1)) - 1); + av2_decoder_model_update_reference_buffers(model, &refresh); + } + av2_decoder_model_set_initial_presentation_delay(model, 0); + + const uint64_t owner_tus[] = { 0, 4, 5, 6 }; + const uint64_t generations[] = { 1, 3, 2, 4 }; + const int map_indices[] = { 0, 2, 1, 3 }; + for (uint32_t i = 0; i < 4; ++i) { + Av2DmOutputEvent output = Output(10 + i, generations[i], map_indices[i]); + output.temporal_unit_index = owner_tus[i]; + av2_decoder_model_output_frame(model, &output); + Av2DmState state; + ASSERT_TRUE(av2_decoder_model_get_state(model, &state)); + ASSERT_TRUE(state.last_presentation_offset_valid); + ExpectEqualRational(state.last_presentation_offset, i, 30); + ASSERT_TRUE(state.last_output_temporal_unit_valid); + EXPECT_EQ(state.last_output_temporal_unit, owner_tus[i]); + } + + av2_decoder_model_finish(model); + Av2DmResult result; + ASSERT_TRUE(av2_decoder_model_get_result(model, &result)); + EXPECT_FALSE(result.arithmetic_failed); + EXPECT_FALSE(result.missing_required_input); + EXPECT_EQ(result.output_frames, 4u); + EXPECT_EQ(result.reordered_outputs, 1u); + av2_decoder_model_destroy(model); +} + +TEST(DecoderModelProcessTest, + FixedRateSameOwnerTuSharesTimeAndAccumulatesSamples) { + Av2DmConfig config = MakeModelConfig(AV2_DM_DECODING_SCHEDULE_MODE); + config.initial_display_delay = 3; + Av2DecoderModel *model = av2_decoder_model_create(&config, nullptr, nullptr); + ASSERT_NE(model, nullptr); + + const uint64_t decode_tus[] = { 0, 0, 1 }; + for (uint32_t i = 0; i < 3; ++i) { + Av2DmFrameEvent frame = MakeFrame(i, i + 1, i * 9000); + frame.temporal_unit_index = decode_tus[i]; + av2_decoder_model_start_frame(model, &frame); + const Av2DmReferenceUpdateEvent refresh = + Refresh(1u << i, (1u << (i + 1)) - 1); + av2_decoder_model_update_reference_buffers(model, &refresh); + } + av2_decoder_model_set_initial_presentation_delay(model, 0); + + const uint64_t owner_tus[] = { 0, 0, 1 }; + for (uint32_t i = 0; i < 3; ++i) { + Av2DmOutputEvent output = Output(10 + i, i + 1, i); + output.temporal_unit_index = owner_tus[i]; + av2_decoder_model_output_frame(model, &output); + Av2DmState state; + ASSERT_TRUE(av2_decoder_model_get_state(model, &state)); + ExpectEqualRational(state.last_presentation_offset, i == 2 ? 1 : 0, + i == 2 ? 30 : 1); + if (i == 1) { + ASSERT_TRUE(state.last_temporal_unit_output_time_valid); + ExpectEqualRational(state.last_temporal_unit_output_time, 0, 1); + EXPECT_EQ(state.last_temporal_unit_output_frames, 2u); + EXPECT_EQ(state.last_temporal_unit_output_luma_samples, 8192u); + } + } + + Av2DmResult result; + ASSERT_TRUE(av2_decoder_model_get_result(model, &result)); + EXPECT_FALSE(result.arithmetic_failed); + EXPECT_FALSE(result.missing_required_input); + EXPECT_EQ(result.output_frames, 3u); + av2_decoder_model_destroy(model); +} + +TEST(DecoderModelProcessTest, VariableRatePresentationUsesRapBasesExactly) { + Av2DmConfig config = MakeModelConfig(AV2_DM_DECODING_SCHEDULE_MODE); + config.equal_picture_interval = false; + config.initial_display_delay = 4; + Av2DecoderModel *model = av2_decoder_model_create(&config, nullptr, nullptr); + ASSERT_NE(model, nullptr); + + for (uint32_t i = 0; i < 4; ++i) { + Av2DmFrameEvent frame = MakeFrame(i, i + 1, i * 9000); + if (i == 2) { + frame.random_access_point = true; + frame.coded_as_closed_loop_key = true; + frame.frame_is_intra = true; + } + av2_decoder_model_start_frame(model, &frame); + const Av2DmReferenceUpdateEvent refresh = + Refresh(1u << i, (1u << (i + 1)) - 1); + av2_decoder_model_update_reference_buffers(model, &refresh); + } + av2_decoder_model_set_initial_presentation_delay(model, 0); + + const uint64_t presentation_ticks[] = { 0, 2, 30, 3 }; + const uint64_t expected_numerators[] = { 0, 1, 1, 11 }; + const uint64_t expected_denominators[] = { 1, 45000, 3000, 30000 }; + for (uint32_t i = 0; i < 4; ++i) { + Av2DmOutputEvent output = Output(10 + i, i + 1, i); + output.presentation_time_present = true; + output.presentation_time_ticks = presentation_ticks[i]; + av2_decoder_model_output_frame(model, &output); + Av2DmState state; + ASSERT_TRUE(av2_decoder_model_get_state(model, &state)); + ASSERT_TRUE(state.last_presentation_offset_valid); + ExpectEqualRational(state.last_presentation_offset, expected_numerators[i], + expected_denominators[i]); + } + + Av2DmResult result; + ASSERT_TRUE(av2_decoder_model_get_result(model, &result)); + EXPECT_FALSE(result.arithmetic_failed); + EXPECT_FALSE(result.missing_required_input); + EXPECT_EQ(result.output_frames, 4u); + av2_decoder_model_destroy(model); +} + +TEST(DecoderModelProcessTest, + MissingVariableRatePresentationTimingIsIndeterminate) { + Av2DmConfig config = MakeModelConfig(AV2_DM_DECODING_SCHEDULE_MODE); + config.equal_picture_interval = false; + config.initial_display_delay = 1; + Av2DecoderModel *model = av2_decoder_model_create(&config, nullptr, nullptr); + ASSERT_NE(model, nullptr); + Av2DmFrameEvent frame = MakeFrame(0, 1); + av2_decoder_model_start_frame(model, &frame); + const Av2DmReferenceUpdateEvent refresh = Refresh(1, 1); + av2_decoder_model_update_reference_buffers(model, &refresh); + av2_decoder_model_set_initial_presentation_delay(model, 0); + Av2DmOutputEvent output = Output(1, 1, 0); + output.temporal_unit_index = 0; + av2_decoder_model_output_frame(model, &output); + + Av2DmResult result; + ASSERT_TRUE(av2_decoder_model_get_result(model, &result)); + EXPECT_EQ(result.status, AV2_DM_RESULT_INDETERMINATE); + EXPECT_TRUE(result.missing_required_input); + EXPECT_EQ(result.violations, 0u); + EXPECT_EQ(result.output_frames, 0u); + av2_decoder_model_destroy(model); +} + +TEST(DecoderModelProcessTest, ExplicitTemporalUnitOutputTimeIsPreserved) { + Av2DmConfig config = MakeModelConfig(AV2_DM_DECODING_SCHEDULE_MODE); + config.initial_display_delay = 1; + Av2DecoderModel *model = av2_decoder_model_create(&config, nullptr, nullptr); + ASSERT_NE(model, nullptr); + Av2DmFrameEvent frame = MakeFrame(0, 1); + frame.temporal_unit_output_time_present = true; + ASSERT_TRUE(av2_dm_rational_make(7, 3, &frame.temporal_unit_output_time)); + av2_decoder_model_start_frame(model, &frame); + const Av2DmReferenceUpdateEvent refresh = Refresh(1, 1); + av2_decoder_model_update_reference_buffers(model, &refresh); + av2_decoder_model_set_initial_presentation_delay(model, 0); + Av2DmOutputEvent output = Output(1, 1, 0); + output.temporal_unit_index = 0; + av2_decoder_model_output_frame(model, &output); + + Av2DmState state; + ASSERT_TRUE(av2_decoder_model_get_state(model, &state)); + ASSERT_TRUE(state.last_temporal_unit_output_time_valid); + ExpectEqualRational(state.last_temporal_unit_output_time, 7, 3); + av2_decoder_model_destroy(model); +} + +TEST(DecoderModelProcessTest, LongStreamRebasingPreservesDecisions) { + Av2DmResult results[2]; + for (uint32_t run = 0; run < 2; ++run) { + Av2DmConfig config = MakeModelConfig(AV2_DM_DECODING_SCHEDULE_MODE); + config.initial_display_delay = 1; + config.rebase_interval_events = run == 0 ? UINT32_MAX : 32; + Av2DecoderModel *model = + av2_decoder_model_create(&config, nullptr, nullptr); + ASSERT_NE(model, nullptr); + for (uint32_t i = 0; i < 256; ++i) { + Av2DmFrameEvent frame = MakeFrame(i, i + 1, i * 9000); + av2_decoder_model_start_frame(model, &frame); + if (i == 0) { + const Av2DmReferenceUpdateEvent refresh = Refresh(1, 1); + av2_decoder_model_update_reference_buffers(model, &refresh); + av2_decoder_model_set_initial_presentation_delay(model, 0); + } + } + ASSERT_TRUE(av2_decoder_model_get_result(model, &results[run])); + EXPECT_FALSE(results[run].arithmetic_failed); + EXPECT_EQ(results[run].decoded_frames, 256u); + av2_decoder_model_destroy(model); + } + EXPECT_EQ(results[0].status, results[1].status); + EXPECT_EQ(results[0].violations, results[1].violations); +} + +TEST(DecoderModelConformanceTest, AnnexALevelFactorsAreExact) { + Av2DmLevelLimits limits; + ASSERT_TRUE(av2_dm_get_level_limits(4, 0, 3, &limits)); + ExpectEqualRational(limits.bit_rate, 20004000, 1); + EXPECT_EQ(limits.picture_size_profile_factor, 20u); + EXPECT_EQ(limits.max_tile_width, 4096u); + EXPECT_EQ(limits.max_tile_area, 4096u * 2304u); + ASSERT_TRUE(av2_dm_get_level_limits(4, 1, 4, &limits)); + ExpectEqualRational(limits.bit_rate, 75000000, 1); + EXPECT_EQ(limits.picture_size_profile_factor, 30u); + EXPECT_FALSE(av2_dm_get_level_limits(0, 1, 0, &limits)); + ASSERT_TRUE(av2_dm_get_level_limits(21, 1, 0, &limits)); + EXPECT_EQ(limits.max_decode_rate, UINT64_C(75296145408)); + EXPECT_EQ(limits.max_tile_width, 16384u); +} + +TEST(DecoderModelConformanceTest, MultistreamFactorsAreAppliedExactly) { + Av2DmLevelLimits limits; + ASSERT_TRUE(av2_dm_get_level_limits(21, 0, 0, &limits)); + EXPECT_FALSE(av2_dm_apply_multistream_limits(3, 0, 0, 3, 2, &limits)); + ASSERT_TRUE(av2_dm_apply_multistream_limits(4, 0, 0, 3, 2, &limits)); + EXPECT_EQ(limits.max_picture_size, 896u * 1600u); + EXPECT_EQ(limits.max_horizontal_size, 896u); + EXPECT_EQ(limits.max_vertical_size, 1600u); + EXPECT_EQ(limits.max_display_rate, 47185920u); + EXPECT_EQ(limits.max_decode_rate, 51904512u); + EXPECT_EQ(limits.max_header_rate, 132u); + EXPECT_EQ(limits.max_tiles, 21u); + EXPECT_EQ(limits.max_tile_columns, 7u); + ExpectEqualRational(limits.bit_rate, 8000000, 1); + EXPECT_FALSE(av2_dm_apply_multistream_limits(4, 0, 0, 2, 1, &limits)); +} + +TEST(DecoderModelConformanceTest, StaticLevelBoundariesAreInclusive) { + const auto run = [](const Av2DmConfig &config, const Av2DmFrameEvent &frame, + Av2DmViolationCode code, bool expected) { + Av2DmConfig isolated_config = config; + // Keep the independent sequence-level reference constraint away from the + // per-frame static boundary under test. + isolated_config.num_ref_frames = 1; + ViolationCollector collector; + Av2DecoderModel *model = av2_decoder_model_create( + &isolated_config, CollectViolation, &collector); + ASSERT_NE(model, nullptr); + av2_decoder_model_start_frame(model, &frame); + EXPECT_EQ(CountViolations(collector, code), expected ? 1u : 0u); + EXPECT_EQ(collector.violations.size(), expected ? 1u : 0u); + av2_decoder_model_destroy(model); + }; + + for (const int delta : { -1, 0, 1 }) { + Av2DmConfig config = MakeModelConfig(AV2_DM_RESOURCE_AVAILABILITY_MODE); + Av2DmFrameEvent frame = MakeFrame(0, 1); + config.level_limits.max_picture_size = 4096 + delta; + run(config, frame, AV2_DM_VIOLATION_MAX_PICTURE_SIZE, delta < 0); + + config = MakeModelConfig(AV2_DM_RESOURCE_AVAILABILITY_MODE); + config.level_limits.max_horizontal_size = 64 + delta; + run(config, frame, AV2_DM_VIOLATION_MAX_HORIZONTAL_SIZE, delta < 0); + + config = MakeModelConfig(AV2_DM_RESOURCE_AVAILABILITY_MODE); + config.level_limits.max_vertical_size = 64 + delta; + run(config, frame, AV2_DM_VIOLATION_MAX_VERTICAL_SIZE, delta < 0); + + config = MakeModelConfig(AV2_DM_RESOURCE_AVAILABILITY_MODE); + frame.num_tiles = 2; + config.level_limits.max_tiles = 2 + delta; + run(config, frame, AV2_DM_VIOLATION_MAX_TILES, delta < 0); + + config = MakeModelConfig(AV2_DM_RESOURCE_AVAILABILITY_MODE); + frame = MakeFrame(0, 1); + frame.num_tiles = 2; + frame.tile_columns = 2; + config.level_limits.max_tile_columns = 2 + delta; + run(config, frame, AV2_DM_VIOLATION_MAX_TILE_COLUMNS, delta < 0); + + config = MakeModelConfig(AV2_DM_RESOURCE_AVAILABILITY_MODE); + frame = MakeFrame(0, 1); + config.level_limits.max_tile_width = 64 + delta; + run(config, frame, AV2_DM_VIOLATION_MAX_TILE_WIDTH, delta < 0); + + config = MakeModelConfig(AV2_DM_RESOURCE_AVAILABILITY_MODE); + config.level_limits.max_tile_area = 4096 + delta; + run(config, frame, AV2_DM_VIOLATION_MAX_TILE_AREA, delta < 0); + } + + for (const uint32_t dimension : { 15u, 16u, 17u }) { + Av2DmConfig config = MakeModelConfig(AV2_DM_RESOURCE_AVAILABILITY_MODE); + Av2DmFrameEvent frame = MakeFrame(0, 1); + frame.frame_width = dimension; + run(config, frame, AV2_DM_VIOLATION_MIN_HORIZONTAL_SIZE, dimension < 16); + + frame = MakeFrame(0, 1); + frame.frame_height = dimension; + run(config, frame, AV2_DM_VIOLATION_MIN_VERTICAL_SIZE, dimension < 16); + } + + Av2DmConfig config = MakeModelConfig(AV2_DM_RESOURCE_AVAILABILITY_MODE); + Av2DmFrameEvent frame = MakeFrame(0, 1); + run(config, frame, AV2_DM_VIOLATION_MIN_TILE_WIDTH, false); + frame.non_rightmost_tile_width_valid = false; + run(config, frame, AV2_DM_VIOLATION_MIN_TILE_WIDTH, true); +} + +TEST(DecoderModelConformanceTest, FatalModeStopsAfterFirstViolation) { + Av2DmConfig config = MakeModelConfig(AV2_DM_RESOURCE_AVAILABILITY_MODE); + config.stop_after_first_violation = true; + config.level_limits.max_picture_size = 1; + config.level_limits.max_horizontal_size = 1; + ViolationCollector collector; + Av2DecoderModel *const model = + av2_decoder_model_create(&config, CollectViolation, &collector); + ASSERT_NE(model, nullptr); + + const Av2DmFrameEvent frame = MakeFrame(0, 1); + av2_decoder_model_start_frame(model, &frame); + av2_decoder_model_finish(model); + + ASSERT_EQ(collector.violations.size(), 1u); + EXPECT_EQ(collector.violations[0].code, AV2_DM_VIOLATION_MAX_PICTURE_SIZE); + Av2DmResult result; + ASSERT_TRUE(av2_decoder_model_get_result(model, &result)); + EXPECT_EQ(result.status, AV2_DM_RESULT_NON_CONFORMANT); + EXPECT_EQ(result.violations, 1u); + av2_decoder_model_destroy(model); +} + +TEST(DecoderModelConformanceTest, FatalModeStopsOnTerminalOnlyViolation) { + Av2DmConfig config = MakeModelConfig(AV2_DM_DECODING_SCHEDULE_MODE); + config.sequence_low_delay_mode = true; + config.defer_nonterminal_checks_for_testing = true; + config.stop_after_first_violation = true; + ASSERT_TRUE(av2_dm_rational_make(1000, 1, &config.level_limits.bit_rate)); + ASSERT_TRUE(av2_dm_rational_make(100, 1, &config.level_limits.buffer_size)); + ViolationCollector collector; + Av2DecoderModel *const model = + av2_decoder_model_create(&config, CollectViolation, &collector); + ASSERT_NE(model, nullptr); + + Av2DmFrameEvent frame = MakeFrame(0, 1); + frame.coded_bits = 150; + av2_decoder_model_start_frame(model, &frame); + EXPECT_TRUE(collector.violations.empty()); + + av2_decoder_model_finish(model); + ASSERT_EQ(collector.violations.size(), 1u); + EXPECT_EQ(collector.violations[0].code, + AV2_DM_VIOLATION_SMOOTHING_BUFFER_OVERFLOW); + Av2DmResult result; + ASSERT_TRUE(av2_decoder_model_get_result(model, &result)); + EXPECT_EQ(result.status, AV2_DM_RESULT_NON_CONFORMANT); + EXPECT_EQ(result.violations, 1u); + av2_decoder_model_destroy(model); +} + +TEST(DecoderModelConformanceTest, FrameParsingBoundariesAreExact) { + struct Boundary { + Av2DmViolationCode code; + uint64_t below; + uint64_t equal; + uint64_t above; + }; + const Boundary boundaries[] = { + { AV2_DM_VIOLATION_MAX_COMPRESSED_SIZE, 3839, 3840, 3841 }, + { AV2_DM_VIOLATION_MAX_FRAME_SYMBOLS, 28585, 28586, 28587 }, + }; + for (const Boundary &boundary : boundaries) { + for (const uint64_t observed : + { boundary.below, boundary.equal, boundary.above }) { + Av2DmConfig config = MakeModelConfig(AV2_DM_DECODING_SCHEDULE_MODE); + config.time_scale = 1000000; + config.num_units_in_decoding_tick = 1; + ViolationCollector collector; + Av2DecoderModel *model = + av2_decoder_model_create(&config, CollectViolation, &collector); + ASSERT_NE(model, nullptr); + Av2DmFrameEvent first = MakeFrame(0, 1); + if (boundary.code == AV2_DM_VIOLATION_MAX_COMPRESSED_SIZE) { + first.compressed_size_bytes = 128 + observed; + } else { + first.frame_symbol_count = observed; + } + av2_decoder_model_start_frame(model, &first); + Av2DmFrameEvent second = MakeFrame(1, 2, 4096); + av2_decoder_model_start_frame(model, &second); + EXPECT_EQ(CountViolations(collector, boundary.code), + observed == boundary.above ? 1u : 0u); + EXPECT_EQ(HasViolation(collector, AV2_DM_VIOLATION_MAX_COMPRESSED_SIZE), + boundary.code == AV2_DM_VIOLATION_MAX_COMPRESSED_SIZE && + observed == boundary.above); + EXPECT_EQ(HasViolation(collector, AV2_DM_VIOLATION_MAX_FRAME_SYMBOLS), + boundary.code == AV2_DM_VIOLATION_MAX_FRAME_SYMBOLS && + observed == boundary.above); + if (observed == boundary.above) { + const Av2DmViolation *const violation = + FindViolation(collector, boundary.code); + ASSERT_NE(violation, nullptr); + EXPECT_EQ(violation->event_index, 1u); + } + av2_decoder_model_destroy(model); + } + } +} + +TEST(DecoderModelConformanceTest, FrameTileRateBoundaryIsExact) { + for (const uint32_t num_tiles : { 1u, 2u, 3u }) { + Av2DmConfig config = MakeModelConfig(AV2_DM_DECODING_SCHEDULE_MODE); + config.time_scale = 180; + config.num_units_in_decoding_tick = 1; + config.level_limits.max_tiles = 3; + ViolationCollector collector; + Av2DecoderModel *model = + av2_decoder_model_create(&config, CollectViolation, &collector); + ASSERT_NE(model, nullptr); + Av2DmFrameEvent first = MakeFrame(0, 1); + first.num_tiles = num_tiles; + av2_decoder_model_start_frame(model, &first); + Av2DmFrameEvent second = MakeFrame(1, 2, 1); + av2_decoder_model_start_frame(model, &second); + EXPECT_EQ(CountViolations(collector, AV2_DM_VIOLATION_FRAME_TILE_RATE), + num_tiles > 2 ? 1u : 0u); + EXPECT_EQ(collector.violations.size(), num_tiles > 2 ? 1u : 0u); + if (num_tiles > 2) EXPECT_EQ(collector.violations[0].event_index, 1u); + av2_decoder_model_destroy(model); + } +} + +TEST(DecoderModelConformanceTest, FrameDecodeRateBoundaryIsExact) { + for (const uint32_t removal_ticks : { 4095u, 4096u, 4097u }) { + Av2DmConfig config = MakeModelConfig(AV2_DM_DECODING_SCHEDULE_MODE); + config.time_scale = 1000000; + config.num_units_in_decoding_tick = 1; + ViolationCollector collector; + Av2DecoderModel *model = + av2_decoder_model_create(&config, CollectViolation, &collector); + ASSERT_NE(model, nullptr); + Av2DmFrameEvent first = MakeFrame(0, 1); + av2_decoder_model_start_frame(model, &first); + Av2DmFrameEvent second = MakeFrame(1, 2, removal_ticks); + av2_decoder_model_start_frame(model, &second); + EXPECT_EQ(HasViolation(collector, AV2_DM_VIOLATION_FRAME_DECODE_RATE), + removal_ticks < 4096); + EXPECT_EQ(HasViolation(collector, AV2_DM_VIOLATION_MINIMUM_DECODE_TIME), + removal_ticks < 4096); + EXPECT_EQ(HasViolation(collector, + AV2_DM_VIOLATION_SCHEDULE_BEFORE_RESOURCE_REMOVAL), + removal_ticks < 4096); + // These three inequalities share the same exact decode-completion + // boundary for this vector, so crossing it necessarily changes all three. + EXPECT_EQ(collector.violations.size(), removal_ticks < 4096 ? 3u : 0u); + if (removal_ticks < 4096) { + for (const Av2DmViolation &violation : collector.violations) { + EXPECT_EQ(violation.event_index, 1u); + } + const Av2DmViolation *const rate = + FindViolation(collector, AV2_DM_VIOLATION_FRAME_DECODE_RATE); + ASSERT_NE(rate, nullptr); + ASSERT_EQ(rate->detail.kind, AV2_DM_VIOLATION_DETAIL_FRAME_INTERVAL); + ExpectEqualRational(rate->detail.value.frame_interval, removal_ticks, + 1000000); + EXPECT_EQ(rate->affected_kind, AV2_DM_VIOLATION_AFFECTED_DFG); + EXPECT_EQ(rate->affected_index, 0u); + } + av2_decoder_model_destroy(model); + } +} + +TEST(DecoderModelConformanceTest, MinimumPresentationIntervalIsExact) { + for (const int rate_delta : { 1, 0, -1 }) { + Av2DmConfig config = MakeModelConfig(AV2_DM_RESOURCE_AVAILABILITY_MODE); + config.time_scale = 1000; + config.num_units_in_display_tick = 1; + config.ticks_per_picture = 1; + config.initial_display_delay = 1; + config.level_limits.max_display_rate = 4096000 + rate_delta; + config.level_limits.max_decode_rate = 4096000; + ViolationCollector collector; + Av2DecoderModel *model = + av2_decoder_model_create(&config, CollectViolation, &collector); + ASSERT_NE(model, nullptr); + Av2DmFrameEvent first = MakeFrame(0, 1); + av2_decoder_model_start_frame(model, &first); + const Av2DmReferenceUpdateEvent refresh = Refresh(1, 1); + av2_decoder_model_update_reference_buffers(model, &refresh); + av2_decoder_model_set_initial_presentation_delay(model, 0); + Av2DmOutputEvent first_output = Output(0, 1, 0); + av2_decoder_model_output_frame(model, &first_output); + Av2DmFrameEvent second = MakeFrame(1, 2); + av2_decoder_model_start_frame(model, &second); + Av2DmOutputEvent second_output = Output(1, 2, -1); + av2_decoder_model_output_frame(model, &second_output); + EXPECT_EQ( + HasViolation(collector, AV2_DM_VIOLATION_MINIMUM_PRESENTATION_INTERVAL), + rate_delta < 0); + EXPECT_EQ(HasViolation(collector, AV2_DM_VIOLATION_MAX_DISPLAY_RATE), + rate_delta < 0); + EXPECT_EQ(collector.violations.size(), rate_delta < 0 ? 2u : 0u); + if (rate_delta < 0) { + for (const Av2DmViolation &violation : collector.violations) { + EXPECT_EQ(violation.event_index, 1u); + } + } + av2_decoder_model_destroy(model); + } +} + +TEST(DecoderModelConformanceTest, DisplayAndDecodeDeadlineBoundaryIsExact) { + for (const uint32_t ticks_per_picture : { 4095u, 4096u, 4097u }) { + Av2DmConfig config = MakeModelConfig(AV2_DM_RESOURCE_AVAILABILITY_MODE); + config.time_scale = 1000000; + config.ticks_per_picture = ticks_per_picture; + config.initial_display_delay = 1; + ViolationCollector collector; + Av2DecoderModel *model = + av2_decoder_model_create(&config, CollectViolation, &collector); + ASSERT_NE(model, nullptr); + Av2DmFrameEvent first = MakeFrame(0, 1); + av2_decoder_model_start_frame(model, &first); + const Av2DmReferenceUpdateEvent refresh = Refresh(1, 1); + av2_decoder_model_update_reference_buffers(model, &refresh); + av2_decoder_model_set_initial_presentation_delay(model, 0); + Av2DmOutputEvent first_output = Output(10, 1, -1); + first_output.temporal_unit_index = 0; + av2_decoder_model_output_frame(model, &first_output); + + Av2DmFrameEvent second = MakeFrame(1, 2); + av2_decoder_model_start_frame(model, &second); + Av2DmOutputEvent second_output = Output(11, 2, -1); + second_output.temporal_unit_index = 1; + av2_decoder_model_output_frame(model, &second_output); + + const bool late = ticks_per_picture < 4096; + EXPECT_EQ(CountViolations(collector, AV2_DM_VIOLATION_DISPLAY_FRAME_LATE), + late ? 1u : 0u); + EXPECT_EQ(CountViolations(collector, AV2_DM_VIOLATION_DECODE_DEADLINE), + late ? 1u : 0u); + // Output and decode completion coincide for this vector, so the two + // normative comparisons cross their common exact boundary together. + EXPECT_EQ(collector.violations.size(), late ? 2u : 0u); + av2_decoder_model_destroy(model); + } +} + +TEST(DecoderModelConformanceTest, + OutputDurationAndPresentationIntervalUseSeparateClocks) { + struct TimingCase { + uint32_t time_scale; + uint32_t output_duration_denominator; + bool display_rate_violation; + bool presentation_interval_violation; + }; + const TimingCase cases[] = { + { 10, 20, true, false }, + { 20, 10, false, true }, + }; + for (const TimingCase &timing : cases) { + Av2DmConfig config = MakeModelConfig(AV2_DM_RESOURCE_AVAILABILITY_MODE); + config.time_scale = timing.time_scale; + config.ticks_per_picture = 1; + config.initial_display_delay = 1; + config.level_limits.max_display_rate = 40960; + ViolationCollector collector; + Av2DecoderModel *model = + av2_decoder_model_create(&config, CollectViolation, &collector); + ASSERT_NE(model, nullptr); + + Av2DmFrameEvent first = MakeFrame(0, 1); + first.temporal_unit_output_time_present = true; + ASSERT_TRUE(av2_dm_rational_make(0, 1, &first.temporal_unit_output_time)); + av2_decoder_model_start_frame(model, &first); + const Av2DmReferenceUpdateEvent refresh = Refresh(1, 1); + av2_decoder_model_update_reference_buffers(model, &refresh); + av2_decoder_model_set_initial_presentation_delay(model, 0); + Av2DmOutputEvent first_output = Output(10, 1, -1); + first_output.temporal_unit_index = 0; + av2_decoder_model_output_frame(model, &first_output); + + Av2DmFrameEvent second = MakeFrame(1, 2); + second.temporal_unit_output_time_present = true; + ASSERT_TRUE(av2_dm_rational_make(1, timing.output_duration_denominator, + &second.temporal_unit_output_time)); + av2_decoder_model_start_frame(model, &second); + Av2DmOutputEvent second_output = Output(11, 2, -1); + second_output.temporal_unit_index = 1; + av2_decoder_model_output_frame(model, &second_output); + + EXPECT_EQ(HasViolation(collector, AV2_DM_VIOLATION_MAX_DISPLAY_RATE), + timing.display_rate_violation); + EXPECT_EQ( + HasViolation(collector, AV2_DM_VIOLATION_MINIMUM_PRESENTATION_INTERVAL), + timing.presentation_interval_violation); + av2_decoder_model_destroy(model); + } +} + +TEST(DecoderModelConformanceTest, + FinishReusesDurationOnlyForLastTuDisplayRate) { + Av2DmConfig config = MakeModelConfig(AV2_DM_RESOURCE_AVAILABILITY_MODE); + config.time_scale = 10; + config.ticks_per_picture = 1; + config.initial_display_delay = 1; + config.level_limits.max_display_rate = 40960; + ViolationCollector collector; + Av2DecoderModel *model = + av2_decoder_model_create(&config, CollectViolation, &collector); + ASSERT_NE(model, nullptr); + + Av2DmFrameEvent first = MakeFrame(0, 1); + first.temporal_unit_output_time_present = true; + ASSERT_TRUE(av2_dm_rational_make(0, 1, &first.temporal_unit_output_time)); + av2_decoder_model_start_frame(model, &first); + const Av2DmReferenceUpdateEvent first_refresh = Refresh(1, 1); + av2_decoder_model_update_reference_buffers(model, &first_refresh); + av2_decoder_model_set_initial_presentation_delay(model, 0); + Av2DmOutputEvent first_output = Output(10, 1, 0); + first_output.temporal_unit_index = 0; + av2_decoder_model_output_frame(model, &first_output); + + Av2DmFrameEvent second = MakeFrame(1, 2); + second.temporal_unit_output_time_present = true; + ASSERT_TRUE(av2_dm_rational_make(1, 5, &second.temporal_unit_output_time)); + av2_decoder_model_start_frame(model, &second); + const Av2DmReferenceUpdateEvent second_refresh = Refresh(2, 3); + av2_decoder_model_update_reference_buffers(model, &second_refresh); + Av2DmOutputEvent second_output = Output(11, 2, -1); + second_output.temporal_unit_index = 1; + av2_decoder_model_output_frame(model, &second_output); + Av2DmOutputEvent repeated_output = Output(12, 2, 1); + repeated_output.temporal_unit_index = 1; + repeated_output.ref_valid_mask = 3; + av2_decoder_model_output_frame(model, &repeated_output); + + EXPECT_FALSE( + HasViolation(collector, AV2_DM_VIOLATION_MINIMUM_PRESENTATION_INTERVAL)); + av2_decoder_model_finish(model); + EXPECT_FALSE( + HasViolation(collector, AV2_DM_VIOLATION_MINIMUM_PRESENTATION_INTERVAL)); + EXPECT_FALSE(HasViolation(collector, AV2_DM_VIOLATION_MAX_DISPLAY_RATE)); + av2_decoder_model_destroy(model); +} + +TEST(DecoderModelConformanceTest, + ReorderedFinalTuReusesLastDisplayDurationExactly) { + Av2DmConfig config = MakeModelConfig(AV2_DM_RESOURCE_AVAILABILITY_MODE); + config.initial_display_delay = 3; + config.level_limits.max_display_rate = 122880; + ViolationCollector collector; + Av2DecoderModel *model = + av2_decoder_model_create(&config, CollectViolation, &collector); + ASSERT_NE(model, nullptr); + + const uint64_t decode_tus[] = { 0, 5, 4 }; + for (uint32_t i = 0; i < 3; ++i) { + Av2DmFrameEvent frame = MakeFrame(i, i + 1); + frame.temporal_unit_index = decode_tus[i]; + av2_decoder_model_start_frame(model, &frame); + const Av2DmReferenceUpdateEvent refresh = + Refresh(1u << i, (1u << (i + 1)) - 1); + av2_decoder_model_update_reference_buffers(model, &refresh); + } + av2_decoder_model_set_initial_presentation_delay(model, 0); + + const uint64_t owner_tus[] = { 0, 4, 5 }; + const uint64_t generations[] = { 1, 3, 2 }; + const int map_indices[] = { 0, 2, 1 }; + for (uint32_t i = 0; i < 3; ++i) { + Av2DmOutputEvent output = Output(10 + i, generations[i], map_indices[i]); + output.temporal_unit_index = owner_tus[i]; + output.output_luma_samples = i == 2 ? 4097 : 4096; + av2_decoder_model_output_frame(model, &output); + } + EXPECT_FALSE(HasViolation(collector, AV2_DM_VIOLATION_MAX_DISPLAY_RATE)); + + av2_decoder_model_finish(model); + EXPECT_EQ(CountViolations(collector, AV2_DM_VIOLATION_MAX_DISPLAY_RATE), 1u); + EXPECT_EQ(collector.violations.size(), 1u); + av2_decoder_model_destroy(model); +} + +TEST(DecoderModelConformanceTest, DisplayRateBoundaryIsExact) { + for (const uint64_t max_display_rate : + { UINT64_C(40959), UINT64_C(40960), UINT64_C(40961) }) { + Av2DmConfig config = MakeModelConfig(AV2_DM_RESOURCE_AVAILABILITY_MODE); + config.time_scale = 1; + config.ticks_per_picture = 1; + config.initial_display_delay = 1; + config.level_limits.max_display_rate = max_display_rate; + ViolationCollector collector; + Av2DecoderModel *model = + av2_decoder_model_create(&config, CollectViolation, &collector); + ASSERT_NE(model, nullptr); + Av2DmFrameEvent first = MakeFrame(0, 1); + first.temporal_unit_output_time_present = true; + ASSERT_TRUE(av2_dm_rational_make(0, 1, &first.temporal_unit_output_time)); + av2_decoder_model_start_frame(model, &first); + const Av2DmReferenceUpdateEvent refresh = Refresh(1, 1); + av2_decoder_model_update_reference_buffers(model, &refresh); + av2_decoder_model_set_initial_presentation_delay(model, 0); + Av2DmOutputEvent first_output = Output(10, 1, -1); + first_output.temporal_unit_index = 0; + av2_decoder_model_output_frame(model, &first_output); + Av2DmFrameEvent second = MakeFrame(1, 2); + second.temporal_unit_output_time_present = true; + ASSERT_TRUE(av2_dm_rational_make(1, 10, &second.temporal_unit_output_time)); + av2_decoder_model_start_frame(model, &second); + Av2DmOutputEvent second_output = Output(11, 2, -1); + second_output.temporal_unit_index = 1; + av2_decoder_model_output_frame(model, &second_output); + EXPECT_EQ(CountViolations(collector, AV2_DM_VIOLATION_MAX_DISPLAY_RATE), + max_display_rate < 40960 ? 1u : 0u); + EXPECT_EQ(collector.violations.size(), max_display_rate < 40960 ? 1u : 0u); + if (max_display_rate < 40960) { + EXPECT_EQ(collector.violations[0].event_index, 11u); + } + av2_decoder_model_destroy(model); + } +} + +TEST(DecoderModelConformanceTest, HeaderRateUsesInclusiveOneSecondWindow) { + for (const uint32_t header_count : { 1u, 2u, 3u }) { + Av2DmConfig config = MakeModelConfig(AV2_DM_RESOURCE_AVAILABILITY_MODE); + config.level_limits.max_header_rate = 2; + ViolationCollector collector; + Av2DecoderModel *model = + av2_decoder_model_create(&config, CollectViolation, &collector); + ASSERT_NE(model, nullptr); + for (uint32_t i = 0; i < header_count; ++i) { + Av2DmFrameEvent frame = MakeFrame(i, i + 1); + frame.show_existing_frame = true; + frame.temporal_unit_output_time_present = true; + ASSERT_TRUE(av2_dm_rational_make(i, header_count == 1 ? 1 : 2, + &frame.temporal_unit_output_time)); + av2_decoder_model_start_frame(model, &frame); + EXPECT_EQ(HasViolation(collector, AV2_DM_VIOLATION_MAX_HEADER_RATE), + i == 2); + } + EXPECT_EQ(HasViolation(collector, AV2_DM_VIOLATION_MAX_HEADER_RATE), + header_count > 2); + if (header_count > 2) { + ASSERT_EQ(collector.violations.size(), 1u); + EXPECT_EQ(collector.violations[0].event_index, 2u); + } + const size_t online_violations = collector.violations.size(); + av2_decoder_model_finish(model); + EXPECT_EQ(HasViolation(collector, AV2_DM_VIOLATION_MAX_HEADER_RATE), + header_count > 2); + EXPECT_EQ(collector.violations.size(), online_violations); + av2_decoder_model_destroy(model); + } +} + +TEST(DecoderModelConformanceTest, + SameTuHeaderRateUsesLatestCountedHeaderEvent) { + Av2DmConfig config = MakeModelConfig(AV2_DM_RESOURCE_AVAILABILITY_MODE); + config.level_limits.max_header_rate = 2; + ViolationCollector collector; + Av2DecoderModel *model = + av2_decoder_model_create(&config, CollectViolation, &collector); + ASSERT_NE(model, nullptr); + + for (uint64_t event_index = 10; event_index <= 12; ++event_index) { + Av2DmFrameEvent frame = MakeFrame(event_index, event_index + 1); + frame.temporal_unit_index = 7; + frame.show_existing_frame = true; + frame.temporal_unit_output_time_present = true; + ASSERT_TRUE(av2_dm_rational_make(0, 1, &frame.temporal_unit_output_time)); + av2_decoder_model_start_frame(model, &frame); + EXPECT_EQ(CountViolations(collector, AV2_DM_VIOLATION_MAX_HEADER_RATE), + event_index == 12 ? 1u : 0u); + } + const Av2DmViolation *const violation = + FindViolation(collector, AV2_DM_VIOLATION_MAX_HEADER_RATE); + ASSERT_NE(violation, nullptr); + EXPECT_EQ(violation->event_index, 12u); + av2_decoder_model_destroy(model); +} + +TEST(DecoderModelConformanceTest, + GlobalMaximumTileRetainsEachAffectedHeaderWindow) { + Av2DmConfig config = MakeModelConfig(AV2_DM_RESOURCE_AVAILABILITY_MODE); + config.level_limits.max_header_rate = 100; + config.level_limits.max_tile_size_header_rate_product = 150; + ViolationCollector collector; + Av2DecoderModel *model = + av2_decoder_model_create(&config, CollectViolation, &collector); + ASSERT_NE(model, nullptr); + + Av2DmFrameEvent initial_tile = MakeFrame(1, 1); + initial_tile.count_frame_header = false; + initial_tile.max_tile_area = 50; + av2_decoder_model_start_frame(model, &initial_tile); + for (uint64_t i = 0; i < 3; ++i) { + Av2DmFrameEvent header = MakeFrame(10 + i, 10 + i); + header.temporal_unit_index = 10 + i; + header.show_existing_frame = true; + header.temporal_unit_output_time_present = true; + ASSERT_TRUE(av2_dm_rational_make(i, 2, &header.temporal_unit_output_time)); + av2_decoder_model_start_frame(model, &header); + } + EXPECT_FALSE(HasViolation(collector, AV2_DM_VIOLATION_TILE_SIZE_HEADER_RATE)); + + Av2DmFrameEvent larger_tile = MakeFrame(20, 20); + larger_tile.count_frame_header = false; + larger_tile.max_tile_area = 100; + av2_decoder_model_start_frame(model, &larger_tile); + ASSERT_EQ(CountViolations(collector, AV2_DM_VIOLATION_TILE_SIZE_HEADER_RATE), + 2u); + for (const Av2DmViolation &violation : collector.violations) { + EXPECT_EQ(violation.event_index, 20u); + } + + Av2DmFrameEvent recheck = MakeFrame(21, 21); + recheck.temporal_unit_index = 20; + recheck.show_existing_frame = true; + recheck.count_frame_header = false; + av2_decoder_model_start_frame(model, &recheck); + EXPECT_EQ(CountViolations(collector, AV2_DM_VIOLATION_TILE_SIZE_HEADER_RATE), + 2u); + av2_decoder_model_destroy(model); +} + +TEST(DecoderModelConformanceTest, + RetiredHeaderWindowsProduceOneConsolidatedTileWarning) { + Av2DmConfig config = MakeModelConfig(AV2_DM_DECODING_SCHEDULE_MODE); + config.initial_display_delay = 1; + config.ticks_per_picture = 9000; + config.level_limits.max_header_rate = 100; + config.level_limits.max_tile_size_header_rate_product = 200; + ViolationCollector collector; + Av2DecoderModel *model = + av2_decoder_model_create(&config, CollectViolation, &collector); + ASSERT_NE(model, nullptr); + + const uint64_t output_time_numerators[] = { 0, 1, 2, 6 }; + for (uint32_t i = 0; i < 4; ++i) { + Av2DmFrameEvent frame = MakeFrame(i, i + 1, i * 9000); + frame.max_tile_area = 50; + frame.temporal_unit_output_time_present = true; + ASSERT_TRUE(av2_dm_rational_make(output_time_numerators[i], 2, + &frame.temporal_unit_output_time)); + av2_decoder_model_start_frame(model, &frame); + const Av2DmReferenceUpdateEvent refresh = + Refresh(1u << i, (1u << (i + 1)) - 1); + av2_decoder_model_update_reference_buffers(model, &refresh); + av2_decoder_model_set_initial_presentation_delay(model, i); + Av2DmOutputEvent output = Output(10 + i, i + 1, -1); + output.temporal_unit_index = i; + av2_decoder_model_output_frame(model, &output); + } + EXPECT_FALSE(HasViolation(collector, AV2_DM_VIOLATION_TILE_SIZE_HEADER_RATE)); + + Av2DmFrameEvent larger_tile = MakeFrame(20, 20, 36000); + larger_tile.count_frame_header = false; + larger_tile.max_tile_area = 100; + av2_decoder_model_start_frame(model, &larger_tile); + ASSERT_EQ(CountViolations(collector, AV2_DM_VIOLATION_TILE_SIZE_HEADER_RATE), + 1u); + const Av2DmViolation *const violation = + FindViolation(collector, AV2_DM_VIOLATION_TILE_SIZE_HEADER_RATE); + ASSERT_NE(violation, nullptr); + EXPECT_EQ(violation->event_index, 20u); + + larger_tile.event_index = 21; + larger_tile.generation = 21; + larger_tile.max_tile_area = 101; + av2_decoder_model_start_frame(model, &larger_tile); + EXPECT_EQ(CountViolations(collector, AV2_DM_VIOLATION_TILE_SIZE_HEADER_RATE), + 1u); + av2_decoder_model_destroy(model); +} + +TEST(DecoderModelConformanceTest, + RetiredHeaderSummaryDoesNotRepeatProvenTileViolation) { + Av2DmConfig config = MakeModelConfig(AV2_DM_DECODING_SCHEDULE_MODE); + config.initial_display_delay = 1; + config.level_limits.max_header_rate = 100; + config.level_limits.max_tile_size_header_rate_product = 150; + ViolationCollector collector; + Av2DecoderModel *model = + av2_decoder_model_create(&config, CollectViolation, &collector); + ASSERT_NE(model, nullptr); + + const uint64_t output_time_numerators[] = { 0, 1, 2, 6 }; + for (uint32_t i = 0; i < 4; ++i) { + Av2DmFrameEvent frame = MakeFrame(i, (uint64_t)i + 1, i * 9000); + frame.count_frame_header = i != 3; + frame.max_tile_area = 100; + frame.temporal_unit_output_time_present = true; + ASSERT_TRUE(av2_dm_rational_make(output_time_numerators[i], 2, + &frame.temporal_unit_output_time)); + av2_decoder_model_start_frame(model, &frame); + const Av2DmReferenceUpdateEvent refresh = Refresh(1, 1); + av2_decoder_model_update_reference_buffers(model, &refresh); + av2_decoder_model_set_initial_presentation_delay(model, i); + Av2DmOutputEvent output = Output(100 + i, (uint64_t)i + 1, -1); + output.temporal_unit_index = i; + av2_decoder_model_output_frame(model, &output); + } + const size_t directly_reported = + CountViolations(collector, AV2_DM_VIOLATION_TILE_SIZE_HEADER_RATE); + ASSERT_GT(directly_reported, 0u); + + Av2DmFrameEvent larger_tile = MakeFrame(20, 20, 36000); + larger_tile.count_frame_header = false; + larger_tile.max_tile_area = 200; + av2_decoder_model_start_frame(model, &larger_tile); + EXPECT_EQ(CountViolations(collector, AV2_DM_VIOLATION_TILE_SIZE_HEADER_RATE), + directly_reported); + av2_decoder_model_destroy(model); +} + +TEST(DecoderModelConformanceTest, + ReorderedTuHeaderWindowsUseOutputTimeOrderExactly) { + for (const uint32_t maximum_headers : { 1u, 2u }) { + Av2DmConfig config = MakeModelConfig(AV2_DM_RESOURCE_AVAILABILITY_MODE); + config.time_scale = 5; + config.ticks_per_picture = 3; + config.initial_display_delay = 4; + config.level_limits.max_header_rate = maximum_headers; + config.level_limits.max_tile_size_header_rate_product = + 4096 * maximum_headers; + ViolationCollector collector; + Av2DecoderModel *model = + av2_decoder_model_create(&config, CollectViolation, &collector); + ASSERT_NE(model, nullptr); + + const uint64_t decode_tus[] = { 0, 5, 4, 6 }; + for (uint32_t i = 0; i < 4; ++i) { + Av2DmFrameEvent frame = MakeFrame(i, i + 1); + frame.temporal_unit_index = decode_tus[i]; + av2_decoder_model_start_frame(model, &frame); + const Av2DmReferenceUpdateEvent refresh = + Refresh(1u << i, (1u << (i + 1)) - 1); + av2_decoder_model_update_reference_buffers(model, &refresh); + } + av2_decoder_model_set_initial_presentation_delay(model, 0); + + const uint64_t owner_tus[] = { 0, 4, 5, 6 }; + const uint64_t generations[] = { 1, 3, 2, 4 }; + const int map_indices[] = { 0, 2, 1, 3 }; + for (uint32_t i = 0; i < 4; ++i) { + Av2DmOutputEvent output = Output(10 + i, generations[i], map_indices[i]); + output.temporal_unit_index = owner_tus[i]; + av2_decoder_model_output_frame(model, &output); + } + const size_t expected = maximum_headers == 1 ? 3u : 0u; + EXPECT_EQ(CountViolations(collector, AV2_DM_VIOLATION_MAX_HEADER_RATE), + expected); + EXPECT_EQ( + CountViolations(collector, AV2_DM_VIOLATION_TILE_SIZE_HEADER_RATE), + expected); + EXPECT_EQ(collector.violations.size(), 2 * expected); + av2_decoder_model_finish(model); + EXPECT_EQ(CountViolations(collector, AV2_DM_VIOLATION_MAX_HEADER_RATE), + expected); + EXPECT_EQ( + CountViolations(collector, AV2_DM_VIOLATION_TILE_SIZE_HEADER_RATE), + expected); + EXPECT_EQ(collector.violations.size(), 2 * expected); + av2_decoder_model_destroy(model); + } +} + +TEST(DecoderModelConformanceTest, TileHeaderRateUsesGlobalMaximumTile) { + for (const uint32_t header_count : { 1u, 2u, 3u }) { + Av2DmConfig config = MakeModelConfig(AV2_DM_RESOURCE_AVAILABILITY_MODE); + config.level_limits.max_header_rate = 100; + config.level_limits.max_tile_size_header_rate_product = 8192; + ViolationCollector collector; + Av2DecoderModel *model = + av2_decoder_model_create(&config, CollectViolation, &collector); + ASSERT_NE(model, nullptr); + Av2DmFrameEvent largest_tile = MakeFrame(100, 100); + largest_tile.temporal_unit_index = 0; + largest_tile.count_frame_header = false; + largest_tile.max_tile_area = 4096; + largest_tile.temporal_unit_output_time_present = true; + ASSERT_TRUE( + av2_dm_rational_make(0, 1, &largest_tile.temporal_unit_output_time)); + av2_decoder_model_start_frame(model, &largest_tile); + for (uint32_t i = 0; i < header_count; ++i) { + Av2DmFrameEvent frame = MakeFrame(i, i + 1); + frame.show_existing_frame = true; + frame.max_tile_area = 1; + frame.temporal_unit_output_time_present = true; + ASSERT_TRUE(av2_dm_rational_make(i, header_count == 1 ? 1 : 2, + &frame.temporal_unit_output_time)); + av2_decoder_model_start_frame(model, &frame); + } + av2_decoder_model_finish(model); + EXPECT_EQ(HasViolation(collector, AV2_DM_VIOLATION_TILE_SIZE_HEADER_RATE), + header_count > 2); + EXPECT_EQ( + CountViolations(collector, AV2_DM_VIOLATION_TILE_SIZE_HEADER_RATE), + header_count > 2 ? 1u : 0u); + av2_decoder_model_destroy(model); + } +} + +TEST(DecoderModelConformanceTest, ReferenceFrameBoundaryIsExact) { + for (const uint64_t max_picture_size : + { UINT64_C(4095), UINT64_C(4096), UINT64_C(4097) }) { + Av2DmConfig config = MakeModelConfig(AV2_DM_RESOURCE_AVAILABILITY_MODE); + config.level_limits.max_picture_size = max_picture_size; + ViolationCollector collector; + Av2DecoderModel *model = + av2_decoder_model_create(&config, CollectViolation, &collector); + ASSERT_NE(model, nullptr); + Av2DmFrameEvent frame = MakeFrame(0, 1); + frame.frame_width = 16; + frame.frame_height = 16; + av2_decoder_model_start_frame(model, &frame); + EXPECT_EQ(CountViolations(collector, AV2_DM_VIOLATION_MAX_REFERENCE_FRAMES), + max_picture_size < 4096 ? 1u : 0u); + if (max_picture_size < 4096) { + ASSERT_EQ(collector.violations.size(), 1u); + EXPECT_EQ(collector.violations[0].event_index, 0u); + } + const size_t online_violations = collector.violations.size(); + av2_decoder_model_finish(model); + EXPECT_EQ(CountViolations(collector, AV2_DM_VIOLATION_MAX_REFERENCE_FRAMES), + max_picture_size < 4096 ? 1u : 0u); + EXPECT_EQ(collector.violations.size(), online_violations); + EXPECT_EQ(HasViolation(collector, AV2_DM_VIOLATION_MAX_PICTURE_SIZE), + false); + av2_decoder_model_destroy(model); + } +} + +TEST(DecoderModelConformanceTest, DecodeCountCanReserveReferenceBuffer) { + Av2DmConfig config = MakeModelConfig(AV2_DM_RESOURCE_AVAILABILITY_MODE); + config.level_limits.max_picture_size = 4096; + ViolationCollector collector; + Av2DecoderModel *model = + av2_decoder_model_create(&config, CollectViolation, &collector); + ASSERT_NE(model, nullptr); + Av2DmFrameEvent frame = MakeFrame(0, 1); + frame.allow_global_intrabc = true; + frame.inloop_filtering_enabled = true; + frame.coded_as_closed_loop_key = false; + av2_decoder_model_start_frame(model, &frame); + EXPECT_TRUE(HasViolation(collector, AV2_DM_VIOLATION_MAX_REFERENCE_FRAMES)); + ASSERT_EQ(collector.violations.size(), 1u); + EXPECT_EQ(collector.violations[0].event_index, 0u); + const size_t online_violations = collector.violations.size(); + av2_decoder_model_finish(model); + EXPECT_TRUE(HasViolation(collector, AV2_DM_VIOLATION_MAX_REFERENCE_FRAMES)); + EXPECT_EQ(collector.violations.size(), online_violations); + av2_decoder_model_destroy(model); +} + +TEST(DecoderModelConformanceTest, + ReferenceLimitIsNotRepeatedWhenDecodeCountReservesBuffer) { + Av2DmConfig config = MakeModelConfig(AV2_DM_RESOURCE_AVAILABILITY_MODE); + config.level_limits.max_picture_size = 3584; + ViolationCollector collector; + Av2DecoderModel *model = + av2_decoder_model_create(&config, CollectViolation, &collector); + ASSERT_NE(model, nullptr); + + Av2DmFrameEvent first = MakeFrame(0, 1); + first.frame_width = 16; + first.frame_height = 16; + av2_decoder_model_start_frame(model, &first); + ASSERT_EQ(CountViolations(collector, AV2_DM_VIOLATION_MAX_REFERENCE_FRAMES), + 1u); + EXPECT_EQ(collector.violations[0].event_index, 0u); + + Av2DmFrameEvent second = MakeFrame(1, 2); + second.frame_width = 16; + second.frame_height = 16; + second.allow_global_intrabc = true; + second.inloop_filtering_enabled = true; + second.coded_as_closed_loop_key = false; + av2_decoder_model_start_frame(model, &second); + + EXPECT_EQ(CountViolations(collector, AV2_DM_VIOLATION_MAX_REFERENCE_FRAMES), + 1u); + av2_decoder_model_finish(model); + EXPECT_EQ(CountViolations(collector, AV2_DM_VIOLATION_MAX_REFERENCE_FRAMES), + 1u); + av2_decoder_model_destroy(model); +} + +TEST(DecoderModelConformanceTest, ScheduleDelayZeroAndTooLargeAreReported) { + Av2DmConfig config = MakeModelConfig(AV2_DM_DECODING_SCHEDULE_MODE); + config.sequence_decoder_buffer_delay = 0; + ASSERT_TRUE(av2_dm_rational_make(1, 100, &config.level_limits.buffer_size)); + ViolationCollector collector; + Av2DecoderModel *model = + av2_decoder_model_create(&config, CollectViolation, &collector); + ASSERT_NE(model, nullptr); + const Av2DmFrameEvent frame = MakeFrame(0, 1); + av2_decoder_model_start_frame(model, &frame); + EXPECT_TRUE( + HasViolation(collector, AV2_DM_VIOLATION_DECODER_BUFFER_DELAY_ZERO)); + av2_decoder_model_destroy(model); + + collector.violations.clear(); + config.sequence_decoder_buffer_delay = 9000; + model = av2_decoder_model_create(&config, CollectViolation, &collector); + ASSERT_NE(model, nullptr); + av2_decoder_model_start_frame(model, &frame); + EXPECT_TRUE( + HasViolation(collector, AV2_DM_VIOLATION_DECODER_BUFFER_DELAY_TOO_LARGE)); + av2_decoder_model_destroy(model); +} + +TEST(DecoderModelConformanceTest, DecoderBufferDelayMaximumIsExact) { + for (const uint32_t decoder_delay : { 8999u, 9000u, 9001u }) { + Av2DmConfig config = MakeModelConfig(AV2_DM_DECODING_SCHEDULE_MODE); + config.sequence_decoder_buffer_delay = decoder_delay; + config.sequence_encoder_buffer_delay = 9000; + ASSERT_TRUE(av2_dm_rational_make(90000, 1, &config.level_limits.bit_rate)); + ASSERT_TRUE( + av2_dm_rational_make(9000, 1, &config.level_limits.buffer_size)); + ViolationCollector collector; + Av2DecoderModel *model = + av2_decoder_model_create(&config, CollectViolation, &collector); + ASSERT_NE(model, nullptr); + Av2DmFrameEvent frame = MakeFrame(0, 1); + frame.coded_bits = 1000; + av2_decoder_model_start_frame(model, &frame); + EXPECT_EQ(CountViolations(collector, + AV2_DM_VIOLATION_DECODER_BUFFER_DELAY_TOO_LARGE), + decoder_delay > 9000 ? 1u : 0u); + EXPECT_EQ(collector.violations.size(), decoder_delay > 9000 ? 1u : 0u); + av2_decoder_model_destroy(model); + } +} + +TEST(DecoderModelConformanceTest, SmoothingOverflowIsCheckedExactly) { + Av2DmConfig config = MakeModelConfig(AV2_DM_DECODING_SCHEDULE_MODE); + config.sequence_low_delay_mode = true; + config.time_scale = 10; + config.num_units_in_decoding_tick = 1; + ASSERT_TRUE(av2_dm_rational_make(1000, 1, &config.level_limits.bit_rate)); + ASSERT_TRUE(av2_dm_rational_make(100, 1, &config.level_limits.buffer_size)); + ViolationCollector collector; + Av2DecoderModel *model = + av2_decoder_model_create(&config, CollectViolation, &collector); + ASSERT_NE(model, nullptr); + Av2DmFrameEvent frame = MakeFrame(0, 1); + frame.coded_bits = 150; + av2_decoder_model_start_frame(model, &frame); + EXPECT_TRUE( + HasViolation(collector, AV2_DM_VIOLATION_SMOOTHING_BUFFER_OVERFLOW)); + ASSERT_EQ(collector.violations.size(), 1u); + EXPECT_EQ(collector.violations[0].event_index, 0u); + const size_t online_violations = collector.violations.size(); + av2_decoder_model_finish(model); + EXPECT_TRUE( + HasViolation(collector, AV2_DM_VIOLATION_SMOOTHING_BUFFER_OVERFLOW)); + EXPECT_EQ(collector.violations.size(), online_violations); + av2_decoder_model_destroy(model); +} + +TEST(DecoderModelConformanceTest, + SmoothingOverflowRetainsEachAffectedDfgAtProvingEvent) { + Av2DmConfig config = MakeModelConfig(AV2_DM_DECODING_SCHEDULE_MODE); + ASSERT_TRUE(av2_dm_rational_make(1000000, 1, &config.level_limits.bit_rate)); + ASSERT_TRUE(av2_dm_rational_make(1500, 1, &config.level_limits.buffer_size)); + ViolationCollector collector; + Av2DecoderModel *model = + av2_decoder_model_create(&config, CollectViolation, &collector); + ASSERT_NE(model, nullptr); + + Av2DmFrameEvent first = MakeFrame(10, 1); + first.coded_bits = 1000; + av2_decoder_model_start_frame(model, &first); + EXPECT_FALSE( + HasViolation(collector, AV2_DM_VIOLATION_SMOOTHING_BUFFER_OVERFLOW)); + + Av2DmFrameEvent second = MakeFrame(11, 2, 9000); + second.coded_bits = 1000; + av2_decoder_model_start_frame(model, &second); + ASSERT_EQ( + CountViolations(collector, AV2_DM_VIOLATION_SMOOTHING_BUFFER_OVERFLOW), + 2u); + for (const Av2DmViolation &violation : collector.violations) { + if (violation.code == AV2_DM_VIOLATION_SMOOTHING_BUFFER_OVERFLOW) { + EXPECT_EQ(violation.event_index, 11u); + } + } + av2_decoder_model_finish(model); + EXPECT_EQ( + CountViolations(collector, AV2_DM_VIOLATION_SMOOTHING_BUFFER_OVERFLOW), + 2u); + av2_decoder_model_destroy(model); +} + +TEST(DecoderModelConformanceTest, + ParameterUpdatePartitionsSmoothingButRetainsAdjacentDfgTiming) { + Av2DmConfig config = MakeModelConfig(AV2_DM_DECODING_SCHEDULE_MODE); + config.sequence_low_delay_mode = true; + ASSERT_TRUE(av2_dm_rational_make(1000, 1, &config.level_limits.bit_rate)); + ASSERT_TRUE(av2_dm_rational_make(150, 1, &config.level_limits.buffer_size)); + ViolationCollector collector; + Av2DecoderModel *model = + av2_decoder_model_create(&config, CollectViolation, &collector); + ASSERT_NE(model, nullptr); + + Av2DmFrameEvent first = MakeFrame(0, 1); + first.coded_bits = 100; + av2_decoder_model_start_frame(model, &first); + const Av2DmReferenceUpdateEvent refresh = Refresh(1, 1); + av2_decoder_model_update_reference_buffers(model, &refresh); + + Av2DmConfig replacement = config; + ASSERT_TRUE( + av2_dm_rational_make(2000, 1, &replacement.level_limits.bit_rate)); + ASSERT_TRUE(av2_decoder_model_update_parameters(model, &replacement, 1)); + Av2DmFrameEvent updated = MakeFrame(1, 2); + updated.coded_bits = 100; + updated.random_access_point = true; + updated.decoder_model_parameters_updated = true; + av2_decoder_model_start_frame(model, &updated); + + EXPECT_FALSE( + HasViolation(collector, AV2_DM_VIOLATION_SMOOTHING_BUFFER_OVERFLOW)); + EXPECT_EQ(CountViolations(collector, + AV2_DM_VIOLATION_DECODER_BUFFER_DELAY_INCONSISTENT), + 1u); + Av2DmState state; + ASSERT_TRUE(av2_decoder_model_get_state(model, &state)); + ExpectEqualRational(state.first_bit_arrival, 0, 1); + ExpectEqualRational(state.last_bit_arrival, 1, 20); + ASSERT_NE(state.buffer_pool.vbi[0], -1); + EXPECT_EQ(state.buffer_pool.buffers[state.buffer_pool.vbi[0]].generation, 1u); + av2_decoder_model_destroy(model); +} + +TEST(DecoderModelConformanceTest, + ParameterUpdateToResourceModeUsesContinuousResourceLane) { + Av2DmConfig config = MakeModelConfig(AV2_DM_DECODING_SCHEDULE_MODE); + config.scope.whole_xlayer = false; + config.sequence_parameters_present = false; + config.operating_point_parameters_present = true; + config.operating_point_decoder_buffer_delay = 9000; + config.operating_point_encoder_buffer_delay = 9000; + config.sequence_low_delay_mode = true; + Av2DecoderModel *model = av2_decoder_model_create(&config, nullptr, nullptr); + ASSERT_NE(model, nullptr); + + Av2DmFrameEvent first = MakeFrame(0, 1); + av2_decoder_model_start_frame(model, &first); + const Av2DmReferenceUpdateEvent refresh = Refresh(1, 1); + av2_decoder_model_update_reference_buffers(model, &refresh); + + Av2DmConfig replacement = config; + replacement.mode = AV2_DM_RESOURCE_AVAILABILITY_MODE; + replacement.operating_point_parameters_present = false; + ASSERT_TRUE(av2_decoder_model_update_parameters(model, &replacement, 1)); + Av2DmFrameEvent updated = MakeFrame(1, 2); + updated.random_access_point = true; + updated.decoder_model_parameters_updated = true; + av2_decoder_model_start_frame(model, &updated); + + Av2DmState state; + ASSERT_TRUE(av2_decoder_model_get_state(model, &state)); + EXPECT_EQ(state.frame_number, 2u); + ASSERT_NE(state.buffer_pool.vbi[0], -1); + EXPECT_EQ(state.buffer_pool.buffers[state.buffer_pool.vbi[0]].generation, 1u); + Av2DmResult result; + ASSERT_TRUE(av2_decoder_model_get_result(model, &result)); + EXPECT_EQ(result.mode, AV2_DM_RESOURCE_AVAILABILITY_MODE); + EXPECT_FALSE(result.missing_required_input); + av2_decoder_model_destroy(model); +} + +TEST(DecoderModelConformanceTest, + ParameterUpdateKeepsAffectedDfgLimitsWithPreviousFrame) { + Av2DmConfig config = MakeModelConfig(AV2_DM_DECODING_SCHEDULE_MODE); + config.sequence_low_delay_mode = true; + ViolationCollector collector; + Av2DecoderModel *model = + av2_decoder_model_create(&config, CollectViolation, &collector); + ASSERT_NE(model, nullptr); + + Av2DmFrameEvent first = MakeFrame(0, 1); + av2_decoder_model_start_frame(model, &first); + Av2DmConfig replacement = config; + replacement.level_limits.max_decode_rate = 100000; + ASSERT_TRUE(av2_decoder_model_update_parameters(model, &replacement, 1)); + + Av2DmFrameEvent updated = MakeFrame(1, 2, 900); + updated.random_access_point = true; + updated.decoder_model_parameters_updated = true; + av2_decoder_model_start_frame(model, &updated); + EXPECT_EQ(CountViolations(collector, AV2_DM_VIOLATION_FRAME_DECODE_RATE), 0u); + + Av2DmFrameEvent next = MakeFrame(2, 3, 900); + av2_decoder_model_start_frame(model, &next); + EXPECT_EQ(CountViolations(collector, AV2_DM_VIOLATION_FRAME_DECODE_RATE), 1u); + av2_decoder_model_destroy(model); +} + +TEST(DecoderModelConformanceTest, + ParameterUpdateFromResourceToSchedulePreservesDpb) { + Av2DmConfig config = MakeModelConfig(AV2_DM_RESOURCE_AVAILABILITY_MODE); + Av2DecoderModel *model = av2_decoder_model_create(&config, nullptr, nullptr); + ASSERT_NE(model, nullptr); + + Av2DmFrameEvent first = MakeFrame(0, 1); + av2_decoder_model_start_frame(model, &first); + const Av2DmReferenceUpdateEvent refresh = Refresh(1, 1); + av2_decoder_model_update_reference_buffers(model, &refresh); + + Av2DmConfig replacement = config; + replacement.mode = AV2_DM_DECODING_SCHEDULE_MODE; + ASSERT_TRUE(av2_decoder_model_update_parameters(model, &replacement, 1)); + Av2DmFrameEvent updated = MakeFrame(1, 2, 9000); + updated.random_access_point = true; + updated.decoder_model_parameters_updated = true; + av2_decoder_model_start_frame(model, &updated); + + Av2DmState state; + ASSERT_TRUE(av2_decoder_model_get_state(model, &state)); + EXPECT_EQ(state.frame_number, 2u); + ASSERT_NE(state.buffer_pool.vbi[0], -1); + EXPECT_EQ(state.buffer_pool.buffers[state.buffer_pool.vbi[0]].generation, 1u); + Av2DmResult result; + ASSERT_TRUE(av2_decoder_model_get_result(model, &result)); + EXPECT_EQ(result.mode, AV2_DM_DECODING_SCHEDULE_MODE); + EXPECT_FALSE(result.missing_required_input); + av2_decoder_model_destroy(model); +} + +TEST(DecoderModelConformanceTest, ParameterUpdateRejectsImmutableClockChange) { + Av2DmConfig config = MakeModelConfig(AV2_DM_RESOURCE_AVAILABILITY_MODE); + Av2DecoderModel *model = av2_decoder_model_create(&config, nullptr, nullptr); + ASSERT_NE(model, nullptr); + Av2DmFrameEvent first = MakeFrame(0, 1); + av2_decoder_model_start_frame(model, &first); + + Av2DmConfig replacement = config; + replacement.time_scale += 1; + EXPECT_FALSE(av2_decoder_model_update_parameters(model, &replacement, 1)); + Av2DmResult result; + ASSERT_TRUE(av2_decoder_model_get_result(model, &result)); + EXPECT_EQ(result.status, AV2_DM_RESULT_INDETERMINATE); + EXPECT_TRUE(result.missing_required_input); + av2_decoder_model_destroy(model); +} + +TEST(DecoderModelConformanceTest, SmoothingBoundariesAreInclusive) { + for (const uint64_t coded_bits : + { UINT64_C(99), UINT64_C(100), UINT64_C(101) }) { + Av2DmConfig config = MakeModelConfig(AV2_DM_DECODING_SCHEDULE_MODE); + ASSERT_TRUE(av2_dm_rational_make(1000, 1, &config.level_limits.bit_rate)); + config.level_limits.buffer_size = config.level_limits.bit_rate; + ViolationCollector collector; + Av2DecoderModel *model = + av2_decoder_model_create(&config, CollectViolation, &collector); + ASSERT_NE(model, nullptr); + Av2DmFrameEvent frame = MakeFrame(0, 1); + frame.coded_bits = coded_bits; + av2_decoder_model_start_frame(model, &frame); + EXPECT_EQ( + HasViolation(collector, AV2_DM_VIOLATION_SMOOTHING_BUFFER_UNDERFLOW), + coded_bits > 100); + EXPECT_EQ(collector.violations.size(), coded_bits > 100 ? 1u : 0u); + av2_decoder_model_destroy(model); + } + for (const uint64_t buffer_bits : + { UINT64_C(151), UINT64_C(150), UINT64_C(149) }) { + Av2DmConfig config = MakeModelConfig(AV2_DM_DECODING_SCHEDULE_MODE); + config.sequence_low_delay_mode = true; + config.time_scale = 10; + config.num_units_in_decoding_tick = 1; + ASSERT_TRUE(av2_dm_rational_make(1000, 1, &config.level_limits.bit_rate)); + ASSERT_TRUE( + av2_dm_rational_make(buffer_bits, 1, &config.level_limits.buffer_size)); + ViolationCollector collector; + Av2DecoderModel *model = + av2_decoder_model_create(&config, CollectViolation, &collector); + ASSERT_NE(model, nullptr); + Av2DmFrameEvent frame = MakeFrame(0, 1); + frame.coded_bits = 150; + av2_decoder_model_start_frame(model, &frame); + EXPECT_EQ( + HasViolation(collector, AV2_DM_VIOLATION_SMOOTHING_BUFFER_OVERFLOW), + buffer_bits < 150); + const size_t online_violations = collector.violations.size(); + av2_decoder_model_finish(model); + EXPECT_EQ( + HasViolation(collector, AV2_DM_VIOLATION_SMOOTHING_BUFFER_OVERFLOW), + buffer_bits < 150); + EXPECT_EQ(collector.violations.size(), buffer_bits < 150 ? 1u : 0u); + EXPECT_EQ(collector.violations.size(), online_violations); + av2_decoder_model_destroy(model); + } +} + +TEST(DecoderModelConformanceTest, OnlineAndDeferredSmoothingResultsMatch) { + Av2DmConfig online_config = MakeModelConfig(AV2_DM_DECODING_SCHEDULE_MODE); + online_config.sequence_low_delay_mode = true; + online_config.time_scale = 10; + online_config.num_units_in_decoding_tick = 1; + ASSERT_TRUE( + av2_dm_rational_make(1000, 1, &online_config.level_limits.bit_rate)); + ASSERT_TRUE( + av2_dm_rational_make(100, 1, &online_config.level_limits.buffer_size)); + Av2DmConfig deferred_config = online_config; + deferred_config.defer_nonterminal_checks_for_testing = true; + ViolationCollector online_collector; + ViolationCollector deferred_collector; + Av2DecoderModel *online = av2_decoder_model_create( + &online_config, CollectViolation, &online_collector); + Av2DecoderModel *deferred = av2_decoder_model_create( + &deferred_config, CollectViolation, &deferred_collector); + ASSERT_NE(online, nullptr); + ASSERT_NE(deferred, nullptr); + + Av2DmFrameEvent frame = MakeFrame(7, 1); + frame.coded_bits = 150; + av2_decoder_model_start_frame(online, &frame); + av2_decoder_model_start_frame(deferred, &frame); + EXPECT_TRUE(HasViolation(online_collector, + AV2_DM_VIOLATION_SMOOTHING_BUFFER_OVERFLOW)); + EXPECT_TRUE(deferred_collector.violations.empty()); + av2_decoder_model_finish(online); + av2_decoder_model_finish(deferred); + ExpectSameViolationMultiset(online_collector, deferred_collector); + ExpectSameResult(online, deferred); + av2_decoder_model_destroy(online); + av2_decoder_model_destroy(deferred); +} + +TEST(DecoderModelConformanceTest, OnlineAndDeferredReferenceResultsMatch) { + Av2DmConfig online_config = + MakeModelConfig(AV2_DM_RESOURCE_AVAILABILITY_MODE); + online_config.level_limits.max_picture_size = 4095; + Av2DmConfig deferred_config = online_config; + deferred_config.defer_nonterminal_checks_for_testing = true; + ViolationCollector online_collector; + ViolationCollector deferred_collector; + Av2DecoderModel *online = av2_decoder_model_create( + &online_config, CollectViolation, &online_collector); + Av2DecoderModel *deferred = av2_decoder_model_create( + &deferred_config, CollectViolation, &deferred_collector); + ASSERT_NE(online, nullptr); + ASSERT_NE(deferred, nullptr); + + Av2DmFrameEvent frame = MakeFrame(7, 1); + frame.frame_width = 16; + frame.frame_height = 16; + av2_decoder_model_start_frame(online, &frame); + av2_decoder_model_start_frame(deferred, &frame); + EXPECT_TRUE( + HasViolation(online_collector, AV2_DM_VIOLATION_MAX_REFERENCE_FRAMES)); + EXPECT_TRUE(deferred_collector.violations.empty()); + av2_decoder_model_finish(online); + av2_decoder_model_finish(deferred); + ExpectSameViolationMultiset(online_collector, deferred_collector); + ExpectSameResult(online, deferred); + av2_decoder_model_destroy(online); + av2_decoder_model_destroy(deferred); +} + +TEST(DecoderModelConformanceTest, OnlineAndDeferredHeaderResultsMatch) { + Av2DmConfig online_config = + MakeModelConfig(AV2_DM_RESOURCE_AVAILABILITY_MODE); + online_config.level_limits.max_header_rate = 2; + online_config.level_limits.max_tile_size_header_rate_product = 8192; + Av2DmConfig deferred_config = online_config; + deferred_config.defer_nonterminal_checks_for_testing = true; + ViolationCollector online_collector; + ViolationCollector deferred_collector; + Av2DecoderModel *online = av2_decoder_model_create( + &online_config, CollectViolation, &online_collector); + Av2DecoderModel *deferred = av2_decoder_model_create( + &deferred_config, CollectViolation, &deferred_collector); + ASSERT_NE(online, nullptr); + ASSERT_NE(deferred, nullptr); + + Av2DmFrameEvent initial_tile = MakeFrame(1, 1); + initial_tile.count_frame_header = false; + initial_tile.temporal_unit_output_time_present = true; + ASSERT_TRUE( + av2_dm_rational_make(0, 1, &initial_tile.temporal_unit_output_time)); + av2_decoder_model_start_frame(online, &initial_tile); + av2_decoder_model_start_frame(deferred, &initial_tile); + + for (uint64_t event_index = 10; event_index <= 12; ++event_index) { + Av2DmFrameEvent frame = MakeFrame(event_index, event_index + 1); + frame.temporal_unit_index = 7; + frame.show_existing_frame = true; + frame.temporal_unit_output_time_present = true; + ASSERT_TRUE(av2_dm_rational_make(0, 1, &frame.temporal_unit_output_time)); + av2_decoder_model_start_frame(online, &frame); + av2_decoder_model_start_frame(deferred, &frame); + } + EXPECT_EQ(online_collector.violations.size(), 2u); + EXPECT_TRUE(deferred_collector.violations.empty()); + av2_decoder_model_finish(online); + av2_decoder_model_finish(deferred); + ExpectSameViolationMultiset(online_collector, deferred_collector); + ExpectSameResult(online, deferred); + av2_decoder_model_destroy(online); + av2_decoder_model_destroy(deferred); +} + +TEST(DecoderModelConformanceTest, RapDelayConsistencyUsesCeiling) { + for (const uint64_t coded_bits : + { UINT64_C(8999), UINT64_C(9000), UINT64_C(9001) }) { + Av2DmConfig config = MakeModelConfig(AV2_DM_DECODING_SCHEDULE_MODE); + config.sequence_low_delay_mode = true; + ASSERT_TRUE(av2_dm_rational_make(90000, 1, &config.level_limits.bit_rate)); + ASSERT_TRUE( + av2_dm_rational_make(90000, 1, &config.level_limits.buffer_size)); + ViolationCollector collector; + Av2DecoderModel *model = + av2_decoder_model_create(&config, CollectViolation, &collector); + ASSERT_NE(model, nullptr); + Av2DmFrameEvent first = MakeFrame(0, 1); + first.coded_bits = coded_bits; + av2_decoder_model_start_frame(model, &first); + Av2DmFrameEvent second = MakeFrame(1, 2, 9000); + second.random_access_point = true; + second.coded_as_closed_loop_key = true; + av2_decoder_model_start_frame(model, &second); + EXPECT_EQ(HasViolation(collector, + AV2_DM_VIOLATION_DECODER_BUFFER_DELAY_INCONSISTENT), + coded_bits > 9000); + EXPECT_EQ(collector.violations.size(), coded_bits > 9000 ? 1u : 0u); + if (coded_bits > 9000) { + const Av2DmViolation *const violation = FindViolation( + collector, AV2_DM_VIOLATION_DECODER_BUFFER_DELAY_INCONSISTENT); + ASSERT_NE(violation, nullptr); + ASSERT_EQ(violation->detail.kind, + AV2_DM_VIOLATION_DETAIL_DELAY_CONSISTENCY); + EXPECT_EQ( + violation->detail.value.delay_consistency.decoder_buffer_delay_ticks, + 9000u); + EXPECT_TRUE( + violation->detail.value.delay_consistency.ceil_time_delta_present); + ExpectEqualRational( + violation->detail.value.delay_consistency.ceil_time_delta_ticks, 8999, + 1); + } + av2_decoder_model_destroy(model); + } +} + +TEST(DecoderModelConformanceTest, MinimumDecodeTimeUsesExactBoundary) { + Av2DmConfig config = MakeModelConfig(AV2_DM_DECODING_SCHEDULE_MODE); + ViolationCollector collector; + Av2DecoderModel *model = + av2_decoder_model_create(&config, CollectViolation, &collector); + ASSERT_NE(model, nullptr); + Av2DmFrameEvent first = MakeFrame(0, 1); + av2_decoder_model_start_frame(model, &first); + Av2DmFrameEvent second = MakeFrame(1, 2, 1); + av2_decoder_model_start_frame(model, &second); + EXPECT_TRUE(HasViolation(collector, AV2_DM_VIOLATION_MINIMUM_DECODE_TIME)); + const Av2DmViolation *const violation = + FindViolation(collector, AV2_DM_VIOLATION_MINIMUM_DECODE_TIME); + ASSERT_NE(violation, nullptr); + ASSERT_EQ(violation->detail.kind, + AV2_DM_VIOLATION_DETAIL_MINIMUM_DECODE_TIME); + EXPECT_EQ(violation->event_index, 1u); + EXPECT_EQ(violation->affected_kind, AV2_DM_VIOLATION_AFFECTED_DFG); + EXPECT_EQ(violation->affected_index, 0u); + ExpectEqualRational( + violation->detail.value.minimum_decode_time.frame_decode_time, 64, 15625); + ExpectEqualRational( + violation->detail.value.minimum_decode_time.one_header_time, 1, 1000); + av2_decoder_model_destroy(model); +} + +TEST(DecoderModelConformanceTest, VariablePresentationMustNotDecrease) { + Av2DmConfig config = MakeModelConfig(AV2_DM_DECODING_SCHEDULE_MODE); + config.equal_picture_interval = false; + config.initial_display_delay = 1; + ViolationCollector collector; + Av2DecoderModel *model = + av2_decoder_model_create(&config, CollectViolation, &collector); + ASSERT_NE(model, nullptr); + Av2DmFrameEvent frame = MakeFrame(0, 1); + av2_decoder_model_start_frame(model, &frame); + const Av2DmReferenceUpdateEvent refresh = Refresh(1, 1); + av2_decoder_model_update_reference_buffers(model, &refresh); + av2_decoder_model_set_initial_presentation_delay(model, 0); + Av2DmOutputEvent first = Output(1, 1, 0); + first.presentation_time_present = true; + av2_decoder_model_output_frame(model, &first); + + Av2DmFrameEvent second_frame = MakeFrame(2, 2, 9000); + av2_decoder_model_start_frame(model, &second_frame); + Av2DmOutputEvent second = Output(3, 2, -1); + second.presentation_time_present = true; + second.presentation_time_ticks = 10; + av2_decoder_model_output_frame(model, &second); + + Av2DmFrameEvent next_rap = MakeFrame(4, 3, 18000); + next_rap.random_access_point = true; + next_rap.coded_as_closed_loop_key = true; + av2_decoder_model_start_frame(model, &next_rap); + Av2DmFrameEvent leading = MakeFrame(5, 4, 1000); + av2_decoder_model_start_frame(model, &leading); + Av2DmOutputEvent decreased = Output(6, 4, -1); + decreased.leading_frame = true; + decreased.presentation_time_present = true; + decreased.presentation_time_ticks = 9; + av2_decoder_model_output_frame(model, &decreased); + EXPECT_TRUE( + HasViolation(collector, AV2_DM_VIOLATION_PRESENTATION_TIME_DECREASE)); + av2_decoder_model_destroy(model); +} + +TEST(DecoderModelConformanceTest, + ShowExistingPresentationUsesCurrentRapAndTemporalPoint) { + Av2DmConfig config = MakeModelConfig(AV2_DM_DECODING_SCHEDULE_MODE); + config.equal_picture_interval = false; + config.initial_display_delay = 1; + ViolationCollector collector; + Av2DecoderModel *model = + av2_decoder_model_create(&config, CollectViolation, &collector); + ASSERT_NE(model, nullptr); + + Av2DmFrameEvent first_rap = MakeFrame(0, 1); + av2_decoder_model_start_frame(model, &first_rap); + const Av2DmReferenceUpdateEvent first_refresh = Refresh(1, 1); + av2_decoder_model_update_reference_buffers(model, &first_refresh); + av2_decoder_model_set_initial_presentation_delay(model, 0); + Av2DmOutputEvent first_output = Output(1, 1, -1); + first_output.presentation_uses_current_frame = true; + first_output.presentation_random_access_point = true; + first_output.presentation_time_present = true; + av2_decoder_model_output_frame(model, &first_output); + + Av2DmFrameEvent second_rap = MakeFrame(2, 2, 90000); + second_rap.random_access_point = true; + second_rap.coded_as_closed_loop_key = true; + av2_decoder_model_start_frame(model, &second_rap); + const Av2DmReferenceUpdateEvent second_refresh = Refresh(2, 3); + av2_decoder_model_update_reference_buffers(model, &second_refresh); + Av2DmOutputEvent second_output = Output(3, 2, -1); + second_output.presentation_uses_current_frame = true; + second_output.presentation_random_access_point = true; + second_output.presentation_time_present = true; + second_output.presentation_time_ticks = 90000; + av2_decoder_model_output_frame(model, &second_output); + + Av2DmFrameEvent show_existing = MakeFrame(4, 1, 180000); + show_existing.show_existing_frame = true; + show_existing.random_access_point = false; + av2_decoder_model_start_frame(model, &show_existing); + Av2DmOutputEvent repeated_output = Output(5, 1, 0); + repeated_output.presentation_uses_current_frame = true; + repeated_output.presentation_time_present = true; + repeated_output.presentation_time_ticks = 90000; + av2_decoder_model_output_frame(model, &repeated_output); + + EXPECT_FALSE( + HasViolation(collector, AV2_DM_VIOLATION_PRESENTATION_TIME_DECREASE)); + EXPECT_FALSE( + HasViolation(collector, AV2_DM_VIOLATION_MINIMUM_PRESENTATION_INTERVAL)); + av2_decoder_model_destroy(model); +} + +TEST(DecoderModelConformanceTest, PresentationNonDecreaseBoundaryIsExact) { + for (const uint32_t presentation_ticks : { 89999u, 90000u, 90001u }) { + Av2DmConfig config = MakeModelConfig(AV2_DM_DECODING_SCHEDULE_MODE); + config.equal_picture_interval = false; + config.initial_display_delay = 3; + ViolationCollector collector; + Av2DecoderModel *model = + av2_decoder_model_create(&config, CollectViolation, &collector); + ASSERT_NE(model, nullptr); + + for (uint32_t i = 0; i < 3; ++i) { + Av2DmFrameEvent frame = MakeFrame(i, i + 1, i * 9000); + av2_decoder_model_start_frame(model, &frame); + const Av2DmReferenceUpdateEvent refresh = + Refresh(1u << i, (1u << (i + 1)) - 1); + av2_decoder_model_update_reference_buffers(model, &refresh); + } + av2_decoder_model_set_initial_presentation_delay(model, 0); + + Av2DmOutputEvent first = Output(10, 1, 0); + first.temporal_unit_index = 0; + first.presentation_time_present = true; + av2_decoder_model_output_frame(model, &first); + Av2DmOutputEvent second = Output(11, 2, 1); + second.temporal_unit_index = 1; + second.presentation_time_present = true; + second.presentation_time_ticks = 90000; + av2_decoder_model_output_frame(model, &second); + Av2DmOutputEvent boundary = Output(12, 3, 2); + boundary.temporal_unit_index = 1; + boundary.presentation_time_present = true; + boundary.presentation_time_ticks = presentation_ticks; + av2_decoder_model_output_frame(model, &boundary); + + EXPECT_EQ( + CountViolations(collector, AV2_DM_VIOLATION_PRESENTATION_TIME_DECREASE), + presentation_ticks < 90000 ? 1u : 0u); + EXPECT_EQ(collector.violations.size(), + presentation_ticks < 90000 ? 1u : 0u); + av2_decoder_model_destroy(model); + } +} + +TEST(DecoderModelConformanceTest, MissingInputsAndMaximumLevelAreDistinct) { + Av2DmConfig config = MakeModelConfig(AV2_DM_DECODING_SCHEDULE_MODE); + config.sequence_parameters_present = false; + Av2DecoderModel *model = av2_decoder_model_create(&config, nullptr, nullptr); + ASSERT_NE(model, nullptr); + Av2DmResult result; + ASSERT_TRUE(av2_decoder_model_get_result(model, &result)); + EXPECT_EQ(result.status, AV2_DM_RESULT_INDETERMINATE); + av2_decoder_model_destroy(model); + + config = MakeModelConfig(AV2_DM_RESOURCE_AVAILABILITY_MODE); + config.level_idx = 31; + model = av2_decoder_model_create(&config, nullptr, nullptr); + ASSERT_NE(model, nullptr); + ASSERT_TRUE(av2_decoder_model_get_result(model, &result)); + EXPECT_EQ(result.status, AV2_DM_RESULT_NOT_APPLICABLE); + av2_decoder_model_finish(model); + ASSERT_TRUE(av2_decoder_model_get_result(model, &result)); + EXPECT_TRUE(result.finished); + EXPECT_EQ(result.status, AV2_DM_RESULT_NOT_APPLICABLE); + av2_decoder_model_destroy(model); +} + +} // namespace diff --git a/test/level_test.cc b/test/level_test.cc index 4a5d6c25be..fd44f7aa1f 100644 --- a/test/level_test.cc +++ b/test/level_test.cc @@ -14,6 +14,10 @@ #include "third_party/googletest/src/googletest/include/gtest/gtest.h" #include "av2/common/enums.h" +extern "C" { +#include "av2/common/level.h" +#include "av2/encoder/encoder.h" +} #include "test/codec_factory.h" #include "test/encode_test_driver.h" #include "test/i420_video_source.h" @@ -22,6 +26,25 @@ #include "test/yuv_video_source.h" namespace { +TEST(LevelDecoderModelTest, DisplayClockTickUsesDisplayTimebaseUnits) { + std::unique_ptr cpi(new AV2_COMP()); + cpi->common.seq_params.ref_frames = REF_FRAMES; + cpi->common.seq_params.seq_profile_idc = MAIN_420_10_IP0; + cpi->common.seq_params.subsampling_x = 1; + cpi->common.seq_params.subsampling_y = 1; + cpi->common.ci_params_encoder.ci_timing_info_present_flag = 1; + cpi->common.ci_params_encoder.timing_info.num_units_in_display_tick = 1001; + cpi->common.ci_params_encoder.timing_info.time_scale = 30000; + cpi->common.ci_params_encoder.timing_info.num_ticks_per_elemental_duration = + 7; + + DECODER_MODEL decoder_model; + av2_decoder_model_init(cpi.get(), SEQ_LEVEL_4_0, 0, &decoder_model); + + EXPECT_DOUBLE_EQ(1001.0 / 30000, decoder_model.display_clock_tick); + EXPECT_EQ(7, decoder_model.num_ticks_per_picture); +} + // Speed settings tested static const int kCpuUsedVectors[] = { 2, diff --git a/test/ops_test.cc b/test/ops_test.cc index 975fe307cd..5dc3daa30c 100644 --- a/test/ops_test.cc +++ b/test/ops_test.cc @@ -16,6 +16,7 @@ #include "av2/encoder/ops_syntax.h" #include "av2/decoder/decoder.h" +#include "av2/decoder/decoder_model.h" #include "av2/decoder/decodeframe.h" #include "avm_dsp/bitwriter_buffer.h" #include "avm_dsp/bitreader_buffer.h" @@ -63,6 +64,8 @@ class OpsTest : public ::testing::Test { }; TEST_F(OpsTest, LocalOpsRoundtrip) { + av2_decoder_model_verifier_init(pbi_); + ASSERT_NE(pbi_->decoder_model_verifier, nullptr); const int xlayer_id = 0; OperatingPointSet src; av2_set_ops_params(&src, xlayer_id, 0, 1); @@ -98,6 +101,40 @@ TEST_F(OpsTest, LocalOpsRoundtrip) { EXPECT_EQ(dop->ops_mlayer_count[xlayer_id], 2); EXPECT_EQ(dop->mlayer_info.ops_mlayer_map[xlayer_id], 0x3); EXPECT_EQ(dop->mlayer_info.OPMLayerCount[xlayer_id], 2); + + Av2DmVerifierStats stats; + ASSERT_TRUE(av2_decoder_model_verifier_get_stats(pbi_, &stats)); + EXPECT_EQ(stats.contexts, 1u); + av2_decoder_model_verifier_destroy(pbi_); +} + +TEST_F(OpsTest, DecoderModelLevelPairRoundtripsThroughOpsSyntax) { + const int xlayer_id = 0; + const int levels[] = { SEQ_LEVEL_3_0, SEQ_LEVEL_2_1 }; + + for (int ops_id = 0; ops_id < 2; ++ops_id) { + OperatingPointSet src; + av2_set_ops_params(&src, xlayer_id, ops_id, 1); + src.ops_ptl_present_flag = 1; + OperatingPoint *const op = &src.op[0]; + op->ops_seq_profile_idc[xlayer_id] = MAIN_420_10_IP0; + op->ops_level_idx[xlayer_id] = levels[ops_id]; + op->ops_mlayer_count[xlayer_id] = 1; + op->mlayer_info.ops_mlayer_map[xlayer_id] = 1; + op->mlayer_info.ops_tlayer_map[xlayer_id][0] = 1; + + memset(buf_, 0, sizeof(buf_)); + const uint32_t written = write_ops_obu(&src, xlayer_id, buf_); + ASSERT_GT(written, 0u); + struct avm_read_bit_buffer rb = { buf_, buf_ + written, 0, nullptr, + rb_error_handler }; + ASSERT_EQ(av2_read_operating_point_set_obu(pbi_, xlayer_id, &rb), written); + + const OperatingPointSet *const parsed = &pbi_->ops_list[xlayer_id][ops_id]; + ASSERT_TRUE(parsed->valid); + ASSERT_EQ(parsed->ops_cnt, 1); + EXPECT_EQ(parsed->op[0].ops_level_idx[xlayer_id], levels[ops_id]); + } } TEST_F(OpsTest, LocalOpsDefaultParams) { @@ -119,6 +156,7 @@ TEST_F(OpsTest, LocalOpsDefaultParams) { EXPECT_EQ(dst->ops_cnt, 1); EXPECT_EQ(dst->ops_ptl_present_flag, 0); EXPECT_EQ(dst->ops_color_info_present_flag, 0); + EXPECT_FALSE(dst->op[0].ops_initial_display_delay_present_flag); EXPECT_EQ(dst->op[0].ops_initial_display_delay, BUFFER_POOL_MAX_SIZE); } @@ -165,6 +203,7 @@ TEST_F(OpsTest, LocalOpsDisplayDelay) { ASSERT_EQ(read, written); const OperatingPointSet *dst = &pbi_->ops_list[xlayer_id][0]; + EXPECT_TRUE(dst->op[0].ops_initial_display_delay_present_flag); EXPECT_EQ(dst->op[0].ops_initial_display_delay, 4); } diff --git a/test/test.cmake b/test/test.cmake index 3f68ac459f..0cc66f6790 100644 --- a/test/test.cmake +++ b/test/test.cmake @@ -36,6 +36,7 @@ list( "${AVM_ROOT}/test/codec_factory.h" "${AVM_ROOT}/test/decode_test_driver.cc" "${AVM_ROOT}/test/decode_test_driver.h" + "${AVM_ROOT}/test/decoder_model_test.cc" "${AVM_ROOT}/test/function_equivalence_test.h" "${AVM_ROOT}/test/log2_test.cc" "${AVM_ROOT}/test/md5_helper.h" @@ -50,6 +51,8 @@ list( list( APPEND AVM_UNIT_TEST_DECODER_SOURCES + "${AVM_ROOT}/test/decoder_model_integration_test.cc" + "${AVM_ROOT}/test/decoder_model_parser_test.cc" "${AVM_ROOT}/test/decode_api_test.cc" "${AVM_ROOT}/test/external_frame_buffer_test.cc" "${AVM_ROOT}/test/invalid_file_test.cc" From 443e639fb8915d9477b4fac552d29e446794e9e1 Mon Sep 17 00:00:00 2001 From: Andrey Norkin Date: Thu, 6 Aug 2026 15:05:39 -0700 Subject: [PATCH 3/5] Fix decoder model CI portability --- av2/common/av2_common_int.h | 1 + av2/common/decoder_model.c | 42 +++++++++++++++++++++++-------- av2/decoder/decoder_model.c | 2 +- avm/avmdx.h | 3 ++- test/decoder_model_parser_test.cc | 2 ++ test/decoder_model_test.cc | 4 ++- 6 files changed, 40 insertions(+), 14 deletions(-) diff --git a/av2/common/av2_common_int.h b/av2/common/av2_common_int.h index c327d18994..4ab6ba03b5 100644 --- a/av2/common/av2_common_int.h +++ b/av2/common/av2_common_int.h @@ -2984,6 +2984,7 @@ typedef struct AV2Common { * Temporal point info */ avm_metadata_temporal_point_info_t temporal_point_info_metadata; + /*! Whether temporal point information is present. */ bool temporal_point_info_present; /*! diff --git a/av2/common/decoder_model.c b/av2/common/decoder_model.c index 7c7578ef87..78c3ebc39d 100644 --- a/av2/common/decoder_model.c +++ b/av2/common/decoder_model.c @@ -59,8 +59,21 @@ static int wide_compare(Av2DmUnsignedWide left, Av2DmUnsignedWide right) { return 0; } -static bool wide_add(Av2DmUnsignedWide left, Av2DmUnsignedWide right, - Av2DmUnsignedWide *result) { +#if defined(__clang__) && defined(__has_attribute) +#if __has_attribute(no_sanitize) +#define AV2_DM_NO_UNSIGNED_OVERFLOW_CHECK \ + __attribute__(( \ + no_sanitize("unsigned-integer-overflow", "unsigned-shift-base"))) +#endif +#endif + +#ifndef AV2_DM_NO_UNSIGNED_OVERFLOW_CHECK +#define AV2_DM_NO_UNSIGNED_OVERFLOW_CHECK +#endif + +AV2_DM_NO_UNSIGNED_OVERFLOW_CHECK static bool wide_add( + Av2DmUnsignedWide left, Av2DmUnsignedWide right, + Av2DmUnsignedWide *result) { uint64_t carry = 0; for (uint32_t i = 0; i < AV2_DM_WIDE_LIMBS; ++i) { const uint64_t partial = left.limbs[i] + right.limbs[i]; @@ -75,8 +88,8 @@ static bool wide_add(Av2DmUnsignedWide left, Av2DmUnsignedWide right, // Subtraction is modulo 2^256. Callers either establish left >= right or use // the wraparound result as one step of long division with a 257th carry bit. -static Av2DmUnsignedWide wide_subtract(Av2DmUnsignedWide left, - Av2DmUnsignedWide right) { +AV2_DM_NO_UNSIGNED_OVERFLOW_CHECK static Av2DmUnsignedWide wide_subtract( + Av2DmUnsignedWide left, Av2DmUnsignedWide right) { Av2DmUnsignedWide result; uint64_t borrow = 0; for (uint32_t i = 0; i < AV2_DM_WIDE_LIMBS; ++i) { @@ -97,7 +110,8 @@ static void wide_set_bit(Av2DmUnsignedWide *value, uint32_t bit_index) { value->limbs[bit_index / 64] |= UINT64_C(1) << (bit_index % 64); } -static bool wide_shift_left_one(Av2DmUnsignedWide *value) { +AV2_DM_NO_UNSIGNED_OVERFLOW_CHECK static bool wide_shift_left_one( + Av2DmUnsignedWide *value) { const bool overflow = (value->limbs[AV2_DM_WIDE_LIMBS - 1] >> 63) != 0; for (int i = AV2_DM_WIDE_LIMBS - 1; i > 0; --i) { value->limbs[i] = (value->limbs[i] << 1) | (value->limbs[i - 1] >> 63); @@ -112,8 +126,10 @@ static bool wide_divide(Av2DmUnsignedWide dividend, Av2DmUnsignedWide divisor, Av2DmUnsignedWide *remainder) { if (wide_is_zero(divisor)) return false; if (wide_fits_u64(dividend) && wide_fits_u64(divisor)) { - *quotient = wide_from_u64(dividend.limbs[0] / divisor.limbs[0]); - *remainder = wide_from_u64(dividend.limbs[0] % divisor.limbs[0]); + const uint64_t divisor_low = divisor.limbs[0]; + if (divisor_low == 0) return false; + *quotient = wide_from_u64(dividend.limbs[0] / divisor_low); + *remainder = wide_from_u64(dividend.limbs[0] % divisor_low); return true; } Av2DmUnsignedWide result = { { 0, 0, 0, 0 } }; @@ -158,8 +174,9 @@ static Av2DmUnsignedWide wide_gcd(Av2DmUnsignedWide left, // Computes the complete 64-by-64-bit product using only fixed-width portable // C arithmetic. Unsigned wraparound in the 32-bit partial-product assembly is // intentional and defined by the C language. -static void multiply_64(uint64_t left, uint64_t right, uint64_t *product_low, - uint64_t *product_high) { +AV2_DM_NO_UNSIGNED_OVERFLOW_CHECK static void multiply_64( + uint64_t left, uint64_t right, uint64_t *product_low, + uint64_t *product_high) { const uint64_t mask = UINT32_MAX; const uint64_t left_low = left & mask; const uint64_t left_high = left >> 32; @@ -172,8 +189,9 @@ static void multiply_64(uint64_t left, uint64_t right, uint64_t *product_low, *product_low = (middle_2 << 32) | (low & mask); } -static bool product_add_at(Av2DmUnsignedProduct *product, uint32_t index, - uint64_t low, uint64_t high) { +AV2_DM_NO_UNSIGNED_OVERFLOW_CHECK static bool product_add_at( + Av2DmUnsignedProduct *product, uint32_t index, uint64_t low, + uint64_t high) { if (index >= AV2_DM_PRODUCT_LIMBS) return low == 0 && high == 0; const uint64_t old_low = product->limbs[index]; product->limbs[index] += low; @@ -197,6 +215,8 @@ static bool product_add_at(Av2DmUnsignedProduct *product, uint32_t index, return carry == 0; } +#undef AV2_DM_NO_UNSIGNED_OVERFLOW_CHECK + static bool wide_multiply(Av2DmUnsignedWide left, Av2DmUnsignedWide right, Av2DmUnsignedProduct *product) { memset(product, 0, sizeof(*product)); diff --git a/av2/decoder/decoder_model.c b/av2/decoder/decoder_model.c index 4f82ed630a..9d4a5a967f 100644 --- a/av2/decoder/decoder_model.c +++ b/av2/decoder/decoder_model.c @@ -2020,7 +2020,7 @@ void av2_decoder_model_verifier_on_output(AV2Decoder *pbi, const int owner_tlayer = current_owner != NULL ? current_owner->temporal_id : implicit_owner != NULL ? implicit_owner->temporal_id - : frame->tlayer_id; + : (int)frame->tlayer_id; const bool owner_leading = current_owner != NULL ? current_owner->leading_frame diff --git a/avm/avmdx.h b/avm/avmdx.h index 0f263ffbaa..fa04a6e7a7 100644 --- a/avm/avmdx.h +++ b/avm/avmdx.h @@ -180,7 +180,8 @@ typedef enum avm_decoder_model_check_mode { AVM_DECODER_MODEL_CHECK_OFF = 0, AVM_DECODER_MODEL_CHECK_FATAL, AVM_DECODER_MODEL_CHECK_WARN, -} avm_decoder_model_check_mode_t; +} avm_decoder_model_check_mode_t; /**< alias for enum + avm_decoder_model_check_mode */ /*!\enum avm_dec_control_id * \brief AVM decoder control functions diff --git a/test/decoder_model_parser_test.cc b/test/decoder_model_parser_test.cc index 87f815c69e..c08508e00a 100644 --- a/test/decoder_model_parser_test.cc +++ b/test/decoder_model_parser_test.cc @@ -703,6 +703,7 @@ TEST(DecoderModelAnnexFTest, GlobalOperatingPointIsScopedPerXlayer) { av2_sbe_should_retain_obu(&scope, OBU_REGULAR_TILE_GROUP, 1, 0, 0)); } +#if CONFIG_AV2_ENCODER static bool DecodeCountedSymbols() { uint8_t buffer[64] = { 0 }; avm_cdf_prob write_cdf[3] = { AVM_CDF2(16384) }; @@ -758,5 +759,6 @@ TEST(DecoderModelSymbolCountTest, IndependentReadersAreThreadLocal) { for (std::thread &thread : threads) thread.join(); for (int result : results) EXPECT_EQ(result, 1); } +#endif // CONFIG_AV2_ENCODER } // namespace diff --git a/test/decoder_model_test.cc b/test/decoder_model_test.cc index 501d24d46c..9dd53ef310 100644 --- a/test/decoder_model_test.cc +++ b/test/decoder_model_test.cc @@ -1519,7 +1519,9 @@ TEST(DecoderModelConformanceTest, FrameTileRateBoundaryIsExact) { EXPECT_EQ(CountViolations(collector, AV2_DM_VIOLATION_FRAME_TILE_RATE), num_tiles > 2 ? 1u : 0u); EXPECT_EQ(collector.violations.size(), num_tiles > 2 ? 1u : 0u); - if (num_tiles > 2) EXPECT_EQ(collector.violations[0].event_index, 1u); + if (num_tiles > 2) { + EXPECT_EQ(collector.violations[0].event_index, 1u); + } av2_decoder_model_destroy(model); } } From 53b2cdc9b59a04a5365d6b8d1631ff641b17bbc4 Mon Sep 17 00:00:00 2001 From: Andrey Norkin Date: Thu, 6 Aug 2026 17:16:53 -0700 Subject: [PATCH 4/5] Fix decoder model shared test builds --- test/decoder_model_parser_test.cc | 6 +++--- test/level_test.cc | 2 ++ test/test.cmake | 12 +++++++++--- 3 files changed, 14 insertions(+), 6 deletions(-) diff --git a/test/decoder_model_parser_test.cc b/test/decoder_model_parser_test.cc index c08508e00a..9eefcd27fc 100644 --- a/test/decoder_model_parser_test.cc +++ b/test/decoder_model_parser_test.cc @@ -706,7 +706,7 @@ TEST(DecoderModelAnnexFTest, GlobalOperatingPointIsScopedPerXlayer) { #if CONFIG_AV2_ENCODER static bool DecodeCountedSymbols() { uint8_t buffer[64] = { 0 }; - avm_cdf_prob write_cdf[3] = { AVM_CDF2(16384) }; + avm_cdf_prob write_cdf[CDF_SIZE(2)] = { AVM_CDF2(16384) }; avm_writer writer; memset(&writer, 0, sizeof(writer)); avm_start_encode(&writer, buffer); @@ -714,7 +714,7 @@ static bool DecodeCountedSymbols() { avm_write_symbol(&writer, 1, write_cdf, 2); avm_stop_encode(&writer); - avm_cdf_prob read_cdf[3] = { AVM_CDF2(16384) }; + avm_cdf_prob read_cdf[CDF_SIZE(2)] = { AVM_CDF2(16384) }; avm_reader reader; if (avm_reader_init(&reader, buffer, writer.pos) != 0) return false; reader.allow_update_cdf = 0; @@ -729,7 +729,7 @@ TEST(DecoderModelSymbolCountTest, MirrorsEncoderLiteralAndSymbolCount) { TEST(DecoderModelSymbolCountTest, DirectCdfAndBitAreNotFrameSymbols) { uint8_t buffer[64] = { 0 }; - avm_cdf_prob cdf[3] = { AVM_CDF2(16384) }; + avm_cdf_prob cdf[CDF_SIZE(2)] = { AVM_CDF2(16384) }; avm_writer writer; memset(&writer, 0, sizeof(writer)); avm_start_encode(&writer, buffer); diff --git a/test/level_test.cc b/test/level_test.cc index fd44f7aa1f..089979be4e 100644 --- a/test/level_test.cc +++ b/test/level_test.cc @@ -26,6 +26,7 @@ extern "C" { #include "test/yuv_video_source.h" namespace { +#if !CONFIG_SHARED TEST(LevelDecoderModelTest, DisplayClockTickUsesDisplayTimebaseUnits) { std::unique_ptr cpi(new AV2_COMP()); cpi->common.seq_params.ref_frames = REF_FRAMES; @@ -44,6 +45,7 @@ TEST(LevelDecoderModelTest, DisplayClockTickUsesDisplayTimebaseUnits) { EXPECT_DOUBLE_EQ(1001.0 / 30000, decoder_model.display_clock_tick); EXPECT_EQ(7, decoder_model.num_ticks_per_picture); } +#endif // !CONFIG_SHARED // Speed settings tested static const int kCpuUsedVectors[] = { diff --git a/test/test.cmake b/test/test.cmake index 0cc66f6790..dc099e0714 100644 --- a/test/test.cmake +++ b/test/test.cmake @@ -36,7 +36,6 @@ list( "${AVM_ROOT}/test/codec_factory.h" "${AVM_ROOT}/test/decode_test_driver.cc" "${AVM_ROOT}/test/decode_test_driver.h" - "${AVM_ROOT}/test/decoder_model_test.cc" "${AVM_ROOT}/test/function_equivalence_test.h" "${AVM_ROOT}/test/log2_test.cc" "${AVM_ROOT}/test/md5_helper.h" @@ -51,8 +50,6 @@ list( list( APPEND AVM_UNIT_TEST_DECODER_SOURCES - "${AVM_ROOT}/test/decoder_model_integration_test.cc" - "${AVM_ROOT}/test/decoder_model_parser_test.cc" "${AVM_ROOT}/test/decode_api_test.cc" "${AVM_ROOT}/test/external_frame_buffer_test.cc" "${AVM_ROOT}/test/invalid_file_test.cc" @@ -86,6 +83,15 @@ list(APPEND AVM_TEST_INTRA_PRED_SPEED_SOURCES "${AVM_GEN_SRC_DIR}/usage_exit.c" "${AVM_ROOT}/test/test_intra_pred_speed.cc") if(NOT BUILD_SHARED_LIBS) + list(APPEND AVM_UNIT_TEST_COMMON_SOURCES + "${AVM_ROOT}/test/decoder_model_test.cc") + + list( + APPEND + AVM_UNIT_TEST_DECODER_SOURCES + "${AVM_ROOT}/test/decoder_model_integration_test.cc" + "${AVM_ROOT}/test/decoder_model_parser_test.cc") + list( APPEND AVM_UNIT_TEST_COMMON_SOURCES From 8d451e60c3242db9c1fed999655e4ece50961e2d Mon Sep 17 00:00:00 2001 From: Andrey Norkin Date: Sun, 9 Aug 2026 18:25:39 -0700 Subject: [PATCH 5/5] Fix decoder model CMake formatting --- test/test.cmake | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/test/test.cmake b/test/test.cmake index dc099e0714..ca0a507788 100644 --- a/test/test.cmake +++ b/test/test.cmake @@ -86,11 +86,9 @@ if(NOT BUILD_SHARED_LIBS) list(APPEND AVM_UNIT_TEST_COMMON_SOURCES "${AVM_ROOT}/test/decoder_model_test.cc") - list( - APPEND - AVM_UNIT_TEST_DECODER_SOURCES - "${AVM_ROOT}/test/decoder_model_integration_test.cc" - "${AVM_ROOT}/test/decoder_model_parser_test.cc") + list(APPEND AVM_UNIT_TEST_DECODER_SOURCES + "${AVM_ROOT}/test/decoder_model_integration_test.cc" + "${AVM_ROOT}/test/decoder_model_parser_test.cc") list( APPEND