//! Cursor-as-metadata: the `SPA_META_Cursor` parser and the CPU-path composite blits.
//!
//! Split out of `linux/pipewire.rs` (sweep Phase 5.2). Both halves are producer-driven and
//! bounds-critical: [`update_cursor_meta`] reads a bitmap at offsets the COMPOSITOR chose (its own
//! SAFETY proof notes that a missing bound SIGSEGVs inside the PipeWire `.process` callback, where
//! `catch_unwind` cannot help), and the `composite_cursor*` blits clip a caller-positioned bitmap
//! into a frame buffer. Separating them from the stream machinery is what makes them testable
//! without a compositor.

use super::PixelFormat;
use pipewire as pw;
use pw::spa;
use std::sync::Arc;

/// Latest cursor state parsed from `SPA_META_Cursor` (cursor-as-metadata mode). Position is
/// refreshed every buffer that carries the meta (including Mutter's cursor-only "corrupted"
/// buffers we otherwise skip for their stale frame); the RGBA bitmap is cached and only
/// replaced when the compositor sends a fresh one (`bitmap_offset != 0`).
#[derive(Default)]
pub(super) struct CursorState {
    /// True when the compositor reports a visible pointer (`spa_meta_cursor.id != 0`).
    visible: bool,
    /// Top-left where the bitmap is drawn = reported position − hotspot.
    x: i32,
    y: i32,
    /// Cached straight-alpha RGBA pixels (`bw*bh*4`, bytes R,G,B,A). `Arc` so the overlay handed
    /// to each GPU frame is a refcount bump, not a copy. Empty until the first bitmap arrives.
    rgba: Arc<Vec<u8>>,
    bw: u32,
    bh: u32,
    /// Bumps whenever the bitmap (`rgba`/`bw`/`bh`) changes — stable across position-only moves,
    /// so the GPU encoder re-uploads its cursor texture only on change.
    serial: u64,
    /// The compositor-reported hotspot — carried on the overlay for the cursor-forward
    /// channel (the blend path uses the pre-adjusted `x`/`y` and never reads it).
    hot_x: i32,
    hot_y: i32,
    /// One-shot breadcrumb latch: this stream saw a `SPA_META_Cursor` region (the Meta param
    /// negotiated). Per-stream deliberately — a host serves many sessions per process, and a
    /// process-wide latch made the second session's triage read as "no meta".
    seen_meta: bool,
    /// This stream's producer rewrites the cursor meta on EVERY buffer, so an `id == 0` meta is
    /// an authoritative "pointer hidden / off this output" rather than a stale recycled region.
    /// True for KWin virtual outputs; false for the stale-meta producers (Mutter) — see
    /// [`note_cursor_id`].
    id0_hides: bool,
}

impl CursorState {
    /// The per-stream state, declaring which `id == 0` contract the producer follows
    /// ([`Self::id0_hides`]).
    pub(super) fn new(id0_hides: bool) -> CursorState {
        CursorState {
            id0_hides,
            ..CursorState::default()
        }
    }

    /// A shareable overlay for the encode/forward paths, or `None` before the first bitmap
    /// arrived. A HIDDEN pointer still yields `Some` (with `visible: false`): the
    /// cursor-forward channel needs "known but hidden" — an app grabbed the pointer, the
    /// client's relative-mode hint (M3) — which is a different fact from "no cursor yet".
    /// The encode loop strips invisible overlays before any blend path sees the frame.
    /// Cheap: clones an `Arc` + a few scalars.
    pub(super) fn overlay(&self) -> Option<pf_frame::CursorOverlay> {
        if self.rgba.is_empty() {
            return None;
        }
        Some(pf_frame::CursorOverlay {
            x: self.x,
            y: self.y,
            w: self.bw,
            h: self.bh,
            rgba: self.rgba.clone(),
            serial: self.serial,
            hot_x: self.hot_x.max(0) as u32,
            hot_y: self.hot_y.max(0) as u32,
            visible: self.visible,
        })
    }
}

/// Extract straight (R,G,B,A) from one 4-byte cursor-bitmap pixel, honoring the bitmap's SPA
/// video format (portals emit RGBA or BGRA; ARGB/ABGR handled for completeness). Unknown
/// 4-byte formats are read as RGBA.
pub(super) fn decode_bitmap_pixel(vfmt: u32, s: &[u8]) -> (u8, u8, u8, u8) {
    match vfmt {
        x if x == spa::sys::SPA_VIDEO_FORMAT_RGBA => (s[0], s[1], s[2], s[3]),
        x if x == spa::sys::SPA_VIDEO_FORMAT_BGRA => (s[2], s[1], s[0], s[3]),
        x if x == spa::sys::SPA_VIDEO_FORMAT_ARGB => (s[1], s[2], s[3], s[0]),
        x if x == spa::sys::SPA_VIDEO_FORMAT_ABGR => (s[3], s[2], s[1], s[0]),
        _ => (s[0], s[1], s[2], s[3]),
    }
}

/// Apply one parsed `spa_meta_cursor.id` to the visibility state; returns whether the rest of the
/// meta region (position, bitmap) is worth parsing.
///
/// Two producer contracts meet on `id == 0`. **KWin** rewrites the cursor meta on EVERY enqueued
/// buffer, and writes id 0 whenever `Cursor::isOnOutput` says the pointer is not in this stream —
/// which covers a globally hidden cursor AND a client null-cursor surface (empty cursor geometry
/// intersects nothing). There id 0 is the authoritative hide, and honoring it is what lets a game
/// or Big Picture hide the pointer mid-stream ([`CursorState::id0_hides`], set for KWin virtual
/// outputs; without it the composited arrow outlived every hide — the 0.22.0 field report).
/// **Mutter** only rewrites a buffer's meta region when the cursor changed, so recycled buffers
/// between damage frames carry a stale id-0 meta — treating that as hidden flickered the cursor
/// off between hovers (on-glass round 5). There the last-known state holds, and a pointer that
/// really left/hid simply stops producing updates (the M3 hidden hint has no Mutter signal —
/// Windows has its own CURSOR_SUPPRESSED source).
fn note_cursor_id(cursor: &mut CursorState, id: u32) -> bool {
    if id == 0 {
        if cursor.id0_hides {
            cursor.visible = false;
        }
        return false;
    }
    cursor.visible = true;
    true
}

/// Update `cursor` from the newest buffer's `SPA_META_Cursor` (no-op when the buffer carries no
/// cursor meta — producer doesn't support it, or the portal isn't in Metadata cursor mode).
/// Called for EVERY dequeued buffer, before the stale-frame skip, so pointer-only movements
/// (which Mutter delivers as metadata-only "corrupted" buffers) still refresh the position.
pub(super) fn update_cursor_meta(cursor: &mut CursorState, spa_buf: *mut spa::sys::spa_buffer) {
    // SAFETY: `spa_buf` is the live buffer we still hold (dequeued, not yet requeued).
    // `spa_buffer_find_meta` returns the `spa_meta` (type + byte `size` + `data` pointer) for
    // `SPA_META_Cursor`, or null. We take `find_meta` rather than `find_meta_data` specifically
    // to obtain the region's real `size`: the bitmap offset, pixel offset and stride read below
    // are ALL producer-written, and without a bound against the actual region they drive
    // out-of-bounds pointer arithmetic and an oversized `slice::from_raw_parts` — an OOB read
    // that SIGSEGVs inside the PipeWire `.process` callback (a segfault `catch_unwind` cannot
    // catch). Every offset below is validated against `region_size` with checked arithmetic,
    // mirroring the fd-length guard the main frame path already applies to xdg-desktop-portal-wlr.
    let meta = unsafe { spa::sys::spa_buffer_find_meta(spa_buf, spa::sys::SPA_META_Cursor) };
    if meta.is_null() {
        return;
    }
    // One-shot breadcrumb (per stream): the producer DID attach a cursor-meta region (the Meta
    // param negotiated). Field triage for a cursorless stream starts by grepping for this line
    // — its absence means the negotiation dropped the meta, not that the pointer never moved.
    if !cursor.seen_meta {
        cursor.seen_meta = true;
        tracing::info!("cursor meta: first SPA_META_Cursor region observed on this stream");
    }
    // SAFETY: `meta` is non-null and points into the held buffer's metadata array.
    let (region_size, data) = unsafe { ((*meta).size as usize, (*meta).data as *const u8) };
    if data.is_null() || region_size < std::mem::size_of::<spa::sys::spa_meta_cursor>() {
        return;
    }
    let cur = data as *const spa::sys::spa_meta_cursor;
    // SAFETY: `region_size >= size_of::<spa_meta_cursor>()` checked above, so every field is in bounds.
    let (id, pos_x, pos_y, hot_x, hot_y, bmp_off) = unsafe {
        (
            (*cur).id,
            (*cur).position.x,
            (*cur).position.y,
            (*cur).hotspot.x,
            (*cur).hotspot.y,
            (*cur).bitmap_offset,
        )
    };
    if !note_cursor_id(cursor, id) {
        return;
    }
    cursor.x = pos_x - hot_x;
    cursor.y = pos_y - hot_y;
    cursor.hot_x = hot_x;
    cursor.hot_y = hot_y;
    if bmp_off == 0 {
        // Position-only update — keep the cached bitmap.
        return;
    }
    let bmp_off = bmp_off as usize;
    // The `spa_meta_bitmap` header must fit entirely inside the region before we read it —
    // `bitmap_offset` is producer-controlled and otherwise reads past the metadata.
    match bmp_off.checked_add(std::mem::size_of::<spa::sys::spa_meta_bitmap>()) {
        Some(end) if end <= region_size => {}
        _ => return,
    }
    // SAFETY: `bmp_off + size_of::<spa_meta_bitmap>() <= region_size` (checked directly above),
    // so the header is fully in bounds for a read of that many bytes. `read_unaligned` is
    // REQUIRED, not defensive: `bmp_off` is producer-written and nothing in the SPA contract or
    // in this function establishes that `data + bmp_off` meets `spa_meta_bitmap`'s alignment —
    // the previous field reads through an aligned `*const` asserted an invariant the code never
    // proved. The struct is `Copy` POD, so one unaligned read yields an owned, aligned local.
    let bmp = unsafe { (data.add(bmp_off) as *const spa::sys::spa_meta_bitmap).read_unaligned() };
    let (vfmt, bw, bh, stride, pix_off) = (
        bmp.format,
        bmp.size.width,
        bmp.size.height,
        bmp.stride.max(0) as usize,
        bmp.offset as usize,
    );
    // Ignore empty or implausibly large bitmaps (the meta-size request covers <= 1024×1024;
    // real cursors are ≤96px — the cursor channel downscales >120px for the wire anyway).
    if bw == 0 || bh == 0 || bw > 1024 || bh > 1024 {
        return;
    }
    // SPA's second "no image data" signal, distinct from the `bitmap_offset == 0` position-only
    // case above: `spa_meta_bitmap.offset` is the offset of the PIXELS within the bitmap struct,
    // and 0 means there are none. Without this, `pix_off == 0` made the pixel extent start at the
    // `spa_meta_bitmap` header itself, so a producer signalling an invisible pointer got its own
    // header words (format/size/stride/offset) decoded and cached as the cursor bitmap. In bounds,
    // so not unsound — just garbage pixels blitted into every later frame.
    if pix_off == 0 {
        return;
    }
    let row = bw as usize * 4;
    let stride = if stride < row { row } else { stride };
    let Some(extent) = bitmap_extent(bmp_off, pix_off, stride, row, bh as usize, region_size)
    else {
        return;
    };
    // SAFETY: `bitmap_extent` returned `Some`, which means (see its contract) the whole range
    // `[bmp_off + pix_off, +len)` lies inside `region_size` and `len` is EXACTLY the extent the
    // strided loop below reads. `data` is the producer's meta-region base, live for this callback.
    let src = unsafe { std::slice::from_raw_parts(data.add(extent.start), extent.len()) };
    let mut rgba = vec![0u8; bw as usize * bh as usize * 4];
    for y in 0..bh as usize {
        for x in 0..bw as usize {
            let so = y * stride + x * 4;
            let (r, g, b, a) = decode_bitmap_pixel(vfmt, &src[so..so + 4]);
            let d = (y * bw as usize + x) * 4;
            rgba[d] = r;
            rgba[d + 1] = g;
            rgba[d + 2] = b;
            rgba[d + 3] = a;
        }
    }
    cursor.rgba = Arc::new(rgba);
    cursor.bw = bw;
    cursor.bh = bh;
    cursor.serial = cursor.serial.wrapping_add(1);
    // One-shot sibling of the region breadcrumb above (per stream, via the serial's 0→1 edge):
    // the first BITMAP — before this line has fired, `overlay()` is `None` and every
    // blend/forward path is cursorless by construction.
    if cursor.serial == 1 {
        tracing::info!(w = bw, h = bh, "cursor meta: first cursor bitmap received");
    }
}

/// The byte range inside the producer's cursor-meta region that a `bh`-row, `row`-wide,
/// `stride`-strided bitmap at `bmp_off + pix_off` occupies — or `None` when it does not fit, or when
/// any of the arithmetic overflows.
///
/// THE bound on `update_cursor_meta`. Every input except `region_size` is producer-written, and
/// `region_size` is the real byte size of the meta region libspa handed us (which is why the caller
/// takes `spa_buffer_find_meta` rather than `find_meta_data`). Without this check the offsets drive
/// out-of-bounds pointer arithmetic and an oversized `slice::from_raw_parts` — an OOB read that
/// SIGSEGVs inside the PipeWire `.process` callback, where `catch_unwind` cannot help. A stride near
/// `i32::MAX` is enough to overflow the multiply on its own, so every step is checked.
///
/// `len()` of the returned range is EXACTLY `stride·(bh−1) + row`: the last row contributes only its
/// `row` visible bytes, not a full stride, so a bitmap that ends flush against the region's end is
/// accepted rather than rejected by a padding byte that is never read.
///
/// Extracted (sweep Phase 6.1) purely so it can be tested — the caller is unreachable without a live
/// compositor.
fn bitmap_extent(
    bmp_off: usize,
    pix_off: usize,
    stride: usize,
    row: usize,
    bh: usize,
    region_size: usize,
) -> Option<std::ops::Range<usize>> {
    if bh == 0 || row == 0 || stride < row {
        return None;
    }
    let span = stride.checked_mul(bh - 1)?.checked_add(row)?;
    let start = bmp_off.checked_add(pix_off)?;
    let end = start.checked_add(span)?;
    (end <= region_size).then_some(start..end)
}

/// Destination channel byte offsets (R,G,B) and bytes-per-pixel for a packed-RGB `PixelFormat`,
/// or `None` for a layout the CPU cursor blit doesn't handle (YUV/10-bit — those never reach
/// the CPU de-pad path anyway).
pub(super) fn dst_offsets(fmt: PixelFormat) -> Option<(usize, usize, usize, usize)> {
    Some(match fmt {
        PixelFormat::Bgrx | PixelFormat::Bgra => (2, 1, 0, 4),
        PixelFormat::Rgbx | PixelFormat::Rgba => (0, 1, 2, 4),
        PixelFormat::Rgb => (0, 1, 2, 3),
        PixelFormat::Bgr => (2, 1, 0, 3),
        _ => return None,
    })
}

/// Alpha-blend the cached cursor bitmap into a packed 10-bit (`X2Rgb10`/`X2Bgr10`) CPU frame:
/// unpack each u32, blend the 8-bit cursor channels scaled to 10 bits (`v<<2 | v>>6`), repack.
/// The frame samples are PQ-encoded, so like the 8-bit gamma-space blend this is a display-
/// referred approximation — fine for a cursor. `r_shift` is the R channel's bit offset (20 for
/// x:R:G:B, 0 for x:B:G:R); G is always at 10 and B mirrors R.
pub(super) fn composite_cursor_rgb10(
    tight: &mut [u8],
    w: usize,
    h: usize,
    r_shift: u32,
    cursor: &CursorState,
) {
    let b_shift = 20 - r_shift; // 0 or 20 — the opposite end from R
    let (bw, bh) = (cursor.bw as i32, cursor.bh as i32);
    for cy in 0..bh {
        let dy = cursor.y + cy;
        if dy < 0 || dy as usize >= h {
            continue;
        }
        for cx in 0..bw {
            let dx = cursor.x + cx;
            if dx < 0 || dx as usize >= w {
                continue;
            }
            let s = ((cy * bw + cx) as usize) * 4;
            let a = cursor.rgba[s + 3] as u32;
            if a == 0 {
                continue;
            }
            // 8-bit cursor channel → 10-bit (replicate the top bits into the bottom).
            let up10 = |v: u8| ((v as u32) << 2) | ((v as u32) >> 6);
            let (sr, sg, sb) = (
                up10(cursor.rgba[s]),
                up10(cursor.rgba[s + 1]),
                up10(cursor.rgba[s + 2]),
            );
            let di = (dy as usize * w + dx as usize) * 4;
            let px = u32::from_le_bytes(tight[di..di + 4].try_into().unwrap());
            let blend = |dst: u32, src: u32| (src * a + dst * (255 - a)) / 255;
            let dr = blend((px >> r_shift) & 0x3ff, sr);
            let dg = blend((px >> 10) & 0x3ff, sg);
            let db = blend((px >> b_shift) & 0x3ff, sb);
            let out = (px & 0xc000_0000) | (dr << r_shift) | (dg << 10) | (db << b_shift);
            tight[di..di + 4].copy_from_slice(&out.to_le_bytes());
        }
    }
}

/// Alpha-blend the cached cursor bitmap into the tightly-packed CPU frame at its latched
/// position. Cheap: a straight-alpha blit over at most 1024×1024 pixels (the accepted cap; real
/// cursors are ≤96 px), clipped to the frame —
/// the whole point of cursor-as-metadata (no forced full-frame composite on the producer).
pub(super) fn composite_cursor(
    tight: &mut [u8],
    w: usize,
    h: usize,
    fmt: PixelFormat,
    cursor: &CursorState,
) {
    if !cursor.visible || cursor.rgba.is_empty() {
        return;
    }
    // The packed 10-bit HDR layouts blend via bit unpack/repack, not byte offsets.
    match fmt {
        PixelFormat::X2Rgb10 => return composite_cursor_rgb10(tight, w, h, 20, cursor),
        PixelFormat::X2Bgr10 => return composite_cursor_rgb10(tight, w, h, 0, cursor),
        _ => {}
    }
    let Some((ri, gi, bi, bpp)) = dst_offsets(fmt) else {
        return;
    };
    let (bw, bh) = (cursor.bw as i32, cursor.bh as i32);
    for cy in 0..bh {
        let dy = cursor.y + cy;
        if dy < 0 || dy as usize >= h {
            continue;
        }
        for cx in 0..bw {
            let dx = cursor.x + cx;
            if dx < 0 || dx as usize >= w {
                continue;
            }
            let s = ((cy * bw + cx) as usize) * 4;
            let a = cursor.rgba[s + 3] as u32;
            if a == 0 {
                continue;
            }
            let (sr, sg, sb) = (
                cursor.rgba[s] as u32,
                cursor.rgba[s + 1] as u32,
                cursor.rgba[s + 2] as u32,
            );
            let di = (dy as usize * w + dx as usize) * bpp;
            let blend = |dst: u8, src: u32| ((src * a + dst as u32 * (255 - a)) / 255) as u8;
            tight[di + ri] = blend(tight[di + ri], sr);
            tight[di + gi] = blend(tight[di + gi], sg);
            tight[di + bi] = blend(tight[di + bi], sb);
        }
    }
}

#[cfg(test)]
mod tests {
    use super::*;

    /// A solid-colour cursor state at `(x, y)`, `w`×`h`, alpha `a`.
    fn cursor(x: i32, y: i32, w: u32, h: u32, rgb: (u8, u8, u8), a: u8) -> CursorState {
        let mut px = Vec::with_capacity((w * h * 4) as usize);
        for _ in 0..w * h {
            px.extend_from_slice(&[rgb.0, rgb.1, rgb.2, a]);
        }
        CursorState {
            visible: true,
            x,
            y,
            rgba: Arc::new(px),
            bw: w,
            bh: h,
            serial: 1,
            hot_x: 0,
            hot_y: 0,
            seen_meta: true,
            id0_hides: false,
        }
    }

    // ---- note_cursor_id: the two producer id-0 contracts --------------------------------------

    #[test]
    fn id_zero_hides_only_on_a_rewriting_producer() {
        // KWin contract (`id0_hides`): id 0 is written fresh on every buffer, so it IS the hide —
        // a game or Big Picture hiding the pointer must reach the stream.
        let mut kwin = cursor(10, 10, 8, 8, (255, 255, 255), 255);
        kwin.id0_hides = true;
        assert!(!note_cursor_id(&mut kwin, 0), "id 0 parses no further");
        let o = kwin.overlay().expect("bitmap stays cached across a hide");
        assert!(!o.visible, "KWin id 0 must hide the overlay");
        // The pointer coming back re-shows the SAME cached bitmap.
        assert!(note_cursor_id(&mut kwin, 1));
        assert!(kwin.overlay().expect("still cached").visible);

        // Mutter contract: recycled buffers carry stale id-0 metas — the last-known state holds
        // (honoring them flickered the cursor off between hovers, on-glass round 5).
        let mut mutter = cursor(10, 10, 8, 8, (255, 255, 255), 255);
        assert!(!note_cursor_id(&mut mutter, 0));
        assert!(
            mutter.overlay().expect("cached").visible,
            "a stale-meta producer's id 0 must NOT hide"
        );
    }

    #[test]
    fn id_zero_before_any_bitmap_yields_no_overlay() {
        // A KWin stream whose pointer was never on the output: hides arrive before any bitmap —
        // `overlay()` must stay `None` (nothing to blend), not a phantom empty cursor.
        let mut c = CursorState::new(true);
        assert!(!note_cursor_id(&mut c, 0));
        assert!(c.overlay().is_none());
    }

    // ---- bitmap_extent: the guard whose absence SIGSEGVs uncatchably -------------------------

    #[test]
    fn bitmap_extent_accepts_a_bitmap_that_fits() {
        // 4×2 RGBA, tightly packed: 32 bytes at offset 0.
        assert_eq!(bitmap_extent(0, 0, 16, 16, 2, 32), Some(0..32));
        // …and the same bitmap behind a header + pixel offset.
        assert_eq!(bitmap_extent(24, 8, 16, 16, 2, 64), Some(32..64));
    }

    #[test]
    fn bitmap_extent_charges_the_last_row_only_its_visible_bytes() {
        // stride 32, row 16, 3 rows ⇒ 32*2 + 16 = 80, NOT 96. A bitmap ending flush against the
        // region must be accepted: the trailing stride padding is never read.
        assert_eq!(bitmap_extent(0, 0, 32, 16, 3, 80), Some(0..80));
        assert_eq!(bitmap_extent(0, 0, 32, 16, 3, 79), None, "one byte short");
    }

    #[test]
    fn bitmap_extent_rejects_anything_past_the_region() {
        assert_eq!(bitmap_extent(0, 0, 16, 16, 2, 31), None);
        // An offset alone can push it out.
        assert_eq!(bitmap_extent(1, 0, 16, 16, 2, 32), None);
        assert_eq!(bitmap_extent(0, 1, 16, 16, 2, 32), None);
        // A region of zero accepts nothing.
        assert_eq!(bitmap_extent(0, 0, 16, 16, 1, 0), None);
    }

    /// The producer picks `stride` and both offsets, so each is an overflow vector on its own.
    #[test]
    fn bitmap_extent_survives_hostile_arithmetic() {
        // stride × (bh-1) overflows.
        assert_eq!(bitmap_extent(0, 0, usize::MAX, 16, 3, usize::MAX), None);
        // span + row overflows. Needs ≥2 rows so `stride·(bh−1)` is already at the ceiling: with a
        // SINGLE row `stride·0 == 0`, and even a `usize::MAX`-wide row is then arithmetically in
        // range — which is correct rather than a miss, since the caller has already capped `bw` at
        // 1024 and `row` is therefore ≤ 4096.
        assert_eq!(
            bitmap_extent(0, 0, usize::MAX, usize::MAX, 2, usize::MAX),
            None
        );
        // bmp_off + pix_off overflows.
        assert_eq!(bitmap_extent(usize::MAX, 1, 16, 16, 1, usize::MAX), None);
        // start + span overflows.
        assert_eq!(
            bitmap_extent(usize::MAX - 8, 0, 16, 16, 1, usize::MAX),
            None
        );
        // A near-i32::MAX stride — the case the SAFETY comment calls out — must not wrap.
        assert_eq!(bitmap_extent(0, 0, i32::MAX as usize, 16, 1024, 4096), None);
    }

    #[test]
    fn bitmap_extent_rejects_degenerate_geometry() {
        assert_eq!(bitmap_extent(0, 0, 16, 16, 0, 4096), None, "zero rows");
        assert_eq!(bitmap_extent(0, 0, 16, 0, 2, 4096), None, "zero-width row");
        assert_eq!(bitmap_extent(0, 0, 8, 16, 2, 4096), None, "stride < row");
    }

    // ---- composite_cursor: clipping, alpha, and every layout --------------------------------

    /// Read pixel `(x, y)`'s (R, G, B) out of a packed frame, honouring the layout's byte order.
    fn px_rgb(buf: &[u8], w: usize, x: usize, y: usize, fmt: PixelFormat) -> (u8, u8, u8) {
        let (ri, gi, bi, bpp) = dst_offsets(fmt).expect("packed layout");
        let i = (y * w + x) * bpp;
        (buf[i + ri], buf[i + gi], buf[i + bi])
    }

    #[test]
    fn every_packed_layout_lands_the_colour_in_its_own_channels() {
        for fmt in [
            PixelFormat::Bgrx,
            PixelFormat::Bgra,
            PixelFormat::Rgbx,
            PixelFormat::Rgba,
            PixelFormat::Rgb,
            PixelFormat::Bgr,
        ] {
            let bpp = dst_offsets(fmt).unwrap().3;
            let (w, h) = (4usize, 4usize);
            let mut buf = vec![0u8; w * h * bpp];
            // Opaque pure red at (1, 1).
            composite_cursor(&mut buf, w, h, fmt, &cursor(1, 1, 1, 1, (255, 0, 0), 255));
            assert_eq!(px_rgb(&buf, w, 1, 1, fmt), (255, 0, 0), "{fmt:?}");
            // Nothing else moved.
            assert_eq!(px_rgb(&buf, w, 0, 0, fmt), (0, 0, 0), "{fmt:?}");
            assert_eq!(px_rgb(&buf, w, 2, 1, fmt), (0, 0, 0), "{fmt:?}");
        }
    }

    #[test]
    fn a_cursor_hanging_off_every_edge_is_clipped_not_wrapped() {
        let (w, h, fmt) = (4usize, 4usize, PixelFormat::Bgrx);
        // Top-left: only the bottom-right quarter of a 2×2 lands, at (0, 0).
        let mut buf = vec![0u8; w * h * 4];
        composite_cursor(
            &mut buf,
            w,
            h,
            fmt,
            &cursor(-1, -1, 2, 2, (10, 20, 30), 255),
        );
        assert_eq!(px_rgb(&buf, w, 0, 0, fmt), (10, 20, 30));
        assert_eq!(px_rgb(&buf, w, 1, 0, fmt), (0, 0, 0));
        assert_eq!(px_rgb(&buf, w, 0, 1, fmt), (0, 0, 0));
        // Bottom-right: only the top-left quarter lands, at (3, 3).
        let mut buf = vec![0u8; w * h * 4];
        composite_cursor(&mut buf, w, h, fmt, &cursor(3, 3, 2, 2, (10, 20, 30), 255));
        assert_eq!(px_rgb(&buf, w, 3, 3, fmt), (10, 20, 30));
        assert_eq!(px_rgb(&buf, w, 2, 3, fmt), (0, 0, 0));
        // Fully outside in each direction: the frame is untouched.
        for pos in [(-2, 0), (0, -2), (4, 0), (0, 4), (-9, -9), (99, 99)] {
            let mut buf = vec![0u8; w * h * 4];
            composite_cursor(
                &mut buf,
                w,
                h,
                fmt,
                &cursor(pos.0, pos.1, 2, 2, (255, 255, 255), 255),
            );
            assert!(buf.iter().all(|&b| b == 0), "drew something at {pos:?}");
        }
    }

    #[test]
    fn transparent_and_hidden_cursors_draw_nothing() {
        let (w, h, fmt) = (2usize, 2usize, PixelFormat::Bgrx);
        // Alpha 0 — every pixel skipped.
        let mut buf = vec![0u8; w * h * 4];
        composite_cursor(&mut buf, w, h, fmt, &cursor(0, 0, 2, 2, (255, 255, 255), 0));
        assert!(buf.iter().all(|&b| b == 0));
        // `visible: false` — the whole blit is skipped.
        let mut c = cursor(0, 0, 2, 2, (255, 255, 255), 255);
        c.visible = false;
        let mut buf = vec![0u8; w * h * 4];
        composite_cursor(&mut buf, w, h, fmt, &c);
        assert!(buf.iter().all(|&b| b == 0));
        // No bitmap yet — likewise.
        let mut c = cursor(0, 0, 2, 2, (255, 255, 255), 255);
        c.rgba = Arc::new(Vec::new());
        let mut buf = vec![0u8; w * h * 4];
        composite_cursor(&mut buf, w, h, fmt, &c);
        assert!(buf.iter().all(|&b| b == 0));
    }

    #[test]
    fn half_alpha_blends_toward_the_destination() {
        let (w, h, fmt) = (1usize, 1usize, PixelFormat::Bgrx);
        // dst = white, src = black at 50% ⇒ mid grey (integer blend: (0*128 + 255*127)/255 = 127).
        let mut buf = vec![255u8; w * h * 4];
        composite_cursor(&mut buf, w, h, fmt, &cursor(0, 0, 1, 1, (0, 0, 0), 128));
        assert_eq!(px_rgb(&buf, w, 0, 0, fmt), (127, 127, 127));
    }

    /// A layout the CPU blit cannot address must be declined, not mis-blitted.
    #[test]
    fn unsupported_layouts_are_declined() {
        assert!(dst_offsets(PixelFormat::Nv12).is_none());
        assert!(dst_offsets(PixelFormat::Yuv444).is_none());
        let (w, h) = (2usize, 2usize);
        let mut buf = vec![0u8; w * h * 4];
        composite_cursor(
            &mut buf,
            w,
            h,
            PixelFormat::Nv12,
            &cursor(0, 0, 2, 2, (255, 255, 255), 255),
        );
        assert!(buf.iter().all(|&b| b == 0), "NV12 must not be blitted");
    }

    // ---- composite_cursor_rgb10: the 10-bit unpack/repack round trip -------------------------

    /// Pack an `x:R:G:B` (`X2Rgb10`) pixel — R at bit 20, G at 10, B at 0 — the way a producer does.
    fn pack_x2rgb10(r: u32, g: u32, b: u32) -> [u8; 4] {
        (0xC000_0000 | (r << 20) | (g << 10) | b).to_le_bytes()
    }

    #[test]
    fn the_10bit_path_round_trips_an_untouched_pixel() {
        // Alpha 0 ⇒ the blend is skipped entirely, so the packed pixel must come back bit-identical
        // (including the top two bits, which are alpha and must survive the repack).
        for (r, g, b) in [(0, 0, 0), (1023, 1023, 1023), (940, 64, 512), (1, 2, 3)] {
            let src = pack_x2rgb10(r, g, b);
            let mut buf = src.to_vec();
            composite_cursor(
                &mut buf,
                1,
                1,
                PixelFormat::X2Rgb10,
                &cursor(0, 0, 1, 1, (255, 255, 255), 0),
            );
            assert_eq!(buf, src, "({r},{g},{b}) was modified by a zero-alpha blend");
        }
    }

    #[test]
    fn the_10bit_path_writes_the_right_channel_at_the_right_shift() {
        // Opaque pure red, 8-bit 255 → 10-bit 1023 (the `v<<2 | v>>6` expansion).
        let mut buf = pack_x2rgb10(0, 0, 0).to_vec();
        composite_cursor(
            &mut buf,
            1,
            1,
            PixelFormat::X2Rgb10,
            &cursor(0, 0, 1, 1, (255, 0, 0), 255),
        );
        let v = u32::from_le_bytes(buf[..4].try_into().unwrap());
        assert_eq!((v >> 20) & 0x3ff, 1023, "R");
        assert_eq!((v >> 10) & 0x3ff, 0, "G");
        assert_eq!(v & 0x3ff, 0, "B");
        assert_eq!(v & 0xc000_0000, 0xc000_0000, "alpha bits preserved");

        // X2Bgr10 puts R at bit 0 and B at 20 — the SAME cursor must land in the other end.
        let mut buf = pack_x2rgb10(0, 0, 0).to_vec();
        composite_cursor(
            &mut buf,
            1,
            1,
            PixelFormat::X2Bgr10,
            &cursor(0, 0, 1, 1, (255, 0, 0), 255),
        );
        let v = u32::from_le_bytes(buf[..4].try_into().unwrap());
        assert_eq!(v & 0x3ff, 1023, "R at bit 0 for x:B:G:R");
        assert_eq!((v >> 20) & 0x3ff, 0, "B untouched");
    }

    #[test]
    fn the_10bit_path_clips_like_the_8bit_one() {
        let (w, h) = (2usize, 2usize);
        let mut buf: Vec<u8> = (0..w * h).flat_map(|_| pack_x2rgb10(0, 0, 0)).collect();
        let before = buf.clone();
        // Entirely off-frame.
        composite_cursor(
            &mut buf,
            w,
            h,
            PixelFormat::X2Rgb10,
            &cursor(-5, -5, 2, 2, (255, 255, 255), 255),
        );
        assert_eq!(buf, before);
        // Straddling the top-left corner: only (0, 0) is written.
        composite_cursor(
            &mut buf,
            w,
            h,
            PixelFormat::X2Rgb10,
            &cursor(-1, -1, 2, 2, (255, 255, 255), 255),
        );
        let p0 = u32::from_le_bytes(buf[0..4].try_into().unwrap());
        let p1 = u32::from_le_bytes(buf[4..8].try_into().unwrap());
        assert_eq!((p0 >> 20) & 0x3ff, 1023);
        assert_eq!((p1 >> 20) & 0x3ff, 0);
    }

    // ---- decode_bitmap_pixel: the producer's byte order ------------------------------------

    #[test]
    fn each_bitmap_format_is_decoded_to_straight_rgba() {
        let s = [1u8, 2, 3, 4];
        assert_eq!(
            decode_bitmap_pixel(spa::sys::SPA_VIDEO_FORMAT_RGBA, &s),
            (1, 2, 3, 4)
        );
        assert_eq!(
            decode_bitmap_pixel(spa::sys::SPA_VIDEO_FORMAT_BGRA, &s),
            (3, 2, 1, 4)
        );
        assert_eq!(
            decode_bitmap_pixel(spa::sys::SPA_VIDEO_FORMAT_ARGB, &s),
            (2, 3, 4, 1)
        );
        assert_eq!(
            decode_bitmap_pixel(spa::sys::SPA_VIDEO_FORMAT_ABGR, &s),
            (4, 3, 2, 1)
        );
        // An unknown 4-byte format reads as RGBA rather than being rejected — documented behaviour.
        assert_eq!(decode_bitmap_pixel(0xdead_beef, &s), (1, 2, 3, 4));
    }
}
