gba_agb_font_renderer 0.2.0

Bitmap font renderer for GBA/AGB
use crate::font::AgbFont;
use agb::display::tiled::{DynamicTile16, RegularBackground, TileEffect};
use agb::fixnum::{Vector2D, vec2};
use alloc::vec::Vec;

pub struct TextRenderer {
    tiles: Vec<(i32, i32, DynamicTile16)>,
}

impl Default for TextRenderer {
    fn default() -> Self {
        Self {
            tiles: Vec::with_capacity(32),
        }
    }
}

impl TextRenderer {
    /// # Returns
    /// (x of last line, y of last line, longest line)
    /// these are relative to pos
    pub fn draw_text<T: AgbFont>(
        &mut self,
        text: &[u8],
        font: &T,
        background: &mut RegularBackground,
        pos: Vector2D<i32>,
        wrap_at: Option<u8>,
        palette_id: u8,
    ) -> (i32, i32, i32) {
        let wrap_at = wrap_at.map(|n| n as i32);
        let mut cursor_y = pos.y;
        let mut cursor_x = pos.x;
        let mut longest = 0;
        for &c in text {
            if c == b'\n' {
                cursor_y += font.glyph_height() as i32;
                if cursor_x > longest {
                    longest = cursor_x;
                }
                cursor_x = pos.x;
            } else {
                let char_w = font.char_width(c) as i32;
                if let Some(wrap_at) = wrap_at
                    && cursor_x - pos.x + char_w > wrap_at
                {
                    cursor_y += font.glyph_height() as i32;
                    if cursor_x > longest {
                        longest = cursor_x;
                    }
                    cursor_x = pos.x;
                }
                self.draw_glyph(
                    font.glyph(c),
                    font,
                    background,
                    cursor_x,
                    cursor_y,
                    palette_id,
                );
                cursor_x += char_w;
            }
        }
        if cursor_x > longest {
            longest = cursor_x;
        }
        (cursor_x - pos.x, cursor_y - pos.y, longest - pos.x)
    }

    fn draw_glyph<T: AgbFont>(
        &mut self,
        glyph: &[u32],
        font: &T,
        background: &mut RegularBackground,
        pixel_x: i32,
        pixel_y: i32,
        palette_id: u8,
    ) {
        let row_u32s = font.row_u32s();
        let height = font.glyph_height() as i32;

        for chunk in 0..row_u32s {
            let px_left = pixel_x + ((chunk as i32) << 3);
            let tile_x = px_left >> 3;
            let x_shift = (px_left & 7) as u32;
            let shift_bits = x_shift << 2;

            let mut last_tile_y = -1;
            let mut left_idx = 0;
            let mut right_idx = None;

            for row in 0..height {
                let abs_y = pixel_y + row;
                let current_tile_y = abs_y >> 3;
                let row_in_tile = (abs_y & 7) as usize;

                if current_tile_y != last_tile_y {
                    left_idx = self.ensure_tile_idx(tile_x, current_tile_y, background, palette_id);
                    right_idx = if x_shift > 0 {
                        Some(self.ensure_tile_idx(
                            tile_x + 1,
                            current_tile_y,
                            background,
                            palette_id,
                        ))
                    } else {
                        None
                    };
                    last_tile_y = current_tile_y;
                }

                let pixel_data = glyph[(row as usize * row_u32s) + chunk];

                let left_row = &mut self.tiles[left_idx].2.data_mut()[row_in_tile];
                blit_pixel_row_arm(left_row, pixel_data << shift_bits);

                if let Some(r_idx) = right_idx {
                    let right_row = &mut self.tiles[r_idx].2.data_mut()[row_in_tile];
                    blit_pixel_row_arm(right_row, pixel_data >> (32 - shift_bits));
                }
            }
        }
    }

    fn ensure_tile_idx(&mut self, tx: i32, ty: i32, bg: &mut RegularBackground, pal: u8) -> usize {
        if let Some(pos) = self
            .tiles
            .iter()
            .rposition(|(x, y, _)| *x == tx && *y == ty)
        {
            return pos;
        }
        let tile = DynamicTile16::new().fill_with(0);
        bg.set_tile_dynamic16(vec2(tx, ty), &tile, TileEffect::default().palette(pal));
        self.tiles.push((tx, ty, tile));
        self.tiles.len() - 1
    }

    /// Clear a pixel region by zeroing the tile rows it covers.
    ///
    /// Note: zeros entire 8-pixel-wide tile rows, so pixels in tiles that extend
    /// outside the given rect are also cleared.
    pub fn clear_pixel_rect(
        &mut self,
        background: &mut RegularBackground,
        pos: Vector2D<i32>,
        width: i32,
        height: i32,
        palette_id: u8,
    ) {
        for row in 0..height {
            let abs_y = pos.y + row;
            let tile_y = abs_y >> 3;
            let row_in_tile = (abs_y & 7) as usize;

            // Loop through the width in 8-pixel steps
            for x_off in (0..width).step_by(8) {
                let abs_x = pos.x + x_off;
                let tile_x = abs_x >> 3;
                let x_shift = (abs_x & 7) as u32;

                let left_idx = self.ensure_tile_idx(tile_x, tile_y, background, palette_id);

                self.tiles[left_idx].2.data_mut()[row_in_tile] = 0;

                if x_shift > 0 {
                    let right_idx =
                        self.ensure_tile_idx(tile_x + 1, tile_y, background, palette_id);
                    self.tiles[right_idx].2.data_mut()[row_in_tile] = 0;
                }
            }
        }
    }
}

#[unsafe(link_section = ".iwram")]
#[instruction_set(arm::a32)]
fn blit_pixel_row_arm(target: &mut u32, src: u32) {
    if src == 0 {
        return;
    }

    let hi = src & 0x8888_8888;
    let lo = src & 0x7777_7777;

    // SIMD mask generation
    let set_nybbles = (hi | ((lo + 0x7777_7777) & 0x8888_8888)) >> 3;
    let mask = set_nybbles * 0xF;

    *target = (*target & !mask) | src;
}