use core::cmp::min;
use zenpixels::{InPlacePixels, Orientation, PixelBuffer, PixelSlice, PixelSliceMut};
use crate::error::ConvertError;
#[cfg(feature = "fast-transpose")]
use archmage::prelude::*;
#[cfg(feature = "fast-transpose")]
use magetypes::simd::generic::f32x4 as GenericF32x4;
const TILE: u32 = 32;
#[must_use]
pub fn apply_orientation(src: PixelSlice<'_>, orientation: Orientation) -> PixelBuffer {
let (ow, oh) = orientation.output_dimensions(src.width(), src.rows());
let desc = src.descriptor();
let mut out = PixelBuffer::new(ow, oh, desc);
apply_orientation_into(src, orientation, out.as_slice_mut())
.expect("apply_orientation: freshly allocated buffer matches output geometry");
out
}
pub fn apply_orientation_into(
src: PixelSlice<'_>,
orientation: Orientation,
mut dst: PixelSliceMut<'_>,
) -> Result<(), ConvertError> {
let w = src.width();
let h = src.rows();
let bpp = src.descriptor().bytes_per_pixel();
let (ow, oh) = orientation.output_dimensions(w, h);
let dst_bpp = dst.descriptor().bytes_per_pixel();
if dst.width() != ow || dst.rows() != oh || dst_bpp != bpp {
return Err(ConvertError::BufferSize {
expected: ow as usize * oh as usize * bpp,
actual: dst.width() as usize * dst.rows() as usize * dst_bpp,
});
}
if w == 0 || h == 0 || bpp == 0 {
return Ok(());
}
{
match orientation {
Orientation::Identity => {
for y in 0..h {
dst.row_mut(y).copy_from_slice(src.row(y));
}
}
Orientation::FlipV => {
for y in 0..h {
dst.row_mut(y).copy_from_slice(src.row(h - 1 - y));
}
}
Orientation::FlipH => {
for y in 0..h {
reverse_row(src.row(y), dst.row_mut(y), w as usize, bpp);
}
}
Orientation::Rotate180 => {
for y in 0..h {
reverse_row(src.row(h - 1 - y), dst.row_mut(y), w as usize, bpp);
}
}
Orientation::Transpose
| Orientation::Rotate90
| Orientation::Rotate270
| Orientation::Transverse
| _ => {
do_transpose(&src, &mut dst, orientation, w, h, bpp);
}
}
}
Ok(())
}
const MAX_INPLACE_BPP: usize = 16;
pub fn apply_orientation_in_place(
dst: &mut PixelBuffer,
orientation: Orientation,
) -> Result<(), ConvertError> {
let bpp = dst.descriptor().bytes_per_pixel();
if bpp == 0 || bpp > MAX_INPLACE_BPP {
return Err(ConvertError::BufferSize {
expected: MAX_INPLACE_BPP,
actual: bpp,
});
}
if !matches!(
orientation,
Orientation::Identity
| Orientation::FlipH
| Orientation::FlipV
| Orientation::Rotate180
| Orientation::Transpose
| Orientation::Rotate90
| Orientation::Rotate270
| Orientation::Transverse
) {
return Err(ConvertError::BufferSize {
expected: MAX_INPLACE_BPP,
actual: 0,
});
}
dst.transform_in_place(|px| orient_in_place_impl(px, orientation));
Ok(())
}
fn orient_in_place_impl(px: InPlacePixels<'_>, orientation: Orientation) -> PixelSliceMut<'_> {
let InPlacePixels {
bytes,
width: w,
rows: h,
stride: in_stride,
descriptor: desc,
color,
..
} = px;
let bpp = desc.bytes_per_pixel();
let (ow, oh) = orientation.output_dimensions(w, h);
let tight = w as usize * bpp;
let out_stride = ow as usize * bpp;
let out_len = out_stride * oh as usize;
fn rewrap<'b>(
bytes: &'b mut [u8],
ow: u32,
oh: u32,
out_stride: usize,
desc: zenpixels::PixelDescriptor,
color: Option<alloc::sync::Arc<zenpixels::ColorContext>>,
) -> PixelSliceMut<'b> {
let out = PixelSliceMut::new(bytes, ow, oh, out_stride, desc)
.expect("oriented in-place geometry is always valid");
match color {
Some(c) => out.with_color_context(c),
None => out,
}
}
if w == 0 || h == 0 {
return rewrap(&mut bytes[..out_len], ow, oh, out_stride, desc, color);
}
if in_stride != tight {
for y in 1..h as usize {
bytes.copy_within(y * in_stride..y * in_stride + tight, y * tight);
}
}
let content = &mut bytes[..tight * h as usize];
match orientation {
Orientation::Identity => {}
Orientation::FlipH => inplace_flip_h(content, w, h, bpp),
Orientation::FlipV => inplace_flip_v(content, w, h, bpp),
Orientation::Rotate180 => inplace_reverse_elements(content, bpp),
Orientation::Transpose => inplace_transpose(content, w, h, bpp),
Orientation::Rotate90 => {
inplace_transpose(content, w, h, bpp);
inplace_flip_h(content, ow, oh, bpp); }
Orientation::Rotate270 => {
inplace_transpose(content, w, h, bpp);
inplace_flip_v(content, ow, oh, bpp); }
Orientation::Transverse => {
inplace_transpose(content, w, h, bpp);
inplace_reverse_elements(content, bpp); }
_ => {}
}
rewrap(&mut bytes[..out_len], ow, oh, out_stride, desc, color)
}
fn inplace_flip_h(a: &mut [u8], w: u32, h: u32, bpp: usize) {
let w = w as usize;
let row_len = w * bpp;
for y in 0..h as usize {
let row = &mut a[y * row_len..y * row_len + row_len];
let (mut lo, mut hi) = (0usize, w - 1);
while lo < hi {
let (al, ah) = (lo * bpp, hi * bpp);
for k in 0..bpp {
row.swap(al + k, ah + k);
}
lo += 1;
hi -= 1;
}
}
}
fn inplace_flip_v(a: &mut [u8], w: u32, h: u32, bpp: usize) {
let row_len = w as usize * bpp;
let h = h as usize;
let (mut top, mut bot) = (0usize, h - 1);
while top < bot {
let split = bot * row_len;
let (head, tail) = a.split_at_mut(split);
head[top * row_len..top * row_len + row_len].swap_with_slice(&mut tail[..row_len]);
top += 1;
bot -= 1;
}
}
fn inplace_reverse_elements(a: &mut [u8], bpp: usize) {
let n = a.len() / bpp;
if n < 2 {
return;
}
let (mut lo, mut hi) = (0usize, n - 1);
while lo < hi {
let (al, ah) = (lo * bpp, hi * bpp);
for k in 0..bpp {
a.swap(al + k, ah + k);
}
lo += 1;
hi -= 1;
}
}
fn inplace_transpose(a: &mut [u8], w: u32, h: u32, bpp: usize) {
if w == h {
let n = w as usize;
for i in 0..n {
for j in (i + 1)..n {
let (p, q) = ((i * n + j) * bpp, (j * n + i) * bpp);
for k in 0..bpp {
a.swap(p + k, q + k);
}
}
}
return;
}
let (w, h) = (w as usize, h as usize);
let n = w * h;
if n <= 1 {
return;
}
let mn1 = n - 1;
let mut moved = alloc::vec![false; n];
moved[0] = true;
moved[mn1] = true;
let mut tmp = [0u8; MAX_INPLACE_BPP];
let mut start = 1;
while start < mn1 {
if moved[start] {
start += 1;
continue;
}
tmp[..bpp].copy_from_slice(&a[start * bpp..start * bpp + bpp]);
let mut cur = start;
loop {
moved[cur] = true;
let prev = (cur * w) % mn1; if prev == start {
break;
}
a.copy_within(prev * bpp..prev * bpp + bpp, cur * bpp);
cur = prev;
}
a[cur * bpp..cur * bpp + bpp].copy_from_slice(&tmp[..bpp]);
start += 1;
}
}
#[cfg(feature = "__bench_orient")]
#[doc(hidden)]
#[must_use]
pub fn __bench_apply_orientation_scalar(
src: PixelSlice<'_>,
orientation: Orientation,
) -> PixelBuffer {
let w = src.width();
let h = src.rows();
let desc = src.descriptor();
let bpp = desc.bytes_per_pixel();
let (ow, oh) = orientation.output_dimensions(w, h);
let mut out = PixelBuffer::new(ow, oh, desc);
if w == 0 || h == 0 || bpp == 0 {
return out;
}
{
let mut dst = out.as_slice_mut();
transpose_blocked(&src, &mut dst, orientation, w, h, bpp);
}
out
}
#[inline]
fn reverse_row(s: &[u8], d: &mut [u8], width: usize, bpp: usize) {
for x in 0..width {
let si = (width - 1 - x) * bpp;
let di = x * bpp;
d[di..di + bpp].copy_from_slice(&s[si..si + bpp]);
}
}
#[inline]
#[allow(clippy::too_many_arguments)] fn scatter_pixel(
s: &[u8],
dst: &mut PixelSliceMut<'_>,
orientation: Orientation,
sx: u32,
sy: u32,
w: u32,
h: u32,
bpp: usize,
) {
let (dx, dy) = orientation.forward_map(sx, sy, w, h);
let si = sx as usize * bpp;
let di = dx as usize * bpp;
dst.row_mut(dy)[di..di + bpp].copy_from_slice(&s[si..si + bpp]);
}
fn do_transpose(
src: &PixelSlice<'_>,
dst: &mut PixelSliceMut<'_>,
orientation: Orientation,
w: u32,
h: u32,
bpp: usize,
) {
if let Some(flips) = inverse_flips(orientation) {
#[cfg(all(feature = "fast-transpose", target_arch = "x86_64"))]
{
macro_rules! v3_kernel {
($kernel:path) => {
if let Some(token) = X64V3Token::summon() {
$kernel(token, src, dst, orientation, w, h);
return;
}
};
}
match bpp {
1 => v3_kernel!(pxn_x86::transpose1_v3),
2 => v3_kernel!(pxn_x86::transpose2_v3),
3 => v3_kernel!(rgb3_x86::transpose3_v3),
4 => v3_kernel!(pxn_x86::transpose4_v3),
6 => v3_kernel!(pxn_x86::transpose6_v3),
8 => v3_kernel!(pxn_x86::transpose8_v3),
12 => v3_kernel!(pxn_x86::transpose12_v3),
16 => v3_kernel!(pxn_x86::transpose16_v3),
_ => {}
}
}
#[cfg(all(feature = "fast-transpose", target_arch = "aarch64"))]
{
macro_rules! neon_kernel {
($kernel:path) => {
if let Some(token) = NeonToken::summon() {
$kernel(token, src, dst, orientation, w, h);
return;
}
};
}
match bpp {
1 => neon_kernel!(pxn_neon::transpose1_neon),
2 => neon_kernel!(pxn_neon::transpose2_neon),
3 => neon_kernel!(pxn_neon::transpose3_neon),
4 => neon_kernel!(pxn_neon::transpose4_neon),
6 => neon_kernel!(pxn_neon::transpose6_neon),
8 => neon_kernel!(pxn_neon::transpose8_neon),
12 => neon_kernel!(pxn_neon::transpose12_neon),
16 => {
transpose16_deep(src, dst, orientation, w, h);
return;
}
_ => {}
}
}
#[cfg(feature = "fast-transpose")]
if bpp == 4 {
incant!(
transpose4_simd(src, dst, orientation, w, h),
[v3, neon, wasm128, scalar]
);
return;
}
match bpp {
1 => return transpose_tiled::<1>(src, dst, flips, w, h),
2 => return transpose_tiled::<2>(src, dst, flips, w, h),
3 => return transpose_tiled::<3>(src, dst, flips, w, h),
4 => return transpose_tiled::<4>(src, dst, flips, w, h),
6 => return transpose_tiled::<6>(src, dst, flips, w, h),
8 => return transpose_tiled::<8>(src, dst, flips, w, h),
12 => return transpose_tiled::<12>(src, dst, flips, w, h),
16 => return transpose16_deep(src, dst, orientation, w, h),
_ => {}
}
}
transpose_blocked(src, dst, orientation, w, h, bpp);
}
#[inline]
fn inverse_flips(orientation: Orientation) -> Option<(bool, bool)> {
match orientation {
Orientation::Transpose => Some((false, false)),
Orientation::Rotate90 => Some((false, true)),
Orientation::Rotate270 => Some((true, false)),
Orientation::Transverse => Some((true, true)),
_ => None,
}
}
fn transpose_tiled<const BPP: usize>(
src: &PixelSlice<'_>,
dst: &mut PixelSliceMut<'_>,
(flip_sx, flip_sy): (bool, bool),
w: u32,
h: u32,
) {
debug_assert_eq!(src.descriptor().bytes_per_pixel(), BPP);
let sbytes = src.as_strided_bytes();
let sstride = src.stride();
let sstep: isize = if flip_sy {
-(sstride as isize)
} else {
sstride as isize
};
let (ow, oh) = (h, w);
let mut ty = 0;
while ty < oh {
let ty_end = min(ty + TILE, oh);
let mut tx = 0;
while tx < ow {
let tx_end = min(tx + TILE, ow);
let sy0 = (if flip_sy { h - 1 - tx } else { tx }) as usize;
for dy in ty..ty_end {
let sx = (if flip_sx { w - 1 - dy } else { dy }) as usize;
let mut soff = (sy0 * sstride + sx * BPP) as isize;
let drow = &mut dst.row_mut(dy)[tx as usize * BPP..tx_end as usize * BPP];
for dpx in drow.chunks_exact_mut(BPP) {
let s = soff as usize;
let px: [u8; BPP] = sbytes[s..s + BPP].try_into().unwrap();
dpx.copy_from_slice(&px);
soff += sstep;
}
}
tx += TILE;
}
ty += TILE;
}
}
fn transpose_blocked(
src: &PixelSlice<'_>,
dst: &mut PixelSliceMut<'_>,
orientation: Orientation,
w: u32,
h: u32,
bpp: usize,
) {
let mut tile_y = 0;
while tile_y < h {
let y_end = min(tile_y + TILE, h);
let mut tile_x = 0;
while tile_x < w {
let x_end = min(tile_x + TILE, w);
for sy in tile_y..y_end {
let s = src.row(sy);
for sx in tile_x..x_end {
scatter_pixel(s, dst, orientation, sx, sy, w, h, bpp);
}
}
tile_x += TILE;
}
tile_y += TILE;
}
}
macro_rules! staged_micro {
($name:ident, $bpp:literal) => {
#[cfg(feature = "__bench_orient")]
#[inline]
fn $name<const FLIP_R: bool, const FLIP_C: bool>(
sbytes: &[u8],
sstride: usize,
sx0: usize, sy0: usize, dst: &mut PixelSliceMut<'_>,
dx0: usize, dy0: usize, ) {
const T: usize = 16;
let mut stage = [[0u8; T * $bpp]; T];
for (r, row) in stage.iter_mut().enumerate() {
let off = (sy0 + r) * sstride + sx0 * $bpp;
row.copy_from_slice(&sbytes[off..off + T * $bpp]);
}
for c in 0..T {
let cc = if FLIP_C { T - 1 - c } else { c };
let drow = &mut dst.row_mut((dy0 + c) as u32)[dx0 * $bpp..(dx0 + T) * $bpp];
for (k, dpx) in drow.chunks_exact_mut($bpp).enumerate() {
let r = if FLIP_R { T - 1 - k } else { k };
dpx.copy_from_slice(&stage[r][cc * $bpp..cc * $bpp + $bpp]);
}
}
}
};
}
staged_micro!(staged_micro_1, 1);
staged_micro!(staged_micro_2, 2);
staged_micro!(staged_micro_3, 3);
staged_micro!(staged_micro_4, 4);
#[cfg(feature = "__bench_orient")]
fn transpose_staged(
src: &PixelSlice<'_>,
dst: &mut PixelSliceMut<'_>,
orientation: Orientation,
w: u32,
h: u32,
bpp: usize,
) {
const T: u32 = 16;
let Some((flip_sx, flip_sy)) = inverse_flips(orientation) else {
return transpose_blocked(src, dst, orientation, w, h, bpp);
};
let sbytes = src.as_strided_bytes();
let sstride = src.stride();
let full_w = w & !(T - 1);
let full_h = h & !(T - 1);
let mut sy = 0;
while sy < full_h {
let mut sx = 0;
while sx < full_w {
let dx0 = if flip_sy { h - T - sy } else { sy } as usize;
let dy0 = if flip_sx { w - T - sx } else { sx } as usize;
let (sx_eff, sy_eff) = (sx as usize, sy as usize);
macro_rules! call {
($f:ident) => {
match (flip_sy, flip_sx) {
(false, false) => {
$f::<false, false>(sbytes, sstride, sx_eff, sy_eff, dst, dx0, dy0)
}
(true, false) => {
$f::<true, false>(sbytes, sstride, sx_eff, sy_eff, dst, dx0, dy0)
}
(false, true) => {
$f::<false, true>(sbytes, sstride, sx_eff, sy_eff, dst, dx0, dy0)
}
(true, true) => {
$f::<true, true>(sbytes, sstride, sx_eff, sy_eff, dst, dx0, dy0)
}
}
};
}
match bpp {
1 => call!(staged_micro_1),
2 => call!(staged_micro_2),
3 => call!(staged_micro_3),
4 => call!(staged_micro_4),
_ => unreachable!("staged path is dispatched for bpp 1..=4 only"),
}
sx += T;
}
sy += T;
}
transpose_edges(src, dst, orientation, w, h, bpp, full_w, full_h);
}
#[cfg(feature = "__bench_orient")]
#[doc(hidden)]
pub fn __bench_apply_orientation_staged(
src: PixelSlice<'_>,
orientation: Orientation,
mut dst: PixelSliceMut<'_>,
) -> Result<(), ConvertError> {
let w = src.width();
let h = src.rows();
let bpp = src.descriptor().bytes_per_pixel();
let (ow, oh) = orientation.output_dimensions(w, h);
if dst.width() != ow || dst.rows() != oh || dst.descriptor().bytes_per_pixel() != bpp {
return Err(ConvertError::BufferSize {
expected: ow as usize * oh as usize * bpp,
actual: dst.width() as usize * dst.rows() as usize * dst.descriptor().bytes_per_pixel(),
});
}
if w == 0 || h == 0 || bpp == 0 {
return Ok(());
}
if (1..=4).contains(&bpp) {
transpose_staged(&src, &mut dst, orientation, w, h, bpp);
} else {
transpose_blocked(&src, &mut dst, orientation, w, h, bpp);
}
Ok(())
}
#[allow(clippy::too_many_arguments)] #[cfg(feature = "fast-transpose")]
fn transpose_edges(
src: &PixelSlice<'_>,
dst: &mut PixelSliceMut<'_>,
orientation: Orientation,
w: u32,
h: u32,
bpp: usize,
full_w: u32,
full_h: u32,
) {
for sy in 0..full_h {
let s = src.row(sy);
for sx in full_w..w {
scatter_pixel(s, dst, orientation, sx, sy, w, h, bpp);
}
}
for sy in full_h..h {
let s = src.row(sy);
for sx in 0..w {
scatter_pixel(s, dst, orientation, sx, sy, w, h, bpp);
}
}
}
#[cfg(feature = "fast-transpose")]
#[inline]
fn tile_dest(
orientation: Orientation,
bx: u32,
by: u32,
r: u32,
w: u32,
h: u32,
) -> (u32, u32, bool) {
match orientation {
Orientation::Transpose => (bx + r, by, false),
Orientation::Rotate90 => (bx + r, h - 4 - by, true),
Orientation::Rotate270 => (w - 1 - bx - r, by, false),
Orientation::Transverse => (w - 1 - bx - r, h - 4 - by, true),
_ => unreachable!("tile_dest only handles the four transposing orientations"),
}
}
#[cfg(feature = "fast-transpose")]
#[magetypes(v3, neon, wasm128, scalar)]
fn transpose4_simd(
token: Token,
src: &PixelSlice<'_>,
dst: &mut PixelSliceMut<'_>,
orientation: Orientation,
w: u32,
h: u32,
) {
#[allow(non_camel_case_types)]
type f32x4 = GenericF32x4<Token>;
let full_w = w & !3; let full_h = h & !3;
let mut by = 0;
while by < full_h {
let mut bx = 0;
while bx < full_w {
let xb = bx as usize * 4;
let f0: [f32; 4] =
bytemuck::cast::<[u8; 16], _>(src.row(by)[xb..xb + 16].try_into().unwrap());
let f1: [f32; 4] =
bytemuck::cast::<[u8; 16], _>(src.row(by + 1)[xb..xb + 16].try_into().unwrap());
let f2: [f32; 4] =
bytemuck::cast::<[u8; 16], _>(src.row(by + 2)[xb..xb + 16].try_into().unwrap());
let f3: [f32; 4] =
bytemuck::cast::<[u8; 16], _>(src.row(by + 3)[xb..xb + 16].try_into().unwrap());
let mut rows = [
f32x4::load(token, &f0),
f32x4::load(token, &f1),
f32x4::load(token, &f2),
f32x4::load(token, &f3),
];
f32x4::transpose_4x4(&mut rows);
for r in 0..4u32 {
let mut lanes = [0f32; 4];
rows[r as usize].store(&mut lanes);
let (drow, dcol, rev) = tile_dest(orientation, bx, by, r, w, h);
if rev {
lanes.reverse();
}
let bytes: [u8; 16] = bytemuck::cast(lanes);
let db = dcol as usize * 4;
dst.row_mut(drow)[db..db + 16].copy_from_slice(&bytes);
}
bx += 4;
}
by += 4;
}
transpose_edges(src, dst, orientation, w, h, 4, full_w, full_h);
}
#[cfg(all(feature = "fast-transpose", target_arch = "x86_64"))]
mod rgb3_x86;
fn transpose16_deep(
src: &PixelSlice<'_>,
dst: &mut PixelSliceMut<'_>,
orientation: Orientation,
w: u32,
h: u32,
) {
let Some((flip_sx, flip_sy)) = inverse_flips(orientation) else {
return transpose_blocked(src, dst, orientation, w, h, 16);
};
let sbytes = src.as_strided_bytes();
let sstride = src.stride();
let dstride = dst.stride();
let dbytes = dst.as_strided_bytes_mut();
const T: usize = 16;
let full_w = w & !15;
let full_h = h & !15;
let ntx = (full_w / 16) as usize;
let nty = (full_h / 16) as usize;
for tyi in 0..nty {
let ty = if flip_sy { nty - 1 - tyi } else { tyi } * T;
let dx = if flip_sy { h as usize - T - ty } else { ty };
for txi in 0..ntx {
let tx = (if flip_sx { ntx - 1 - txi } else { txi }) * T;
let mut srows: [&[u8]; T] = [&[]; T];
for (r, sr) in srows.iter_mut().enumerate() {
let off = (ty + r) * sstride + tx * 16;
*sr = &sbytes[off..off + T * 16];
}
for c in 0..T {
let dy = if flip_sx {
w as usize - 1 - (tx + c)
} else {
tx + c
};
let drow = &mut dbytes[dy * dstride + dx * 16..dy * dstride + (dx + T) * 16];
for (k, chunk) in drow.chunks_exact_mut(16).enumerate() {
let r = if flip_sy { T - 1 - k } else { k };
chunk.copy_from_slice(&srows[r][c * 16..c * 16 + 16]);
}
}
}
}
let (sb, db) = (sbytes, dbytes);
for y in 0..full_h {
for x in full_w..w {
let (dxx, dyy) = orientation.forward_map(x, y, w, h);
let so = y as usize * sstride + x as usize * 16;
let dofs = dyy as usize * dstride + dxx as usize * 16;
db[dofs..dofs + 16].copy_from_slice(&sb[so..so + 16]);
}
}
for y in full_h..h {
for x in 0..w {
let (dxx, dyy) = orientation.forward_map(x, y, w, h);
let so = y as usize * sstride + x as usize * 16;
let dofs = dyy as usize * dstride + dxx as usize * 16;
db[dofs..dofs + 16].copy_from_slice(&sb[so..so + 16]);
}
}
}
#[cfg(all(feature = "fast-transpose", target_arch = "x86_64"))]
mod pxn_x86;
#[cfg(all(feature = "fast-transpose", target_arch = "aarch64"))]
mod pxn_neon;
#[cfg(test)]
mod tests;