Skip to main content

Module orient

Module orient 

Source
Expand description

Physical orientation baking — rotate / flip a whole pixel buffer.

apply_orientation takes a (possibly strided) PixelSlice and an Orientation and returns a fresh, tightly-allocated PixelBuffer with the pixels physically rearranged. It is the “bake” half of the zen orientation model: codecs that decode to a raster buffer and are asked to resolve orientation (OrientationHint::bakes() is true) call this; the cheap coordinate math (Orientation::forward_map / output_dimensions) lives in zenpixels, and this is the buffer operation that consumes it. apply_orientation_into writes a caller-owned buffer (no allocation), and apply_orientation_in_place permutes the backing allocation itself.

§Algorithm

The eight orientations split into two classes:

  • Non-transposing (Identity, FlipH, FlipV, Rotate180) — the output has the same dimensions, so each output row maps to exactly one input row. Identity/FlipV are pure row copies (memcpy, reordered for FlipV); FlipH/Rotate180 additionally reverse the bpp-sized elements within each row. These are memory-bandwidth bound — a scalar copy_from_slice per row already runs at copy speed.

  • Transposing (Transpose, Rotate90, Rotate270, Transverse) — width and height swap, and the access pattern is a matrix transpose, which is the cache-hostile case: a naïve element loop strides one of the two buffers by a full row per step and thrashes the cache once the image exceeds L1/L2. We use the standard fix — loop tiling (cache blocking): process the image in TILE×TILE blocks so each block’s source and destination footprints (TILE*TILE*bpp bytes each) stay resident while we transpose them. The orientation’s reflection (the h-1-sy / w-1-sx terms that turn a bare transpose into a 90°/270° rotation or anti-diagonal flip) is folded into the per-element destination address via forward_map, so the whole thing is a single pass with no intermediate buffer.

The transpose runs in one of two tiers — selected at compile time by the fast-transpose feature and at runtime by CPU capability. Both share the cache-blocking structure above and fold the reflection into the destination address; they differ only in how each tile is moved. [do_transpose] picks.

  • Portable scalar — the default, and the path on pre-AVX2 x86 and any arch without a SIMD kernel. [transpose_tiled] is a per-width monomorphised gather: the four transposing maps are separable, so along one destination row the source column is fixed and the source offset steps by ±stride. That replaces the generic path’s per-element forward_map, row_mut, and variable-length copy with one bounds check and a fixed-size BPP-byte copy per pixel, writing the destination sequentially (zenjpeg#150 measured the generic path losing to a naive linear-write gather at 3 bpp; this beats both). 16-byte pixels use [transpose16_deep] — a transpose there is pure block movement that autovectorises — and the generic [transpose_blocked] scatter covers any other width.

  • SIMD register transpose (fast-transpose) — per-pixel-width kernels that transpose a register-sized tile with an unpack/zip shuffle cascade, the shape production transpose libraries (ermig1979/Simd, fast_transpose, libyuv) win with. x86-64-v3 (AVX2) kernels cover 1/2/3/4/6/8/12 bpp in pxn_x86 (24-bit RGB8 in rgb3_x86, which expands 3→4 bytes, transposes u32 lanes, then compresses); aarch64 NEON kernels cover the same widths in pxn_neon; 16-byte pixels fall to [transpose16_deep]. On other fast-transpose arches (wasm, …) only 4-byte pixels are SIMD — via magetypes’ f32x4::transpose_4x4 (the classic _MM_TRANSPOSE4_PS shuffle, generated for SSE/NEON/SIMD128/scalar from one #[magetypes] body and dispatched by incant!) — and other widths take the scalar tier.

A 4-byte pixel rides a transpose as one 32-bit lane; the kernels shuffle whole lanes only (no arithmetic), so the byte reinterpret is bit-exact for any 4-byte format, NaN patterns included. Every SIMD kernel transposes the full tiles and leaves the right/bottom edge strips to a scalar forward_map scatter (scalar_rect / scalar_edges). [transpose_blocked] (the generic forward_map scatter) is the parity oracle every fast path is tested against (simd_transpose_matches_scalar_reference_rgba8, tiled_transpose_matches_blocked_reference_across_bpp, exhaustive_dense_dims_all_orientations_vs_oracle) and the correct fallback for any orientation added to the #[non_exhaustive] enum later.

Functions§

apply_orientation
Apply orientation to src, returning a freshly-allocated buffer with the pixels physically rearranged.
apply_orientation_in_place
Bake orientation into dst in place, reusing its allocation — no second pixel buffer (the transposing orientations would otherwise need a 2× transient).
apply_orientation_into
Apply orientation to src, writing into a caller-provided dstno allocation, so callers can reuse / pool the target across many calls.