Commit 5eaf6062 for libheif

commit 5eaf60622c637b0b181fc44d373d9954f725260e
Author: Michael Wallner <mike@php.net>
Date:   Thu Oct 8 19:58:26 2026 +0200

    Rotate planes by 90 and 270 degrees in cache-sized blocks

    ComponentStorage::rotate_ccw() read the input row by row and wrote each
    row into a column of the output, which misses the cache on nearly every
    sample. The plane is now rotated in blocks of 64x64 samples, whose rows
    stay in the cache while a block is copied. Within a block, 8-bit samples
    move as 8x8 tiles, read as eight 64-bit words and transposed in
    registers, on little-endian hosts. 180 degrees is unchanged.

    tests/rotate_planes.cc checks every sample of 8- and 16-bit planes
    rotated by 90, 180 and 270 degrees, at sizes below, at and above the
    tile and block sizes.

diff --git a/libheif/image/pixelimage.cc b/libheif/image/pixelimage.cc
index 11396f89..1fe9b5f7 100644
--- a/libheif/image/pixelimage.cc
+++ b/libheif/image/pixelimage.cc
@@ -30,6 +30,7 @@
 #include <utility>
 #include <limits>
 #include <algorithm>
+#include <bit>
 #include <map>
 #include <string>
 #include <sstream>
@@ -1722,6 +1723,28 @@ Result<std::shared_ptr<HeifPixelImage>> HeifPixelImage::rotate_ccw(int angle_deg
   return out_img;
 }

+// Transposes the 8x8 block of 8-bit samples held in eight 64-bit words, row i in r[i] and its
+// column j in byte j of that word (little-endian), by exchanging its 4x4, 2x2 and 1x1 sub-blocks.
+static void transpose_8x8(uint64_t r[8])
+{
+  for (int i = 0; i < 4; i++) {
+    uint64_t t = ((r[i] >> 32) ^ r[i + 4]) & 0x00000000FFFFFFFFULL;
+    r[i] ^= t << 32;
+    r[i + 4] ^= t;
+  }
+  for (int i : {0, 1, 4, 5}) {
+    uint64_t t = ((r[i] >> 16) ^ r[i + 2]) & 0x0000FFFF0000FFFFULL;
+    r[i] ^= t << 16;
+    r[i + 2] ^= t;
+  }
+  for (int i : {0, 2, 4, 6}) {
+    uint64_t t = ((r[i] >> 8) ^ r[i + 1]) & 0x00FF00FF00FF00FFULL;
+    r[i] ^= t << 8;
+    r[i + 1] ^= t;
+  }
+}
+
+
 template<typename T>
 void HeifPixelImage::ComponentStorage::rotate_ccw(int angle_degrees,
                                             ComponentStorage& out_plane) const
@@ -1735,21 +1758,78 @@ void HeifPixelImage::ComponentStorage::rotate_ccw(int angle_degrees,
   size_t out_stride = out_plane.stride / sizeof(T);
   T* out_data = static_cast<T*>(out_plane.mem);

-  if (angle_degrees == 270) {
-    for (uint32_t x = 0; x < h; x++)
-      for (uint32_t y = 0; y < w; y++) {
-        out_data[y * out_stride + x] = in_data[(h - 1 - x) * in_stride + y];
-      }
-  } else if (angle_degrees == 180) {
+  if (angle_degrees == 180) {
     for (uint32_t y = 0; y < h; y++)
       for (uint32_t x = 0; x < w; x++) {
         out_data[y * out_stride + x] = in_data[(h - 1 - y) * in_stride + (w - 1 - x)];
       }
-  } else if (angle_degrees == 90) {
-    for (uint32_t x = 0; x < h; x++)
-      for (uint32_t y = 0; y < w; y++) {
-        out_data[y * out_stride + x] = in_data[x * in_stride + (w - 1 - y)];
+    return;
+  }
+
+  if (angle_degrees != 90 && angle_degrees != 270) {
+    return;
+  }
+
+  // Rotating by 90 or 270 degrees transposes the plane: going along the rows of one plane goes down
+  // the columns of the other, which misses the cache on nearly every sample. The plane is therefore
+  // rotated in blocks of 64x64 samples, whose rows stay in the cache while the block is copied.
+  constexpr uint32_t block = 64;
+
+  // 8-bit samples move as 8x8 tiles, read as eight 64-bit words and transposed in registers. The
+  // columns and rows at the edges that do not fill a tile are left to the loop below.
+  uint32_t w8 = 0;
+  uint32_t h8 = 0;
+  if constexpr (sizeof(T) == 1 && std::endian::native == std::endian::little) {
+    w8 = w & ~7U;
+    h8 = h & ~7U;
+    for (uint32_t y0 = 0; y0 < w8; y0 += block) {
+      for (uint32_t x0 = 0; x0 < h8; x0 += block) {
+        for (uint32_t y = y0; y < std::min(y0 + block, w8); y += 8) {
+          for (uint32_t x = x0; x < std::min(x0 + block, h8); x += 8) {
+            // Output rows y to y+7, columns x to x+7.
+            uint64_t r[8];
+            if (angle_degrees == 270) {
+              for (uint32_t j = 0; j < 8; j++) {
+                memcpy(&r[j], in_data + (h - 1 - x - j) * in_stride + y, 8);
+              }
+              transpose_8x8(r);
+            }
+            else {
+              for (uint32_t j = 0; j < 8; j++) {
+                memcpy(&r[j], in_data + (x + j) * in_stride + (w - 8 - y), 8);
+              }
+              transpose_8x8(r);
+            }
+            // Word i is output row y+i at 270 degrees and row y+7-i at 90 degrees.
+            for (uint32_t i = 0; i < 8; i++) {
+              uint32_t row = angle_degrees == 270 ? y + i : y + 7 - i;
+              memcpy(out_data + row * out_stride + x, &r[i], 8);
+            }
+          }
+        }
+      }
+    }
+  }
+
+  for (uint32_t y0 = 0; y0 < w; y0 += block) {
+    uint32_t y1 = std::min(y0 + block, w);
+    for (uint32_t x0 = 0; x0 < h; x0 += block) {
+      uint32_t x1 = std::min(x0 + block, h);
+      for (uint32_t y = y0; y < y1; y++) {
+        T* out_row = out_data + y * out_stride;
+        uint32_t x_start = std::max(x0, y < w8 ? h8 : 0);
+        if (angle_degrees == 270) {
+          for (uint32_t x = x_start; x < x1; x++) {
+            out_row[x] = in_data[(h - 1 - x) * in_stride + y];
+          }
+        }
+        else {
+          for (uint32_t x = x_start; x < x1; x++) {
+            out_row[x] = in_data[x * in_stride + (w - 1 - y)];
+          }
+        }
       }
+    }
   }
 }

diff --git a/tests/CMakeLists.txt b/tests/CMakeLists.txt
index 616a0b8d..1d3567ae 100644
--- a/tests/CMakeLists.txt
+++ b/tests/CMakeLists.txt
@@ -60,6 +60,7 @@ add_libheif_test(sdtp_sample_limit)
     add_libheif_test(crop_plane_checks)
     add_libheif_test(extract_area_plane_checks)
     add_libheif_test(extend_to_size_checks)
+    add_libheif_test(rotate_planes)
     add_libheif_test(pixelimage_overlay)
     add_libheif_test(overlay_spec_parse)
     add_libheif_test(add_channel_checks)
diff --git a/tests/rotate_planes.cc b/tests/rotate_planes.cc
new file mode 100644
index 00000000..56a1047c
--- /dev/null
+++ b/tests/rotate_planes.cc
@@ -0,0 +1,111 @@
+/*
+  libheif unit tests
+
+  MIT License
+
+  Copyright (c) 2026 Michael Wallner <mike@php.net>
+
+  Permission is hereby granted, free of charge, to any person obtaining a copy
+  of this software and associated documentation files (the "Software"), to deal
+  in the Software without restriction, including without limitation the rights
+  to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+  copies of the Software, and to permit persons to whom the Software is
+  furnished to do so, subject to the following conditions:
+
+  The above copyright notice and this permission notice shall be included in all
+  copies or substantial portions of the Software.
+
+  THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+  IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+  FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+  AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+  LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+  OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+  SOFTWARE.
+*/
+
+#include "catch_amalgamated.hpp"
+#include "libheif/heif.h"
+#include "image/pixelimage.h"
+
+#include <cstdint>
+#include <memory>
+
+
+// A sample value that differs from its neighbors', so that one moved to the wrong place shows.
+static uint32_t pattern(uint32_t x, uint32_t y)
+{
+  return (x * 2654435761U) ^ (y * 40503U) ^ (x * y);
+}
+
+
+// Rotates a plane of w x h samples of T by angle degrees counter-clockwise and checks every sample
+// of the result against the definition of the rotation.
+template <typename T>
+static void check_rotation(uint32_t w, uint32_t h, int angle)
+{
+  CAPTURE(sizeof(T) * 8, w, h, angle);
+  auto* limits = heif_get_global_security_limits();
+
+  HeifPixelImage image;
+  image.create(w, h, heif_colorspace_monochrome, heif_chroma_monochrome);
+  REQUIRE(image.add_channel(heif_channel_Y, w, h, sizeof(T) * 8, limits,
+                            heif_component_datatype_unsigned_integer).error_code == heif_error_Ok);
+
+  size_t in_stride;
+  T* in = image.get_channel_memory<T>(heif_channel_Y, &in_stride);
+  in_stride /= sizeof(T);
+  for (uint32_t y = 0; y < h; y++) {
+    for (uint32_t x = 0; x < w; x++) {
+      in[y * in_stride + x] = static_cast<T>(pattern(x, y));
+    }
+  }
+
+  auto rotated = image.rotate_ccw(angle, limits);
+  REQUIRE(rotated.error().error_code == heif_error_Ok);
+  std::shared_ptr<HeifPixelImage> out_image = *rotated;
+
+  uint32_t out_w = angle == 180 ? w : h;
+  uint32_t out_h = angle == 180 ? h : w;
+  REQUIRE(out_image->get_width(heif_channel_Y) == out_w);
+  REQUIRE(out_image->get_height(heif_channel_Y) == out_h);
+
+  size_t out_stride;
+  const T* out = out_image->get_channel_memory<T>(heif_channel_Y, &out_stride);
+  out_stride /= sizeof(T);
+
+  uint32_t wrong = 0;
+  uint32_t first_wrong_x = 0;
+  uint32_t first_wrong_y = 0;
+  for (uint32_t y = 0; y < out_h; y++) {
+    for (uint32_t x = 0; x < out_w; x++) {
+      // Where output sample (x, y) comes from in the input.
+      uint32_t sx = angle == 90 ? w - 1 - y : angle == 180 ? w - 1 - x : y;
+      uint32_t sy = angle == 90 ? x : angle == 180 ? h - 1 - y : h - 1 - x;
+      if (out[y * out_stride + x] != static_cast<T>(pattern(sx, sy))) {
+        if (wrong == 0) {
+          first_wrong_x = x;
+          first_wrong_y = y;
+        }
+        wrong++;
+      }
+    }
+  }
+  CAPTURE(first_wrong_x, first_wrong_y);
+  REQUIRE(wrong == 0);
+}
+
+
+TEST_CASE("rotate_ccw moves every sample where the rotation puts it")
+{
+  // Sizes below, at and above the 8x8 tiles and the 64x64 blocks 8-bit planes are rotated in,
+  // with edges that do not fill a tile.
+  const uint32_t sizes[][2] = {{1, 1}, {7, 9}, {9, 7}, {8, 8}, {64, 64}, {65, 130}, {130, 65}, {200, 123}};
+
+  for (auto& size : sizes) {
+    for (int angle : {90, 180, 270}) {
+      check_rotation<uint8_t>(size[0], size[1], angle);
+      check_rotation<uint16_t>(size[0], size[1], angle);
+    }
+  }
+}