Commit 8cd4d41cb4 for aom
commit 8cd4d41cb464b87ee3e4776dcf830eb2a7774027
Author: Julio Barba <juliobbv@gmail.com>
Date: Tue Feb 24 18:19:47 2026 -0500
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 6fb50eb505..9d412af331 100644
--- a/aom/aom_encoder.h
+++ b/aom/aom_encoder.h
@@ -890,11 +890,14 @@ typedef struct aom_codec_enc_cfg {
*/
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 ad28435460..e8809cbe4f 100644
--- a/av1/arg_defs.c
+++ b/av1/arg_defs.c
@@ -641,11 +641,10 @@ const av1_codec_arg_definitions_t g_av1_codec_arg_defs = {
.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 00c6147f05..36df9636ca 100644
--- a/av1/av1_cx_iface.c
+++ b/av1/av1_cx_iface.c
@@ -851,7 +851,7 @@ static aom_codec_err_t validate_config(aom_codec_alg_priv_t *ctx,
}
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 @@ static void set_encoder_config(AV1EncoderConfig *oxcf,
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 bebb1f6718..6182f1d781 100644
--- a/av1/encoder/encoder.c
+++ b/av1/encoder/encoder.c
@@ -4610,7 +4610,7 @@ int av1_encode(AV1_COMP *const cpi, uint8_t *const dest, size_t dest_size,
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 52bb9754c0..a5b334feca 100644
--- a/av1/encoder/encoder.h
+++ b/av1/encoder/encoder.h
@@ -791,9 +791,12 @@ typedef struct {
} 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 e68938561a..2bc36d3937 100644
--- a/av1/encoder/encoder_utils.c
+++ b/av1/encoder/encoder_utils.c
@@ -682,55 +682,65 @@ void av1_set_size_dependent_vars(AV1_COMP *cpi, int *q, int *bottom_index,
}
#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;
+ }
+
+ 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;
}
- *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 43e6a78fb6..fcca31242e 100644
--- a/av1/encoder/rd.c
+++ b/av1/encoder/rd.c
@@ -439,7 +439,7 @@ int av1_compute_rd_mult(const int qindex, const aom_bit_depth_t bit_depth,
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 8877b55364..c50f84a2de 100644
--- a/test/test.cmake
+++ b/test/test.cmake
@@ -237,6 +237,7 @@ if(NOT BUILD_SHARED_LIBS)
"${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 0000000000..949e4e3a2a
--- /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