Commit 936b31c4d4 for aom

commit 936b31c4d41687fa46bf52024e3ffb42d8db19f0
Author: Ihor Buhaiov <freedbrt@gmail.com>
Date:   Fri Oct 23 17:56:47 2026 +0200

    Raise the max frame size on a key frame

    Without g_forced_max_frame_width/height, the sequence header keeps the
    max frame size of the first frame for the whole stream. A later
    aom_codec_enc_config_set() with a larger g_w/g_h forces a key frame,
    but that key frame still writes the old max, so
    write_uncompressed_header_obu() fails with "Frame dimensions are
    larger than the maximum values". Every later frame fails the same way,
    and AOM_EFLAG_FORCE_KF does not help.

    A shown key frame starts a new coded video sequence, and its sequence
    header may carry a new max. The decoder already handles that case in
    read_uncompressed_header(). So on such a key frame, set the max again
    the same way init_seq_coding_tools() does and update the level.
    set_bitstream_level_tier() now gets the error info from its caller. On
    such a key frame av1_encode() passes cm->error, so a profile, level and
    tier error goes through the error handler of av1_get_compressed_data().

    encoder_set_config() also forces a key frame now when the new size is
    larger than the max of the sequence header. This covers a forced max
    raised after the first frame, where no key frame was forced before.

    Streams without such an upsize do not change.

    Repro: 640x480 clip coded at 320x240, config_set to 640x480 before
    frame 60, 150 frames. Each frame is decoded and compared with the
    encoder recon, and aomdec --md5 checks the IVF.

    x86, MSVC O2 with asserts, default and CONFIG_REALTIME_ONLY builds.
    RT speed 7 and 10, 1 and 4 threads, VBR, CBR, screen, 4:2:0 and 4:4:4:
      before: frame 60 and every later frame fail with the error above
      after: 150 frames, 0 recon mismatches, aomdec md5 matches
    Good quality speed 6, 1 and 4 threads, 4:2:0 and 4:4:4 (default
    build): same before and after.
    Forced max raised by config_set at frame 30, upsize at frame 60:
      before: fails from frame 60, after: 150 frames, 0 mismatches
    No upsize, upsize inside a forced max, down then up within the max:
      same bitstream hash before and after.

    Arm Mac, -O2 with asserts, default and CONFIG_REALTIME_ONLY builds:
    park_joy_90p 8 and 10 bit (10 frames) and screendata 1920x1080 shifted
    to 10 bit (20 frames), 4:2:0 and 4:4:4, 1 and 4 threads, the cases
    above at clip length: 0 failed frames (before: every frame from the
    upsize on fails). Streams without an upsize keep the same hash. The
    decoder matches the recon, except for down then up on park_joy with
    4 threads, which mismatches the same way before the change.

    The new AV1ResolutionChange.UpscalePastMaxFrameSize and
    UpscalePastMaxFrameSizeLateForcedMax tests fail before the change and
    pass after it.

    Change-Id: I9269198d9a5b841ba9341b1964387a2e9b88f75c

diff --git a/aom/aom_encoder.h b/aom/aom_encoder.h
index 7c7579c1ff..3669f5fcd9 100644
--- a/aom/aom_encoder.h
+++ b/aom/aom_encoder.h
@@ -452,14 +452,18 @@ typedef struct aom_codec_enc_cfg {
   /*!\brief Forced maximum width of the frame
    *
    * If this value is non-zero then it is used to force the maximum frame
-   * width written in write_sequence_header().
+   * width written in write_sequence_header(). If it is zero, the maximum
+   * starts at g_w. A larger g_w set later with aom_codec_enc_config_set()
+   * forces a key frame whose sequence header raises the maximum.
    */
   unsigned int g_forced_max_frame_width;

   /*!\brief Forced maximum height of the frame
    *
    * If this value is non-zero then it is used to force the maximum frame
-   * height written in write_sequence_header().
+   * height written in write_sequence_header(). If it is zero, the maximum
+   * starts at g_h. A larger g_h set later with aom_codec_enc_config_set()
+   * forces a key frame whose sequence header raises the maximum.
    */
   unsigned int g_forced_max_frame_height;

diff --git a/av1/av1_cx_iface.c b/av1/av1_cx_iface.c
index a76a22d0df..6ec3e27e21 100644
--- a/av1/av1_cx_iface.c
+++ b/av1/av1_cx_iface.c
@@ -1711,6 +1711,13 @@ static aom_codec_err_t encoder_set_config(aom_codec_alg_priv_t *ctx,
            ((int)cfg->g_h > ctx->ppi->cpi->last_coded_height))))) {
       force_key = 1;
     }
+    // Only a key frame can carry a sequence header with a larger maximum
+    // frame size.
+    if (ctx->ppi->seq_params_locked &&
+        ((int)cfg->g_w > ctx->ppi->seq_params.max_frame_width ||
+         (int)cfg->g_h > ctx->ppi->seq_params.max_frame_height)) {
+      force_key = 1;
+    }
   }

   if (ctx->monochrome_on_init && cfg->monochrome == 0) {
diff --git a/av1/encoder/encoder.c b/av1/encoder/encoder.c
index a6aaad9129..f3ded02c2b 100644
--- a/av1/encoder/encoder.c
+++ b/av1/encoder/encoder.c
@@ -468,7 +468,8 @@ static inline int does_level_match(int width, int height, double fps,
 }

 static void set_bitstream_level_tier(AV1_PRIMARY *const ppi, int width,
-                                     int height, double init_framerate) {
+                                     int height, double init_framerate,
+                                     struct aom_internal_error_info *error) {
   SequenceHeader *const seq_params = &ppi->seq_params;
   const AV1LevelParams *const level_params = &ppi->level_params;
   // TODO(any): This is a placeholder function that only addresses dimensions
@@ -557,7 +558,7 @@ static void set_bitstream_level_tier(AV1_PRIMARY *const ppi, int width,
     // check
     if (seq_params->op_params[i].bitrate == 0)
       aom_internal_error(
-          &ppi->error, AOM_CODEC_UNSUP_BITSTREAM,
+          error, AOM_CODEC_UNSUP_BITSTREAM,
           "AV1 does not support this combination of profile, level, and tier.");
     // Buffer size in bits is bitrate in bits/s * 1 s
     seq_params->op_params[i].buffer_size = seq_params->op_params[i].bitrate;
@@ -590,6 +591,30 @@ void av1_set_svc_seq_params(AV1_PRIMARY *const ppi) {
   }
 }

+// Sets the maximum frame size of the sequence header. Without a forced
+// maximum it never shrinks. Returns true if it changed.
+static bool set_max_frame_size(SequenceHeader *seq,
+                               const FrameDimensionCfg *frm_dim_cfg) {
+  const int max_width = frm_dim_cfg->forced_max_frame_width
+                            ? frm_dim_cfg->forced_max_frame_width
+                            : AOMMAX(seq->max_frame_width, frm_dim_cfg->width);
+  const int max_height =
+      frm_dim_cfg->forced_max_frame_height
+          ? frm_dim_cfg->forced_max_frame_height
+          : AOMMAX(seq->max_frame_height, frm_dim_cfg->height);
+  if (max_width == seq->max_frame_width &&
+      max_height == seq->max_frame_height) {
+    return false;
+  }
+  seq->max_frame_width = max_width;
+  seq->max_frame_height = max_height;
+  seq->num_bits_width = (max_width > 1) ? get_msb(max_width - 1) + 1 : 1;
+  seq->num_bits_height = (max_height > 1) ? get_msb(max_height - 1) + 1 : 1;
+  assert(seq->num_bits_width <= 16);
+  assert(seq->num_bits_height <= 16);
+  return true;
+}
+
 static void init_seq_coding_tools(AV1_PRIMARY *const ppi,
                                   const AV1EncoderConfig *oxcf,
                                   int disable_frame_id_numbers) {
@@ -618,19 +643,7 @@ static void init_seq_coding_tools(AV1_PRIMARY *const ppi,
           ? DEFAULT_EXPLICIT_ORDER_HINT_BITS - 1
           : -1;

-  seq->max_frame_width = frm_dim_cfg->forced_max_frame_width
-                             ? frm_dim_cfg->forced_max_frame_width
-                             : AOMMAX(seq->max_frame_width, frm_dim_cfg->width);
-  seq->max_frame_height =
-      frm_dim_cfg->forced_max_frame_height
-          ? frm_dim_cfg->forced_max_frame_height
-          : AOMMAX(seq->max_frame_height, frm_dim_cfg->height);
-  seq->num_bits_width =
-      (seq->max_frame_width > 1) ? get_msb(seq->max_frame_width - 1) + 1 : 1;
-  seq->num_bits_height =
-      (seq->max_frame_height > 1) ? get_msb(seq->max_frame_height - 1) + 1 : 1;
-  assert(seq->num_bits_width <= 16);
-  assert(seq->num_bits_height <= 16);
+  set_max_frame_size(seq, frm_dim_cfg);

   seq->frame_id_length = FRAME_ID_LENGTH;
   seq->delta_frame_id_length = DELTA_FRAME_ID_LENGTH;
@@ -653,7 +666,7 @@ static void init_seq_coding_tools(AV1_PRIMARY *const ppi,
   seq->enable_filter_intra = oxcf->intra_mode_cfg.enable_filter_intra;

   set_bitstream_level_tier(ppi, seq->max_frame_width, seq->max_frame_height,
-                           oxcf->input_cfg.init_framerate);
+                           oxcf->input_cfg.init_framerate, &ppi->error);
   av1_set_svc_seq_params(ppi);
 }

@@ -4587,6 +4600,19 @@ int av1_encode(AV1_COMP *const cpi, uint8_t *const dest, size_t dest_size,
   cm->show_existing_frame = frame_params->show_existing_frame;
   cpi->existing_fb_idx_to_show = frame_params->existing_fb_idx_to_show;

+  // A shown key frame starts a new coded video sequence, so its sequence
+  // header can raise the maximum frame size to fit a larger frame.
+  if (cpi->ppi->seq_params_locked && !is_stat_generation_stage(cpi) &&
+      cm->current_frame.frame_type == KEY_FRAME && cm->show_frame &&
+      !cm->show_existing_frame) {
+    SequenceHeader *const seq_params = cm->seq_params;
+    if (set_max_frame_size(seq_params, &cpi->oxcf.frm_dim_cfg)) {
+      set_bitstream_level_tier(cpi->ppi, seq_params->max_frame_width,
+                               seq_params->max_frame_height,
+                               cpi->oxcf.input_cfg.init_framerate, cm->error);
+    }
+  }
+
   if (cpi->oxcf.mode == GOOD &&
       cpi->oxcf.q_cfg.deltaq_mode == DELTA_Q_OBJECTIVE &&
       cpi->oxcf.algo_cfg.enable_tpl_model && cpi->oxcf.q_cfg.aq_mode == NO_AQ &&
diff --git a/test/frame_size_tests.cc b/test/frame_size_tests.cc
index 97bbc59ca5..e1fc105c31 100644
--- a/test/frame_size_tests.cc
+++ b/test/frame_size_tests.cc
@@ -9,9 +9,17 @@
  * PATENTS file, you can obtain it at www.aomedia.org/license/patent.
  */

+#include <algorithm>
 #include <array>
+#include <cstring>
 #include <memory>
+#include <vector>

+#include "config/aom_config.h"
+#if CONFIG_AV1_DECODER
+#include "aom/aom_decoder.h"
+#include "aom/aomdx.h"
+#endif
 #include "gtest/gtest.h"
 #include "test/codec_factory.h"
 #include "test/video_source.h"
@@ -243,6 +251,147 @@ TEST_P(AV1ResolutionChange, RandomInput) {
   EXPECT_EQ(frame_count, kNumFramesPerResolution * kFrameSizes.size());
 }

+#if CONFIG_AV1_DECODER
+bool ImagesEqual(const aom_image_t &a, const aom_image_t &b) {
+  if (a.d_w != b.d_w || a.d_h != b.d_h ||
+      (a.fmt & AOM_IMG_FMT_HIGHBITDEPTH) !=
+          (b.fmt & AOM_IMG_FMT_HIGHBITDEPTH)) {
+    return false;
+  }
+  const int bytes_per_sample = (a.fmt & AOM_IMG_FMT_HIGHBITDEPTH) ? 2 : 1;
+  for (int plane = 0; plane < 3; ++plane) {
+    const int row_bytes = aom_img_plane_width(&a, plane) * bytes_per_sample;
+    for (int y = 0; y < aom_img_plane_height(&a, plane); ++y) {
+      if (memcmp(a.planes[plane] + y * a.stride[plane],
+                 b.planes[plane] + y * b.stride[plane], row_bytes)) {
+        return false;
+      }
+    }
+  }
+  return true;
+}
+#endif  // CONFIG_AV1_DECODER
+
+struct UpscaleFrameSize {
+  unsigned int width;
+  unsigned int height;
+};
+
+// Encodes two frames at each size in |sizes|. If |forced_max| is non-zero, it
+// is set as the forced maximum frame width and height together with the
+// second size. Each frame larger than the maximum frame size of the sequence
+// header so far must be a key frame, and the stream must decode and match the
+// encoder's reconstruction.
+void RunUpscalePastMaxFrameSize(int usage, aom_rc_mode rc_mode, int cpu_used,
+                                const std::vector<UpscaleFrameSize> &sizes,
+                                unsigned int forced_max) {
+  aom_codec_iface_t *iface = aom_codec_av1_cx();
+  aom_codec_enc_cfg_t cfg;
+  ASSERT_EQ(aom_codec_enc_config_default(iface, &cfg, usage), AOM_CODEC_OK);
+
+  // Resolution changes are only permitted with one pass encoding with no lag.
+  cfg.g_pass = AOM_RC_ONE_PASS;
+  cfg.g_lag_in_frames = 0;
+  cfg.rc_end_usage = rc_mode;
+  cfg.g_w = sizes[0].width;
+  cfg.g_h = sizes[0].height;
+
+  aom_codec_ctx_t ctx;
+  ASSERT_EQ(aom_codec_enc_init(&ctx, iface, &cfg, 0), AOM_CODEC_OK);
+  std::unique_ptr<aom_codec_ctx_t, decltype(&aom_codec_destroy)> enc(
+      &ctx, &aom_codec_destroy);
+  ASSERT_EQ(aom_codec_control(enc.get(), AOME_SET_CPUUSED, cpu_used),
+            AOM_CODEC_OK);
+
+#if CONFIG_AV1_DECODER
+  // Decode to 8 bit buffers like the encoder reconstruction, even when
+  // FORCE_HIGHBITDEPTH_DECODING is on.
+  aom_codec_dec_cfg_t dec_cfg = {};
+  dec_cfg.allow_lowbitdepth = 1;
+  aom_codec_ctx_t dec_ctx;
+  ASSERT_EQ(aom_codec_dec_init(&dec_ctx, aom_codec_av1_dx(), &dec_cfg, 0),
+            AOM_CODEC_OK);
+  std::unique_ptr<aom_codec_ctx_t, decltype(&aom_codec_destroy)> dec(
+      &dec_ctx, &aom_codec_destroy);
+#endif
+
+  size_t frame_count = 0;
+  unsigned int max_width = 0;
+  unsigned int max_height = 0;
+  ::libaom_test::RandomVideoSource video;
+  video.Begin();
+  constexpr int kNumFramesPerResolution = 2;
+  for (size_t s = 0; s < sizes.size(); ++s) {
+    cfg.g_w = sizes[s].width;
+    cfg.g_h = sizes[s].height;
+    if (s == 1 && forced_max != 0) {
+      cfg.g_forced_max_frame_width = forced_max;
+      cfg.g_forced_max_frame_height = forced_max;
+    }
+    ASSERT_EQ(aom_codec_enc_config_set(enc.get(), &cfg), AOM_CODEC_OK);
+    video.SetSize(cfg.g_w, cfg.g_h);
+    const bool larger = cfg.g_w > max_width || cfg.g_h > max_height;
+    max_width = std::max(max_width, cfg.g_w);
+    max_height = std::max(max_height, cfg.g_h);
+
+    for (int i = 0; i < kNumFramesPerResolution; ++i) {
+      video.Next();  // SetSize() does not call FillFrame().
+      ASSERT_EQ(aom_codec_encode(enc.get(), video.img(), video.pts(),
+                                 video.duration(), /*flags=*/0),
+                AOM_CODEC_OK)
+          << aom_codec_error_detail(enc.get());
+
+      aom_codec_iter_t iter = nullptr;
+      const aom_codec_cx_pkt_t *pkt;
+      while ((pkt = aom_codec_get_cx_data(enc.get(), &iter)) != nullptr) {
+        ASSERT_EQ(pkt->kind, AOM_CODEC_CX_FRAME_PKT);
+        if (i == 0 && larger) {
+          EXPECT_NE(pkt->data.frame.flags & AOM_FRAME_IS_KEY, 0u)
+              << "frame " << frame_count;
+        }
+#if CONFIG_AV1_DECODER
+        ASSERT_EQ(
+            aom_codec_decode(dec.get(),
+                             static_cast<const uint8_t *>(pkt->data.frame.buf),
+                             pkt->data.frame.sz, nullptr),
+            AOM_CODEC_OK)
+            << "frame " << frame_count << ": "
+            << aom_codec_error_detail(dec.get());
+        aom_codec_iter_t dec_iter = nullptr;
+        const aom_image_t *dec_img = aom_codec_get_frame(dec.get(), &dec_iter);
+        ASSERT_NE(dec_img, nullptr) << "frame " << frame_count;
+        EXPECT_EQ(dec_img->d_w, cfg.g_w) << "frame " << frame_count;
+        EXPECT_EQ(dec_img->d_h, cfg.g_h) << "frame " << frame_count;
+        aom_image_t recon;
+        ASSERT_EQ(aom_codec_control(enc.get(), AV1_GET_NEW_FRAME_IMAGE, &recon),
+                  AOM_CODEC_OK);
+        EXPECT_TRUE(ImagesEqual(recon, *dec_img)) << "frame " << frame_count;
+#endif
+        frame_count++;
+      }
+    }
+  }
+
+  EXPECT_EQ(frame_count, kNumFramesPerResolution * sizes.size());
+}
+
+// Without g_forced_max_frame_width/height, the maximum frame size of the
+// sequence header follows the largest frame size so far.
+TEST_P(AV1ResolutionChange, UpscalePastMaxFrameSize) {
+  RunUpscalePastMaxFrameSize(
+      usage_, rc_mode_, cpu_used_,
+      { { 64, 64 }, { 128, 64 }, { 128, 128 }, { 96, 96 }, { 200, 150 } },
+      /*forced_max=*/0);
+}
+
+// A forced maximum set after the first frame reaches the sequence header with
+// the next key frame.
+TEST_P(AV1ResolutionChange, UpscalePastMaxFrameSizeLateForcedMax) {
+  RunUpscalePastMaxFrameSize(usage_, rc_mode_, cpu_used_,
+                             { { 64, 64 }, { 128, 128 } },
+                             /*forced_max=*/256);
+}
+
 TEST_P(AV1ResolutionChange, InvalidInputSize) {
   struct FrameSize {
     unsigned int width;