tomolib 1.1.0

Library for reading, modifying, and extracting Tomodachi Life data formats.
Documentation
#[inline]
fn round_up(n: u32, a: u32) -> u32 {
    n.div_ceil(a) * a
}

#[inline]
fn block_linear_address(x: u32, y: u32, width: u32, bpp: u32, block_height: u32) -> u64 {
    let width_in_gobs = (width * bpp).div_ceil(64);
    let gob = u64::from((y / (8 * block_height)) * 512 * block_height * width_in_gobs)
        + u64::from((x * bpp / 64) * 512 * block_height)
        + u64::from((y % (8 * block_height) / 8) * 512);
    let xb = x * bpp;
    gob + u64::from(
        ((xb % 64) / 32) * 256
            + ((y % 8) / 2) * 64
            + ((xb % 32) / 16) * 32
            + (y % 2) * 16
            + (xb % 16),
    )
}

#[must_use]
pub(crate) fn swizzled_surface_size(
    width_blocks: u32,
    height_blocks: u32,
    bpp: u32,
    block_height: u32,
    linear: bool,
    round_pitch: bool,
) -> usize {
    if linear {
        let mut pitch = width_blocks * bpp;
        if round_pitch {
            pitch = round_up(pitch, 32);
        }
        (pitch * height_blocks) as usize
    } else {
        let pitch = round_up(width_blocks * bpp, 64);
        (pitch * round_up(height_blocks, block_height * 8)) as usize
    }
}

#[must_use]
pub(crate) fn mip_block_height(height_blocks: u32, base_block_height: u32) -> u32 {
    let mut bh = base_block_height;
    while bh > 1 && height_blocks.div_ceil(8) <= bh / 2 {
        bh /= 2;
    }
    bh
}

#[derive(Debug, Clone, Copy)]
pub(crate) struct Surface {
    pub(crate) width: u32,
    pub(crate) height: u32,
    pub(crate) blk_width: u32,
    pub(crate) blk_height: u32,
    pub(crate) bpp: u32,
    pub(crate) tile_mode: u16,
    pub(crate) block_height: u32,
}

#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub(crate) enum Direction {
    ToLinear,
    ToSwizzled,
}

pub(crate) fn convert(surface: &Surface, data: &[u8], dir: Direction) -> crate::Result<Vec<u8>> {
    let Surface {
        width,
        height,
        blk_width,
        blk_height,
        bpp,
        tile_mode,
        block_height,
    } = *surface;
    let to_swizzle = dir == Direction::ToSwizzled;
    let width_blocks = width.div_ceil(blk_width);
    let height_blocks = height.div_ceil(blk_height);
    let linear = tile_mode == 1;
    let round_pitch = linear;

    let surf_size = swizzled_surface_size(
        width_blocks,
        height_blocks,
        bpp,
        block_height,
        linear,
        round_pitch,
    );
    let linear_size = (width_blocks * height_blocks * bpp) as usize;

    let src_size = if to_swizzle { linear_size } else { surf_size };
    if data.len() < src_size {
        return Err(crate::Error::truncated(
            "BNTX swizzle surface",
            0,
            src_size,
            data.len(),
        ));
    }

    let bpp_us = bpp as usize;
    let pitch = if linear {
        let p = width_blocks * bpp;
        if round_pitch { round_up(p, 32) } else { p }
    } else {
        0
    };

    let mut out = vec![0u8; if to_swizzle { surf_size } else { linear_size }];

    for y in 0..height_blocks {
        for x in 0..width_blocks {
            let swizzled = if linear {
                (y * pitch + x * bpp) as usize
            } else {
                usize::try_from(block_linear_address(x, y, width_blocks, bpp, block_height))
                    .unwrap_or(usize::MAX)
            };
            let row_major = ((y * width_blocks + x) * bpp) as usize;

            let (src, dst, base) = if to_swizzle {
                (row_major, swizzled, surf_size)
            } else {
                (swizzled, row_major, linear_size)
            };
            if src + bpp_us <= data.len() && dst + bpp_us <= base {
                out[dst..dst + bpp_us].copy_from_slice(&data[src..src + bpp_us]);
            }
        }
    }
    Ok(out)
}

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

    #[test]
    fn round_trip_swizzle_is_identity() {
        let (w, h, bw, bh, bpp, block_height) = (64u32, 64u32, 4u32, 4u32, 8u32, 2u32);
        let wb = w.div_ceil(bw);
        let hb = h.div_ceil(bh);
        let linear: Vec<u8> = (0..(wb * hb * bpp)).map(|i| (i % 251) as u8).collect();

        let surface = Surface {
            width: w,
            height: h,
            blk_width: bw,
            blk_height: bh,
            bpp,
            tile_mode: 0,
            block_height,
        };
        let swizzled = convert(&surface, &linear, Direction::ToSwizzled).unwrap();
        let back = convert(&surface, &swizzled, Direction::ToLinear).unwrap();
        assert_eq!(back, linear);
    }

    #[test]
    fn deswizzle_rejects_truncated_input() {
        let surface = Surface {
            width: 64,
            height: 64,
            blk_width: 4,
            blk_height: 4,
            bpp: 8,
            tile_mode: 0,
            block_height: 2,
        };
        let too_short = vec![0u8; 16];
        assert!(convert(&surface, &too_short, Direction::ToLinear).is_err());
    }

    #[test]
    fn mip_block_height_shrinks() {
        assert_eq!(mip_block_height(64, 8), 8);
        assert_eq!(mip_block_height(4, 8), 1);
        assert_eq!(mip_block_height(1, 16), 1);
    }
}