Commit bc384518c7 for aom
commit bc384518c76b41b0bc2662e11a17e6bf01ecea08
Author: Erik Språng <sprang@google.com>
Date: Tue Sep 1 18:43:23 2026 +0200
Allow dynamic upscaling across frames without forcing keyframes
AV1 reference frame scaling supports up to 16x upscaling and 2x
downscaling from reference frames. However, when dynamic resolution
changes were configured with g_forced_max_frame_width/height in single-
layer realtime mode, encoder_set_config was forcing a keyframe
whenever new dimensions exceeded the last coded width/height.
This CL enables dynamic upscaling without forcing keyframes:
1. In single-pass realtime mode without lookahead (g_lag_in_frames == 0
and g_pass == AOM_RC_ONE_PASS), and with the maximum frame size
declared up front via g_forced_max_frame_width/height, removes
keyframe forcing for upscaled dimensions in encoder_set_config as
long as reference frame scaling constraints are met. The forced
maximum frame size is required since it is what makes the internal
buffers large enough for the upscaled frame; without it they are
only sized for the current frame size.
2. Sizes internal buffers (compressor data allocation, segmentation
maps, cyclic refresh, active maps, and transform coefficient buffers)
using forced_max_frame_width/height when specified, preventing
out-of-bounds access upon upscaling.
3. Updates set_bitstream_level_tier to compute the Sequence Header
operating point level index using seq->max_frame_width/height,
ensuring the level's MaxPicSize accommodates the maximum sequence
resolution.
4. Adds unit tests in test/forced_max_frame_width_height_test.cc to
verify dynamic upscaling up to 16x without keyframes and level
index calculation.
5. Updates AV1ResolutionChange.RandomInput, which asserted that every
resolution change produces a keyframe. All the resolution changes it
performs are in fact within the reference frame scaling limits, so in
realtime mode they are now coded as inter frames that scale their
references. The test now verifies that behaviour, and that the other
modes still force a keyframe.
This fix will allow us to re-enable the `ReferenceFrameScaling` test
case in
https://source.chromium.org/chromium/chromium/src/+/main:third_party/webrtc/api/video_codecs/test/video_encoder_functional_unittest.cc
Bug: webrtc:496266459
Change-Id: Iee39e485db367e29b7d918ebd0708be72cb69bfe
diff --git a/av1/av1_cx_iface.c b/av1/av1_cx_iface.c
index 18922ad304..cced80c024 100644
--- a/av1/av1_cx_iface.c
+++ b/av1/av1_cx_iface.c
@@ -1672,21 +1672,32 @@ static aom_codec_err_t encoder_set_config(aom_codec_alg_priv_t *ctx,
if (cfg->g_lag_in_frames > 1 || cfg->g_pass != AOM_RC_ONE_PASS)
ERROR("Cannot change width or height after initialization");
// Note: function encoder_set_config() is allowed to be called multiple
- // times. However, when the original frame width or height is less than two
- // times of the new frame width or height, a forced key frame should be
- // used (for the case of single spatial layer, since otherwise a previous
+ // times. In single-pass realtime mode without lookahead (g_lag_in_frames ==
+ // 0), and with the maximum frame size declared up front via
+ // g_forced_max_frame_width/height, reference frame scaling allows upscaling
+ // up to 16x and downscaling by up to 2x without forcing a keyframe. The
+ // forced maximum frame size is required because the internal buffers are
+ // then allocated for that maximum; without it they are only sized for the
+ // current frame size and a larger frame would overflow them. Outside of
+ // this mode, or if reference frame scaling constraints are violated, a
+ // keyframe is forced (for single spatial layer, since otherwise a previous
// encoded frame at a lower layer may be the desired reference). To make
- // sure the correct detection of a forced key frame, we need
- // to update the frame width and height only when the actual encoding is
- // performed. cpi->last_coded_width and cpi->last_coded_height are used to
- // track the actual coded frame size.
+ // sure the correct detection of a forced key frame, we need to update the
+ // frame width and height only when the actual encoding is performed.
+ // cpi->last_coded_width and cpi->last_coded_height are used to track the
+ // actual coded frame size.
+ const bool allow_ref_scaled_upscale =
+ cfg->g_forced_max_frame_width && cfg->g_forced_max_frame_height &&
+ ctx->oxcf.mode == REALTIME && cfg->g_pass == AOM_RC_ONE_PASS &&
+ cfg->g_lag_in_frames == 0;
if (ctx->ppi->cpi->svc.number_spatial_layers == 1 &&
ctx->ppi->cpi->last_coded_width && ctx->ppi->cpi->last_coded_height &&
(!valid_ref_frame_size(ctx->ppi->cpi->last_coded_width,
ctx->ppi->cpi->last_coded_height, cfg->g_w,
cfg->g_h) ||
- ((int)cfg->g_w > ctx->ppi->cpi->last_coded_width) ||
- ((int)cfg->g_h > ctx->ppi->cpi->last_coded_height))) {
+ (!allow_ref_scaled_upscale &&
+ (((int)cfg->g_w > ctx->ppi->cpi->last_coded_width) ||
+ ((int)cfg->g_h > ctx->ppi->cpi->last_coded_height))))) {
force_key = 1;
}
}
diff --git a/av1/encoder/encoder.c b/av1/encoder/encoder.c
index daf31fba80..6ee258a762 100644
--- a/av1/encoder/encoder.c
+++ b/av1/encoder/encoder.c
@@ -663,7 +663,7 @@ static void init_seq_coding_tools(AV1_PRIMARY *const ppi,
seq->enable_intra_edge_filter = oxcf->intra_mode_cfg.enable_intra_edge_filter;
seq->enable_filter_intra = oxcf->intra_mode_cfg.enable_filter_intra;
- set_bitstream_level_tier(ppi, frm_dim_cfg->width, frm_dim_cfg->height,
+ set_bitstream_level_tier(ppi, seq->max_frame_width, seq->max_frame_height,
oxcf->input_cfg.init_framerate);
av1_set_svc_seq_params(ppi);
}
@@ -758,8 +758,12 @@ static void init_config(struct AV1_COMP *cpi, const AV1EncoderConfig *oxcf) {
alloc_compressor_data(cpi);
- cpi->data_alloc_width = cm->width;
- cpi->data_alloc_height = cm->height;
+ cpi->data_alloc_width = oxcf->frm_dim_cfg.forced_max_frame_width
+ ? oxcf->frm_dim_cfg.forced_max_frame_width
+ : cm->width;
+ cpi->data_alloc_height = oxcf->frm_dim_cfg.forced_max_frame_height
+ ? oxcf->frm_dim_cfg.forced_max_frame_height
+ : cm->height;
cpi->frame_size_related_setup_done = false;
// Single thread case: use counts in common.
@@ -1059,8 +1063,12 @@ void av1_change_config(struct AV1_COMP *cpi, const AV1EncoderConfig *oxcf,
cpi->td.firstpass_ctx = NULL;
alloc_compressor_data(cpi);
realloc_segmentation_maps(cpi);
- cpi->data_alloc_width = cm->width;
- cpi->data_alloc_height = cm->height;
+ cpi->data_alloc_width = oxcf->frm_dim_cfg.forced_max_frame_width
+ ? oxcf->frm_dim_cfg.forced_max_frame_width
+ : cm->width;
+ cpi->data_alloc_height = oxcf->frm_dim_cfg.forced_max_frame_height
+ ? oxcf->frm_dim_cfg.forced_max_frame_height
+ : cm->height;
cpi->frame_size_related_setup_done = false;
}
av1_update_frame_size(cpi);
@@ -1069,6 +1077,12 @@ void av1_change_config(struct AV1_COMP *cpi, const AV1EncoderConfig *oxcf,
if (cpi->oxcf.q_cfg.aq_mode == CYCLIC_REFRESH_AQ) {
int mi_rows = cpi->common.mi_params.mi_rows;
int mi_cols = cpi->common.mi_params.mi_cols;
+ if (cpi->oxcf.frm_dim_cfg.forced_max_frame_width) {
+ mi_cols = size_in_mi(cpi->oxcf.frm_dim_cfg.forced_max_frame_width);
+ }
+ if (cpi->oxcf.frm_dim_cfg.forced_max_frame_height) {
+ mi_rows = size_in_mi(cpi->oxcf.frm_dim_cfg.forced_max_frame_height);
+ }
aom_free(cpi->cyclic_refresh->map);
CHECK_MEM_ERROR(
cm, cpi->cyclic_refresh->map,
@@ -2632,8 +2646,12 @@ static int set_size_literal(AV1_COMP *cpi, int width, int height) {
cpi->td.firstpass_ctx = NULL;
alloc_compressor_data(cpi);
realloc_segmentation_maps(cpi);
- cpi->data_alloc_width = cm->width;
- cpi->data_alloc_height = cm->height;
+ cpi->data_alloc_width = cpi->oxcf.frm_dim_cfg.forced_max_frame_width
+ ? cpi->oxcf.frm_dim_cfg.forced_max_frame_width
+ : cm->width;
+ cpi->data_alloc_height = cpi->oxcf.frm_dim_cfg.forced_max_frame_height
+ ? cpi->oxcf.frm_dim_cfg.forced_max_frame_height
+ : cm->height;
cpi->frame_size_related_setup_done = false;
}
alloc_mb_mode_info_buffers(cpi);
diff --git a/av1/encoder/encoder_alloc.h b/av1/encoder/encoder_alloc.h
index 04a6e89742..ac4a873cc3 100644
--- a/av1/encoder/encoder_alloc.h
+++ b/av1/encoder/encoder_alloc.h
@@ -15,6 +15,7 @@
#include "av1/encoder/block.h"
#include "av1/encoder/encodeframe_utils.h"
#include "av1/encoder/encoder.h"
+#include "av1/encoder/encoder_utils.h"
#include "av1/encoder/encodetxb.h"
#include "av1/encoder/ethread.h"
#include "av1/encoder/global_motion_facade.h"
@@ -105,22 +106,29 @@ static inline void alloc_mb_mode_info_buffers(AV1_COMP *const cpi) {
static inline void realloc_segmentation_maps(AV1_COMP *cpi) {
AV1_COMMON *const cm = &cpi->common;
CommonModeInfoParams *const mi_params = &cm->mi_params;
+ int max_mi_cols = mi_params->mi_cols;
+ int max_mi_rows = mi_params->mi_rows;
+ if (cpi->oxcf.frm_dim_cfg.forced_max_frame_width) {
+ max_mi_cols = size_in_mi(cpi->oxcf.frm_dim_cfg.forced_max_frame_width);
+ }
+ if (cpi->oxcf.frm_dim_cfg.forced_max_frame_height) {
+ max_mi_rows = size_in_mi(cpi->oxcf.frm_dim_cfg.forced_max_frame_height);
+ }
// Create the encoder segmentation map and set all entries to 0
aom_free(cpi->enc_seg.map);
CHECK_MEM_ERROR(cm, cpi->enc_seg.map,
- aom_calloc(mi_params->mi_rows * mi_params->mi_cols, 1));
+ aom_calloc(max_mi_rows * max_mi_cols, 1));
// Create a map used for cyclic background refresh.
if (cpi->cyclic_refresh) av1_cyclic_refresh_free(cpi->cyclic_refresh);
- CHECK_MEM_ERROR(
- cm, cpi->cyclic_refresh,
- av1_cyclic_refresh_alloc(mi_params->mi_rows, mi_params->mi_cols));
+ CHECK_MEM_ERROR(cm, cpi->cyclic_refresh,
+ av1_cyclic_refresh_alloc(max_mi_rows, max_mi_cols));
// Create a map used to mark inactive areas.
aom_free(cpi->active_map.map);
CHECK_MEM_ERROR(cm, cpi->active_map.map,
- aom_calloc(mi_params->mi_rows * mi_params->mi_cols, 1));
+ aom_calloc(max_mi_rows * max_mi_cols, 1));
}
static inline void alloc_obmc_buffers(OBMCBuffer *obmc_buffer,
diff --git a/av1/encoder/encodetxb.c b/av1/encoder/encodetxb.c
index f0607b68e0..6066f9e010 100644
--- a/av1/encoder/encodetxb.c
+++ b/av1/encoder/encodetxb.c
@@ -21,6 +21,7 @@
#include "av1/encoder/bitstream.h"
#include "av1/encoder/cost.h"
#include "av1/encoder/encodeframe.h"
+#include "av1/encoder/encoder_utils.h"
#include "av1/encoder/hash.h"
#include "av1/encoder/rdopt.h"
#include "av1/encoder/tokenize.h"
@@ -28,10 +29,18 @@
void av1_alloc_txb_buf(AV1_COMP *cpi) {
AV1_COMMON *cm = &cpi->common;
CoeffBufferPool *coeff_buf_pool = &cpi->coeff_buffer_pool;
+ int mi_rows = cm->mi_params.mi_rows;
+ int mi_cols = cm->mi_params.mi_cols;
+ if (cpi->oxcf.frm_dim_cfg.forced_max_frame_width) {
+ mi_cols = size_in_mi(cpi->oxcf.frm_dim_cfg.forced_max_frame_width);
+ }
+ if (cpi->oxcf.frm_dim_cfg.forced_max_frame_height) {
+ mi_rows = size_in_mi(cpi->oxcf.frm_dim_cfg.forced_max_frame_height);
+ }
const int num_sb_rows =
- CEIL_POWER_OF_TWO(cm->mi_params.mi_rows, cm->seq_params->mib_size_log2);
+ CEIL_POWER_OF_TWO(mi_rows, cm->seq_params->mib_size_log2);
const int num_sb_cols =
- CEIL_POWER_OF_TWO(cm->mi_params.mi_cols, cm->seq_params->mib_size_log2);
+ CEIL_POWER_OF_TWO(mi_cols, cm->seq_params->mib_size_log2);
const int size = num_sb_rows * num_sb_cols;
const int num_planes = av1_num_planes(cm);
const int subsampling_x = cm->seq_params->subsampling_x;
@@ -899,8 +908,11 @@ CB_COEFF_BUFFER *av1_get_cb_coeff_buffer(const struct AV1_COMP *cpi, int mi_row,
int mi_col) {
const AV1_COMMON *const cm = &cpi->common;
const int mib_size_log2 = cm->seq_params->mib_size_log2;
- const int stride =
- CEIL_POWER_OF_TWO(cm->mi_params.mi_cols, cm->seq_params->mib_size_log2);
+ int mi_cols = cm->mi_params.mi_cols;
+ if (cpi->oxcf.frm_dim_cfg.forced_max_frame_width) {
+ mi_cols = size_in_mi(cpi->oxcf.frm_dim_cfg.forced_max_frame_width);
+ }
+ const int stride = CEIL_POWER_OF_TWO(mi_cols, cm->seq_params->mib_size_log2);
const int offset =
(mi_row >> mib_size_log2) * stride + (mi_col >> mib_size_log2);
return cpi->coeff_buffer_base + offset;
diff --git a/test/forced_max_frame_width_height_test.cc b/test/forced_max_frame_width_height_test.cc
index d5bbdf25ae..762de80114 100644
--- a/test/forced_max_frame_width_height_test.cc
+++ b/test/forced_max_frame_width_height_test.cc
@@ -22,6 +22,10 @@
#include "aom/aomcx.h"
#include "aom/aom_encoder.h"
#include "config/aom_config.h"
+#if CONFIG_AV1_DECODER
+#include "aom/aom_decoder.h"
+#include "aom/aomdx.h"
+#endif
#include "gtest/gtest.h"
namespace {
@@ -310,4 +314,259 @@ TEST(EncodeForcedMaxFrameWidthHeight, ReducedStillPictureHeader) {
EXPECT_EQ(AOM_CODEC_OK, aom_codec_destroy(&enc));
}
+TEST(EncodeForcedMaxFrameWidthHeight, DynamicUpscaleDoesNotForceKeyframe) {
+ constexpr size_t kImageDataSize = 256 * 256 + 2 * 128 * 128;
+ std::unique_ptr<unsigned char[]> img_data(new unsigned char[kImageDataSize]);
+ ASSERT_NE(img_data, nullptr);
+ memset(img_data.get(), 128, kImageDataSize);
+
+ aom_codec_iface_t *iface = aom_codec_av1_cx();
+ aom_codec_enc_cfg_t cfg;
+ EXPECT_EQ(AOM_CODEC_OK,
+ aom_codec_enc_config_default(iface, &cfg, AOM_USAGE_REALTIME));
+ cfg.g_w = 128;
+ cfg.g_h = 128;
+ cfg.g_forced_max_frame_width = 256;
+ cfg.g_forced_max_frame_height = 256;
+ cfg.g_lag_in_frames = 0;
+ cfg.kf_mode = AOM_KF_DISABLED;
+ aom_codec_ctx_t enc;
+ EXPECT_EQ(AOM_CODEC_OK, aom_codec_enc_init(&enc, iface, &cfg, 0));
+ EXPECT_EQ(AOM_CODEC_OK, aom_codec_control(&enc, AOME_SET_CPUUSED, 7));
+
+#if CONFIG_AV1_DECODER
+ aom_codec_ctx_t dec;
+ EXPECT_EQ(AOM_CODEC_OK,
+ aom_codec_dec_init(&dec, aom_codec_av1_dx(), nullptr, 0));
+#endif
+
+ // Frame 0: 128x128 keyframe.
+ aom_image_t img;
+ EXPECT_EQ(&img,
+ aom_img_wrap(&img, AOM_IMG_FMT_I420, 128, 128, 1, img_data.get()));
+ EXPECT_EQ(AOM_CODEC_OK, aom_codec_encode(&enc, &img, 0, 1, 0));
+
+ aom_codec_iter_t iter = nullptr;
+ const aom_codec_cx_pkt_t *pkt = aom_codec_get_cx_data(&enc, &iter);
+ ASSERT_NE(pkt, nullptr);
+ EXPECT_EQ(pkt->kind, AOM_CODEC_CX_FRAME_PKT);
+ EXPECT_TRUE(pkt->data.frame.flags & AOM_FRAME_IS_KEY);
+
+#if CONFIG_AV1_DECODER
+ EXPECT_EQ(
+ AOM_CODEC_OK,
+ aom_codec_decode(&dec, static_cast<const uint8_t *>(pkt->data.frame.buf),
+ pkt->data.frame.sz, nullptr));
+ aom_codec_iter_t dec_iter = nullptr;
+ aom_image_t *dec_img = aom_codec_get_frame(&dec, &dec_iter);
+ ASSERT_NE(dec_img, nullptr);
+ EXPECT_EQ(dec_img->d_w, 128u);
+ EXPECT_EQ(dec_img->d_h, 128u);
+#endif
+
+ // Frame 1: Upscale to 256x256.
+ cfg.g_w = 256;
+ cfg.g_h = 256;
+ EXPECT_EQ(AOM_CODEC_OK, aom_codec_enc_config_set(&enc, &cfg));
+
+ EXPECT_EQ(&img,
+ aom_img_wrap(&img, AOM_IMG_FMT_I420, 256, 256, 1, img_data.get()));
+ EXPECT_EQ(AOM_CODEC_OK, aom_codec_encode(&enc, &img, 1, 1, 0));
+
+ iter = nullptr;
+ pkt = aom_codec_get_cx_data(&enc, &iter);
+ ASSERT_NE(pkt, nullptr);
+ EXPECT_EQ(pkt->kind, AOM_CODEC_CX_FRAME_PKT);
+ EXPECT_FALSE(pkt->data.frame.flags & AOM_FRAME_IS_KEY);
+
+#if CONFIG_AV1_DECODER
+ EXPECT_EQ(
+ AOM_CODEC_OK,
+ aom_codec_decode(&dec, static_cast<const uint8_t *>(pkt->data.frame.buf),
+ pkt->data.frame.sz, nullptr));
+ dec_iter = nullptr;
+ dec_img = aom_codec_get_frame(&dec, &dec_iter);
+ ASSERT_NE(dec_img, nullptr);
+ EXPECT_EQ(dec_img->d_w, 256u);
+ EXPECT_EQ(dec_img->d_h, 256u);
+ EXPECT_EQ(AOM_CODEC_OK, aom_codec_destroy(&dec));
+#endif
+
+ EXPECT_EQ(AOM_CODEC_OK, aom_codec_encode(&enc, nullptr, 0, 0, 0));
+ EXPECT_EQ(AOM_CODEC_OK, aom_codec_destroy(&enc));
+}
+
+TEST(EncodeForcedMaxFrameWidthHeight, DynamicUpscaleUpTo16x) {
+ constexpr size_t kImageDataSize = 2048 * 2048 + 2 * 1024 * 1024;
+ std::unique_ptr<unsigned char[]> img_data(new unsigned char[kImageDataSize]);
+ ASSERT_NE(img_data, nullptr);
+ memset(img_data.get(), 128, kImageDataSize);
+
+ aom_codec_iface_t *iface = aom_codec_av1_cx();
+ aom_codec_enc_cfg_t cfg;
+ EXPECT_EQ(AOM_CODEC_OK,
+ aom_codec_enc_config_default(iface, &cfg, AOM_USAGE_REALTIME));
+ cfg.g_w = 128;
+ cfg.g_h = 128;
+ cfg.g_forced_max_frame_width = 2048;
+ cfg.g_forced_max_frame_height = 2048;
+ cfg.g_lag_in_frames = 0;
+ cfg.kf_mode = AOM_KF_DISABLED;
+ aom_codec_ctx_t enc;
+ EXPECT_EQ(AOM_CODEC_OK, aom_codec_enc_init(&enc, iface, &cfg, 0));
+ EXPECT_EQ(AOM_CODEC_OK, aom_codec_control(&enc, AOME_SET_CPUUSED, 7));
+
+#if CONFIG_AV1_DECODER
+ aom_codec_ctx_t dec;
+ EXPECT_EQ(AOM_CODEC_OK,
+ aom_codec_dec_init(&dec, aom_codec_av1_dx(), nullptr, 0));
+#endif
+
+ // Frame 0: 128x128 keyframe.
+ aom_image_t img;
+ EXPECT_EQ(&img,
+ aom_img_wrap(&img, AOM_IMG_FMT_I420, 128, 128, 1, img_data.get()));
+ EXPECT_EQ(AOM_CODEC_OK, aom_codec_encode(&enc, &img, 0, 1, 0));
+
+ aom_codec_iter_t iter = nullptr;
+ const aom_codec_cx_pkt_t *pkt = aom_codec_get_cx_data(&enc, &iter);
+ ASSERT_NE(pkt, nullptr);
+ EXPECT_EQ(pkt->kind, AOM_CODEC_CX_FRAME_PKT);
+ EXPECT_TRUE(pkt->data.frame.flags & AOM_FRAME_IS_KEY);
+
+#if CONFIG_AV1_DECODER
+ EXPECT_EQ(
+ AOM_CODEC_OK,
+ aom_codec_decode(&dec, static_cast<const uint8_t *>(pkt->data.frame.buf),
+ pkt->data.frame.sz, nullptr));
+#endif
+
+ // Frame 1: Upscale 16x to 2048x2048.
+ cfg.g_w = 2048;
+ cfg.g_h = 2048;
+ EXPECT_EQ(AOM_CODEC_OK, aom_codec_enc_config_set(&enc, &cfg));
+
+ EXPECT_EQ(&img, aom_img_wrap(&img, AOM_IMG_FMT_I420, 2048, 2048, 1,
+ img_data.get()));
+ EXPECT_EQ(AOM_CODEC_OK, aom_codec_encode(&enc, &img, 1, 1, 0));
+
+ iter = nullptr;
+ pkt = aom_codec_get_cx_data(&enc, &iter);
+ ASSERT_NE(pkt, nullptr);
+ EXPECT_EQ(pkt->kind, AOM_CODEC_CX_FRAME_PKT);
+ EXPECT_FALSE(pkt->data.frame.flags & AOM_FRAME_IS_KEY);
+
+#if CONFIG_AV1_DECODER
+ EXPECT_EQ(
+ AOM_CODEC_OK,
+ aom_codec_decode(&dec, static_cast<const uint8_t *>(pkt->data.frame.buf),
+ pkt->data.frame.sz, nullptr));
+ aom_codec_iter_t dec_iter = nullptr;
+ aom_image_t *dec_img = aom_codec_get_frame(&dec, &dec_iter);
+ ASSERT_NE(dec_img, nullptr);
+ EXPECT_EQ(dec_img->d_w, 2048u);
+ EXPECT_EQ(dec_img->d_h, 2048u);
+ EXPECT_EQ(AOM_CODEC_OK, aom_codec_destroy(&dec));
+#endif
+
+ EXPECT_EQ(AOM_CODEC_OK, aom_codec_encode(&enc, nullptr, 0, 0, 0));
+ EXPECT_EQ(AOM_CODEC_OK, aom_codec_destroy(&enc));
+}
+
+// Values of the AV1_LEVEL enum in av1/common/enums.h, which is not part of the
+// public API.
+//
+// Passing kLevelKeepStats as the target level only switches on level statistics
+// collection. Unlike a real target level it does not constrain the encoder, so
+// it does not change what the rest of the test measures.
+constexpr int kSeqLevelIdx5_0 = 12;
+constexpr int kLevelKeepStats = 32;
+constexpr int kMaxOperatingPoints = 32;
+
+TEST(EncodeForcedMaxFrameWidthHeight,
+ SequenceLevelIndexUsesForcedMaxDimensions) {
+ constexpr size_t kImageDataSize = 2560 * 1536 + 2 * 1280 * 768;
+ std::unique_ptr<unsigned char[]> img_data(new unsigned char[kImageDataSize]);
+ ASSERT_NE(img_data, nullptr);
+ memset(img_data.get(), 128, kImageDataSize);
+
+ aom_codec_iface_t *iface = aom_codec_av1_cx();
+ aom_codec_enc_cfg_t cfg;
+ EXPECT_EQ(AOM_CODEC_OK,
+ aom_codec_enc_config_default(iface, &cfg, AOM_USAGE_REALTIME));
+ cfg.g_w = 640;
+ cfg.g_h = 384;
+ cfg.g_forced_max_frame_width = 2560;
+ cfg.g_forced_max_frame_height = 1536;
+ cfg.g_lag_in_frames = 0;
+ cfg.kf_mode = AOM_KF_DISABLED;
+ aom_codec_ctx_t enc;
+ EXPECT_EQ(AOM_CODEC_OK, aom_codec_enc_init(&enc, iface, &cfg, 0));
+ EXPECT_EQ(AOM_CODEC_OK, aom_codec_control(&enc, AOME_SET_CPUUSED, 7));
+ EXPECT_EQ(AOM_CODEC_OK, aom_codec_control(&enc, AV1E_SET_TARGET_SEQ_LEVEL_IDX,
+ kLevelKeepStats));
+
+#if CONFIG_AV1_DECODER
+ aom_codec_ctx_t dec;
+ EXPECT_EQ(AOM_CODEC_OK,
+ aom_codec_dec_init(&dec, aom_codec_av1_dx(), nullptr, 0));
+#endif
+
+ // Frame 0: 640x384 keyframe.
+ aom_image_t img;
+ EXPECT_EQ(&img,
+ aom_img_wrap(&img, AOM_IMG_FMT_I420, 640, 384, 1, img_data.get()));
+ EXPECT_EQ(AOM_CODEC_OK, aom_codec_encode(&enc, &img, 0, 1, 0));
+
+ aom_codec_iter_t iter = nullptr;
+ const aom_codec_cx_pkt_t *pkt = aom_codec_get_cx_data(&enc, &iter);
+ ASSERT_NE(pkt, nullptr);
+ EXPECT_EQ(pkt->kind, AOM_CODEC_CX_FRAME_PKT);
+
+#if CONFIG_AV1_DECODER
+ EXPECT_EQ(
+ AOM_CODEC_OK,
+ aom_codec_decode(&dec, static_cast<const uint8_t *>(pkt->data.frame.buf),
+ pkt->data.frame.sz, nullptr));
+#endif
+
+ // Frame 1: Upscale 4x to 2560x1536.
+ cfg.g_w = 2560;
+ cfg.g_h = 1536;
+ EXPECT_EQ(AOM_CODEC_OK, aom_codec_enc_config_set(&enc, &cfg));
+
+ EXPECT_EQ(&img, aom_img_wrap(&img, AOM_IMG_FMT_I420, 2560, 1536, 1,
+ img_data.get()));
+ EXPECT_EQ(AOM_CODEC_OK, aom_codec_encode(&enc, &img, 1, 1, 0));
+
+ iter = nullptr;
+ pkt = aom_codec_get_cx_data(&enc, &iter);
+ ASSERT_NE(pkt, nullptr);
+ EXPECT_EQ(pkt->kind, AOM_CODEC_CX_FRAME_PKT);
+ EXPECT_FALSE(pkt->data.frame.flags & AOM_FRAME_IS_KEY);
+
+#if CONFIG_AV1_DECODER
+ EXPECT_EQ(
+ AOM_CODEC_OK,
+ aom_codec_decode(&dec, static_cast<const uint8_t *>(pkt->data.frame.buf),
+ pkt->data.frame.sz, nullptr));
+ aom_codec_iter_t dec_iter = nullptr;
+ aom_image_t *dec_img = aom_codec_get_frame(&dec, &dec_iter);
+ ASSERT_NE(dec_img, nullptr);
+ EXPECT_EQ(dec_img->d_w, 2560u);
+ EXPECT_EQ(dec_img->d_h, 1536u);
+ EXPECT_EQ(AOM_CODEC_OK, aom_codec_destroy(&dec));
+#endif
+
+ // The level the encoder derives from the statistics it collected has to
+ // account for the upscaled frame, not just the initial 640x384 one.
+ int seq_level_idx[kMaxOperatingPoints];
+ EXPECT_EQ(AOM_CODEC_OK,
+ aom_codec_control(&enc, AV1E_GET_SEQ_LEVEL_IDX, seq_level_idx));
+ EXPECT_EQ(seq_level_idx[0], kSeqLevelIdx5_0);
+
+ EXPECT_EQ(AOM_CODEC_OK, aom_codec_encode(&enc, nullptr, 0, 0, 0));
+ EXPECT_EQ(AOM_CODEC_OK, aom_codec_destroy(&enc));
+}
+
} // namespace
diff --git a/test/frame_size_tests.cc b/test/frame_size_tests.cc
index 4ed8354ad2..96d544880e 100644
--- a/test/frame_size_tests.cc
+++ b/test/frame_size_tests.cc
@@ -222,11 +222,25 @@ TEST_P(AV1ResolutionChange, RandomInput) {
iter = nullptr;
while ((pkt = aom_codec_get_cx_data(enc.get(), &iter)) != nullptr) {
ASSERT_EQ(pkt->kind, AOM_CODEC_CX_FRAME_PKT);
- // The frame following a resolution change should be a keyframe as the
- // change is too extreme to allow previous references to be used.
- if (i == 0 || usage_ == AOM_USAGE_ALL_INTRA) {
+ // All the resolution changes above are within the reference frame
+ // scaling limits (up to 16x up and 2x down). In single pass realtime
+ // mode without lookahead, and with the maximum frame size declared up
+ // front via g_forced_max_frame_width/height, such changes are coded as
+ // inter frames that scale their references, so only the very first
+ // frame is a keyframe. Other modes force a keyframe on every
+ // resolution change.
+ const bool scales_references = usage_ == AOM_USAGE_REALTIME;
+ if (usage_ == AOM_USAGE_ALL_INTRA || frame_count == 0) {
EXPECT_NE(pkt->data.frame.flags & AOM_FRAME_IS_KEY, 0u)
<< "frame " << frame_count;
+ } else if (i == 0) {
+ if (scales_references) {
+ EXPECT_EQ(pkt->data.frame.flags & AOM_FRAME_IS_KEY, 0u)
+ << "frame " << frame_count;
+ } else {
+ EXPECT_NE(pkt->data.frame.flags & AOM_FRAME_IS_KEY, 0u)
+ << "frame " << frame_count;
+ }
}
frame_count++;
}