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;