Introduce `use_fixed_qp_offsets = 2` In this mode, the encoder doesn't apply any QP offsets to frames at different levels of the pyramid. Instead, the frame qp is directly derived from `rc_cfg.cq_level`. This is useful when consumers of libaom desire full control of each frame's QP, by adjusting `rc_cfg.cq_level` between each encoded frame. This behavior is modeled after SVT-AV1's `use-fixed-qindex-offsets = 1`, specifically the behavior where the encoder will assign the same QP to every frame (referred to as "disabling QP scaling" in SVT-AV1's code), if the user doesn't pass in an array of QP offsets explicitly. Change-Id: Iff3eccee99907d68707a22ada9ec667d2e35883a
diff --git a/aom/aom_encoder.h b/aom/aom_encoder.h index 6fb50eb..9d412af 100644 --- a/aom/aom_encoder.h +++ b/aom/aom_encoder.h
@@ -890,11 +890,14 @@ */ int tile_heights[MAX_TILE_HEIGHTS]; - /*!\brief Whether encoder should use fixed QP offsets. + /*!\brief Controls how the encoder applies fixed QP offsets * + * If a value of 0 is provided, encoder will adaptively choose QP offsets for + * frames at different levels of the pyramid. * If a value of 1 is provided, encoder will use fixed QP offsets for frames * at different levels of the pyramid. - * If a value of 0 is provided, encoder will NOT use fixed QP offsets. + * If a value of 2 is provided, encoder will use the same QP for all frames + * at different levels of the pyramid (i.e. no QP offsets are applied). * Note: This option is only relevant for --end-usage=q. */ unsigned int use_fixed_qp_offsets;
diff --git a/av1/arg_defs.c b/av1/arg_defs.c index ad28435..e8809cb 100644 --- a/av1/arg_defs.c +++ b/av1/arg_defs.c
@@ -641,11 +641,10 @@ .use_fixed_qp_offsets = ARG_DEF(NULL, "use-fixed-qp-offsets", 1, - "Enable fixed QP offsets for frames at different levels of the " - "pyramid. Selected automatically from --cq-level if " - "--fixed-qp-offsets is not provided. If this option is not " - "specified (default), offsets are adaptively chosen by the " - "encoder."), + "Controls how the encoder applies fixed QP offsets for frames at " + "different levels of the pyramid (0: adaptively-chosen offsets " + "from --cq-level if --fixed-qp-offsets is not provided " + "(default), 1: fixed QP offsets, 2: no QP offsets)"), .fixed_qp_offsets = ARG_DEF( NULL, "fixed-qp-offsets", 1,
diff --git a/av1/av1_cx_iface.c b/av1/av1_cx_iface.c index 00c6147..36df963 100644 --- a/av1/av1_cx_iface.c +++ b/av1/av1_cx_iface.c
@@ -851,7 +851,7 @@ } if (cfg->rc_end_usage == AOM_Q) { - RANGE_CHECK_HI(cfg, use_fixed_qp_offsets, 1); + RANGE_CHECK_HI(cfg, use_fixed_qp_offsets, 2); } else { if (cfg->use_fixed_qp_offsets > 0) { ERROR("--use_fixed_qp_offsets can only be used with --end-usage=q"); @@ -1291,7 +1291,8 @@ q_cfg->deltaq_mode = extra_cfg->deltaq_mode; q_cfg->deltaq_strength = extra_cfg->deltaq_strength; q_cfg->use_fixed_qp_offsets = - cfg->use_fixed_qp_offsets && (rc_cfg->mode == AOM_Q); + (rc_cfg->mode == AOM_Q) ? cfg->use_fixed_qp_offsets : 0; + q_cfg->enable_hdr_deltaq = (q_cfg->deltaq_mode == DELTA_Q_HDR) && (cfg->g_bit_depth == AOM_BITS_10) &&
diff --git a/av1/encoder/encoder.c b/av1/encoder/encoder.c index bebb1f6..6182f1d 100644 --- a/av1/encoder/encoder.c +++ b/av1/encoder/encoder.c
@@ -4610,7 +4610,7 @@ if (is_stat_generation_stage(cpi)) { #if !CONFIG_REALTIME_ONLY - if (cpi->oxcf.q_cfg.use_fixed_qp_offsets) + if (cpi->oxcf.q_cfg.use_fixed_qp_offsets != 0) av1_noop_first_pass_frame(cpi, frame_input->ts_duration); else av1_first_pass(cpi, frame_input->ts_duration);
diff --git a/av1/encoder/encoder.h b/av1/encoder/encoder.h index 52bb975..a5b334f 100644 --- a/av1/encoder/encoder.h +++ b/av1/encoder/encoder.h
@@ -791,9 +791,12 @@ } InputCfg; typedef struct { - // If true, encoder will use fixed QP offsets, that are either: + // Controls how the encoder applies fixed QP offsets. + // If the value is 0, QP offsets are chosen adaptively. + // If the value is 1, fixed QP offsets are either: // - Given by the user, and stored in 'fixed_qp_offsets' array, OR // - Picked automatically from cq_level. + // If the value is 2, no QP offsets will be applied. int use_fixed_qp_offsets; // Indicates the minimum flatness of the quantization matrix. int qm_minlevel;
diff --git a/av1/encoder/encoder_utils.c b/av1/encoder/encoder_utils.c index e689385..2bc36d3 100644 --- a/av1/encoder/encoder_utils.c +++ b/av1/encoder/encoder_utils.c
@@ -682,55 +682,65 @@ } #endif - // Decide q and q bounds. - *q = av1_rc_pick_q_and_bounds(cpi, cm->width, cm->height, cpi->gf_frame_index, - bottom_index, top_index); + if (cpi->oxcf.q_cfg.use_fixed_qp_offsets == 2 && + cpi->oxcf.rc_cfg.mode == AOM_Q) { + // Disable scaling, and use the same q for all frames of the pyramid + *q = cpi->oxcf.rc_cfg.cq_level; + *top_index = *bottom_index = *q; + cpi->ppi->p_rc.arf_q = *q; + } else { + // Decide q and q bounds. + *q = av1_rc_pick_q_and_bounds(cpi, cm->width, cm->height, + cpi->gf_frame_index, bottom_index, top_index); - if (cpi->oxcf.rc_cfg.mode == AOM_CBR && cpi->rc.force_max_q) { - *q = cpi->rc.worst_quality; - cpi->rc.force_max_q = 0; - } + if (cpi->oxcf.rc_cfg.mode == AOM_CBR && cpi->rc.force_max_q) { + *q = cpi->rc.worst_quality; + cpi->rc.force_max_q = 0; + } #if !CONFIG_REALTIME_ONLY - if (cpi->oxcf.rc_cfg.mode == AOM_Q && - cpi->ppi->tpl_data.tpl_frame[cpi->gf_frame_index].is_valid && - !is_lossless_requested(&cpi->oxcf.rc_cfg)) { - const RateControlCfg *const rc_cfg = &cpi->oxcf.rc_cfg; - const int tpl_q = av1_tpl_get_q_index( - &cpi->ppi->tpl_data, cpi->gf_frame_index, cpi->rc.active_worst_quality, - cm->seq_params->bit_depth); - *q = clamp(tpl_q, rc_cfg->best_allowed_q, rc_cfg->worst_allowed_q); - *top_index = *bottom_index = *q; - if (gf_group->update_type[cpi->gf_frame_index] == ARF_UPDATE) - cpi->ppi->p_rc.arf_q = *q; - } - - if (cpi->oxcf.q_cfg.use_fixed_qp_offsets && cpi->oxcf.rc_cfg.mode == AOM_Q) { - if (is_frame_tpl_eligible(gf_group, cpi->gf_frame_index)) { - const double qratio_grad = - cpi->ppi->p_rc.baseline_gf_interval > 20 ? 0.2 : 0.3; - const double qstep_ratio = - 0.2 + - (1.0 - (double)cpi->rc.active_worst_quality / MAXQ) * qratio_grad; - *q = av1_get_q_index_from_qstep_ratio( - cpi->rc.active_worst_quality, qstep_ratio, cm->seq_params->bit_depth); + if (cpi->oxcf.rc_cfg.mode == AOM_Q && + cpi->ppi->tpl_data.tpl_frame[cpi->gf_frame_index].is_valid && + !is_lossless_requested(&cpi->oxcf.rc_cfg)) { + const RateControlCfg *const rc_cfg = &cpi->oxcf.rc_cfg; + const int tpl_q = av1_tpl_get_q_index( + &cpi->ppi->tpl_data, cpi->gf_frame_index, + cpi->rc.active_worst_quality, cm->seq_params->bit_depth); + *q = clamp(tpl_q, rc_cfg->best_allowed_q, rc_cfg->worst_allowed_q); *top_index = *bottom_index = *q; - if (gf_group->update_type[cpi->gf_frame_index] == ARF_UPDATE || - gf_group->update_type[cpi->gf_frame_index] == KF_UPDATE || - gf_group->update_type[cpi->gf_frame_index] == GF_UPDATE) + if (gf_group->update_type[cpi->gf_frame_index] == ARF_UPDATE) cpi->ppi->p_rc.arf_q = *q; - } else if (gf_group->layer_depth[cpi->gf_frame_index] < - gf_group->max_layer_depth) { - int this_height = gf_group->layer_depth[cpi->gf_frame_index]; - int arf_q = cpi->ppi->p_rc.arf_q; - while (this_height > 1) { - arf_q = (arf_q + cpi->oxcf.rc_cfg.cq_level + 1) / 2; - --this_height; - } - *top_index = *bottom_index = *q = arf_q; } - } + + if (cpi->oxcf.q_cfg.use_fixed_qp_offsets == 1 && + cpi->oxcf.rc_cfg.mode == AOM_Q) { + if (is_frame_tpl_eligible(gf_group, cpi->gf_frame_index)) { + const double qratio_grad = + cpi->ppi->p_rc.baseline_gf_interval > 20 ? 0.2 : 0.3; + const double qstep_ratio = + 0.2 + + (1.0 - (double)cpi->rc.active_worst_quality / MAXQ) * qratio_grad; + *q = av1_get_q_index_from_qstep_ratio(cpi->rc.active_worst_quality, + qstep_ratio, + cm->seq_params->bit_depth); + *top_index = *bottom_index = *q; + if (gf_group->update_type[cpi->gf_frame_index] == ARF_UPDATE || + gf_group->update_type[cpi->gf_frame_index] == KF_UPDATE || + gf_group->update_type[cpi->gf_frame_index] == GF_UPDATE) + cpi->ppi->p_rc.arf_q = *q; + } else if (gf_group->layer_depth[cpi->gf_frame_index] < + gf_group->max_layer_depth) { + int this_height = gf_group->layer_depth[cpi->gf_frame_index]; + int arf_q = cpi->ppi->p_rc.arf_q; + while (this_height > 1) { + arf_q = (arf_q + cpi->oxcf.rc_cfg.cq_level + 1) / 2; + --this_height; + } + *top_index = *bottom_index = *q = arf_q; + } + } #endif + } // Configure experimental use of segmentation for enhanced coding of // static regions if indicated.
diff --git a/av1/encoder/rd.c b/av1/encoder/rd.c index 43e6a78..fcca312 100644 --- a/av1/encoder/rd.c +++ b/av1/encoder/rd.c
@@ -439,7 +439,7 @@ const aom_tune_metric tuning) { int64_t rdmult = av1_compute_rd_mult_based_on_qindex(bit_depth, update_type, qindex, tuning); - if (is_stat_consumption_stage && !use_fixed_qp_offsets && + if (is_stat_consumption_stage && (use_fixed_qp_offsets == 0) && (frame_type != KEY_FRAME)) { // Layer depth adjustment rdmult = (rdmult * rd_layer_depth_factor[layer_depth]) >> 7;
diff --git a/test/test.cmake b/test/test.cmake index 8877b55..c50f84a 100644 --- a/test/test.cmake +++ b/test/test.cmake
@@ -237,6 +237,7 @@ "${AOM_ROOT}/test/subtract_test.cc" "${AOM_ROOT}/test/sum_squares_test.cc" "${AOM_ROOT}/test/sse_sum_test.cc" + "${AOM_ROOT}/test/use_fixed_qp_offsets_test.cc" "${AOM_ROOT}/test/variance_test.cc" "${AOM_ROOT}/test/warp_filter_test.cc" "${AOM_ROOT}/test/warp_filter_test_util.cc"
diff --git a/test/use_fixed_qp_offsets_test.cc b/test/use_fixed_qp_offsets_test.cc new file mode 100644 index 0000000..949e4e3 --- /dev/null +++ b/test/use_fixed_qp_offsets_test.cc
@@ -0,0 +1,85 @@ +/* + * Copyright (c) 2026, Alliance for Open Media. All rights reserved. + * + * This source code is subject to the terms of the BSD 2 Clause License and + * the Alliance for Open Media Patent License 1.0. If the BSD 2 Clause License + * was not distributed with this source code in the LICENSE file, you can + * obtain it at www.aomedia.org/license/software. 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 www.aomedia.org/license/patent. + */ +#include "config/aom_config.h" + +#include "gtest/gtest.h" +#include "test/codec_factory.h" +#include "test/encode_test_driver.h" +#include "test/i420_video_source.h" +#include "test/util.h" + +namespace { + +const ::libaom_test::TestMode kTestMode[] = +#if CONFIG_REALTIME_ONLY + { ::libaom_test::kRealTime }; +#else + { ::libaom_test::kRealTime, ::libaom_test::kOnePassGood }; +#endif + +const int kUseFixedQPOffsetsMode[] = { 1, 2 }; + +class UseFixedQPOffsetsTest + : public ::libaom_test::CodecTestWith2Params<libaom_test::TestMode, int>, + public ::libaom_test::EncoderTest { + protected: + UseFixedQPOffsetsTest() : EncoderTest(GET_PARAM(0)) {} + ~UseFixedQPOffsetsTest() override = default; + + void SetUp() override { + InitializeConfig(GET_PARAM(1)); + cfg_.kf_max_dist = 9999; + cfg_.rc_end_usage = AOM_Q; + cfg_.use_fixed_qp_offsets = GET_PARAM(2); + } + + void PreEncodeFrameHook(::libaom_test::VideoSource *video, + ::libaom_test::Encoder *encoder) override { + if (video->frame() == 0) { + encoder->Control(AOME_SET_CPUUSED, 6); + encoder->Control(AOME_SET_CQ_LEVEL, frame_qp_); + } + } + + void PostEncodeFrameHook(::libaom_test::Encoder *encoder) override { + int qp = 0; + encoder->Control(AOME_GET_LAST_QUANTIZER_64, &qp); + + // If a call to encoder->EncodeFrame() results in a last QP of 0, + // interpret as the frame being read into the lookahead buffer. + if (qp == 0) return; + + if (use_fixed_qp_offsets_ == 2) { + // Setting use_fixed_qp_offsets = 2 means every frame should use the same + // QP + ASSERT_EQ(qp, frame_qp_); + } else { + ASSERT_LE(qp, frame_qp_); + } + } + + void DoTest() { + ::libaom_test::I420VideoSource video("hantro_collage_w352h288.yuv", 352, + 288, 30, 1, 0, 33); + frame_qp_ = 35; + ASSERT_NO_FATAL_FAILURE(RunLoop(&video)); + } + + int frame_qp_; + int use_fixed_qp_offsets_; +}; + +TEST_P(UseFixedQPOffsetsTest, TestQPOffsets) { DoTest(); } + +AV1_INSTANTIATE_TEST_SUITE(UseFixedQPOffsetsTest, + ::testing::ValuesIn(kTestMode), + ::testing::ValuesIn(kUseFixedQPOffsetsMode)); +} // namespace