Commit b7cd5ffb57 for aom

commit b7cd5ffb57a80883551a69be9fec05e5f4520eb0
Author: Erik Språng <sprang@google.com>
Date:   Thu Oct 8 09:39:43 2026 +0200

    rtc-svc: Fix refresh and unused ref_idx in set_ref_frame_config

    With AV1E_SET_SVC_REF_FRAME_CONFIG, refresh[] is indexed by buffer slot
    and the ref_idx[] entries of unused references are don't-cares, but:

    - A slot was only refreshed, and its refresh tracked, if a ref_idx[]
      entry pointed to it.
    - Refreshing a slot that an unused reference pointed to (e.g. slot 0
      in a zero-initialized config) could make the frame a golden/altref
      update, which affects rate control and RD decisions.
    - All seven references are signaled, and decoders reject frames with
      a reference of invalid size, e.g. from another spatial layer. With
      one spatial layer and no order hints, the bitstream writer replaced
      unused references, but the encoder did not, which could break frame
      size signaling.

    Build the refresh mask from refresh[], only classify refreshes of used
    references, and without order hints, remap unused references to the
    first used one in the encoder instead of the bitstream writer. With
    order hints, unused references also affect decoding (e.g. temporal MV
    projection), so they are left as configured.

    Only encoders using set_ref_frame_config, directly or via fixed SVC
    mode, are affected. For the affected svc_encoder_rtc layering modes,
    BD-rate changes have no consistent direction (mean -0.02%).

    Also update the aom_svc_ref_frame_config_t comments in aomcx.h to
    describe the new behavior.

    Bug: webrtc:496266459
    Change-Id: I2bb6a2274c0a441af54709928edec6a4f8661be0

diff --git a/aom/aomcx.h b/aom/aomcx.h
index 23d10a26f5..b7e0886941 100644
--- a/aom/aomcx.h
+++ b/aom/aomcx.h
@@ -1891,21 +1891,28 @@ typedef struct aom_svc_params {

 /*!\brief Parameters for setting ref frame config */
 typedef struct aom_svc_ref_frame_config {
-  // Three arrays need to be set: reference[], ref_id[], refresh[].
+  // Three arrays need to be set: reference[], ref_idx[], refresh[].
   // reference[i]: is a boolean flag to indicate which of the 7 possible
   // references are used for prediction. Values are 0 (not used as reference)
   // or 1 (use as reference). The index 0 - 6 refers to the references:
   // last(0), last2(1), last3(2), golden(3), bwdref(4), altref2(5), altref(6).
-  // ref_idx[i]: maps a reference to one of the 8 buffers slots, values are
-  // 0 - 7. The ref_idx for a unused reference (reference[i] = 1, and not used
-  // for refresh, see below) can be set to the ref_idx of the first reference
-  // used (usually LAST).
-  // refresh[i] is a boolean flag to indicate if a buffer is updated/refreshed
-  // with the current encoded frame. Values are 0 (no refresh) or 1 (refresh).
-  // The refresh is done internally by looking at the ref_idx[j], for j = 0 - 6,
-  // so to refresh a buffer slot (i) a reference must be mapped to that slot
-  // (i = ref_idx[j]).
-  // Examples for usage (for RTC encoding) are in: examples/svc_encoder_rtc.c.
+  // ref_idx[i]: maps a reference to one of the 8 buffer slots, values are
+  // 0 - 7. Inter frames signal all seven references in the bitstream,
+  // including the unused ones (reference[i] = 0):
+  // - With order hints disabled (see AV1E_SET_ENABLE_ORDER_HINT), the encoder
+  //   points each unused reference to the slot of the first used reference
+  //   instead, so their ref_idx[i] may point to any slot, even one holding a
+  //   frame that the current frame cannot reference, e.g. due to its size.
+  // - With order hints enabled, unused references also affect the decoding
+  //   process (e.g. temporal motion vector projection) and are signaled as
+  //   configured, so their ref_idx[i] must point to slots holding valid
+  //   references for the current frame, e.g. the slot of the first used
+  //   reference (usually LAST).
+  // refresh[i]: is a boolean flag to indicate if buffer slot i (0 - 7) is
+  // updated/refreshed with the current encoded frame. Values are 0 (no refresh)
+  // or 1 (refresh). A slot can be refreshed whether or not any ref_idx[] entry
+  // points to it. Key frames refresh all slots.
+  // Examples for usage (for RTC encoding) are in: examples/svc_encoder_rtc.cc.
   int reference[7]; /**< Reference flag for each of the 7 references. */
   /*! Buffer slot index (0..7) for each of 7 references indexed above. */
   int ref_idx[7];
diff --git a/av1/encoder/bitstream.c b/av1/encoder/bitstream.c
index 69751959e9..efac97a036 100644
--- a/av1/encoder/bitstream.c
+++ b/av1/encoder/bitstream.c
@@ -3102,33 +3102,11 @@ static inline void write_uncompressed_header_obu(
       const int gld_ref = get_ref_frame_map_idx(cm, GOLDEN_FRAME);
       aom_wb_write_literal(wb, gld_ref, REF_FRAMES_LOG2);
     }
-    int first_ref_map_idx = INVALID_IDX;
-    if (cpi->ppi->rtc_ref.set_ref_frame_config) {
-      for (ref_frame = LAST_FRAME; ref_frame <= ALTREF_FRAME; ++ref_frame) {
-        if (cpi->ppi->rtc_ref.reference[ref_frame - 1] == 1) {
-          first_ref_map_idx = cpi->ppi->rtc_ref.ref_idx[ref_frame - 1];
-          break;
-        }
-      }
-    }
     for (ref_frame = LAST_FRAME; ref_frame <= ALTREF_FRAME; ++ref_frame) {
       assert(get_ref_frame_map_idx(cm, ref_frame) != INVALID_IDX);
       if (!current_frame->frame_refs_short_signaling) {
-        if (cpi->ppi->rtc_ref.set_ref_frame_config &&
-            first_ref_map_idx != INVALID_IDX &&
-            cpi->svc.number_spatial_layers == 1 &&
-            !seq_params->order_hint_info.enable_order_hint) {
-          // For the usage of set_ref_frame_config:
-          // for any reference not used set their ref_map_idx
-          // to the first used reference.
-          const int map_idx = cpi->ppi->rtc_ref.reference[ref_frame - 1]
-                                  ? get_ref_frame_map_idx(cm, ref_frame)
-                                  : first_ref_map_idx;
-          aom_wb_write_literal(wb, map_idx, REF_FRAMES_LOG2);
-        } else {
-          aom_wb_write_literal(wb, get_ref_frame_map_idx(cm, ref_frame),
-                               REF_FRAMES_LOG2);
-        }
+        aom_wb_write_literal(wb, get_ref_frame_map_idx(cm, ref_frame),
+                             REF_FRAMES_LOG2);
       }
       if (seq_params->frame_id_numbers_present_flag) {
         int i = get_ref_frame_map_idx(cm, ref_frame);
diff --git a/av1/encoder/encode_strategy.c b/av1/encoder/encode_strategy.c
index d660b39dce..02be53f227 100644
--- a/av1/encoder/encode_strategy.c
+++ b/av1/encoder/encode_strategy.c
@@ -656,10 +656,10 @@ int av1_get_refresh_frame_flags(
   if (ext_refresh_frame_flags->update_pending) {
     if (rtc_ref->set_ref_frame_config ||
         use_rtc_reference_structure_one_layer(cpi)) {
-      for (unsigned int i = 0; i < INTER_REFS_PER_FRAME; i++) {
-        int ref_frame_map_idx = rtc_ref->ref_idx[i];
-        refresh_mask |= rtc_ref->refresh[ref_frame_map_idx]
-                        << ref_frame_map_idx;
+      // refresh[] is indexed by buffer slot. A refreshed slot does not have to
+      // be pointed to by any of the ref_idx[] entries.
+      for (unsigned int i = 0; i < REF_FRAMES; i++) {
+        refresh_mask |= rtc_ref->refresh[i] << i;
       }
       return refresh_mask;
     }
@@ -1641,8 +1641,31 @@ int av1_encode_strategy(AV1_COMP *const cpi, size_t *const size,
                            cpi->gf_frame_index, 1, cm->remapped_ref_idx);
       } else if (cpi->ppi->rtc_ref.set_ref_frame_config ||
                  use_rtc_reference_structure_one_layer(cpi)) {
-        for (unsigned int i = 0; i < INTER_REFS_PER_FRAME; i++)
-          cm->remapped_ref_idx[i] = cpi->ppi->rtc_ref.ref_idx[i];
+        // With set_ref_frame_config, the ref_idx[] entries of references
+        // that are not in use are don't-cares, and may point to slots that
+        // are not valid references for this frame, e.g. a slot of another
+        // spatial layer. All seven references are signaled in the bitstream
+        // though, and decoders reject a frame with an invalid reference.
+        // Without order hints, unused references otherwise only affect how
+        // the frame size is signaled, so point them to the first reference in
+        // use. With order hints, they also affect the decoding process (e.g.
+        // temporal motion vector projection), so leave them as configured.
+        int first_ref_map_idx = INVALID_IDX;
+        if (cpi->ppi->rtc_ref.set_ref_frame_config &&
+            !cm->seq_params->order_hint_info.enable_order_hint) {
+          for (unsigned int i = 0; i < INTER_REFS_PER_FRAME; i++) {
+            if (cpi->ppi->rtc_ref.reference[i]) {
+              first_ref_map_idx = cpi->ppi->rtc_ref.ref_idx[i];
+              break;
+            }
+          }
+        }
+        for (unsigned int i = 0; i < INTER_REFS_PER_FRAME; i++) {
+          cm->remapped_ref_idx[i] = (first_ref_map_idx != INVALID_IDX &&
+                                     !cpi->ppi->rtc_ref.reference[i])
+                                        ? first_ref_map_idx
+                                        : cpi->ppi->rtc_ref.ref_idx[i];
+        }
       }
     }

diff --git a/av1/encoder/encoder.c b/av1/encoder/encoder.c
index 0250f97d44..5a606d304a 100644
--- a/av1/encoder/encoder.c
+++ b/av1/encoder/encoder.c
@@ -5984,14 +5984,21 @@ static void rtc_set_updates_ref_frame_config(
     ExtRefreshFrameFlagsInfo *const ext_refresh_frame_flags,
     RTC_REF *const rtc_ref) {
   ext_refresh_frame_flags->update_pending = 1;
-  ext_refresh_frame_flags->last_frame = rtc_ref->refresh[rtc_ref->ref_idx[0]];
-  ext_refresh_frame_flags->golden_frame = rtc_ref->refresh[rtc_ref->ref_idx[3]];
+  // The ref_idx[] entries of references that are not in use are don't-cares
+  // for the application, and often all point to the same slot, which may be
+  // the one being refreshed. Only classify a refresh as an update of a
+  // particular reference (which e.g. makes rate control treat the frame as a
+  // golden or altref update) if that reference is in use.
+  ext_refresh_frame_flags->last_frame =
+      rtc_ref->reference[0] && rtc_ref->refresh[rtc_ref->ref_idx[0]];
+  ext_refresh_frame_flags->golden_frame =
+      rtc_ref->reference[3] && rtc_ref->refresh[rtc_ref->ref_idx[3]];
   ext_refresh_frame_flags->bwd_ref_frame =
-      rtc_ref->refresh[rtc_ref->ref_idx[4]];
+      rtc_ref->reference[4] && rtc_ref->refresh[rtc_ref->ref_idx[4]];
   ext_refresh_frame_flags->alt2_ref_frame =
-      rtc_ref->refresh[rtc_ref->ref_idx[5]];
+      rtc_ref->reference[5] && rtc_ref->refresh[rtc_ref->ref_idx[5]];
   ext_refresh_frame_flags->alt_ref_frame =
-      rtc_ref->refresh[rtc_ref->ref_idx[6]];
+      rtc_ref->reference[6] && rtc_ref->refresh[rtc_ref->ref_idx[6]];
   rtc_ref->non_reference_frame = 1;
   for (int i = 0; i < REF_FRAMES; i++) {
     if (rtc_ref->refresh[i] == 1) {
diff --git a/av1/encoder/svc_layercontext.c b/av1/encoder/svc_layercontext.c
index 0f549a14fc..732bc22bb9 100644
--- a/av1/encoder/svc_layercontext.c
+++ b/av1/encoder/svc_layercontext.c
@@ -307,12 +307,10 @@ void av1_svc_update_buffer_slot_refreshed(AV1_COMP *const cpi) {
       rtc_ref->buffer_spatial_layer[i] = svc->spatial_layer_id;
     }
   } else if (rtc_ref->set_ref_frame_config) {
-    for (unsigned int i = 0; i < INTER_REFS_PER_FRAME; i++) {
-      const int ref_frame_map_idx = rtc_ref->ref_idx[i];
-      if (rtc_ref->refresh[ref_frame_map_idx]) {
-        rtc_ref->buffer_time_index[ref_frame_map_idx] = current_frame;
-        rtc_ref->buffer_spatial_layer[ref_frame_map_idx] =
-            svc->spatial_layer_id;
+    for (unsigned int i = 0; i < REF_FRAMES; i++) {
+      if (rtc_ref->refresh[i]) {
+        rtc_ref->buffer_time_index[i] = current_frame;
+        rtc_ref->buffer_spatial_layer[i] = svc->spatial_layer_id;
       }
     }
   }
diff --git a/test/encode_api_test.cc b/test/encode_api_test.cc
index 1b564e95c0..352a51c633 100644
--- a/test/encode_api_test.cc
+++ b/test/encode_api_test.cc
@@ -3909,4 +3909,471 @@ TEST(EncodeAPI, Buganizer567944558) {
   }
 }

+// Fills `img` with deterministic content that changes from frame to frame, so
+// that inter prediction and rate control have something to work with.
+static void FillMovingPattern(aom_image_t *img, int frame) {
+  for (unsigned int r = 0; r < img->d_h; ++r) {
+    for (unsigned int c = 0; c < img->d_w; ++c) {
+      const int texture = (((r + frame) / 8 + (c + 2 * frame) / 8) & 1) * 32;
+      img->planes[AOM_PLANE_Y][r * img->stride[AOM_PLANE_Y] + c] =
+          static_cast<uint8_t>((r + 2 * c + 3 * frame + texture) & 0xff);
+    }
+  }
+  for (int plane = AOM_PLANE_U; plane <= AOM_PLANE_V; ++plane) {
+    const unsigned int h =
+        (img->d_h + img->y_chroma_shift) >> img->y_chroma_shift;
+    for (unsigned int r = 0; r < h; ++r) {
+      memset(img->planes[plane] + r * img->stride[plane], 128,
+             (img->d_w + img->x_chroma_shift) >> img->x_chroma_shift);
+    }
+  }
+}
+
+#if CONFIG_AV1_DECODER
+// AV1E_SET_SVC_REF_FRAME_CONFIG: the refresh[] flags are indexed by buffer
+// slot, and a frame may refresh a slot that none of the seven ref_idx[] entries
+// point to. A typical example is a "ping-pong" buffer structure, where each
+// frame predicts from one slot (the only reference in use) and stores its
+// reconstruction in another slot, which it does not reference.
+//
+// Such refreshes used to be dropped: the slot was left out of
+// refresh_frame_flags, so it silently kept its old content. This test checks,
+// on the decoder side, which slots each frame refreshes.
+TEST(EncodeAPI, SvcRefFrameConfigRefreshesSlotNotInRefIdx) {
+  aom_codec_iface_t *const 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 = 64;
+  cfg.g_h = 64;
+  cfg.g_lag_in_frames = 0;
+  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, 9), AOM_CODEC_OK);
+  aom_codec_ctx_t dec;
+  ASSERT_EQ(aom_codec_dec_init(&dec, aom_codec_av1_dx(), nullptr, 0),
+            AOM_CODEC_OK);
+  aom_image_t *img =
+      aom_img_alloc(nullptr, AOM_IMG_FMT_I420, cfg.g_w, cfg.g_h, 1);
+  ASSERT_NE(img, nullptr);
+
+  // Frame 0 is a key frame, which refreshes all slots. Frame n > 0 predicts
+  // from slot n - 1 and refreshes slot n. All seven ref_idx[] entries point
+  // to the predicted slot, so the refreshed slot is not among them.
+  for (int frame = 0; frame < 4; ++frame) {
+    SCOPED_TRACE(frame);
+    aom_svc_ref_frame_config_t ref_cfg = {};
+    if (frame > 0) {
+      for (int i = 0; i < 7; ++i) ref_cfg.ref_idx[i] = frame - 1;
+      ref_cfg.reference[0] = 1;  // LAST.
+    }
+    ref_cfg.refresh[frame] = 1;
+    ASSERT_EQ(aom_codec_control(&enc, AV1E_SET_SVC_REF_FRAME_CONFIG, &ref_cfg),
+              AOM_CODEC_OK);
+    FillMovingPattern(img, frame);
+    ASSERT_EQ(aom_codec_encode(&enc, img, frame, 1, 0), AOM_CODEC_OK);
+    aom_codec_iter_t iter = nullptr;
+    const aom_codec_cx_pkt_t *pkt = aom_codec_get_cx_data(&enc, &iter);
+    ASSERT_NE(pkt, nullptr);
+    ASSERT_EQ(pkt->kind, AOM_CODEC_CX_FRAME_PKT);
+    ASSERT_EQ(aom_codec_decode(
+                  &dec, static_cast<const uint8_t *>(pkt->data.frame.buf),
+                  pkt->data.frame.sz, nullptr),
+              AOM_CODEC_OK);
+    int ref_updates = 0;
+    ASSERT_EQ(aom_codec_control(&dec, AOMD_GET_LAST_REF_UPDATES, &ref_updates),
+              AOM_CODEC_OK);
+    EXPECT_EQ(ref_updates, frame == 0 ? 0xff : 1 << frame);
+  }
+
+  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
+
+// AV1E_SET_SVC_REF_FRAME_CONFIG: the encoder keeps track of when each buffer
+// slot was last refreshed. Among other things, this is used to detect recovery
+// frames with RPS (reference picture selection): if a frame's closest
+// reference is several frames old, but the previous frame did predict from its
+// predecessor, the frame is encoded at a lower QP (see
+// set_flag_rps_bias_recovery_frame()).
+//
+// This bookkeeping used to only be done for refreshed slots that one of the
+// ref_idx[] entries points to. This test runs two encoders with the same RPS
+// structure: frame n is stored in slot n % 6 and predicts from frame n - 1,
+// except for every 8th frame, which predicts from frame n - 5. LAST is the
+// only reference in use. The encoders only differ in where the unused
+// references point:
+//  - "Aliased": the refreshed slot is aliased as LAST2, and the other unused
+//    references point to the predicted slot.
+//  - "Not aliased": all seven ref_idx[] entries point to the predicted slot,
+//    so the refreshed slot is not among them.
+// Both encoders must produce frames with identical QPs and sizes.
+TEST(EncodeAPI, SvcRefFrameConfigTracksRefreshOfSlotNotInRefIdx) {
+  constexpr int kNumFrames = 20;
+  constexpr int kNumSlots = 6;
+  aom_codec_iface_t *const 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 = 320;
+  cfg.g_h = 240;
+  cfg.g_lag_in_frames = 0;
+  cfg.rc_end_usage = AOM_CBR;
+  cfg.rc_target_bitrate = 300;
+
+  aom_codec_ctx_t enc_aliased;
+  aom_codec_ctx_t enc_not_aliased;
+  ASSERT_EQ(aom_codec_enc_init(&enc_aliased, iface, &cfg, 0), AOM_CODEC_OK);
+  ASSERT_EQ(aom_codec_enc_init(&enc_not_aliased, iface, &cfg, 0), AOM_CODEC_OK);
+  for (aom_codec_ctx_t *enc : { &enc_aliased, &enc_not_aliased }) {
+    ASSERT_EQ(aom_codec_control(enc, AOME_SET_CPUUSED, 9), AOM_CODEC_OK);
+    // Order hints are off, as is common for RTC. This also ensures that the
+    // two encoders' frame headers have the same size.
+    ASSERT_EQ(aom_codec_control(enc, AV1E_SET_ENABLE_ORDER_HINT, 0),
+              AOM_CODEC_OK);
+  }
+  aom_image_t *img =
+      aom_img_alloc(nullptr, AOM_IMG_FMT_I420, cfg.g_w, cfg.g_h, 1);
+  ASSERT_NE(img, nullptr);
+
+  for (int frame = 0; frame < kNumFrames; ++frame) {
+    SCOPED_TRACE(frame);
+    const int refreshed_slot = frame % kNumSlots;
+    int predicted_slot = 0;
+    if (frame > 0) {
+      const int predicted_frame = frame % 8 == 0 ? frame - 5 : frame - 1;
+      predicted_slot = predicted_frame % kNumSlots;
+    }
+    aom_svc_ref_frame_config_t not_aliased_cfg = {};
+    for (int i = 0; i < 7; ++i) not_aliased_cfg.ref_idx[i] = predicted_slot;
+    not_aliased_cfg.reference[0] = frame > 0;  // LAST.
+    not_aliased_cfg.refresh[refreshed_slot] = 1;
+    aom_svc_ref_frame_config_t aliased_cfg = not_aliased_cfg;
+    aliased_cfg.ref_idx[1] = refreshed_slot;  // LAST2.
+
+    FillMovingPattern(img, frame);
+    int qp[2];
+    size_t size[2];
+    for (int i = 0; i < 2; ++i) {
+      aom_codec_ctx_t *enc = i == 0 ? &enc_aliased : &enc_not_aliased;
+      ASSERT_EQ(aom_codec_control(enc, AV1E_SET_SVC_REF_FRAME_CONFIG,
+                                  i == 0 ? &aliased_cfg : &not_aliased_cfg),
+                AOM_CODEC_OK);
+      ASSERT_EQ(aom_codec_encode(enc, img, frame, 1, 0), AOM_CODEC_OK);
+      aom_codec_iter_t iter = nullptr;
+      const aom_codec_cx_pkt_t *pkt = aom_codec_get_cx_data(enc, &iter);
+      ASSERT_NE(pkt, nullptr);
+      ASSERT_EQ(pkt->kind, AOM_CODEC_CX_FRAME_PKT);
+      size[i] = pkt->data.frame.sz;
+      ASSERT_EQ(aom_codec_control(enc, AOME_GET_LAST_QUANTIZER, &qp[i]),
+                AOM_CODEC_OK);
+    }
+    EXPECT_EQ(qp[0], qp[1]);
+    EXPECT_EQ(size[0], size[1]);
+  }
+
+  aom_img_free(img);
+  ASSERT_EQ(aom_codec_destroy(&enc_aliased), AOM_CODEC_OK);
+  ASSERT_EQ(aom_codec_destroy(&enc_not_aliased), AOM_CODEC_OK);
+}
+
+// AV1E_SET_SVC_REF_FRAME_CONFIG: the ref_idx[] entries of references whose
+// reference[] flag is 0 are don't-cares, and must not affect the encoding.
+//
+// This test runs two encoders with the same prediction structure (every frame
+// predicts from the previous frame's reconstruction, using LAST only) that
+// only differ in which buffer slots they use:
+//  - "In place": every frame predicts from slot 0 and refreshes slot 0. The six
+//    unused references also point to slot 0, which is what a zero-initialized
+//    config gives (libaom's fixed SVC mode does the same for base layer frames
+//    on the lowest spatial layer).
+//  - "Ping-pong": frames alternate between predicting from slot 0 and
+//    refreshing slot 1, and vice versa. The refreshed slot is aliased as LAST2
+//    (like libaom's internal single layer RTC structure does) and the other
+//    unused references point to the predicted slot.
+// Both encoders must produce frames with identical QPs and sizes.
+//
+// Previously, the in-place encoder treated every frame as a golden and altref
+// update, because the unused GOLDEN/BWDREF/ALTREF aliases pointed to the
+// refreshed slot. How that affected the encoding depends on the rate control
+// mode, see the tests below.
+static void SvcRefFrameConfigUnusedAliasesTest(aom_rc_mode rc_mode) {
+  constexpr int kNumFrames = 20;
+  aom_codec_iface_t *const 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 = 320;
+  cfg.g_h = 240;
+  cfg.g_lag_in_frames = 0;
+  cfg.rc_end_usage = rc_mode;
+  cfg.rc_target_bitrate = 300;
+
+  aom_codec_ctx_t enc_in_place;
+  aom_codec_ctx_t enc_ping_pong;
+  ASSERT_EQ(aom_codec_enc_init(&enc_in_place, iface, &cfg, 0), AOM_CODEC_OK);
+  ASSERT_EQ(aom_codec_enc_init(&enc_ping_pong, iface, &cfg, 0), AOM_CODEC_OK);
+  for (aom_codec_ctx_t *enc : { &enc_in_place, &enc_ping_pong }) {
+    ASSERT_EQ(aom_codec_control(enc, AOME_SET_CPUUSED, 9), AOM_CODEC_OK);
+    ASSERT_EQ(aom_codec_control(enc, AOME_SET_CQ_LEVEL, 40), AOM_CODEC_OK);
+    // Order hints are off, as is common for RTC. This also ensures that the
+    // two encoders' frame headers have the same size.
+    ASSERT_EQ(aom_codec_control(enc, AV1E_SET_ENABLE_ORDER_HINT, 0),
+              AOM_CODEC_OK);
+  }
+  aom_image_t *img =
+      aom_img_alloc(nullptr, AOM_IMG_FMT_I420, cfg.g_w, cfg.g_h, 1);
+  ASSERT_NE(img, nullptr);
+
+  for (int frame = 0; frame < kNumFrames; ++frame) {
+    SCOPED_TRACE(frame);
+    aom_svc_ref_frame_config_t in_place_cfg = {};
+    in_place_cfg.reference[0] = frame > 0;  // LAST.
+    in_place_cfg.refresh[0] = 1;
+
+    aom_svc_ref_frame_config_t ping_pong_cfg = {};
+    const int predicted_slot = (frame + 1) % 2;
+    const int refreshed_slot = frame % 2;
+    for (int i = 0; i < 7; ++i) ping_pong_cfg.ref_idx[i] = predicted_slot;
+    ping_pong_cfg.ref_idx[1] = refreshed_slot;  // LAST2.
+    ping_pong_cfg.reference[0] = frame > 0;     // LAST.
+    ping_pong_cfg.refresh[refreshed_slot] = 1;
+
+    FillMovingPattern(img, frame);
+    int qp[2];
+    size_t size[2];
+    for (int i = 0; i < 2; ++i) {
+      aom_codec_ctx_t *enc = i == 0 ? &enc_in_place : &enc_ping_pong;
+      ASSERT_EQ(aom_codec_control(enc, AV1E_SET_SVC_REF_FRAME_CONFIG,
+                                  i == 0 ? &in_place_cfg : &ping_pong_cfg),
+                AOM_CODEC_OK);
+      ASSERT_EQ(aom_codec_encode(enc, img, frame, 1, 0), AOM_CODEC_OK);
+      aom_codec_iter_t iter = nullptr;
+      const aom_codec_cx_pkt_t *pkt = aom_codec_get_cx_data(enc, &iter);
+      ASSERT_NE(pkt, nullptr);
+      ASSERT_EQ(pkt->kind, AOM_CODEC_CX_FRAME_PKT);
+      size[i] = pkt->data.frame.sz;
+      ASSERT_EQ(aom_codec_control(enc, AOME_GET_LAST_QUANTIZER, &qp[i]),
+                AOM_CODEC_OK);
+    }
+    EXPECT_EQ(qp[0], qp[1]);
+    EXPECT_EQ(size[0], size[1]);
+  }
+
+  aom_img_free(img);
+  ASSERT_EQ(aom_codec_destroy(&enc_in_place), AOM_CODEC_OK);
+  ASSERT_EQ(aom_codec_destroy(&enc_ping_pong), AOM_CODEC_OK);
+}
+
+// In single layer CBR mode, among other things, the average inter frame QP is
+// not updated on golden and altref updates.
+TEST(EncodeAPI, SvcRefFrameConfigUnusedAliasesDoNotAffectEncodingInCbrMode) {
+  SvcRefFrameConfigUnusedAliasesTest(AOM_CBR);
+}
+
+// In AOM_Q mode, golden and altref updates are encoded at a lower QP than the
+// configured cq-level.
+TEST(EncodeAPI, SvcRefFrameConfigUnusedAliasesDoNotAffectEncodingInQMode) {
+  SvcRefFrameConfigUnusedAliasesTest(AOM_Q);
+}
+
+#if CONFIG_AV1_DECODER
+// Encodes the next frame from `enc`, and decodes it with `dec`. Returns the
+// decoder's status, and the size of the decoded frame in `decoded_w` and
+// `decoded_h`.
+static aom_codec_err_t EncodeAndDecodeFrame(
+    aom_codec_ctx_t *enc, aom_codec_ctx_t *dec, const aom_image_t *img,
+    aom_codec_pts_t pts, unsigned int *decoded_w, unsigned int *decoded_h) {
+  *decoded_w = 0;
+  *decoded_h = 0;
+  EXPECT_EQ(aom_codec_encode(enc, img, pts, 1, 0), AOM_CODEC_OK);
+  aom_codec_iter_t iter = nullptr;
+  const aom_codec_cx_pkt_t *pkt = aom_codec_get_cx_data(enc, &iter);
+  EXPECT_NE(pkt, nullptr);
+  if (pkt == nullptr) return AOM_CODEC_ERROR;
+  EXPECT_EQ(pkt->kind, AOM_CODEC_CX_FRAME_PKT);
+  const aom_codec_err_t status =
+      aom_codec_decode(dec, static_cast<const uint8_t *>(pkt->data.frame.buf),
+                       pkt->data.frame.sz, nullptr);
+  if (status != AOM_CODEC_OK) return status;
+  aom_codec_iter_t dec_iter = nullptr;
+  const aom_image_t *decoded = aom_codec_get_frame(dec, &dec_iter);
+  if (decoded != nullptr) {
+    *decoded_w = decoded->d_w;
+    *decoded_h = decoded->d_h;
+  }
+  return status;
+}
+
+// AV1E_SET_SVC_REF_FRAME_CONFIG: the ref_idx[] entries of references whose
+// reference[] flag is 0 are don't-cares, even if they point to a slot that is
+// not a valid reference for the current frame. The frame header signals all
+// seven references though, and decoders (libaom, dav1d) reject a frame if any
+// of them is more than 2x larger, or more than 16x smaller, than the frame.
+//
+// This test encodes two spatial layers, with the lower layer at 1/4 of the
+// resolution of the upper layer. Each layer predicts from its own previous
+// frame (LAST): slot 0 for the lower layer and slot 1 for the upper layer. The
+// upper layer also predicts from the lower layer's frame (GOLDEN, slot 0). All
+// unused references point to slot 1, which for the lower layer is a 4x larger
+// frame from the upper layer, and so an invalid reference.
+//
+// Order hints are off, as is common for RTC. With order hints, unused
+// references also affect the decoding process, so the encoder leaves them as
+// configured, and the application must make sure that they are valid.
+TEST(EncodeAPI, SvcRefFrameConfigUnusedRefIdxMayPointToInvalidReference) {
+  aom_codec_iface_t *const 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 = 320;
+  cfg.g_h = 240;
+  cfg.g_lag_in_frames = 0;
+  cfg.rc_end_usage = AOM_CBR;
+  cfg.rc_target_bitrate = 400;
+  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, 9), AOM_CODEC_OK);
+  ASSERT_EQ(aom_codec_control(&enc, AV1E_SET_ENABLE_ORDER_HINT, 0),
+            AOM_CODEC_OK);
+  aom_svc_params_t svc_params = {};
+  svc_params.number_spatial_layers = 2;
+  svc_params.number_temporal_layers = 1;
+  svc_params.scaling_factor_num[0] = 1;
+  svc_params.scaling_factor_den[0] = 4;
+  svc_params.scaling_factor_num[1] = 1;
+  svc_params.scaling_factor_den[1] = 1;
+  svc_params.layer_target_bitrate[0] = 100;
+  svc_params.layer_target_bitrate[1] = 300;
+  svc_params.framerate_factor[0] = 1;
+  for (int sl = 0; sl < 2; ++sl) {
+    svc_params.min_quantizers[sl] = 0;
+    svc_params.max_quantizers[sl] = 56;
+  }
+  ASSERT_EQ(aom_codec_control(&enc, AV1E_SET_SVC_PARAMS, &svc_params),
+            AOM_CODEC_OK);
+  aom_codec_ctx_t dec;
+  ASSERT_EQ(aom_codec_dec_init(&dec, aom_codec_av1_dx(), nullptr, 0),
+            AOM_CODEC_OK);
+  aom_image_t *img =
+      aom_img_alloc(nullptr, AOM_IMG_FMT_I420, cfg.g_w, cfg.g_h, 1);
+  ASSERT_NE(img, nullptr);
+
+  for (int frame = 0; frame < 3; ++frame) {
+    FillMovingPattern(img, frame);
+    for (int sl = 0; sl < 2; ++sl) {
+      SCOPED_TRACE(testing::Message() << "frame " << frame << ", layer " << sl);
+      aom_svc_layer_id_t layer_id = {};
+      layer_id.spatial_layer_id = sl;
+      ASSERT_EQ(aom_codec_control(&enc, AV1E_SET_SVC_LAYER_ID, &layer_id),
+                AOM_CODEC_OK);
+      aom_svc_ref_frame_config_t ref_cfg = {};
+      for (int i = 0; i < 7; ++i) ref_cfg.ref_idx[i] = 1;
+      ref_cfg.ref_idx[0] = sl;  // LAST.
+      ref_cfg.reference[0] = 1;
+      if (sl == 1) {
+        ref_cfg.ref_idx[3] = 0;  // GOLDEN.
+        ref_cfg.reference[3] = 1;
+      }
+      ref_cfg.refresh[sl] = 1;
+      ASSERT_EQ(
+          aom_codec_control(&enc, AV1E_SET_SVC_REF_FRAME_CONFIG, &ref_cfg),
+          AOM_CODEC_OK);
+      unsigned int decoded_w;
+      unsigned int decoded_h;
+      EXPECT_EQ(
+          EncodeAndDecodeFrame(&enc, &dec, img, frame, &decoded_w, &decoded_h),
+          AOM_CODEC_OK)
+          << aom_codec_error_detail(&dec);
+    }
+  }
+
+  aom_img_free(img);
+  ASSERT_EQ(aom_codec_destroy(&dec), AOM_CODEC_OK);
+  ASSERT_EQ(aom_codec_destroy(&enc), AOM_CODEC_OK);
+}
+
+// Like the test above, but with a single layer that changes resolution. When
+// a frame's size differs from the maximum size, the frame header may signal
+// that it has the same size as one of its seven references. This must take
+// the references that are actually signaled into account.
+//
+// The frames are 320x240, 160x120, 320x240 and 160x120. The last frame
+// predicts from the 320x240 frame (LAST), and its unused references point to
+// the slot holding the first 160x120 frame.
+TEST(EncodeAPI, SvcRefFrameConfigUnusedRefIdxWithResolutionChange) {
+  aom_codec_iface_t *const 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 = 320;
+  cfg.g_h = 240;
+  cfg.g_lag_in_frames = 0;
+  cfg.rc_end_usage = AOM_CBR;
+  cfg.rc_target_bitrate = 300;
+  // Allows changing the resolution without key frames.
+  cfg.g_forced_max_frame_width = 320;
+  cfg.g_forced_max_frame_height = 240;
+  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, 9), AOM_CODEC_OK);
+  // Order hints are off, as is common for RTC.
+  ASSERT_EQ(aom_codec_control(&enc, AV1E_SET_ENABLE_ORDER_HINT, 0),
+            AOM_CODEC_OK);
+  aom_codec_ctx_t dec;
+  ASSERT_EQ(aom_codec_dec_init(&dec, aom_codec_av1_dx(), nullptr, 0),
+            AOM_CODEC_OK);
+
+  struct FrameConfig {
+    unsigned int width;
+    unsigned int height;
+    int predicted_slot;
+    int refreshed_slot;
+    int unused_slot;
+  };
+  // Frame 0 is a key frame, which refreshes all slots.
+  constexpr FrameConfig kFrames[] = { { 320, 240, 0, 0, 0 },
+                                      { 160, 120, 0, 1, 0 },
+                                      { 320, 240, 1, 0, 1 },
+                                      { 160, 120, 0, 2, 1 } };
+  for (int frame = 0; frame < 4; ++frame) {
+    SCOPED_TRACE(frame);
+    const FrameConfig &config = kFrames[frame];
+    cfg.g_w = config.width;
+    cfg.g_h = config.height;
+    ASSERT_EQ(aom_codec_enc_config_set(&enc, &cfg), AOM_CODEC_OK);
+    aom_svc_ref_frame_config_t ref_cfg = {};
+    for (int i = 0; i < 7; ++i) ref_cfg.ref_idx[i] = config.unused_slot;
+    ref_cfg.ref_idx[0] = config.predicted_slot;  // LAST.
+    ref_cfg.reference[0] = frame > 0;
+    ref_cfg.refresh[config.refreshed_slot] = 1;
+    ASSERT_EQ(aom_codec_control(&enc, AV1E_SET_SVC_REF_FRAME_CONFIG, &ref_cfg),
+              AOM_CODEC_OK);
+    aom_image_t *img =
+        aom_img_alloc(nullptr, AOM_IMG_FMT_I420, cfg.g_w, cfg.g_h, 1);
+    ASSERT_NE(img, nullptr);
+    FillMovingPattern(img, frame);
+    unsigned int decoded_w;
+    unsigned int decoded_h;
+    EXPECT_EQ(
+        EncodeAndDecodeFrame(&enc, &dec, img, frame, &decoded_w, &decoded_h),
+        AOM_CODEC_OK)
+        << aom_codec_error_detail(&dec);
+    EXPECT_EQ(decoded_w, config.width);
+    EXPECT_EQ(decoded_h, config.height);
+    // Only frame 0 is a key frame.
+    int ref_updates = 0;
+    ASSERT_EQ(aom_codec_control(&dec, AOMD_GET_LAST_REF_UPDATES, &ref_updates),
+              AOM_CODEC_OK);
+    EXPECT_EQ(ref_updates, frame == 0 ? 0xff : 1 << config.refreshed_slot);
+    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
+
 }  // namespace