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