From b99fff7c8ec970e98d07e449757c6b5708ce42b3 Mon Sep 17 00:00:00 2001 From: Corwin Date: Sun, 25 Jun 2023 12:34:27 +0100 Subject: [PATCH] some object based text rendering --- agb/examples/object_text_render.rs | 78 +++++++ agb/src/display/font.rs | 25 +- agb/src/display/object.rs | 1 + agb/src/display/object/font.rs | 221 ++++++++++++++++++ .../object/sprites/sprite_allocator.rs | 5 +- 5 files changed, 322 insertions(+), 8 deletions(-) create mode 100644 agb/examples/object_text_render.rs create mode 100644 agb/src/display/object/font.rs diff --git a/agb/examples/object_text_render.rs b/agb/examples/object_text_render.rs new file mode 100644 index 00000000..779b7ece --- /dev/null +++ b/agb/examples/object_text_render.rs @@ -0,0 +1,78 @@ +#![no_std] +#![no_main] + +use agb::{ + display::{ + object::{ + font::{Configuration, WordRender}, + PaletteVram, Size, + }, + palette16::Palette16, + Font, + }, + fixnum::num, + include_font, + timer::Divider, +}; +use agb_fixnum::Num; + +use core::fmt::Write; + +const FONT: Font = include_font!("examples/font/yoster.ttf", 12); +#[agb::entry] +fn entry(gba: agb::Gba) -> ! { + main(gba); +} + +fn main(mut gba: agb::Gba) -> ! { + let (mut unmanaged, mut sprites) = gba.display.object.get_unmanaged(); + + let mut palette = [0x0; 16]; + palette[1] = 0xFF_FF; + let palette = Palette16::new(palette); + let palette = PaletteVram::new(&palette).unwrap(); + + let config = Configuration::new(Size::S32x16, palette); + + let mut wr = WordRender::new(&FONT, config); + + let mut number: Num = num!(1.25235); + + let vblank = agb::interrupt::VBlank::get(); + let mut input = agb::input::ButtonController::new(); + + let timer = gba.timers.timers(); + let mut timer = timer.timer2; + + timer.set_enabled(true); + timer.set_divider(agb::timer::Divider::Divider64); + + loop { + vblank.wait_for_vblank(); + input.update(); + + number += num!(0.01) * input.y_tri() as i32; + + let start = timer.value(); + + let _ = writeln!(wr, "abcdefgh ijklmnopq rstuvwxyz"); + let line = wr.get_line(); + let rasterised = timer.value(); + + let oam_frmae = &mut unmanaged.iter(); + line.unwrap().draw(oam_frmae); + let drawn = timer.value(); + + let start_to_end = to_ms(drawn.wrapping_sub(start)); + let raster = to_ms(rasterised.wrapping_sub(start)); + let object = to_ms(drawn.wrapping_sub(rasterised)); + + agb::println!("Start: {start_to_end:.3}"); + agb::println!("Raster: {raster:.3}"); + agb::println!("Object: {object:.3}"); + } +} + +fn to_ms(time: u16) -> Num { + Num::new(time as i32) * num!(3.815) / 1000 +} diff --git a/agb/src/display/font.rs b/agb/src/display/font.rs index b4349887..f26a75c5 100644 --- a/agb/src/display/font.rs +++ b/agb/src/display/font.rs @@ -10,12 +10,12 @@ use super::tiled::{DynamicTile, RegularMap, TileSetting, VRamManager}; /// Does not support any unicode features. /// For usage see the `text_render.rs` example pub struct FontLetter { - width: u8, - height: u8, - data: &'static [u8], - xmin: i8, - ymin: i8, - advance_width: u8, + pub(crate) width: u8, + pub(crate) height: u8, + pub(crate) data: &'static [u8], + pub(crate) xmin: i8, + pub(crate) ymin: i8, + pub(crate) advance_width: u8, } impl FontLetter { @@ -37,6 +37,13 @@ impl FontLetter { advance_width, } } + + pub(crate) const fn bit_absolute(&self, x: usize, y: usize) -> bool { + let position = x + y * self.width as usize; + let byte = self.data[position / 8]; + let bit = position % 8; + ((byte >> bit) & 1) != 0 + } } pub struct Font { @@ -55,9 +62,13 @@ impl Font { } } - fn letter(&self, letter: char) -> &'static FontLetter { + pub(crate) fn letter(&self, letter: char) -> &'static FontLetter { &self.letters[letter as usize] } + + pub(crate) fn ascent(&self) -> i32 { + self.ascent + } } impl Font { diff --git a/agb/src/display/object.rs b/agb/src/display/object.rs index 64c13b76..ce9c5ac4 100644 --- a/agb/src/display/object.rs +++ b/agb/src/display/object.rs @@ -9,6 +9,7 @@ //! harder to integrate into your games depending on how they are architectured. mod affine; +pub mod font; mod managed; mod sprites; mod unmanaged; diff --git a/agb/src/display/object/font.rs b/agb/src/display/object/font.rs new file mode 100644 index 00000000..8180c747 --- /dev/null +++ b/agb/src/display/object/font.rs @@ -0,0 +1,221 @@ +use core::fmt::Write; + +use agb_fixnum::Vector2D; +use alloc::{collections::VecDeque, vec::Vec}; + +use crate::display::{object::ObjectUnmanaged, Font}; + +use super::{DynamicSprite, OamIterator, PaletteVram, Size, SpriteVram}; + +struct LetterGroup { + sprite: SpriteVram, + /// x offset from the *start* of the *word* + offset: i32, +} + +struct Word { + start_index: usize, + end_index: usize, + size: i32, +} + +impl Word { + fn number_of_letter_groups(&self) -> usize { + self.end_index - self.start_index + } +} + +pub struct MetaWords { + letters: Vec, + words: Vec, +} + +impl MetaWords { + const fn new_empty() -> Self { + Self { + letters: Vec::new(), + words: Vec::new(), + } + } + + fn word_iter(&self) -> impl Iterator { + self.words + .iter() + .map(|x| (x.size, &self.letters[x.start_index..x.end_index])) + } + + pub fn draw(&self, oam: &mut OamIterator) { + fn inner_draw(mw: &MetaWords, oam: &mut OamIterator) -> Option<()> { + let mut word_offset = 0; + + for (size, word) in mw.word_iter() { + for letter_group in word.iter() { + let mut object = ObjectUnmanaged::new(letter_group.sprite.clone()); + object.set_position((word_offset + letter_group.offset, 0).into()); + object.show(); + oam.next()?.set(&object); + } + + word_offset += size + 10; + } + + Some(()) + } + + let _ = inner_draw(self, oam); + } +} + +struct WorkingLetter { + dynamic: DynamicSprite, + // the x offset of the current letter with respect to the start of the current letter group + x_position: i32, + // where to render the letter from x_min to x_max + x_offset: usize, +} + +impl WorkingLetter { + fn new(size: Size) -> Self { + Self { + dynamic: DynamicSprite::new(size), + x_position: 0, + x_offset: 0, + } + } +} + +pub struct Configuration { + sprite_size: Size, + palette: PaletteVram, +} + +impl Configuration { + #[must_use] + pub fn new(sprite_size: Size, palette: PaletteVram) -> Self { + Self { + sprite_size, + palette, + } + } +} + +pub struct WordRender<'font> { + font: &'font Font, + working: Working, + finalised_metas: VecDeque, + config: Configuration, +} + +struct Working { + letter: WorkingLetter, + meta: MetaWords, + word_offset: i32, +} + +impl<'font> Write for WordRender<'font> { + fn write_str(&mut self, s: &str) -> core::fmt::Result { + for c in s.chars() { + self.write_char(c); + } + + Ok(()) + } +} + +impl<'font> WordRender<'font> { + #[must_use] + pub fn new(font: &'font Font, config: Configuration) -> Self { + WordRender { + font, + working: Working { + letter: WorkingLetter::new(config.sprite_size), + meta: MetaWords::new_empty(), + word_offset: 0, + }, + finalised_metas: VecDeque::new(), + config, + } + } +} + +impl WordRender<'_> { + pub fn get_line(&mut self) -> Option { + self.finalised_metas.pop_front() + } + + fn write_char(&mut self, c: char) { + if c == '\n' { + self.finalise_line(); + } else if c == ' ' { + self.finalise_word(); + } else { + self.render_char(c); + } + } + + fn finalise_line(&mut self) { + self.finalise_word(); + + let mut final_meta = MetaWords::new_empty(); + core::mem::swap(&mut self.working.meta, &mut final_meta); + self.finalised_metas.push_back(final_meta); + } + + fn finalise_word(&mut self) { + self.finalise_letter(); + + let start_index = self.working.meta.words.last().map_or(0, |x| x.end_index); + let end_index = self.working.meta.letters.len(); + let word = Word { + start_index, + end_index, + size: self.working.word_offset, + }; + + self.working.meta.words.push(word); + self.working.word_offset = 0; + } + + fn finalise_letter(&mut self) { + let mut final_letter = WorkingLetter::new(self.config.sprite_size); + core::mem::swap(&mut final_letter, &mut self.working.letter); + + let sprite = final_letter.dynamic.to_vram(self.config.palette.clone()); + self.working.meta.letters.push(LetterGroup { + sprite, + offset: self.working.word_offset, + }); + self.working.word_offset += final_letter.x_position; + } + + fn render_char(&mut self, c: char) { + let font_letter = self.font.letter(c); + + // uses more than the sprite can hold + if self.working.letter.x_offset + font_letter.width as usize + > self.config.sprite_size.to_width_height().0 + { + self.finalise_letter(); + } + + self.working.letter.x_position += font_letter.xmin as i32; + + let y_position = self.font.ascent() - font_letter.height as i32 - font_letter.ymin as i32; + + for y in 0..font_letter.height as usize { + for x in 0..font_letter.width as usize { + let rendered = font_letter.bit_absolute(x, y); + if rendered { + self.working.letter.dynamic.set_pixel( + x + self.working.letter.x_offset, + (y_position + y as i32) as usize, + 1, + ); + } + } + } + + self.working.letter.x_position += font_letter.advance_width as i32; + self.working.letter.x_offset += font_letter.advance_width as usize; + } +} diff --git a/agb/src/display/object/sprites/sprite_allocator.rs b/agb/src/display/object/sprites/sprite_allocator.rs index 1d9c9a00..1e8e4c9a 100644 --- a/agb/src/display/object/sprites/sprite_allocator.rs +++ b/agb/src/display/object/sprites/sprite_allocator.rs @@ -333,7 +333,10 @@ impl DynamicSprite { let tile_number_to_modify = adjust_tile_x + adjust_tile_y * sprite_tile_x; - let byte_to_modify_in_tile = x / 2 + y * 4; + let (x_in_tile, y_in_tile) = (x % 8, y % 8); + + let byte_to_modify_in_tile = x_in_tile / 2 + y_in_tile * 4; + let byte_to_modify = tile_number_to_modify * BYTES_PER_TILE_4BPP + byte_to_modify_in_tile; let mut byte = self.data[byte_to_modify]; let parity = (x & 0b1) * 4;