avifEncoder: Add the autoTiling option
The autoTiling option, if set to AVIF_TRUE, causes avifEncoder to ignore
the tileRowsLog2 and tileColsLog2 options and automatically choose
suitable tiling values.
Also add the --autotiling option to avifenc, which sets --tilerowslog2
and --tilecolslog2 automatically.
The current implementation uses as many tiles as allowed by the minimum
tile area requirement and imposes a maximum of 8 tiles.
Move the clamping of encoder->tileRowsLog2 and encoder->tileColsLog2
from src/codec_*.c to src/write.c.
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 510475f..14fc6b9 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -33,12 +33,15 @@
* Add avifChromaDownsampling enum
* Add chromaDownsampling field to avifRGBImage struct
* Add imageDimensionLimit field to avifDecoder struct
+* Add autoTiling field to avifEncoder struct
* avifdec: Add --dimension-limit, which specifies the image dimension limit
(width or height) that should be tolerated
* avifenc: Add --sharpyuv, which enables "sharp" RGB to YUV420 conversion, which
reduces artifacts caused by 420 chroma downsampling. Needs libsharpyuv (part
of the libwebp repository) at compile time.
* avifenc: Add --ignore-exif and --ignore-xmp flags.
+* avifenc: Add --autotiling, which sets --tilerowslog2 and --tilecolslog2
+ automatically
## [0.10.1] - 2022-04-11
diff --git a/apps/avifenc.c b/apps/avifenc.c
index c263a56..199f841 100644
--- a/apps/avifenc.c
+++ b/apps/avifenc.c
@@ -84,6 +84,7 @@
AVIF_QUANTIZER_LOSSLESS);
printf(" --tilerowslog2 R : Set log2 of number of tile rows (0-6, default: 0)\n");
printf(" --tilecolslog2 C : Set log2 of number of tile columns (0-6, default: 0)\n");
+ printf(" --autotiling : Set --tilerowslog2 and --tilecolslog2 automatically\n");
printf(" -g,--grid MxN : Encode a single-image grid AVIF with M cols & N rows. Either supply MxN identical W/H/D images, or a single\n");
printf(" image that can be evenly split into the MxN grid and follow AVIF grid image restrictions. The grid will adopt\n");
printf(" the color profile of the first image supplied.\n");
@@ -436,8 +437,9 @@
int maxQuantizer = 26;
int minQuantizerAlpha = AVIF_QUANTIZER_LOSSLESS;
int maxQuantizerAlpha = AVIF_QUANTIZER_LOSSLESS;
- int tileRowsLog2 = 0;
- int tileColsLog2 = 0;
+ int tileRowsLog2 = -1;
+ int tileColsLog2 = -1;
+ avifBool autoTiling = AVIF_FALSE;
int speed = 6;
int paspCount = 0;
uint32_t paspValues[8]; // only the first two are used
@@ -601,6 +603,8 @@
if (tileColsLog2 > 6) {
tileColsLog2 = 6;
}
+ } else if (!strcmp(arg, "--autotiling")) {
+ autoTiling = AVIF_TRUE;
} else if (!strcmp(arg, "-g") || !strcmp(arg, "--grid")) {
NEXTARG();
gridDimsCount = parseU32List(gridDims, arg);
@@ -1114,9 +1118,28 @@
lossyHint = " (Lossless)";
}
printf("AVIF to be written:%s\n", lossyHint);
- avifImageDump(gridCells ? gridCells[0] : image, gridDims[0], gridDims[1], AVIF_PROGRESSIVE_STATE_UNAVAILABLE);
+ const avifImage * avif = gridCells ? gridCells[0] : image;
+ avifImageDump(avif, gridDims[0], gridDims[1], AVIF_PROGRESSIVE_STATE_UNAVAILABLE);
- printf("Encoding with AV1 codec '%s' speed [%d], color QP [%d (%s) <-> %d (%s)], alpha QP [%d (%s) <-> %d (%s)], tileRowsLog2 [%d], tileColsLog2 [%d], %d worker thread(s), please wait...\n",
+ if (autoTiling) {
+ if ((tileRowsLog2 >= 0) || (tileColsLog2 >= 0)) {
+ fprintf(stderr, "ERROR: --autotiling is specified but --tilerowslog2 or --tilecolslog2 is also specified\n");
+ returnCode = 1;
+ goto cleanup;
+ }
+ } else {
+ if (tileRowsLog2 < 0) {
+ tileRowsLog2 = 0;
+ }
+ if (tileColsLog2 < 0) {
+ tileColsLog2 = 0;
+ }
+ }
+
+ char manualTilingStr[128];
+ snprintf(manualTilingStr, sizeof(manualTilingStr), "tileRowsLog2 [%d], tileColsLog2 [%d]", tileRowsLog2, tileColsLog2);
+
+ printf("Encoding with AV1 codec '%s' speed [%d], color QP [%d (%s) <-> %d (%s)], alpha QP [%d (%s) <-> %d (%s)], %s, %d worker thread(s), please wait...\n",
avifCodecName(codecChoice, AVIF_CODEC_FLAG_CAN_ENCODE),
speed,
minQuantizer,
@@ -1127,8 +1150,7 @@
quantizerString(minQuantizerAlpha),
maxQuantizerAlpha,
quantizerString(maxQuantizerAlpha),
- tileRowsLog2,
- tileColsLog2,
+ autoTiling ? "automatic tiling" : manualTilingStr,
jobs);
encoder->maxThreads = jobs;
encoder->minQuantizer = minQuantizer;
@@ -1137,6 +1159,7 @@
encoder->maxQuantizerAlpha = maxQuantizerAlpha;
encoder->tileRowsLog2 = tileRowsLog2;
encoder->tileColsLog2 = tileColsLog2;
+ encoder->autoTiling = autoTiling;
encoder->codecChoice = codecChoice;
encoder->speed = speed;
encoder->timescale = outputTiming.timescale;
diff --git a/doc/avifenc.1.md b/doc/avifenc.1.md
index 95e450e..057a719 100644
--- a/doc/avifenc.1.md
+++ b/doc/avifenc.1.md
@@ -122,6 +122,9 @@
Possible values are in the range **0**-**6**.
Default is 0.
+**\--autotiling**
+: Set **\--tilerowslog2** and **\--tilecolslog2** automatically.
+
**-g**, **\--grid** *M***x***N*
: Encode a single-image grid AVIF with _M_ cols and _N_ rows.
Either supply MxN images of the same width, height and depth, or a single
diff --git a/include/avif/avif.h b/include/avif/avif.h
index 6974b80..a946d94 100644
--- a/include/avif/avif.h
+++ b/include/avif/avif.h
@@ -1020,6 +1020,8 @@
// * Quality range: [AVIF_QUANTIZER_BEST_QUALITY - AVIF_QUANTIZER_WORST_QUALITY]
// * To enable tiling, set tileRowsLog2 > 0 and/or tileColsLog2 > 0.
// Tiling values range [0-6], where the value indicates a request for 2^n tiles in that dimension.
+// If autoTiling is set to AVIF_TRUE, libavif ignores tileRowsLog2 and tileColsLog2 and
+// automatically chooses suitable tiling values.
// * Speed range: [AVIF_SPEED_SLOWEST - AVIF_SPEED_FASTEST]. Slower should make for a better quality
// image in less bytes. AVIF_SPEED_DEFAULT means "Leave the AV1 codec to its default speed settings"./
// If avifEncoder uses rav1e, the speed value is directly passed through (0-10). If libaom is used,
@@ -1043,6 +1045,7 @@
int maxQuantizerAlpha;
int tileRowsLog2;
int tileColsLog2;
+ avifBool autoTiling;
// stats from the most recent write
avifIOStats ioStats;
diff --git a/include/avif/internal.h b/include/avif/internal.h
index 3be3908..a9b460b 100644
--- a/include/avif/internal.h
+++ b/include/avif/internal.h
@@ -172,6 +172,14 @@
avifBool avifDimensionsTooLarge(uint32_t width, uint32_t height, uint32_t imageSizeLimit, uint32_t imageDimensionLimit);
+// Given the number of encoding threads or decoding threads available and the image dimensions,
+// chooses suitable values of *tileRowsLog2 and *tileColsLog2.
+//
+// Note: Although avifSetTileConfiguration() is only used in src/write.c and could be a static
+// function in that file, it is defined as an internal global function so that it can be tested by
+// unit tests.
+void avifSetTileConfiguration(int threads, uint32_t width, uint32_t height, int * tileRowsLog2, int * tileColsLog2);
+
// ---------------------------------------------------------------------------
// Scaling
@@ -283,10 +291,22 @@
// encoded and EncodeFinish is called, the number of samples emitted must match the number of submitted frames.
// avifCodecEncodeImageFunc may return AVIF_RESULT_UNKNOWN_ERROR to automatically emit the appropriate
// AVIF_RESULT_ENCODE_COLOR_FAILED or AVIF_RESULT_ENCODE_ALPHA_FAILED depending on the alpha argument.
+// avifCodecEncodeImageFunc should use tileRowsLog2 and tileColsLog2 instead of
+// encoder->tileRowsLog2, encoder->tileColsLog2, and encoder->autoTiling. The caller of
+// avifCodecEncodeImageFunc is responsible for automatic tiling if encoder->autoTiling is set to
+// AVIF_TRUE. The actual tiling values are passed to avifCodecEncodeImageFunc as parameters.
+//
+// Note: The caller of avifCodecEncodeImageFunc always passes encoder->data->tileRowsLog2 and
+// encoder->data->tileColsLog2 as the tileRowsLog2 and tileColsLog2 arguments. Because
+// encoder->data is of a struct type defined in src/write.c, avifCodecEncodeImageFunc cannot
+// dereference encoder->data and has to receive encoder->data->tileRowsLog2 and
+// encoder->data->tileColsLog2 via function parameters.
typedef avifResult (*avifCodecEncodeImageFunc)(struct avifCodec * codec,
avifEncoder * encoder,
const avifImage * image,
avifBool alpha,
+ int tileRowsLog2,
+ int tileColsLog2,
avifEncoderChanges encoderChanges,
avifAddImageFlags addImageFlags,
avifCodecEncodeOutput * output);
diff --git a/src/codec_aom.c b/src/codec_aom.c
index f91b38f..c643e0f 100644
--- a/src/codec_aom.c
+++ b/src/codec_aom.c
@@ -527,6 +527,8 @@
avifEncoder * encoder,
const avifImage * image,
avifBool alpha,
+ int tileRowsLog2,
+ int tileColsLog2,
avifEncoderChanges encoderChanges,
avifAddImageFlags addImageFlags,
avifCodecEncodeOutput * output)
@@ -737,12 +739,10 @@
if (encoder->maxThreads > 1) {
aom_codec_control(&codec->internal->encoder, AV1E_SET_ROW_MT, 1);
}
- if (encoder->tileRowsLog2 != 0) {
- int tileRowsLog2 = AVIF_CLAMP(encoder->tileRowsLog2, 0, 6);
+ if (tileRowsLog2 != 0) {
aom_codec_control(&codec->internal->encoder, AV1E_SET_TILE_ROWS, tileRowsLog2);
}
- if (encoder->tileColsLog2 != 0) {
- int tileColsLog2 = AVIF_CLAMP(encoder->tileColsLog2, 0, 6);
+ if (tileColsLog2 != 0) {
aom_codec_control(&codec->internal->encoder, AV1E_SET_TILE_COLUMNS, tileColsLog2);
}
if (aomCpuUsed != -1) {
@@ -793,10 +793,10 @@
}
}
if (encoderChanges & AVIF_ENCODER_CHANGE_TILE_ROWS_LOG2) {
- aom_codec_control(&codec->internal->encoder, AV1E_SET_TILE_ROWS, AVIF_CLAMP(encoder->tileRowsLog2, 0, 6));
+ aom_codec_control(&codec->internal->encoder, AV1E_SET_TILE_ROWS, tileRowsLog2);
}
if (encoderChanges & AVIF_ENCODER_CHANGE_TILE_COLS_LOG2) {
- aom_codec_control(&codec->internal->encoder, AV1E_SET_TILE_COLUMNS, AVIF_CLAMP(encoder->tileColsLog2, 0, 6));
+ aom_codec_control(&codec->internal->encoder, AV1E_SET_TILE_COLUMNS, tileColsLog2);
}
if (encoderChanges & AVIF_ENCODER_CHANGE_CODEC_SPECIFIC) {
if (!avifProcessAOMOptionsPostInit(codec, alpha)) {
diff --git a/src/codec_rav1e.c b/src/codec_rav1e.c
index 8e6dbff..7e3a315 100644
--- a/src/codec_rav1e.c
+++ b/src/codec_rav1e.c
@@ -53,6 +53,8 @@
avifEncoder * encoder,
const avifImage * image,
avifBool alpha,
+ int tileRowsLog2,
+ int tileColsLog2,
avifEncoderChanges encoderChanges,
uint32_t addImageFlags,
avifCodecEncodeOutput * output)
@@ -150,14 +152,12 @@
if (rav1e_config_parse_int(rav1eConfig, "quantizer", maxQuantizer) == -1) {
goto cleanup;
}
- if (encoder->tileRowsLog2 != 0) {
- int tileRowsLog2 = AVIF_CLAMP(encoder->tileRowsLog2, 0, 6);
+ if (tileRowsLog2 != 0) {
if (rav1e_config_parse_int(rav1eConfig, "tile_rows", 1 << tileRowsLog2) == -1) {
goto cleanup;
}
}
- if (encoder->tileColsLog2 != 0) {
- int tileColsLog2 = AVIF_CLAMP(encoder->tileColsLog2, 0, 6);
+ if (tileColsLog2 != 0) {
if (rav1e_config_parse_int(rav1eConfig, "tile_cols", 1 << tileColsLog2) == -1) {
goto cleanup;
}
diff --git a/src/codec_svt.c b/src/codec_svt.c
index 3b49881..7249f28 100644
--- a/src/codec_svt.c
+++ b/src/codec_svt.c
@@ -46,6 +46,8 @@
avifEncoder * encoder,
const avifImage * image,
avifBool alpha,
+ int tileRowsLog2,
+ int tileColsLog2,
avifEncoderChanges encoderChanges,
uint32_t addImageFlags,
avifCodecEncodeOutput * output)
@@ -134,11 +136,11 @@
svt_config->qp = AVIF_CLAMP(encoder->maxQuantizer, 0, 63);
}
- if (encoder->tileRowsLog2 != 0) {
- svt_config->tile_rows = AVIF_CLAMP(encoder->tileRowsLog2, 0, 6);
+ if (tileRowsLog2 != 0) {
+ svt_config->tile_rows = tileRowsLog2;
}
- if (encoder->tileColsLog2 != 0) {
- svt_config->tile_columns = AVIF_CLAMP(encoder->tileColsLog2, 0, 6);
+ if (tileColsLog2 != 0) {
+ svt_config->tile_columns = tileColsLog2;
}
if (encoder->speed != AVIF_SPEED_DEFAULT) {
int speed = AVIF_CLAMP(encoder->speed, 0, 8);
diff --git a/src/write.c b/src/write.c
index 45f002f..e76aea2 100644
--- a/src/write.c
+++ b/src/write.c
@@ -40,6 +40,84 @@
static void writeConfigBox(avifRWStream * s, avifCodecConfigurationBox * cfg);
// ---------------------------------------------------------------------------
+// avifSetTileConfiguration
+
+static int countLeadingZeros(uint32_t n)
+{
+ int count = 32;
+ while (n != 0) {
+ --count;
+ n >>= 1;
+ }
+ return count;
+}
+
+static int floorLog2(uint32_t n)
+{
+ assert(n > 0);
+ return 31 ^ countLeadingZeros(n);
+}
+
+// Splits tilesLog2 into *tileDim1Log2 and *tileDim2Log2, considering the ratio of dim1 to dim2.
+//
+// Precondition:
+// dim1 >= dim2
+// Postcondition:
+// tilesLog2 == *tileDim1Log2 + *tileDim2Log2
+// *tileDim1Log2 >= *tileDim2Log2
+static void splitTilesLog2(uint32_t dim1, uint32_t dim2, int tilesLog2, int * tileDim1Log2, int * tileDim2Log2)
+{
+ assert(dim1 >= dim2);
+ uint32_t ratio = dim1 / dim2;
+ int diffLog2 = floorLog2(ratio);
+ int subtract = tilesLog2 - diffLog2;
+ if (subtract < 0) {
+ subtract = 0;
+ }
+ *tileDim2Log2 = subtract / 2;
+ *tileDim1Log2 = tilesLog2 - *tileDim2Log2;
+ assert(*tileDim1Log2 >= *tileDim2Log2);
+}
+
+// Set the tile configuration: the number of tiles and the tile size.
+//
+// Tiles improve encoding and decoding speeds when multiple threads are available. However, for
+// image coding, the total tile boundary length affects the compression efficiency because intra
+// prediction can't go across tile boundaries. So the more tiles there are in an image, the worse
+// the compression ratio is. For a given number of tiles, making the tile size close to a square
+// tends to reduce the total tile boundary length inside the image. Use more tiles along the longer
+// dimension of the image to make the tile size closer to a square.
+void avifSetTileConfiguration(int threads, uint32_t width, uint32_t height, int * tileRowsLog2, int * tileColsLog2)
+{
+ *tileRowsLog2 = 0;
+ *tileColsLog2 = 0;
+ if (threads > 1) {
+ // Avoid small tiles because they are particularly bad for image coding.
+ //
+ // Use no more tiles than the number of threads. Aim for one tile per thread. Using more
+ // than one thread inside one tile could be less efficient. Using more tiles than the
+ // number of threads would result in a compression penalty without much benefit.
+ const uint32_t kMinTileArea = 512 * 512;
+ const uint32_t kMaxTiles = 32;
+ uint32_t imageArea = width * height;
+ uint32_t tiles = (imageArea + kMinTileArea - 1) / kMinTileArea;
+ if (tiles > kMaxTiles) {
+ tiles = kMaxTiles;
+ }
+ if (tiles > (uint32_t)threads) {
+ tiles = threads;
+ }
+ int tilesLog2 = floorLog2(tiles);
+ // If the image's width is greater than the height, use more tile columns than tile rows.
+ if (width >= height) {
+ splitTilesLog2(width, height, tilesLog2, tileColsLog2, tileRowsLog2);
+ } else {
+ splitTilesLog2(height, width, tilesLog2, tileRowsLog2, tileColsLog2);
+ }
+ }
+}
+
+// ---------------------------------------------------------------------------
// avifCodecEncodeOutput
avifCodecEncodeOutput * avifCodecEncodeOutputCreate(void)
@@ -122,7 +200,13 @@
{
avifEncoderItemArray items;
avifEncoderFrameArray frames;
+ // tileRowsLog2 and tileColsLog2 are the actual tiling values after automatic tiling is handled
+ int tileRowsLog2;
+ int tileColsLog2;
avifEncoder lastEncoder;
+ // lastTileRowsLog2 and lastTileColsLog2 are the actual tiling values used last time
+ int lastTileRowsLog2;
+ int lastTileColsLog2;
avifImage * imageMetadata;
uint16_t lastItemID;
uint16_t primaryItemID;
@@ -303,6 +387,7 @@
encoder->maxQuantizerAlpha = AVIF_QUANTIZER_LOSSLESS;
encoder->tileRowsLog2 = 0;
encoder->tileColsLog2 = 0;
+ encoder->autoTiling = AVIF_FALSE;
encoder->data = avifEncoderDataCreate();
encoder->csOptions = avifCodecSpecificOptionsCreate();
return encoder;
@@ -336,8 +421,8 @@
lastEncoder->maxQuantizer = encoder->maxQuantizer;
lastEncoder->minQuantizerAlpha = encoder->minQuantizerAlpha;
lastEncoder->maxQuantizerAlpha = encoder->maxQuantizerAlpha;
- lastEncoder->tileRowsLog2 = encoder->tileRowsLog2;
- lastEncoder->tileColsLog2 = encoder->tileColsLog2;
+ encoder->data->lastTileRowsLog2 = encoder->data->tileRowsLog2;
+ encoder->data->lastTileColsLog2 = encoder->data->tileColsLog2;
}
// This function detects changes made on avifEncoder. It returns true on success (i.e., if every
@@ -371,10 +456,10 @@
if (lastEncoder->maxQuantizerAlpha != encoder->maxQuantizerAlpha) {
*encoderChanges |= AVIF_ENCODER_CHANGE_MAX_QUANTIZER_ALPHA;
}
- if (lastEncoder->tileRowsLog2 != encoder->tileRowsLog2) {
+ if (encoder->data->lastTileRowsLog2 != encoder->data->tileRowsLog2) {
*encoderChanges |= AVIF_ENCODER_CHANGE_TILE_ROWS_LOG2;
}
- if (lastEncoder->tileColsLog2 != encoder->tileColsLog2) {
+ if (encoder->data->lastTileColsLog2 != encoder->data->tileColsLog2) {
*encoderChanges |= AVIF_ENCODER_CHANGE_TILE_COLS_LOG2;
}
if (encoder->csOptions->count > 0) {
@@ -671,12 +756,6 @@
return AVIF_RESULT_NO_CODEC_AVAILABLE;
}
- avifEncoderChanges encoderChanges;
- if (!avifEncoderDetectChanges(encoder, &encoderChanges)) {
- return AVIF_RESULT_CANNOT_CHANGE_SETTING;
- }
- avifEncoderBackupSettings(encoder);
-
// -----------------------------------------------------------------------
// Validate images
@@ -747,6 +826,27 @@
}
// -----------------------------------------------------------------------
+ // Handle automatic tiling
+
+ encoder->data->tileRowsLog2 = AVIF_CLAMP(encoder->tileRowsLog2, 0, 6);
+ encoder->data->tileColsLog2 = AVIF_CLAMP(encoder->tileColsLog2, 0, 6);
+ if (encoder->autoTiling) {
+ // Use as many tiles as allowed by the minimum tile area requirement and impose a maximum
+ // of 8 tiles.
+ const int threads = 8;
+ avifSetTileConfiguration(threads, firstCell->width, firstCell->height, &encoder->data->tileRowsLog2, &encoder->data->tileColsLog2);
+ }
+
+ // -----------------------------------------------------------------------
+ // All encoder settings are known now. Detect changes.
+
+ avifEncoderChanges encoderChanges;
+ if (!avifEncoderDetectChanges(encoder, &encoderChanges)) {
+ return AVIF_RESULT_CANNOT_CHANGE_SETTING;
+ }
+ avifEncoderBackupSettings(encoder);
+
+ // -----------------------------------------------------------------------
if (durationInTimescales == 0) {
durationInTimescales = 1;
@@ -901,8 +1001,15 @@
avifEncoderItem * item = &encoder->data->items.item[itemIndex];
if (item->codec) {
const avifImage * cellImage = cellImages[item->cellIndex];
- avifResult encodeResult =
- item->codec->encodeImage(item->codec, encoder, cellImage, item->alpha, encoderChanges, addImageFlags, item->encodeOutput);
+ avifResult encodeResult = item->codec->encodeImage(item->codec,
+ encoder,
+ cellImage,
+ item->alpha,
+ encoder->data->tileRowsLog2,
+ encoder->data->tileColsLog2,
+ encoderChanges,
+ addImageFlags,
+ item->encodeOutput);
if (encodeResult == AVIF_RESULT_UNKNOWN_ERROR) {
encodeResult = item->alpha ? AVIF_RESULT_ENCODE_ALPHA_FAILED : AVIF_RESULT_ENCODE_COLOR_FAILED;
}
diff --git a/tests/CMakeLists.txt b/tests/CMakeLists.txt
index 1309bdd..2d3f75d 100644
--- a/tests/CMakeLists.txt
+++ b/tests/CMakeLists.txt
@@ -105,6 +105,15 @@
target_include_directories(avifrgbtoyuvtest PRIVATE ${GTEST_INCLUDE_DIRS})
add_test(NAME avifrgbtoyuvtest COMMAND avifrgbtoyuvtest)
+ if(NOT BUILD_SHARED_LIBS)
+ # Test the internal function avifSetTileConfiguration(), which is not exported from the
+ # shared library.
+ add_executable(aviftilingtest gtest/aviftilingtest.cc)
+ target_link_libraries(aviftilingtest avif ${GTEST_BOTH_LIBRARIES})
+ target_include_directories(aviftilingtest PRIVATE ${GTEST_INCLUDE_DIRS})
+ add_test(NAME aviftilingtest COMMAND aviftilingtest)
+ endif()
+
add_executable(avify4mtest gtest/avify4mtest.cc)
target_link_libraries(avify4mtest aviftest_helpers avif_apps ${GTEST_BOTH_LIBRARIES})
target_include_directories(avify4mtest PRIVATE ${GTEST_INCLUDE_DIRS})
diff --git a/tests/gtest/aviftilingtest.cc b/tests/gtest/aviftilingtest.cc
new file mode 100644
index 0000000..edab714
--- /dev/null
+++ b/tests/gtest/aviftilingtest.cc
@@ -0,0 +1,68 @@
+// Copyright 2022 Google LLC. All rights reserved.
+// SPDX-License-Identifier: BSD-2-Clause
+
+#include <ostream>
+
+#include "avif/avif.h"
+#include "avif/internal.h"
+#include "gtest/gtest.h"
+
+namespace {
+
+struct SetTileConfigurationTestParams {
+ int threads;
+ uint32_t width;
+ uint32_t height;
+ int expected_tile_rows_log2;
+ int expected_tile_cols_log2;
+};
+
+std::ostream& operator<<(std::ostream& os,
+ const SetTileConfigurationTestParams& test) {
+ return os << "SetTileConfigurationTestParams { threads:" << test.threads
+ << " width:" << test.width << " height:" << test.height << " }";
+}
+
+TEST(TilingTest, SetTileConfiguration) {
+ constexpr int kThreads = 8;
+ int tile_rows_log2;
+ int tile_cols_log2;
+
+ constexpr SetTileConfigurationTestParams kTests[]{
+ // 144p
+ {kThreads, 256, 144, 0, 0},
+ // 240p
+ {kThreads, 426, 240, 0, 0},
+ // 360p
+ {kThreads, 640, 360, 0, 0},
+ // 480p
+ {kThreads, 854, 480, 0, 1},
+ // 720p
+ {kThreads, 1280, 720, 1, 1},
+ // 1080p
+ {kThreads, 1920, 1080, 1, 2},
+ // 2K
+ {kThreads, 2560, 1440, 1, 2},
+ // 4K
+ {32, 3840, 2160, 2, 3},
+ // 8K
+ {32, 7680, 4320, 2, 3},
+ // Kodak image set: 768x512
+ {kThreads, 768, 512, 0, 1},
+ {kThreads, 16384, 64, 0, 2},
+ };
+
+ for (const auto& test : kTests) {
+ avifSetTileConfiguration(test.threads, test.width, test.height,
+ &tile_rows_log2, &tile_cols_log2);
+ EXPECT_EQ(tile_rows_log2, test.expected_tile_rows_log2) << test;
+ EXPECT_EQ(tile_cols_log2, test.expected_tile_cols_log2) << test;
+ // Swap width and height.
+ avifSetTileConfiguration(test.threads, test.height, test.width,
+ &tile_rows_log2, &tile_cols_log2);
+ EXPECT_EQ(tile_rows_log2, test.expected_tile_cols_log2) << test;
+ EXPECT_EQ(tile_cols_log2, test.expected_tile_rows_log2) << test;
+ }
+}
+
+} // namespace