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