Commit bcc98e0c3a for aom
commit bcc98e0c3a88a3ad1807eca2bc179ef8d7abf091
Author: Ihor Buhaiov <freedbrt@gmail.com>
Date: Fri Nov 13 10:46:27 2026 +0100
rtc: Reset the skip flag when IntraBC wins
av1_nonrd_pick_intra_mode() does not set x->txfm_search_info.skip_txfm
when IntraBC wins, so the block keeps the value an earlier block left.
av1_encode_sb() returns early when the flag is set and codes no
residual in any plane. Plain intra blocks are safe because
encode_b_nonrd() clears their skip flag, but IntraBC blocks count as
inter blocks. The hash search matches luma only, so a skipped IntraBC
block can copy chroma of another color.
The first key frame starts with the flag at 0. A later key frame
starts with the flag of the last block of the inter frame before it,
and if that block was skipped, every IntraBC block of the key frame
is skipped. On a screen capture speeds 10 to 12 change, 7 to 9 do not.
Set the flag to 0 when IntraBC wins, so the residual is coded like in
other blocks of the frame. Taking the flag from the luma estimate and
the chroma model instead still skips most of the wrong color blocks.
Screen captures 2560x1440, aomenc --rt --cpu-used=11
--tune-content=screen, 1 thread, q 20, key frame every 15 frames,
60 frames:
worst chroma 8x8 under 30 dB key frame
on key frames per key frame bytes
capture A 4:2:0 26.90 -> 35.81 dB 7 -> 0 +0.57%
capture A 4:4:4 36.17 -> 36.73 dB 0 -> 0 +0.60%
capture B 4:2:0 35.86 -> 35.86 dB 0 -> 0 +0.62%
capture B 4:4:4 36.47 -> 36.47 dB 0 -> 0 +0.59%
Key frame luma PSNR goes up 0.04 to 0.08 dB, total bytes +0.29% to
+0.49%. Frames before the first changed key frame are bit exact.
A 694x649 crop of capture B with an active map and key frames forced
at 24 and 47: worst U 9.43 -> 40.84 dB and V 13.77 -> 41.49 dB on
4:2:0, worst luma 30.50 -> 37.03 dB on 4:4:4.
Encode time, release, base and fix alternating, median (A/A noise in
brackets):
arm64 macOS, 21 runs (4:2:0) and 9 runs (4:4:4):
4:2:0 694x649 +0.06% (-0.05%), 2560x1440 +0.47% (+0.07%)
4:4:4 694x649 -0.15% (+0.03%), 2560x1440 +0.29% (-0.16%)
x86 WSL gcc 13.3, 2 screen captures, key frame every 15 frames,
7 runs:
4:2:0 1 thread +0.36% (+0.87%), 4 threads +0.28% (+0.42%)
4:4:4 1 thread +0.47% (+0.26%), 4 threads -0.49% (-0.82%)
Test: EncodeAPI.IntraBCKeyFrameCodesChroma fails before and passes
after, default and CONFIG_REALTIME_ONLY builds. aomenc
--test-decode=fatal finds no mismatch on park_joy_90p 8 and 10 bit
4:2:0 and 4:4:4 (unchanged) and screendata 1080p 8 bit (unchanged)
and 10 bit 4:2:0 and 4:4:4 (key frame luma +0.24 to 0.46 dB, bytes
+1.1%; 4:2:0 V -0.5 to -1.6 dB at 56 to 57 dB from 1 LSB drift)
on arm64 macOS. On x86 with gcc and clang, 6 screen and AOM clips
(2 in 10 bit), 4:2:0 and 4:4:4, 1 and 4 threads, map on and off, a key
frame every 15 frames: no recon or dav1d mismatch, gcc and clang give
the same streams, bytes +0% to +2.1%, no worst block gets worse.
Change-Id: I8e26d485938021a8107453654d2490eeaf97f520
diff --git a/av1/encoder/nonrd_pickmode.c b/av1/encoder/nonrd_pickmode.c
index b0c67fc470..0277d73da2 100644
--- a/av1/encoder/nonrd_pickmode.c
+++ b/av1/encoder/nonrd_pickmode.c
@@ -1950,6 +1950,8 @@ void av1_nonrd_pick_intra_mode(AV1_COMP *cpi, MACROBLOCK *x, RD_STATS *rd_cost,
best_mode = DC_PRED;
mi->use_intrabc = 1;
mi->mv[0] = best_dv;
+ // Code the residual. The skip flag may be left from an earlier block.
+ x->txfm_search_info.skip_txfm = 0;
mi->uv_mode = UV_DC_PRED;
mi->motion_mode = SIMPLE_TRANSLATION;
mi->interp_filters = av1_broadcast_interp_filter(BILINEAR);
diff --git a/test/encode_api_test.cc b/test/encode_api_test.cc
index 7a9e280b87..b72872b913 100644
--- a/test/encode_api_test.cc
+++ b/test/encode_api_test.cc
@@ -1980,6 +1980,94 @@ TEST(EncodeAPI, IntraBCFarCopyAboveRealtime) {
TEST(EncodeAPI, IntraBCFarCopyAboveRightRealtime) {
IntraBCFarCopyTest(2120, 576, 2056, 0, 0, 512);
}
+
+// Encodes a static screen and then forces a key frame. The bottom half has the
+// luma of the top half but another color, so the IntraBC blocks there must
+// code their chroma residual. The skipped blocks of the inter frames before
+// the key frame must not make them skip it.
+TEST(EncodeAPI, IntraBCKeyFrameCodesChroma) {
+ constexpr int kWidth = 256;
+ constexpr int kHeight = 256;
+ constexpr int kFrames = 4;
+ constexpr int kKeyFrame = 3;
+ aom_codec_iface_t *iface = aom_codec_av1_cx();
+ aom_codec_enc_cfg_t cfg;
+ ASSERT_EQ(aom_codec_enc_config_default(iface, &cfg, AOM_USAGE_REALTIME),
+ AOM_CODEC_OK);
+ cfg.g_w = kWidth;
+ cfg.g_h = kHeight;
+ cfg.g_lag_in_frames = 0;
+ cfg.g_threads = 1;
+ cfg.rc_min_quantizer = 20;
+ cfg.rc_max_quantizer = 20;
+
+ aom_codec_ctx_t enc;
+ ASSERT_EQ(aom_codec_enc_init(&enc, iface, &cfg, 0), AOM_CODEC_OK);
+ ASSERT_EQ(aom_codec_control(&enc, AOME_SET_CPUUSED, 11), AOM_CODEC_OK);
+ ASSERT_EQ(aom_codec_control(&enc, AV1E_SET_TUNE_CONTENT, AOM_CONTENT_SCREEN),
+ AOM_CODEC_OK);
+ ASSERT_EQ(aom_codec_control(&enc, AV1E_SET_ENABLE_INTRABC, 1), AOM_CODEC_OK);
+
+ aom_codec_dec_cfg_t dec_cfg = {};
+ dec_cfg.allow_lowbitdepth = 1;
+ aom_codec_ctx_t dec;
+ ASSERT_EQ(aom_codec_dec_init(&dec, aom_codec_av1_dx(), &dec_cfg, 0),
+ AOM_CODEC_OK);
+
+ aom_image_t *img =
+ aom_img_alloc(nullptr, AOM_IMG_FMT_I420, kWidth, kHeight, 1);
+ ASSERT_NE(img, nullptr);
+ ::libaom_test::ACMRandom rnd(::libaom_test::ACMRandom::DeterministicSeed());
+ for (int r = 0; r < kHeight / 2; ++r) {
+ uint8_t *top = img->planes[0] + r * img->stride[0];
+ for (int c = 0; c < kWidth; ++c) {
+ top[c] = (rnd.Rand8() & 1) ? 16 : 235;
+ }
+ memcpy(top + (kHeight / 2) * img->stride[0], top, kWidth);
+ }
+ for (int plane = 1; plane < 3; ++plane) {
+ for (int r = 0; r < kHeight / 2; ++r) {
+ uint8_t *row = img->planes[plane] + r * img->stride[plane];
+ const uint8_t bottom_color = plane == 1 ? 64 : 192;
+ memset(row, r < kHeight / 4 ? 128 : bottom_color, kWidth / 2);
+ }
+ }
+
+ for (int i = 0; i < kFrames; ++i) {
+ const aom_enc_frame_flags_t flags = i == kKeyFrame ? AOM_EFLAG_FORCE_KF : 0;
+ ASSERT_EQ(aom_codec_encode(&enc, img, i, 1, flags), AOM_CODEC_OK);
+ aom_codec_iter_t iter = nullptr;
+ while (const aom_codec_cx_pkt_t *pkt = aom_codec_get_cx_data(&enc, &iter)) {
+ if (pkt->kind != AOM_CODEC_CX_FRAME_PKT) continue;
+ ASSERT_EQ(aom_codec_decode(
+ &dec, static_cast<const uint8_t *>(pkt->data.frame.buf),
+ pkt->data.frame.sz, nullptr),
+ AOM_CODEC_OK);
+ aom_codec_iter_t dec_iter = nullptr;
+ const aom_image_t *decoded = aom_codec_get_frame(&dec, &dec_iter);
+ ASSERT_NE(decoded, nullptr);
+ ASSERT_EQ(decoded->fmt, AOM_IMG_FMT_I420);
+ for (int plane = 1; plane < 3; ++plane) {
+ int64_t sse = 0;
+ for (int r = 0; r < kHeight / 2; ++r) {
+ const uint8_t *src = img->planes[plane] + r * img->stride[plane];
+ const uint8_t *out =
+ decoded->planes[plane] + r * decoded->stride[plane];
+ for (int c = 0; c < kWidth / 2; ++c) {
+ const int diff = src[c] - out[c];
+ sse += diff * diff;
+ }
+ }
+ EXPECT_LT(sse, kWidth * kHeight / 4 * 16)
+ << "frame " << i << " plane " << plane;
+ }
+ }
+ }
+
+ aom_img_free(img);
+ ASSERT_EQ(aom_codec_destroy(&dec), AOM_CODEC_OK);
+ ASSERT_EQ(aom_codec_destroy(&enc), AOM_CODEC_OK);
+}
#endif // CONFIG_AV1_DECODER
#if !CONFIG_REALTIME_ONLY