use crate::cursor::Frame;
const HALF_16_16: u64 = 1 << 15;
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
#[allow(
clippy::exhaustive_enums,
reason = "a closed set: there is no further variant to add"
)]
pub enum Rotation {
#[default]
None,
Quarter,
Half,
ThreeQuarter,
}
impl Rotation {
#[must_use]
pub const fn drm_mask(self) -> u64 {
match self {
Self::None => 1 << 0,
Self::Quarter => 1 << 1,
Self::Half => 1 << 2,
Self::ThreeQuarter => 1 << 3,
}
}
#[must_use]
pub const fn swaps_axes(self) -> bool {
matches!(self, Self::Quarter | Self::ThreeQuarter)
}
const fn rotated_size(self, width: u32, height: u32) -> (u32, u32) {
if self.swaps_axes() {
(height, width)
} else {
(width, height)
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
#[non_exhaustive]
pub struct Placement {
pub width: u32,
pub height: u32,
pub x: u32,
pub y: u32,
pub hotspot_x: u32,
pub hotspot_y: u32,
}
fn sample_rotated(frame: &Frame, rotation: Rotation, rx: u32, ry: u32) -> u32 {
let (sx, sy) = match rotation {
Rotation::None => (rx, ry),
Rotation::Quarter => (ry, frame.height - 1 - rx),
Rotation::Half => (frame.width - 1 - rx, frame.height - 1 - ry),
Rotation::ThreeQuarter => (frame.width - 1 - ry, rx),
};
frame.pixels[(sy * frame.width + sx) as usize]
}
struct Target<'a> {
pixels: &'a mut [u32],
offset: usize,
stride_px: usize,
width: u32,
height: u32,
}
fn box_downscale(frame: &Frame, rotation: Rotation, rotated: (u32, u32), target: &mut Target<'_>) {
let (rotated_w, rotated_h) = rotated;
let fx = (u64::from(rotated_w) << 8) / u64::from(target.width);
let fy = (u64::from(rotated_h) << 8) / u64::from(target.height);
for dy in 0..target.height {
let top = u64::from(dy) * fy;
let bottom = top + fy;
let first_row = top >> 8;
let last_row = (bottom + 0xff) >> 8;
for dx in 0..target.width {
let left = u64::from(dx) * fx;
let right = left + fx;
let first_col = left >> 8;
let last_col = (right + 0xff) >> 8;
let mut acc = [0u64; 4];
let mut total = 0u64;
for sy in first_row..last_row {
let covered_h = bottom.min((sy + 1) << 8) - top.max(sy << 8);
for sx in first_col..last_col {
let covered_w = right.min((sx + 1) << 8) - left.max(sx << 8);
let weight = covered_w * covered_h;
let pixel = sample_rotated(
frame,
rotation,
u32::try_from(sx).unwrap_or(0).min(rotated_w - 1),
u32::try_from(sy).unwrap_or(0).min(rotated_h - 1),
);
for (channel, shift) in [24, 16, 8, 0].into_iter().enumerate() {
acc[channel] += weight * u64::from((pixel >> shift) & 0xff);
}
total += weight;
}
}
if total == 0 {
continue;
}
let half = total / 2;
let mut out = 0u32;
for (channel, shift) in [24, 16, 8, 0].into_iter().enumerate() {
let value = u32::try_from((acc[channel] + half) / total).unwrap_or(0xff) & 0xff;
out |= value << shift;
}
let at = target.offset + dy as usize * target.stride_px + dx as usize;
target.pixels[at] = out;
}
}
}
pub fn compose(
frame: &Frame,
rotation: Rotation,
dst: &mut [u32],
dst_w: u32,
dst_h: u32,
stride_px: usize,
) -> Placement {
assert!(
dst.len() >= dst_h as usize * stride_px,
"destination holds {} pixels, needs {}",
dst.len(),
dst_h as usize * stride_px
);
for y in 0..dst_h as usize {
let row = y * stride_px;
dst[row..row + dst_w as usize].fill(0);
}
let (rotated_w, rotated_h) = rotation.rotated_size(frame.width, frame.height);
let shrink = rotated_w > dst_w || rotated_h > dst_h;
let (blit_w, blit_h) = if shrink {
let rx = (u64::from(dst_w) << 16) / u64::from(rotated_w);
let ry = (u64::from(dst_h) << 16) / u64::from(rotated_h);
let ratio = rx.min(ry);
let scaled = |extent: u32| {
let value =
u32::try_from(((u64::from(extent) * ratio) + HALF_16_16) >> 16).unwrap_or(1);
value.max(1)
};
(scaled(rotated_w).min(dst_w), scaled(rotated_h).min(dst_h))
} else {
(rotated_w.min(dst_w), rotated_h.min(dst_h))
};
let x_off = if dst_w > blit_w {
(dst_w - blit_w) / 2
} else {
0
};
let y_off = if dst_h > blit_h {
(dst_h - blit_h) / 2
} else {
0
};
let base = y_off as usize * stride_px + x_off as usize;
if shrink {
box_downscale(
frame,
rotation,
(rotated_w, rotated_h),
&mut Target {
pixels: dst,
offset: base,
stride_px,
width: blit_w,
height: blit_h,
},
);
} else {
for y in 0..blit_h {
for x in 0..blit_w {
let at = base + y as usize * stride_px + x as usize;
dst[at] = sample_rotated(frame, rotation, x, y);
}
}
}
let (hotspot_x, hotspot_y) = hotspot_in_buffer(frame, rotation, blit_w, blit_h, x_off, y_off);
Placement {
width: blit_w,
height: blit_h,
x: x_off,
y: y_off,
hotspot_x,
hotspot_y,
}
}
fn hotspot_in_buffer(
frame: &Frame,
rotation: Rotation,
blit_w: u32,
blit_h: u32,
x_off: u32,
y_off: u32,
) -> (u32, u32) {
let (mut hx, mut hy) = match rotation {
Rotation::None => (frame.xhot, frame.yhot),
Rotation::Quarter => (frame.height.saturating_sub(1 + frame.yhot), frame.xhot),
Rotation::Half => (
frame.width.saturating_sub(1 + frame.xhot),
frame.height.saturating_sub(1 + frame.yhot),
),
Rotation::ThreeQuarter => (frame.yhot, frame.width.saturating_sub(1 + frame.xhot)),
};
let (rotated_w, rotated_h) = rotation.rotated_size(frame.width, frame.height);
if blit_w != 0 && blit_w != rotated_w && rotated_w != 0 {
hx = u32::try_from(u64::from(hx) * u64::from(blit_w) / u64::from(rotated_w)).unwrap_or(0);
}
if blit_h != 0 && blit_h != rotated_h && rotated_h != 0 {
hy = u32::try_from(u64::from(hy) * u64::from(blit_h) / u64::from(rotated_h)).unwrap_or(0);
}
(hx + x_off, hy + y_off)
}