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);
+ }
+ }
+}