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