Commit e025e24ca1 for aom
commit e025e24ca102d04f8b0329929aa302d2d4c5ce07
Author: Ihor Buhaiov <freedbrt@gmail.com>
Date: Fri Oct 30 17:31:07 2026 +0100
Share IntraBC hash tables between tiles
A frame with more than 16 tiles went back to one hash table for the
whole frame. One thread built it, and its buckets filled up with blocks
from other tiles that the current tile can't copy from.
Now neighboring tiles share a table. The tiles are split into at most
16 rectangles, halving the side with more tables until they fit, so 8x4
tiles get 16 tables of 2x1 tiles. A table holds the blocks of its
rectangle, and av1_is_dv_valid() still skips the ones outside the
current tile. Those blocks still take slots in the bucket caps, but
in the runs below size and PSNR barely moved. Frames with up to 16
tiles keep one table per tile and give the same output as before.
Encode time, main -> this change, rt speed 11, screen content, IntraBC
on, every frame a key frame, cq 30, 8 threads, row mt, 20 frames,
median of 3 runs on an M3 Pro. Size and PSNR change by less than
0.03%.
4:2:0 4:4:4
32 tiles 64 tiles 32 tiles 64 tiles
5120x2880 -59.1% -57.2% -55.9% -56.6%
3840x2160 -51.8% -51.1% -48.9% -47.6%
2560x1440 -41.7% -40.3% -41.7% -41.4%
With 1x1, 2x2 and 4x4 tiles, three clips in 4:2:0 and 4:4:4, the output
is bit exact with main (18 of 18).
IntraBCTilesTest now covers 32 and 64 tiles and replaces
IntraBCManyTilesTest. Both new cases fail on main. The test uses 64x64
superblocks. Realtime mode picks 128x128 ones for these bigger frames,
and with those IntraBC finds far fewer copies in 256x256 tiles, with
one table per tile as well.
Bug: 576320193
Change-Id: I6ecb6dd5ea2244e9258c2fca807661727762572c
diff --git a/av1/encoder/encodeframe.c b/av1/encoder/encodeframe.c
index 0fd9691206..072d730c87 100644
--- a/av1/encoder/encodeframe.c
+++ b/av1/encoder/encodeframe.c
@@ -2085,23 +2085,50 @@ static int get_intrabc_hash_stride(int width) {
}
// Gets the region covered by the hash table with the given index. That is the
-// region of the tile with this index, or the whole frame if there is one table.
+// bounding rectangle of the tiles that share the table.
static void get_intrabc_hash_region(const YV12_BUFFER_CONFIG *source,
- const AV1_COMMON *cm, int num_tables,
+ const AV1_COMMON *cm,
+ const IntraBCHashInfo *intrabc_hash_info,
int table_idx, IntraBCHashRegion *region) {
- region->x_start = 0;
- region->y_start = 0;
- region->x_end = source->y_crop_width;
- region->y_end = source->y_crop_height;
- if (num_tables > 1) {
- TileInfo tile;
- av1_tile_init(&tile, cm, table_idx / cm->tiles.cols,
- table_idx % cm->tiles.cols);
- region->x_start = tile.mi_col_start * MI_SIZE;
- region->y_start = tile.mi_row_start * MI_SIZE;
- region->x_end = AOMMIN(tile.mi_col_end * MI_SIZE, region->x_end);
- region->y_end = AOMMIN(tile.mi_row_end * MI_SIZE, region->y_end);
+ const int tile_cols = intrabc_hash_info->tile_cols_per_table;
+ const int tile_rows = intrabc_hash_info->tile_rows_per_table;
+ const int first_row = table_idx / intrabc_hash_info->table_cols * tile_rows;
+ const int first_col = table_idx % intrabc_hash_info->table_cols * tile_cols;
+ const int last_row = AOMMIN(first_row + tile_rows, cm->tiles.rows) - 1;
+ const int last_col = AOMMIN(first_col + tile_cols, cm->tiles.cols) - 1;
+ TileInfo first_tile;
+ TileInfo last_tile;
+ av1_tile_init(&first_tile, cm, first_row, first_col);
+ av1_tile_init(&last_tile, cm, last_row, last_col);
+ region->x_start = first_tile.mi_col_start * MI_SIZE;
+ region->y_start = first_tile.mi_row_start * MI_SIZE;
+ region->x_end = AOMMIN(last_tile.mi_col_end * MI_SIZE, source->y_crop_width);
+ region->y_end = AOMMIN(last_tile.mi_row_end * MI_SIZE, source->y_crop_height);
+}
+
+// Splits the tiles into at most MAX_INTRABC_HASH_TABLES rectangles of
+// neighboring tiles, one for each table. The side with more tables is halved
+// until they fit.
+static void set_intrabc_hash_table_grid(const CommonTileParams *tiles,
+ IntraBCHashInfo *intrabc_hash_info) {
+ int table_cols = tiles->cols;
+ int table_rows = tiles->rows;
+ while (table_cols * table_rows > MAX_INTRABC_HASH_TABLES) {
+ if (table_cols >= table_rows) {
+ table_cols = (table_cols + 1) / 2;
+ } else {
+ table_rows = (table_rows + 1) / 2;
+ }
}
+ const int tile_cols_per_table = (tiles->cols + table_cols - 1) / table_cols;
+ const int tile_rows_per_table = (tiles->rows + table_rows - 1) / table_rows;
+ // Rounding up may leave fewer tables than the halving gave.
+ intrabc_hash_info->tile_cols_per_table = tile_cols_per_table;
+ intrabc_hash_info->tile_rows_per_table = tile_rows_per_table;
+ intrabc_hash_info->table_cols =
+ (tiles->cols + tile_cols_per_table - 1) / tile_cols_per_table;
+ intrabc_hash_info->table_rows =
+ (tiles->rows + tile_rows_per_table - 1) / tile_rows_per_table;
}
bool av1_build_intrabc_hash_table(AV1_COMP *cpi, int table_idx) {
@@ -2118,8 +2145,8 @@ bool av1_build_intrabc_hash_table(AV1_COMP *cpi, int table_idx) {
}
const int hash_stride = get_intrabc_hash_stride(pic_width);
IntraBCHashRegion region;
- get_intrabc_hash_region(cpi->source, cm, intrabc_hash_info->num_tile_tables,
- table_idx, ®ion);
+ get_intrabc_hash_region(cpi->source, cm, intrabc_hash_info, table_idx,
+ ®ion);
av1_generate_block_2x2_hash_value(cpi->source, ®ion, block_hash_values[0],
hash_stride);
@@ -2148,11 +2175,11 @@ bool av1_build_intrabc_hash_table(AV1_COMP *cpi, int table_idx) {
}
// Builds the hash tables for intrabc. A block can only copy from its own tile,
-// so each tile gets its own table with only the blocks that lie inside it. The
-// encoder threads build the tables before the tiles are encoded, one table per
-// job, and av1_intrabc_hash_search() uses the table of the current tile. A
-// frame with more than MAX_INTRABC_HASH_TABLES tiles keeps a single table for
-// the whole frame, which bounds the memory.
+// so up to MAX_INTRABC_HASH_TABLES tiles each tile gets its own table with only
+// the blocks that lie inside it. With more tiles, neighboring tiles share a
+// table, which bounds the memory. The encoder threads build the tables before
+// the tiles are encoded, one table per job, and av1_intrabc_hash_search() uses
+// the table of the current tile.
static void build_intrabc_hash_tables(AV1_COMP *cpi) {
AV1_COMMON *const cm = &cpi->common;
MultiThreadInfo *const mt_info = &cpi->mt_info;
@@ -2160,8 +2187,9 @@ static void build_intrabc_hash_tables(AV1_COMP *cpi) {
const int pic_width = cpi->source->y_crop_width;
const int pic_height = cpi->source->y_crop_height;
const int hash_stride = get_intrabc_hash_stride(pic_width);
- const int num_tiles = cm->tiles.cols * cm->tiles.rows;
- const int num_tables = num_tiles <= MAX_INTRABC_HASH_TABLES ? num_tiles : 1;
+ set_intrabc_hash_table_grid(&cm->tiles, intrabc_hash_info);
+ const int num_tables =
+ intrabc_hash_info->table_cols * intrabc_hash_info->table_rows;
bool error = false;
free_block_hash_buffers(intrabc_hash_info);
diff --git a/av1/encoder/encodeframe.h b/av1/encoder/encodeframe.h
index 1b5356be2b..9e1cc7efe9 100644
--- a/av1/encoder/encodeframe.h
+++ b/av1/encoder/encodeframe.h
@@ -52,9 +52,9 @@ void av1_encode_sb_row(struct AV1_COMP *cpi, struct ThreadData *td,
int tile_row, int tile_col, int mi_row);
// Builds the intrabc hash table with the given index. It holds the blocks of
-// the tile with the same index, or of the whole frame when there is a single
-// table. Returns false if it runs out of memory, or if the frame hash buffers
-// were not allocated for the size of cpi->source.
+// the tiles that share it, see av1_intrabc_hash_table_idx(). Returns false if
+// it runs out of memory, or if the frame hash buffers were not allocated for
+// the size of cpi->source.
bool av1_build_intrabc_hash_table(struct AV1_COMP *cpi, int table_idx);
#ifdef __cplusplus
diff --git a/av1/encoder/ethread.h b/av1/encoder/ethread.h
index 38f28403dc..481320b4b6 100644
--- a/av1/encoder/ethread.h
+++ b/av1/encoder/ethread.h
@@ -42,7 +42,7 @@ void av1_row_mt_sync_write_dummy(AV1EncRowMultiThreadSync *row_mt_sync, int r,
void av1_encode_tiles_mt(struct AV1_COMP *cpi);
void av1_encode_tiles_row_mt(struct AV1_COMP *cpi);
-// Builds the intrabc hash tables of all tiles with num_workers threads.
+// Builds all the intrabc hash tables with num_workers threads.
void av1_build_intrabc_hash_tables_mt(struct AV1_COMP *cpi, int num_workers);
#if !CONFIG_REALTIME_ONLY
diff --git a/av1/encoder/hash_motion.h b/av1/encoder/hash_motion.h
index 95e2b51893..c7b14593e0 100644
--- a/av1/encoder/hash_motion.h
+++ b/av1/encoder/hash_motion.h
@@ -42,10 +42,8 @@ typedef struct {
int y_end;
} IntraBCHashRegion;
-// Frames with more tiles than this use one table for the whole frame, to keep
-// the memory down.
-// TODO: bug 576320193 - Let neighboring tiles share a table when there are
-// more tiles than this, instead of going back to one table.
+// The most tables built for a frame, to keep the memory down. Frames with more
+// tiles than this let neighboring tiles share a table.
#define MAX_INTRABC_HASH_TABLES 16
// A table keeps its buffers from frame to frame. cpi->td.mb owns it and frees
@@ -73,10 +71,16 @@ typedef struct intrabc_hash_info {
// [two buffers used ping-pong]
// buffers are AOM_BUFFER_SIZE_FOR_BLOCK_HASH elements long
uint32_t *hash_value_buffer[2];
- // One table per tile, indexed by tile_row * tile_cols + tile_col. A single
- // table covers the whole frame when there is one tile, or too many.
+ // The tables in raster order, table_cols x table_rows of them. Each one
+ // covers a rectangle of tile_cols_per_table x tile_rows_per_table tiles.
+ // Every tile has its own table unless the frame has more than
+ // MAX_INTRABC_HASH_TABLES tiles.
hash_table *tile_tables;
int num_tile_tables;
+ int table_cols;
+ int table_rows;
+ int tile_cols_per_table;
+ int tile_rows_per_table;
// Hash values of every position of the frame, only needed while the tables
// are built (two buffers, used alternately). Each buffer has
// frame_hash_width * frame_hash_height elements, the luma size of the source
@@ -89,6 +93,14 @@ typedef struct intrabc_hash_info {
int crc_initialized;
} IntraBCHashInfo;
+// Returns the index of the table that covers the given tile.
+static inline int av1_intrabc_hash_table_idx(
+ const IntraBCHashInfo *intra_bc_hash_info, int tile_row, int tile_col) {
+ return tile_row / intra_bc_hash_info->tile_rows_per_table *
+ intra_bc_hash_info->table_cols +
+ tile_col / intra_bc_hash_info->tile_cols_per_table;
+}
+
void av1_hash_table_init(IntraBCHashInfo *intra_bc_hash_info);
// Makes num_tables empty tables. Their buffers are kept for the next frame
// while the number of tables stays the same.
diff --git a/av1/encoder/mcomp.c b/av1/encoder/mcomp.c
index 583a50f35f..39c07439da 100644
--- a/av1/encoder/mcomp.c
+++ b/av1/encoder/mcomp.c
@@ -1944,12 +1944,11 @@ int av1_intrabc_hash_search(const AV1_COMP *cpi, const MACROBLOCKD *xd,
uint32_t hash_value1, hash_value2;
int best_hash_cost = INT_MAX;
- // Every tile has its own table, as a block can only copy from its own tile.
- assert(intrabc_hash_info->num_tile_tables > 0);
- const int table_idx =
- intrabc_hash_info->num_tile_tables > 1
- ? xd->tile.tile_row * cpi->common.tiles.cols + xd->tile.tile_col
- : 0;
+ // A shared table also holds the blocks of other tiles. av1_is_dv_valid()
+ // skips them, as a block can only copy from its own tile.
+ const int table_idx = av1_intrabc_hash_table_idx(
+ intrabc_hash_info, xd->tile.tile_row, xd->tile.tile_col);
+ assert(table_idx < intrabc_hash_info->num_tile_tables);
const hash_table *ref_frame_hash = &intrabc_hash_info->tile_tables[table_idx];
av1_get_block_hash_value(intrabc_hash_info, src, src_stride, block_width,
diff --git a/test/encode_api_test.cc b/test/encode_api_test.cc
index c6900a1bd9..651e4bcdfc 100644
--- a/test/encode_api_test.cc
+++ b/test/encode_api_test.cc
@@ -4915,6 +4915,9 @@ class IntraBCTilesTest : public ::libaom_test::EncoderTest,
encoder->Control(AV1E_SET_TILE_COLUMNS, GetParam().tile_cols_log2);
encoder->Control(AV1E_SET_TILE_ROWS, GetParam().tile_rows_log2);
encoder->Control(AV1E_SET_ROW_MT, GetParam().row_mt);
+ // Realtime mode uses 128x128 superblocks when both sides of the frame are
+ // over 720, and with those IntraBC finds far fewer copies in 256x256 tiles.
+ encoder->Control(AV1E_SET_SUPERBLOCK_SIZE, AOM_SUPERBLOCK_SIZE_64X64);
}
void FramePktHook(const aom_codec_cx_pkt_t *pkt) override {
@@ -4951,22 +4954,11 @@ const IntraBCTilesParams kIntraBCTilesParams[] = {
{ 1, 1, 4, 1 }, // Four tiles on four threads, as in a typical RTC setup.
{ 1, 1, 4, 0 }, // Tile threads without row mt.
{ 2, 1, 2, 1 }, // Eight tiles on two threads.
+ { 3, 2, 4, 1 }, // 32 tiles, two per table.
+ { 3, 3, 4, 1 }, // 64 tiles, four per table.
};
INSTANTIATE_TEST_SUITE_P(AV1, IntraBCTilesTest,
::testing::ValuesIn(kIntraBCTilesParams));
-// With more tiles than the encoder keeps hash tables for, one table covers the
-// frame. The copies are not expected to be found, but the frame must decode.
-class IntraBCManyTilesTest : public IntraBCTilesTest {};
-
-TEST_P(IntraBCManyTilesTest, Decodes) { EXPECT_GT(EncodeKeyFrame(1), 0u); }
-
-const IntraBCTilesParams kIntraBCManyTilesParams[] = {
- { 3, 2, 4, 1 }, // 32 tiles.
-};
-
-INSTANTIATE_TEST_SUITE_P(AV1, IntraBCManyTilesTest,
- ::testing::ValuesIn(kIntraBCManyTilesParams));
-
} // namespace