From aa38a03ac98c88b386f68504920751702376715a Mon Sep 17 00:00:00 2001 From: Corwin Date: Sat, 24 Jun 2023 16:17:32 +0100 Subject: [PATCH 01/24] add read pixel method --- agb/src/display/bitmap3.rs | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/agb/src/display/bitmap3.rs b/agb/src/display/bitmap3.rs index 2674d47e..b29b3177 100644 --- a/agb/src/display/bitmap3.rs +++ b/agb/src/display/bitmap3.rs @@ -30,4 +30,19 @@ impl Bitmap3<'_> { let y = y.try_into().unwrap(); BITMAP_MODE_3.set(x, y, colour); } + + #[must_use] + pub fn read_point(&self, x: i32, y: i32) -> u16 { + let x = x.try_into().unwrap(); + let y = y.try_into().unwrap(); + BITMAP_MODE_3.get(x, y) + } + + pub fn clear(&mut self, colour: u16) { + for y in 0..(HEIGHT as usize) { + for x in 0..(WIDTH as usize) { + BITMAP_MODE_3.set(x, y, colour); + } + } + } } From b99fff7c8ec970e98d07e449757c6b5708ce42b3 Mon Sep 17 00:00:00 2001 From: Corwin Date: Sun, 25 Jun 2023 12:34:27 +0100 Subject: [PATCH 02/24] 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; From 87ac2fe53c2da3e258083d16bc17013931a68e72 Mon Sep 17 00:00:00 2001 From: Corwin Date: Sun, 25 Jun 2023 13:38:13 +0100 Subject: [PATCH 03/24] reuse dynamic sprite --- agb/src/display/object/font.rs | 19 ++++++++++++++----- .../object/sprites/sprite_allocator.rs | 7 +++++++ 2 files changed, 21 insertions(+), 5 deletions(-) diff --git a/agb/src/display/object/font.rs b/agb/src/display/object/font.rs index 8180c747..37e5d2bf 100644 --- a/agb/src/display/object/font.rs +++ b/agb/src/display/object/font.rs @@ -82,6 +82,12 @@ impl WorkingLetter { x_offset: 0, } } + + fn reset(&mut self) { + self.x_position = 0; + self.x_offset = 0; + self.dynamic.clear(0); + } } pub struct Configuration { @@ -177,15 +183,18 @@ impl WordRender<'_> { } 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()); + let sprite = self + .working + .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; + self.working.word_offset += self.working.letter.x_position; + + self.working.letter.reset(); } fn render_char(&mut self, c: char) { diff --git a/agb/src/display/object/sprites/sprite_allocator.rs b/agb/src/display/object/sprites/sprite_allocator.rs index 1e8e4c9a..202860a0 100644 --- a/agb/src/display/object/sprites/sprite_allocator.rs +++ b/agb/src/display/object/sprites/sprite_allocator.rs @@ -345,6 +345,13 @@ impl DynamicSprite { self.data[byte_to_modify] = byte; } + /// Wipes the sprite + pub fn clear(&mut self, paletted_pixel: usize) { + assert!(paletted_pixel < 0x10); + let reset = (paletted_pixel | paletted_pixel << 4) as u8; + self.data.fill(reset); + } + /// Tries to copy the sprite to vram to be used to set object sprites. pub fn try_vram(&self, palette: PaletteVram) -> Result { SpriteVram::new(&self.data, self.size, palette) From b75303863df8915a4a0aa5026a4716c518c6ca55 Mon Sep 17 00:00:00 2001 From: Corwin Date: Mon, 26 Jun 2023 17:07:50 +0100 Subject: [PATCH 04/24] buffered text render --- agb/examples/object_text_render.rs | 89 ++++--- agb/src/display/object/font.rs | 287 +++++++++++++-------- agb/src/display/object/unmanaged/object.rs | 1 - 3 files changed, 224 insertions(+), 153 deletions(-) diff --git a/agb/examples/object_text_render.rs b/agb/examples/object_text_render.rs index 779b7ece..8cac3dfd 100644 --- a/agb/examples/object_text_render.rs +++ b/agb/examples/object_text_render.rs @@ -4,17 +4,15 @@ use agb::{ display::{ object::{ - font::{Configuration, WordRender}, + font::{BufferedWordRender, Configuration}, PaletteVram, Size, }, palette16::Palette16, Font, }, - fixnum::num, include_font, - timer::Divider, + input::Button, }; -use agb_fixnum::Num; use core::fmt::Write; @@ -25,54 +23,55 @@ fn entry(gba: agb::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); + let (mut unmanaged, _sprites) = gba.display.object.get_unmanaged(); loop { - vblank.wait_for_vblank(); - input.update(); + let mut palette = [0x0; 16]; + palette[1] = 0xFF_FF; + let palette = Palette16::new(palette); + let palette = PaletteVram::new(&palette).unwrap(); - number += num!(0.01) * input.y_tri() as i32; + let config = Configuration::new(Size::S32x16, palette); - let start = timer.value(); + let mut wr = BufferedWordRender::new(&FONT, config); + let _ = writeln!( + wr, + "Hello there!\nI spent this weekend\nwriting this text system!\nIs it any good?\n\nOh, by the way, you can\npress A to restart!" + ); - let _ = writeln!(wr, "abcdefgh ijklmnopq rstuvwxyz"); - let line = wr.get_line(); - let rasterised = timer.value(); + let vblank = agb::interrupt::VBlank::get(); + let mut input = agb::input::ButtonController::new(); - let oam_frmae = &mut unmanaged.iter(); - line.unwrap().draw(oam_frmae); - let drawn = timer.value(); + let timer = gba.timers.timers(); + let mut timer: agb::timer::Timer = timer.timer2; - 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)); + timer.set_enabled(true); + timer.set_divider(agb::timer::Divider::Divider64); - agb::println!("Start: {start_to_end:.3}"); - agb::println!("Raster: {raster:.3}"); - agb::println!("Object: {object:.3}"); + let mut num_letters = 0; + let mut frame = 0; + + loop { + vblank.wait_for_vblank(); + input.update(); + let oam_frmae = &mut unmanaged.iter(); + + let start = timer.value(); + wr.draw_partial(oam_frmae, (0, 0).into(), num_letters); + let end = timer.value(); + + agb::println!("Took {} cycles", 64 * (end.wrapping_sub(start) as u32)); + + frame += 1; + + if frame % 4 == 0 { + num_letters += 1; + } + wr.process(); + + if input.is_just_pressed(Button::A) { + break; + } + } } } - -fn to_ms(time: u16) -> Num { - Num::new(time as i32) * num!(3.815) / 1000 -} diff --git a/agb/src/display/object/font.rs b/agb/src/display/object/font.rs index 37e5d2bf..a0e36ca0 100644 --- a/agb/src/display/object/font.rs +++ b/agb/src/display/object/font.rs @@ -9,60 +9,91 @@ use super::{DynamicSprite, OamIterator, PaletteVram, Size, SpriteVram}; struct LetterGroup { sprite: SpriteVram, - /// x offset from the *start* of the *word* - offset: i32, + // the width of the letter group + width: u16, + left: i16, } -struct Word { - start_index: usize, - end_index: usize, - size: i32, +enum WhiteSpace { + Space, + NewLine, } -impl Word { - fn number_of_letter_groups(&self) -> usize { - self.end_index - self.start_index +enum TextElement { + LetterGroup(LetterGroup), + WhiteSpace(WhiteSpace), +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test_case] + fn check_size_of_text_element_is_expected(_: &mut crate::Gba) { + assert_eq!( + core::mem::size_of::(), + core::mem::size_of::() + ); } } -pub struct MetaWords { - letters: Vec, - words: Vec, +pub struct TextBlock { + elements: Vec, + cache: CachedRender, } -impl MetaWords { - const fn new_empty() -> Self { - Self { - letters: Vec::new(), - words: Vec::new(), - } +pub struct CachedRender { + objects: Vec, + up_to: usize, + head_position: Vector2D, + origin: Vector2D, +} + +impl TextBlock { + fn reset_cache(&mut self, position: Vector2D) { + self.cache.objects.clear(); + self.cache.up_to = 0; + self.cache.head_position = position; + self.cache.origin = position; } - fn word_iter(&self) -> impl Iterator { - self.words - .iter() - .map(|x| (x.size, &self.letters[x.start_index..x.end_index])) - } + fn generate_cache(&mut self, up_to: usize) { + let mut head_position = self.cache.head_position; - 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()); + for element in self.elements.iter().take(up_to).skip(self.cache.up_to) { + match element { + TextElement::LetterGroup(group) => { + let mut object = ObjectUnmanaged::new(group.sprite.clone()); object.show(); - oam.next()?.set(&object); + head_position.x += group.left as i32; + object.set_position(head_position); + head_position.x += group.width as i32; + self.cache.objects.push(object); } - - word_offset += size + 10; + TextElement::WhiteSpace(white) => match white { + WhiteSpace::Space => head_position.x += 10, + WhiteSpace::NewLine => { + head_position.x = self.cache.origin.x; + head_position.y += 15; + } + }, } - - Some(()) } - let _ = inner_draw(self, oam); + self.cache.head_position = head_position; + self.cache.up_to = up_to.min(self.elements.len()); + } + + fn draw(&mut self, oam: &mut OamIterator, position: Vector2D, up_to: usize) { + if position != self.cache.origin { + self.reset_cache(position); + } + + self.generate_cache(up_to); + + for (obj, slot) in self.cache.objects.iter().zip(oam) { + slot.set(obj); + } } } @@ -71,7 +102,7 @@ struct WorkingLetter { // 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, + x_offset: i32, } impl WorkingLetter { @@ -105,118 +136,159 @@ impl Configuration { } } -pub struct WordRender<'font> { - font: &'font Font, - working: Working, - finalised_metas: VecDeque, - config: Configuration, +pub struct BufferedWordRender<'font> { + word_render: WordRender<'font>, + block: TextBlock, + buffered_chars: VecDeque, } -struct Working { - letter: WorkingLetter, - meta: MetaWords, - word_offset: i32, -} - -impl<'font> Write for WordRender<'font> { +impl Write for BufferedWordRender<'_> { fn write_str(&mut self, s: &str) -> core::fmt::Result { - for c in s.chars() { - self.write_char(c); + for char in s.chars() { + self.buffered_chars.push_back(char); } Ok(()) } } -impl<'font> WordRender<'font> { +impl<'font> BufferedWordRender<'font> { #[must_use] pub fn new(font: &'font Font, config: Configuration) -> Self { + BufferedWordRender { + word_render: WordRender::new(font, config), + block: TextBlock { + elements: Vec::new(), + cache: CachedRender { + objects: Vec::new(), + up_to: 0, + head_position: (0, 0).into(), + origin: (0, 0).into(), + }, + }, + buffered_chars: VecDeque::new(), + } + } +} + +impl BufferedWordRender<'_> { + pub fn process(&mut self) { + if let Some(char) = self.buffered_chars.pop_front() { + if char == '\n' { + if let Some(group) = self.word_render.finalise_letter() { + self.block.elements.push(TextElement::LetterGroup(group)); + } + self.block + .elements + .push(TextElement::WhiteSpace(WhiteSpace::NewLine)); + } else if char == ' ' { + if let Some(group) = self.word_render.finalise_letter() { + self.block.elements.push(TextElement::LetterGroup(group)); + } + self.block + .elements + .push(TextElement::WhiteSpace(WhiteSpace::Space)); + } else if let Some(group) = self.word_render.render_char(char) { + self.block.elements.push(TextElement::LetterGroup(group)); + } + } + } + + pub fn draw_partial( + &mut self, + oam: &mut OamIterator, + position: Vector2D, + num_groups: usize, + ) { + while self.block.elements.len() < num_groups && !self.buffered_chars.is_empty() { + self.process(); + } + + self.block.draw(oam, position, num_groups); + } + + pub fn draw(&mut self, oam: &mut OamIterator, position: Vector2D) { + while !self.buffered_chars.is_empty() { + self.process(); + } + + self.block.draw(oam, position, usize::MAX); + } +} + +struct WordRender<'font> { + font: &'font Font, + working: Working, + config: Configuration, +} + +struct Working { + letter: WorkingLetter, +} + +impl<'font> WordRender<'font> { + #[must_use] + 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); + #[must_use] + fn finalise_letter(&mut self) -> Option { + if self.working.letter.x_offset == 0 { + return None; } - } - 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 sprite = self .working .letter .dynamic .to_vram(self.config.palette.clone()); - self.working.meta.letters.push(LetterGroup { + let group = LetterGroup { sprite, - offset: self.working.word_offset, - }); - self.working.word_offset += self.working.letter.x_position; - + width: self.working.letter.x_offset as u16, + left: self.working.letter.x_position as i16, + }; self.working.letter.reset(); + + Some(group) } - fn render_char(&mut self, c: char) { + #[must_use] + fn render_char(&mut self, c: char) -> Option { 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 + let group = if self.working.letter.x_offset + font_letter.width as i32 + > self.config.sprite_size.to_width_height().0 as i32 { - self.finalise_letter(); + self.finalise_letter() + } else { + None + }; + + if self.working.letter.x_offset == 0 { + self.working.letter.x_position = font_letter.xmin as i32; + } else { + self.working.letter.x_offset += font_letter.xmin as i32; } - 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; + let y_position = + self.font.ascent() - font_letter.height as i32 - font_letter.ymin as i32 + 4; 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, + x + self.working.letter.x_offset as usize, (y_position + y as i32) as usize, 1, ); @@ -224,7 +296,8 @@ impl WordRender<'_> { } } - self.working.letter.x_position += font_letter.advance_width as i32; - self.working.letter.x_offset += font_letter.advance_width as usize; + self.working.letter.x_offset += font_letter.advance_width as i32; + + group } } diff --git a/agb/src/display/object/unmanaged/object.rs b/agb/src/display/object/unmanaged/object.rs index fb5a42ef..f1012a0e 100644 --- a/agb/src/display/object/unmanaged/object.rs +++ b/agb/src/display/object/unmanaged/object.rs @@ -101,7 +101,6 @@ impl OamSlot<'_> { /// By writing these as two separate functions, one inlined and one not, the /// compiler doesn't have to copy around the slot structure while still /// keeping move semantics. This is slightly faster in benchmarks. - #[inline(never)] fn set_inner(&self, object: &ObjectUnmanaged) { let mut attributes = object.attributes; // SAFETY: This function is not reentrant and we currently hold a mutable borrow of the [UnmanagedOAM]. From ce7bcacb3c97b19ca0fd61352f8b158dde0ce440 Mon Sep 17 00:00:00 2001 From: Corwin Date: Mon, 26 Jun 2023 23:47:19 +0100 Subject: [PATCH 05/24] somewhat mad left align renderer --- agb/examples/object_text_render.rs | 22 +- agb/src/display/object/font.rs | 316 +++++++++++++++++++++-------- 2 files changed, 238 insertions(+), 100 deletions(-) diff --git a/agb/examples/object_text_render.rs b/agb/examples/object_text_render.rs index 8cac3dfd..0a226f70 100644 --- a/agb/examples/object_text_render.rs +++ b/agb/examples/object_text_render.rs @@ -8,11 +8,12 @@ use agb::{ PaletteVram, Size, }, palette16::Palette16, - Font, + Font, WIDTH, }, include_font, input::Button, }; +use agb_fixnum::Rect; use core::fmt::Write; @@ -31,13 +32,13 @@ fn main(mut gba: agb::Gba) -> ! { let palette = Palette16::new(palette); let palette = PaletteVram::new(&palette).unwrap(); - let config = Configuration::new(Size::S32x16, palette); + let config = Configuration::new(Size::S16x16, palette); let mut wr = BufferedWordRender::new(&FONT, config); let _ = writeln!( - wr, - "Hello there!\nI spent this weekend\nwriting this text system!\nIs it any good?\n\nOh, by the way, you can\npress A to restart!" - ); + wr, + "Hello there!\nI spent this weekend writing this text system! Is it any good?\n\nOh, by the way, you can press A to restart!" + ); let vblank = agb::interrupt::VBlank::get(); let mut input = agb::input::ButtonController::new(); @@ -46,7 +47,7 @@ fn main(mut gba: agb::Gba) -> ! { let mut timer: agb::timer::Timer = timer.timer2; timer.set_enabled(true); - timer.set_divider(agb::timer::Divider::Divider64); + timer.set_divider(agb::timer::Divider::Divider256); let mut num_letters = 0; let mut frame = 0; @@ -54,20 +55,21 @@ fn main(mut gba: agb::Gba) -> ! { loop { vblank.wait_for_vblank(); input.update(); - let oam_frmae = &mut unmanaged.iter(); + let oam = &mut unmanaged.iter(); + wr.commit(oam); let start = timer.value(); - wr.draw_partial(oam_frmae, (0, 0).into(), num_letters); + wr.update(Rect::new((0, 0).into(), (WIDTH, 100).into()), num_letters); + wr.process(); let end = timer.value(); - agb::println!("Took {} cycles", 64 * (end.wrapping_sub(start) as u32)); + agb::println!("Took {} cycles", 256 * (end.wrapping_sub(start) as u32)); frame += 1; if frame % 4 == 0 { num_letters += 1; } - wr.process(); if input.is_just_pressed(Button::A) { break; diff --git a/agb/src/display/object/font.rs b/agb/src/display/object/font.rs index a0e36ca0..5501cda0 100644 --- a/agb/src/display/object/font.rs +++ b/agb/src/display/object/font.rs @@ -1,6 +1,6 @@ -use core::fmt::Write; +use core::{cell::Cell, fmt::Write, num::NonZeroUsize}; -use agb_fixnum::Vector2D; +use agb_fixnum::{Rect, Vector2D}; use alloc::{collections::VecDeque, vec::Vec}; use crate::display::{object::ObjectUnmanaged, Font}; @@ -19,82 +19,209 @@ enum WhiteSpace { NewLine, } -enum TextElement { - LetterGroup(LetterGroup), +enum TextElementReference<'text> { + Word(Word<'text>), WhiteSpace(WhiteSpace), } -#[cfg(test)] -mod tests { - use super::*; +struct Word<'letters> { + letters: &'letters [LetterGroup], + width: Cell>, +} - #[test_case] - fn check_size_of_text_element_is_expected(_: &mut crate::Gba) { - assert_eq!( - core::mem::size_of::(), - core::mem::size_of::() - ); +impl<'letters> Word<'letters> { + fn new(letters: &'letters [LetterGroup]) -> Self { + Self { + letters, + width: Cell::new(None), + } } } -pub struct TextBlock { - elements: Vec, - cache: CachedRender, -} +impl Word<'_> { + fn width(&self) -> usize { + match self.width.get() { + Some(width) => width.get(), + None => { + let width = self.letters.iter().fold(0, |acc, letter| { + acc + (letter.width as i32 + letter.left as i32) + }); + let width = width as usize; -pub struct CachedRender { - objects: Vec, - up_to: usize, - head_position: Vector2D, - origin: Vector2D, -} - -impl TextBlock { - fn reset_cache(&mut self, position: Vector2D) { - self.cache.objects.clear(); - self.cache.up_to = 0; - self.cache.head_position = position; - self.cache.origin = position; - } - - fn generate_cache(&mut self, up_to: usize) { - let mut head_position = self.cache.head_position; - - for element in self.elements.iter().take(up_to).skip(self.cache.up_to) { - match element { - TextElement::LetterGroup(group) => { - let mut object = ObjectUnmanaged::new(group.sprite.clone()); - object.show(); - head_position.x += group.left as i32; - object.set_position(head_position); - head_position.x += group.width as i32; - self.cache.objects.push(object); - } - TextElement::WhiteSpace(white) => match white { - WhiteSpace::Space => head_position.x += 10, - WhiteSpace::NewLine => { - head_position.x = self.cache.origin.x; - head_position.y += 15; - } - }, + self.width.set(NonZeroUsize::new(width)); + width } } + } +} - self.cache.head_position = head_position; - self.cache.up_to = up_to.min(self.elements.len()); +#[derive(Clone, Copy)] +struct WordLength(u8); + +#[derive(Clone, Copy)] +enum Element { + Word(u8), + NewLine, + Space, +} +const NEW_LINE: u8 = 0xFF; +const SPACE: u8 = 0xFE; +impl WordLength { + fn parse(self) -> Element { + if self.0 == NEW_LINE { + Element::NewLine + } else if self.0 == SPACE { + Element::Space + } else { + Element::Word(self.0) + } } - fn draw(&mut self, oam: &mut OamIterator, position: Vector2D, up_to: usize) { - if position != self.cache.origin { - self.reset_cache(position); + fn from_element(e: Element) -> Self { + WordLength(match e { + Element::Word(len) => len, + Element::NewLine => NEW_LINE, + Element::Space => SPACE, + }) + } +} + +struct Letters(Vec); +struct Words { + letters: Letters, + word_lengths: Vec, +} + +struct WordRenderCache { + objects: Vec, + state: WordRenderCacheState, + poison_condition: WordRenderPoisonCondition, +} + +struct WordRenderPoisonCondition { + area: Rect, +} + +struct WordRenderCacheState { + depth_in_word_iterator: usize, + depth_in_word: usize, + depth_in_elements: usize, + head_position: Vector2D, +} + +impl WordRenderCache { + fn new() -> Self { + WordRenderCache { + objects: Vec::new(), + state: WordRenderCacheState { + depth_in_word_iterator: 0, + depth_in_word: 0, + depth_in_elements: 0, + head_position: (0, 0).into(), + }, + poison_condition: WordRenderPoisonCondition { + area: Rect::new((0, 0).into(), (0, 0).into()), + }, + } + } + + fn reset_state(&mut self, position: Rect) { + self.state = WordRenderCacheState { + depth_in_elements: 0, + depth_in_word: 0, + depth_in_word_iterator: 0, + head_position: position.position, + }; + + self.poison_condition = WordRenderPoisonCondition { area: position }; + } + + fn generate_cache(&mut self, words: &Words, desired_element_count: usize) { + let position = self.poison_condition.area; + if self.state.depth_in_elements >= desired_element_count { + return; } - self.generate_cache(up_to); + 'outer: for elem in words.iter_words().skip(self.state.depth_in_word_iterator) { + match elem { + TextElementReference::Word(word) => { + let prospective_x = self.state.head_position.x + word.width() as i32; - for (obj, slot) in self.cache.objects.iter().zip(oam) { - slot.set(obj); + if self.state.depth_in_word == 0 + && prospective_x > position.position.x + position.size.x + { + self.state.head_position.x = position.position.x; + self.state.head_position.y += 15; + } + + for letter in word.letters.iter().skip(self.state.depth_in_word) { + self.state.head_position.x += letter.left as i32; + let mut object = ObjectUnmanaged::new(letter.sprite.clone()); + object.show(); + object.set_position(self.state.head_position); + + self.objects.push(object); + + self.state.head_position.x += letter.width as i32; + + self.state.depth_in_elements += 1; + self.state.depth_in_word += 1; + if self.state.depth_in_elements >= desired_element_count { + break 'outer; + } + } + + self.state.depth_in_word = 0; + } + TextElementReference::WhiteSpace(space) => { + match space { + WhiteSpace::Space => self.state.head_position.x += 10, + WhiteSpace::NewLine => { + self.state.head_position.x = position.position.x; + self.state.head_position.y += 15; + } + } + self.state.depth_in_elements += 1; + } + } + self.state.depth_in_word_iterator += 1; + if self.state.depth_in_elements >= desired_element_count { + break 'outer; + } } } + + fn update(&mut self, words: &Words, desired_element_count: usize, position: Rect) { + if self.poison_condition.area != position { + self.reset_state(position); + } + + self.generate_cache(words, desired_element_count); + } + + fn commit(&self, oam: &mut OamIterator) { + for (object, slot) in self.objects.iter().zip(oam) { + slot.set(object); + } + } +} + +impl Words { + fn iter_words(&self) -> impl Iterator { + let mut letters_idx: usize = 0; + + self.word_lengths.iter().map(move |x| match x.parse() { + Element::Word(length) => { + let idx = letters_idx; + let end_idx = idx + length as usize; + letters_idx = end_idx; + + TextElementReference::Word(Word::new(&self.letters.0[idx..end_idx])) + } + Element::NewLine => TextElementReference::WhiteSpace(WhiteSpace::NewLine), + Element::Space => TextElementReference::WhiteSpace(WhiteSpace::Space), + }) + } } struct WorkingLetter { @@ -138,8 +265,11 @@ impl Configuration { pub struct BufferedWordRender<'font> { word_render: WordRender<'font>, - block: TextBlock, + block: Words, + current_word_length: usize, + number_of_elements: usize, buffered_chars: VecDeque, + cache: WordRenderCache, } impl Write for BufferedWordRender<'_> { @@ -157,16 +287,14 @@ impl<'font> BufferedWordRender<'font> { pub fn new(font: &'font Font, config: Configuration) -> Self { BufferedWordRender { word_render: WordRender::new(font, config), - block: TextBlock { - elements: Vec::new(), - cache: CachedRender { - objects: Vec::new(), - up_to: 0, - head_position: (0, 0).into(), - origin: (0, 0).into(), - }, + block: Words { + letters: Letters(Vec::new()), + word_lengths: Vec::new(), }, + current_word_length: 0, + number_of_elements: 0, buffered_chars: VecDeque::new(), + cache: WordRenderCache::new(), } } } @@ -176,43 +304,51 @@ impl BufferedWordRender<'_> { if let Some(char) = self.buffered_chars.pop_front() { if char == '\n' { if let Some(group) = self.word_render.finalise_letter() { - self.block.elements.push(TextElement::LetterGroup(group)); + self.block.letters.0.push(group); + self.current_word_length += 1; } self.block - .elements - .push(TextElement::WhiteSpace(WhiteSpace::NewLine)); + .word_lengths + .push(WordLength::from_element(Element::Word( + self.current_word_length as u8, + ))); + self.block + .word_lengths + .push(WordLength::from_element(Element::NewLine)); + self.number_of_elements += self.current_word_length + 1; + self.current_word_length = 0; } else if char == ' ' { if let Some(group) = self.word_render.finalise_letter() { - self.block.elements.push(TextElement::LetterGroup(group)); + self.block.letters.0.push(group); + self.current_word_length += 1; } self.block - .elements - .push(TextElement::WhiteSpace(WhiteSpace::Space)); + .word_lengths + .push(WordLength::from_element(Element::Word( + self.current_word_length as u8, + ))); + self.block + .word_lengths + .push(WordLength::from_element(Element::Space)); + self.number_of_elements += self.current_word_length + 1; + self.current_word_length = 0; } else if let Some(group) = self.word_render.render_char(char) { - self.block.elements.push(TextElement::LetterGroup(group)); + self.block.letters.0.push(group); + self.current_word_length += 1; } } } - pub fn draw_partial( - &mut self, - oam: &mut OamIterator, - position: Vector2D, - num_groups: usize, - ) { - while self.block.elements.len() < num_groups && !self.buffered_chars.is_empty() { + pub fn update(&mut self, position: Rect, number_of_elements: usize) { + while !self.buffered_chars.is_empty() && self.number_of_elements < number_of_elements { self.process(); } - self.block.draw(oam, position, num_groups); + self.cache.update(&self.block, number_of_elements, position); } - pub fn draw(&mut self, oam: &mut OamIterator, position: Vector2D) { - while !self.buffered_chars.is_empty() { - self.process(); - } - - self.block.draw(oam, position, usize::MAX); + pub fn commit(&self, oam: &mut OamIterator) { + self.cache.commit(oam); } } From fd82b259ccc49ab0f669971fe7328cdae46331b6 Mon Sep 17 00:00:00 2001 From: Corwin Date: Tue, 27 Jun 2023 21:56:09 +0100 Subject: [PATCH 06/24] dynamic sprite allocate directly into sprite vram --- agb/src/agb_alloc/mod.rs | 12 +- .../object/sprites/sprite_allocator.rs | 119 ++++++++++++------ agb/src/no_game.rs | 9 +- 3 files changed, 90 insertions(+), 50 deletions(-) diff --git a/agb/src/agb_alloc/mod.rs b/agb/src/agb_alloc/mod.rs index 8968fd5f..7fae40fe 100644 --- a/agb/src/agb_alloc/mod.rs +++ b/agb/src/agb_alloc/mod.rs @@ -1,4 +1,3 @@ -use core::alloc::{Allocator, Layout}; use core::ops::{Deref, DerefMut}; use core::ptr::NonNull; @@ -45,18 +44,23 @@ static GLOBAL_ALLOC: BlockAllocator = unsafe { macro_rules! impl_zst_allocator { ($name_of_struct: ty, $name_of_static: ident) => { - unsafe impl Allocator for $name_of_struct { - fn allocate(&self, layout: Layout) -> Result, core::alloc::AllocError> { + unsafe impl core::alloc::Allocator for $name_of_struct { + fn allocate( + &self, + layout: core::alloc::Layout, + ) -> Result, core::alloc::AllocError> { $name_of_static.allocate(layout) } - unsafe fn deallocate(&self, ptr: NonNull, layout: Layout) { + unsafe fn deallocate(&self, ptr: core::ptr::NonNull, layout: core::alloc::Layout) { $name_of_static.deallocate(ptr, layout) } } }; } +pub(crate) use impl_zst_allocator; + /// This is the allocator for the External Working Ram. This is currently /// equivalent to the Global Allocator (where things are allocated if no allocator is provided). This implements the allocator trait, so /// is meant to be used in specifying where certain structures should be diff --git a/agb/src/display/object/sprites/sprite_allocator.rs b/agb/src/display/object/sprites/sprite_allocator.rs index 202860a0..8050347b 100644 --- a/agb/src/display/object/sprites/sprite_allocator.rs +++ b/agb/src/display/object/sprites/sprite_allocator.rs @@ -1,14 +1,12 @@ use core::{alloc::Allocator, ptr::NonNull}; use alloc::{ - alloc::Global, boxed::Box, rc::{Rc, Weak}, - vec::Vec, }; use crate::{ - agb_alloc::{block_allocator::BlockAllocator, bump_allocator::StartEnd}, + agb_alloc::{block_allocator::BlockAllocator, bump_allocator::StartEnd, impl_zst_allocator}, display::palette16::Palette16, hash_map::HashMap, }; @@ -18,8 +16,8 @@ use super::{ BYTES_PER_TILE_4BPP, }; -const PALETTE_SPRITE: usize = 0x0500_0200; -const TILE_SPRITE: usize = 0x06010000; +pub const PALETTE_SPRITE: usize = 0x0500_0200; +pub const TILE_SPRITE: usize = 0x06010000; static SPRITE_ALLOCATOR: BlockAllocator = unsafe { BlockAllocator::new(StartEnd { @@ -28,6 +26,10 @@ static SPRITE_ALLOCATOR: BlockAllocator = unsafe { }) }; +pub struct SpriteAllocator; + +impl_zst_allocator!(SpriteAllocator, SPRITE_ALLOCATOR); + static PALETTE_ALLOCATOR: BlockAllocator = unsafe { BlockAllocator::new(StartEnd { start: || PALETTE_SPRITE, @@ -35,6 +37,10 @@ static PALETTE_ALLOCATOR: BlockAllocator = unsafe { }) }; +pub struct PaletteAllocator; + +impl_zst_allocator!(PaletteAllocator, PALETTE_ALLOCATOR); + /// The Sprite Id is a thin wrapper around the pointer to the sprite in /// rom and is therefore a unique identifier to a sprite #[derive(Clone, Copy, PartialEq, Eq, Hash)] @@ -162,13 +168,21 @@ impl SpriteVram { .as_ptr() .copy_from_nonoverlapping(data.as_ptr(), data.len()); } - Ok(SpriteVram { + Ok(unsafe { Self::from_location_size(allocated, size, palette) }) + } + + unsafe fn from_location_size( + data: NonNull, + size: Size, + palette: PaletteVram, + ) -> SpriteVram { + SpriteVram { data: Rc::new(SpriteVramData { - location: Location::from_sprite_ptr(allocated), + location: Location::from_sprite_ptr(data), size, palette, }), - }) + } } pub(crate) fn location(&self) -> u16 { @@ -290,31 +304,54 @@ impl Default for SpriteLoader { } /// Sprite data that can be used to create sprites in vram. -pub struct DynamicSprite { - data: Box<[u8], A>, +pub struct DynamicSprite { + data: Box<[u16], SpriteAllocator>, size: Size, } -impl DynamicSprite { - #[must_use] - /// Creates a new dynamic sprite. - pub fn new(size: Size) -> Self { - Self::new_in(size, Global) +impl Clone for DynamicSprite { + fn clone(&self) -> Self { + let allocation = SpriteAllocator + .allocate(self.size.layout()) + .expect("cannot allocate dynamic sprite"); + + let allocation = core::ptr::slice_from_raw_parts_mut( + allocation.as_ptr() as *mut _, + allocation.len() / 2, + ); + + let mut data = unsafe { Box::from_raw_in(allocation, SpriteAllocator) }; + + data.clone_from_slice(&self.data); + + Self { + data, + size: self.size, + } } } -impl DynamicSprite { +impl DynamicSprite { + /// Creates a new dynamic sprite of a given size + pub fn try_new(size: Size) -> Result { + let allocation = SpriteAllocator + .allocate_zeroed(size.layout()) + .map_err(|_| LoaderError::SpriteFull)?; + + let allocation = core::ptr::slice_from_raw_parts_mut( + allocation.as_ptr() as *mut _, + allocation.len() / 2, + ); + + let data = unsafe { Box::from_raw_in(allocation, SpriteAllocator) }; + + Ok(DynamicSprite { data, size }) + } + #[must_use] - /// Creates a new dynamic sprite of a given size in a given allocator. - pub fn new_in(size: Size, allocator: A) -> Self { - let num_bytes = size.number_of_tiles() * BYTES_PER_TILE_4BPP; - let mut data = Vec::with_capacity_in(num_bytes, allocator); - - data.resize(num_bytes, 0); - - let data = data.into_boxed_slice(); - - DynamicSprite { data, size } + /// Creates a new dynamic sprite of a given size + pub fn new(size: Size) -> Self { + Self::try_new(size).expect("couldn't allocate dynamic sprite") } /// Set the pixel of a sprite to a given paletted pixel. Panics if the @@ -335,32 +372,34 @@ impl DynamicSprite { 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 half_word_to_modify_in_tile = x_in_tile / 4 + y_in_tile * 2; - 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; + let half_word_to_modify = + tile_number_to_modify * BYTES_PER_TILE_4BPP / 2 + half_word_to_modify_in_tile; + let mut half_word = self.data[half_word_to_modify]; - byte = (byte & !(0b1111 << parity)) | ((paletted_pixel as u8) << parity); - self.data[byte_to_modify] = byte; + let nibble_to_modify = (x % 4) * 4; + + half_word = (half_word & !(0b1111 << nibble_to_modify)) + | ((paletted_pixel as u16) << nibble_to_modify); + self.data[half_word_to_modify] = half_word; } /// Wipes the sprite pub fn clear(&mut self, paletted_pixel: usize) { assert!(paletted_pixel < 0x10); - let reset = (paletted_pixel | paletted_pixel << 4) as u8; + let reset = + (paletted_pixel | paletted_pixel << 4 | paletted_pixel << 8 | paletted_pixel << 12) + as u16; self.data.fill(reset); } - /// Tries to copy the sprite to vram to be used to set object sprites. - pub fn try_vram(&self, palette: PaletteVram) -> Result { - SpriteVram::new(&self.data, self.size, palette) - } - #[must_use] /// Tries to copy the sprite to vram to be used to set object sprites. /// Panics if it cannot be allocated. - pub fn to_vram(&self, palette: PaletteVram) -> SpriteVram { - self.try_vram(palette).expect("cannot create sprite") + pub fn to_vram(self, palette: PaletteVram) -> SpriteVram { + let data = unsafe { NonNull::new_unchecked(Box::leak(self.data).as_mut_ptr()) }; + + unsafe { SpriteVram::from_location_size(data.cast(), self.size, palette) } } } diff --git a/agb/src/no_game.rs b/agb/src/no_game.rs index ed82f880..0b84e5f1 100644 --- a/agb/src/no_game.rs +++ b/agb/src/no_game.rs @@ -129,13 +129,10 @@ fn generate_sprites() -> Box<[SpriteVram]> { .collect(); // generate sprites - let mut sprite = DynamicSprite::new(Size::S8x8); + for (palette, colour) in (0..PALETTE.len()).map(|x| (x / 15, x % 15)) { - for y in 0..8 { - for x in 0..8 { - sprite.set_pixel(x, y, colour + 1); - } - } + let mut sprite = DynamicSprite::new(Size::S8x8); + sprite.clear(colour + 1); sprites.push(sprite.to_vram(palettes[palette].clone())); } From 22b3497bb3301c9ae24db5a541ee8a564e728adb Mon Sep 17 00:00:00 2001 From: Corwin Date: Tue, 27 Jun 2023 21:56:26 +0100 Subject: [PATCH 07/24] fix --- agb/examples/font/pixelated.ttf | Bin 0 -> 12656 bytes agb/examples/object_text_render.rs | 28 +++++++++++++++++++++------- agb/src/display/object/font.rs | 19 ++++++++----------- 3 files changed, 29 insertions(+), 18 deletions(-) create mode 100644 agb/examples/font/pixelated.ttf diff --git a/agb/examples/font/pixelated.ttf b/agb/examples/font/pixelated.ttf new file mode 100644 index 0000000000000000000000000000000000000000..29c265a11d8c6873b4b44daf632b6959aed15a5e GIT binary patch literal 12656 zcmeHOO>A6O6+ZLhIE|geP12;HN&A{cRa;t5{z#>&NJ%55DQanyl=7pBcI=GpNjx5R zW?b79KX<``MHgKns8AOmfz(ASBEf=65vl};u27)}SQLT;iz0PXgizz*`_8@R&7C)I zY?lQK#EZ^5_nv$H&$;)#8Be4jBHP4CNs=coR*seSZuz{(*sGvEH{GZ%-u`Ox2aI16 zDc(OjzdG~tt2c@wOUujfV7GdOD(?f<#xRh4}W`0>c^-+gxH!ylrjbnTZ|Vpx0!729-a0wnf_aZyI)P#BkR&fFWuWw}{C9mXRvCZ7%CEs*_U z7>~-sG8M*Kn z@v6_bZ7uu|VHe7b_E_PKFfPch!u2pN%8tT^VO)}Xils0v%c0^Y!+1pY7M}>?EwZaP z9mb=wQd|n-t#V)S?_qqC>?!RF<8AVI$@GlLWNA6j$K|=w_rrL*oG*`r@eX;W{JAjR zEf>ncXT&lwQaaOITy527=Q_!wty*oNQ9X9#XaYETzP?hMuXbveF4Sh1=Bur<&4tdz zPHSnplcY=OAPG!Q*IMm*b0Il8c_dwn1ncdjnsi##OSML|btP%eBxf%^mMFtqb1`{r zp;K!uR6Ed6olhQZOr4wSbQT|~RAykit+pqpn~k2M2T~&kljUkVnOdsPcar6LXU;2C zk!?ZA^78VeDuOAh$<-a1f*0*dd#<@$IdSad$>R^6Y1QD`3$^4-v(dm>a=O!L)u)!| zZgO$1+Nvd|=j&H$LGN^6hb3}PHe0im`TBHipdqT*2x*);hqKP@Wp`V~lV$KPBUe3-d{pKQtGqeC^P}>)Xd~t}EH`1RA}7!vC-LvNJcyoR z|FApi@&ZPQc3A^?pzn7In97P_&b z($kQp#TuXjTdVQ_MpcZDYcExhPxEmKqgnX&JUptRZdWj0P|AytUxXLjCG`F>Z1}n~ zK=Tz_0iFWiC3yrgZFv$Jjza1(A6&)4^M#jXM9M{dpz*01p3}#LZ_VQ0Yw%iNPcss6 zoxS+f1vw?#lY@9K!Mx-OBEr~8}k_3ocRxtj{H`lYbGJb-Ih6>$Hl`|jFl^4IT z8!s`esx&NTxQ9uYbXZ5Nx=!O9!CT38ys_*X?zQREhJbTA&tOhT54<)?Jp~;!c#&ew zoTmwMjkh(+`8=*E}&pxm)fl+KwOpZNF1P-phI z`FV%jwZ5-bd-adL1u)-ijul-)FM6wCZtLth9s5=1XOYiFci7R=y|&J}(^%QIF1P2g z9xN%w*6idi86B);FsHEvKZ{~9OIa+kY1=tAhWX-hYoBYI z=f%cxr?#OvH~v7k-5j`R5i3z1yfnGB=L-&o;wbEPmdjGcSbW85(YJ>&(wk*yDu1s=tw*;7>v8c5}dpmiu?0R7Nq zXY_!-(HTf73OL?yx!|8psGmA<=IaV0AV-8h=AKfIshe5?ChE7Fv5~cDbA(AzJy&;P zlD)8&+HmT%7YJi4W5i03OI2aa>iC#JECYNl{aJN$jujQmi%Ydq8T2EtDjgCV)jT4{tdx@k*1t5zD>pH_}2Fz8p&u4L* zD|^O8+Io6zWcoEn(3G7IqkGOMpDG#69W}9z0r%!OWl}%+Bp&dO>!}|u8V9;%ZB1)u zoNMUA)TZWBzemb_A2h$b?x+qWzXEaJZ+*T+Qawxb&t}fsM^!*o?h%eQb8d1Mu$tCM zY2HDj4qjSq(;aMK&GmJd&ABZp!yX&R0j<#zF)rMr6K2ysQ!edN zKEjx-kIG_%?%+SoyRQcBI$u*R{7FRltWtQI$PKP2kJVP6S9v)K(&P>jz8{UQ3ap>` z-wz=}Ijex_HHo_-s^v$Pt5v9-m=-HK8>9q#cfVX_kXt zd%9aDu^jdQ?3J=lc9nF4UYizt4#z6Jqdj01(C2#2I|A!udzLcZFVhgm$Z@2r|Fq{4 z)?-aPKfE8QmXEbhpi=X*PS(Ici@w*=?5!KG=HGeE=2!5PngHAj#2U+nx~M}V2M)Cj z6wbIBs^FFS9vR4;@ur$o7iYGDUXOb__xYpF28-$o#`M6(y|}b8SkMf>r<{04z^xb9 zyf-?aWO!p#f5J0jY@@xFRuS?8IRgzI=nD37;IQ0T7PzkGl7EH)4tE#eY=@SF>Z^BlB992W1S%aNAH!Y7&BndC^Z_6UF2rNG~ zSF9PspTXCQYy{@Pfy(useL1oD=t2cFvh+lv0>_)rDUj1j1gn_k&jY*Kj=!Pv-1J)e znH_^G%$P^t2m^9laS>?0lb`b1-|;@@cRqb_nykfTbFY#0q8`(yerWDC^DdX_azPsq zX{;JCV^to#M>TZJI6ca@z1h0O_ANfc>00O(pI&s0`I7ozzGV7y(|Kf+ctc-LA3q_ z9&!!Gz7U8k<1-WlNsZditb?r2qwfK;^$GISp?bm`{(}UM)x19|Ky=E8Bl*6z~*d z-1&`~_5v;F+~c(O>2K~CLfxrejMWyks-NCt+=EWt*yv%{Ds3FSRfp05)uA1v`Z%Y4 z>jBJoW>Ihc^D6MC-eRsgf%I~Arw#S|8YrhMBWV=+HxrE^_w9*VGM@W6=+qkS4g&x= z6@15Wj2_`r47D8|3xp}H7jSH^*Dqc_;sr+x(KiKg-(+@<`U=T7Uus9Apq0Lk@rO3- zXbs|12b|s$kA3{T=QIQ7`3uDJFxn9mG`}rE*md9mAPSf2wSC2$<-W~XG^S@*RJNnS zdl4L`r!{XvPLQooMy!N z(UAAnb@f7v>WOq|%v#t~ZWFJ<5@%s)aFUqh4+1O9@v{siy=9%}P|zu_3Z z1b7|rNBsUC>{|{4Ud0bN%7F8LYxvnIWVgP8A5Dw{z5)0J{sX}x0Ke%U18wXX0RBI~ z_ypi3zy~;zuZry01HgO-bnmmV4b?7Sqj^D@smpIS=Gx2Xms2~Oa literal 0 HcmV?d00001 diff --git a/agb/examples/object_text_render.rs b/agb/examples/object_text_render.rs index 0a226f70..a5f8f5bf 100644 --- a/agb/examples/object_text_render.rs +++ b/agb/examples/object_text_render.rs @@ -17,7 +17,7 @@ use agb_fixnum::Rect; use core::fmt::Write; -const FONT: Font = include_font!("examples/font/yoster.ttf", 12); +const FONT: Font = include_font!("examples/font/pixelated.ttf", 8); #[agb::entry] fn entry(gba: agb::Gba) -> ! { main(gba); @@ -32,12 +32,14 @@ fn main(mut gba: agb::Gba) -> ! { let palette = Palette16::new(palette); let palette = PaletteVram::new(&palette).unwrap(); - let config = Configuration::new(Size::S16x16, palette); + let config = Configuration::new(Size::S16x8, palette); let mut wr = BufferedWordRender::new(&FONT, config); let _ = writeln!( wr, - "Hello there!\nI spent this weekend writing this text system! Is it any good?\n\nOh, by the way, you can press A to restart!" + "{}", + "counts for three shoot dice for damage calculation\nmalfunctions all dice after use" + .to_ascii_uppercase() ); let vblank = agb::interrupt::VBlank::get(); @@ -59,7 +61,10 @@ fn main(mut gba: agb::Gba) -> ! { wr.commit(oam); let start = timer.value(); - wr.update(Rect::new((0, 0).into(), (WIDTH, 100).into()), num_letters); + wr.update( + Rect::new((WIDTH / 8, 0).into(), (80, 100).into()), + num_letters, + ); wr.process(); let end = timer.value(); @@ -67,13 +72,22 @@ fn main(mut gba: agb::Gba) -> ! { frame += 1; - if frame % 4 == 0 { - num_letters += 1; - } + // if frame % 2 == 0 { + num_letters += 1; + // } if input.is_just_pressed(Button::A) { break; } } + let start = timer.value(); + drop(wr); + let oam = unmanaged.iter(); + drop(oam); + let end = timer.value(); + agb::println!( + "Drop took {} cycles", + 256 * (end.wrapping_sub(start) as u32) + ); } } diff --git a/agb/src/display/object/font.rs b/agb/src/display/object/font.rs index 5501cda0..7eaf45b2 100644 --- a/agb/src/display/object/font.rs +++ b/agb/src/display/object/font.rs @@ -151,7 +151,7 @@ impl WordRenderCache { && prospective_x > position.position.x + position.size.x { self.state.head_position.x = position.position.x; - self.state.head_position.y += 15; + self.state.head_position.y += 9; } for letter in word.letters.iter().skip(self.state.depth_in_word) { @@ -175,10 +175,10 @@ impl WordRenderCache { } TextElementReference::WhiteSpace(space) => { match space { - WhiteSpace::Space => self.state.head_position.x += 10, + WhiteSpace::Space => self.state.head_position.x += 2, WhiteSpace::NewLine => { self.state.head_position.x = position.position.x; - self.state.head_position.y += 15; + self.state.head_position.y += 14; } } self.state.depth_in_elements += 1; @@ -244,7 +244,6 @@ impl WorkingLetter { fn reset(&mut self) { self.x_position = 0; self.x_offset = 0; - self.dynamic.clear(0); } } @@ -382,11 +381,10 @@ impl WordRender<'_> { return None; } - let sprite = self - .working - .letter - .dynamic - .to_vram(self.config.palette.clone()); + let mut new_sprite = DynamicSprite::new(self.config.sprite_size); + core::mem::swap(&mut self.working.letter.dynamic, &mut new_sprite); + let sprite = new_sprite.to_vram(self.config.palette.clone()); + let group = LetterGroup { sprite, width: self.working.letter.x_offset as u16, @@ -416,8 +414,7 @@ impl WordRender<'_> { self.working.letter.x_offset += font_letter.xmin as i32; } - let y_position = - self.font.ascent() - font_letter.height as i32 - font_letter.ymin as i32 + 4; + 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 { From d5d3d1a658234189557a6bacf084d4c59c7c4edc Mon Sep 17 00:00:00 2001 From: Corwin Date: Tue, 27 Jun 2023 22:00:34 +0100 Subject: [PATCH 08/24] strip to bare renderer --- agb/src/display/object/font.rs | 343 ++------------------------------- 1 file changed, 17 insertions(+), 326 deletions(-) diff --git a/agb/src/display/object/font.rs b/agb/src/display/object/font.rs index 7eaf45b2..4eea710e 100644 --- a/agb/src/display/object/font.rs +++ b/agb/src/display/object/font.rs @@ -1,11 +1,6 @@ -use core::{cell::Cell, fmt::Write, num::NonZeroUsize}; +use crate::display::Font; -use agb_fixnum::{Rect, Vector2D}; -use alloc::{collections::VecDeque, vec::Vec}; - -use crate::display::{object::ObjectUnmanaged, Font}; - -use super::{DynamicSprite, OamIterator, PaletteVram, Size, SpriteVram}; +use super::{DynamicSprite, PaletteVram, Size, SpriteVram}; struct LetterGroup { sprite: SpriteVram, @@ -14,216 +9,6 @@ struct LetterGroup { left: i16, } -enum WhiteSpace { - Space, - NewLine, -} - -enum TextElementReference<'text> { - Word(Word<'text>), - WhiteSpace(WhiteSpace), -} - -struct Word<'letters> { - letters: &'letters [LetterGroup], - width: Cell>, -} - -impl<'letters> Word<'letters> { - fn new(letters: &'letters [LetterGroup]) -> Self { - Self { - letters, - width: Cell::new(None), - } - } -} - -impl Word<'_> { - fn width(&self) -> usize { - match self.width.get() { - Some(width) => width.get(), - None => { - let width = self.letters.iter().fold(0, |acc, letter| { - acc + (letter.width as i32 + letter.left as i32) - }); - let width = width as usize; - - self.width.set(NonZeroUsize::new(width)); - width - } - } - } -} - -#[derive(Clone, Copy)] -struct WordLength(u8); - -#[derive(Clone, Copy)] -enum Element { - Word(u8), - NewLine, - Space, -} -const NEW_LINE: u8 = 0xFF; -const SPACE: u8 = 0xFE; -impl WordLength { - fn parse(self) -> Element { - if self.0 == NEW_LINE { - Element::NewLine - } else if self.0 == SPACE { - Element::Space - } else { - Element::Word(self.0) - } - } - - fn from_element(e: Element) -> Self { - WordLength(match e { - Element::Word(len) => len, - Element::NewLine => NEW_LINE, - Element::Space => SPACE, - }) - } -} - -struct Letters(Vec); -struct Words { - letters: Letters, - word_lengths: Vec, -} - -struct WordRenderCache { - objects: Vec, - state: WordRenderCacheState, - poison_condition: WordRenderPoisonCondition, -} - -struct WordRenderPoisonCondition { - area: Rect, -} - -struct WordRenderCacheState { - depth_in_word_iterator: usize, - depth_in_word: usize, - depth_in_elements: usize, - head_position: Vector2D, -} - -impl WordRenderCache { - fn new() -> Self { - WordRenderCache { - objects: Vec::new(), - state: WordRenderCacheState { - depth_in_word_iterator: 0, - depth_in_word: 0, - depth_in_elements: 0, - head_position: (0, 0).into(), - }, - poison_condition: WordRenderPoisonCondition { - area: Rect::new((0, 0).into(), (0, 0).into()), - }, - } - } - - fn reset_state(&mut self, position: Rect) { - self.state = WordRenderCacheState { - depth_in_elements: 0, - depth_in_word: 0, - depth_in_word_iterator: 0, - head_position: position.position, - }; - - self.poison_condition = WordRenderPoisonCondition { area: position }; - } - - fn generate_cache(&mut self, words: &Words, desired_element_count: usize) { - let position = self.poison_condition.area; - if self.state.depth_in_elements >= desired_element_count { - return; - } - - 'outer: for elem in words.iter_words().skip(self.state.depth_in_word_iterator) { - match elem { - TextElementReference::Word(word) => { - let prospective_x = self.state.head_position.x + word.width() as i32; - - if self.state.depth_in_word == 0 - && prospective_x > position.position.x + position.size.x - { - self.state.head_position.x = position.position.x; - self.state.head_position.y += 9; - } - - for letter in word.letters.iter().skip(self.state.depth_in_word) { - self.state.head_position.x += letter.left as i32; - let mut object = ObjectUnmanaged::new(letter.sprite.clone()); - object.show(); - object.set_position(self.state.head_position); - - self.objects.push(object); - - self.state.head_position.x += letter.width as i32; - - self.state.depth_in_elements += 1; - self.state.depth_in_word += 1; - if self.state.depth_in_elements >= desired_element_count { - break 'outer; - } - } - - self.state.depth_in_word = 0; - } - TextElementReference::WhiteSpace(space) => { - match space { - WhiteSpace::Space => self.state.head_position.x += 2, - WhiteSpace::NewLine => { - self.state.head_position.x = position.position.x; - self.state.head_position.y += 14; - } - } - self.state.depth_in_elements += 1; - } - } - self.state.depth_in_word_iterator += 1; - if self.state.depth_in_elements >= desired_element_count { - break 'outer; - } - } - } - - fn update(&mut self, words: &Words, desired_element_count: usize, position: Rect) { - if self.poison_condition.area != position { - self.reset_state(position); - } - - self.generate_cache(words, desired_element_count); - } - - fn commit(&self, oam: &mut OamIterator) { - for (object, slot) in self.objects.iter().zip(oam) { - slot.set(object); - } - } -} - -impl Words { - fn iter_words(&self) -> impl Iterator { - let mut letters_idx: usize = 0; - - self.word_lengths.iter().map(move |x| match x.parse() { - Element::Word(length) => { - let idx = letters_idx; - let end_idx = idx + length as usize; - letters_idx = end_idx; - - TextElementReference::Word(Word::new(&self.letters.0[idx..end_idx])) - } - Element::NewLine => TextElementReference::WhiteSpace(WhiteSpace::NewLine), - Element::Space => TextElementReference::WhiteSpace(WhiteSpace::Space), - }) - } -} - struct WorkingLetter { dynamic: DynamicSprite, // the x offset of the current letter with respect to the start of the current letter group @@ -262,113 +47,19 @@ impl Configuration { } } -pub struct BufferedWordRender<'font> { - word_render: WordRender<'font>, - block: Words, - current_word_length: usize, - number_of_elements: usize, - buffered_chars: VecDeque, - cache: WordRenderCache, -} - -impl Write for BufferedWordRender<'_> { - fn write_str(&mut self, s: &str) -> core::fmt::Result { - for char in s.chars() { - self.buffered_chars.push_back(char); - } - - Ok(()) - } -} - -impl<'font> BufferedWordRender<'font> { - #[must_use] - pub fn new(font: &'font Font, config: Configuration) -> Self { - BufferedWordRender { - word_render: WordRender::new(font, config), - block: Words { - letters: Letters(Vec::new()), - word_lengths: Vec::new(), - }, - current_word_length: 0, - number_of_elements: 0, - buffered_chars: VecDeque::new(), - cache: WordRenderCache::new(), - } - } -} - -impl BufferedWordRender<'_> { - pub fn process(&mut self) { - if let Some(char) = self.buffered_chars.pop_front() { - if char == '\n' { - if let Some(group) = self.word_render.finalise_letter() { - self.block.letters.0.push(group); - self.current_word_length += 1; - } - self.block - .word_lengths - .push(WordLength::from_element(Element::Word( - self.current_word_length as u8, - ))); - self.block - .word_lengths - .push(WordLength::from_element(Element::NewLine)); - self.number_of_elements += self.current_word_length + 1; - self.current_word_length = 0; - } else if char == ' ' { - if let Some(group) = self.word_render.finalise_letter() { - self.block.letters.0.push(group); - self.current_word_length += 1; - } - self.block - .word_lengths - .push(WordLength::from_element(Element::Word( - self.current_word_length as u8, - ))); - self.block - .word_lengths - .push(WordLength::from_element(Element::Space)); - self.number_of_elements += self.current_word_length + 1; - self.current_word_length = 0; - } else if let Some(group) = self.word_render.render_char(char) { - self.block.letters.0.push(group); - self.current_word_length += 1; - } - } - } - - pub fn update(&mut self, position: Rect, number_of_elements: usize) { - while !self.buffered_chars.is_empty() && self.number_of_elements < number_of_elements { - self.process(); - } - - self.cache.update(&self.block, number_of_elements, position); - } - - pub fn commit(&self, oam: &mut OamIterator) { - self.cache.commit(oam); - } -} - struct WordRender<'font> { font: &'font Font, - working: Working, + working: WorkingLetter, config: Configuration, } -struct Working { - letter: WorkingLetter, -} - impl<'font> WordRender<'font> { #[must_use] fn new(font: &'font Font, config: Configuration) -> Self { WordRender { font, - working: Working { - letter: WorkingLetter::new(config.sprite_size), - }, + working: WorkingLetter::new(config.sprite_size), + config, } } @@ -377,20 +68,20 @@ impl<'font> WordRender<'font> { impl WordRender<'_> { #[must_use] fn finalise_letter(&mut self) -> Option { - if self.working.letter.x_offset == 0 { + if self.working.x_offset == 0 { return None; } let mut new_sprite = DynamicSprite::new(self.config.sprite_size); - core::mem::swap(&mut self.working.letter.dynamic, &mut new_sprite); + core::mem::swap(&mut self.working.dynamic, &mut new_sprite); let sprite = new_sprite.to_vram(self.config.palette.clone()); let group = LetterGroup { sprite, - width: self.working.letter.x_offset as u16, - left: self.working.letter.x_position as i16, + width: self.working.x_offset as u16, + left: self.working.x_position as i16, }; - self.working.letter.reset(); + self.working.reset(); Some(group) } @@ -400,7 +91,7 @@ impl WordRender<'_> { let font_letter = self.font.letter(c); // uses more than the sprite can hold - let group = if self.working.letter.x_offset + font_letter.width as i32 + let group = if self.working.x_offset + font_letter.width as i32 > self.config.sprite_size.to_width_height().0 as i32 { self.finalise_letter() @@ -408,10 +99,10 @@ impl WordRender<'_> { None }; - if self.working.letter.x_offset == 0 { - self.working.letter.x_position = font_letter.xmin as i32; + if self.working.x_offset == 0 { + self.working.x_position = font_letter.xmin as i32; } else { - self.working.letter.x_offset += font_letter.xmin as i32; + self.working.x_offset += font_letter.xmin as i32; } let y_position = self.font.ascent() - font_letter.height as i32 - font_letter.ymin as i32; @@ -420,8 +111,8 @@ impl WordRender<'_> { 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 as usize, + self.working.dynamic.set_pixel( + x + self.working.x_offset as usize, (y_position + y as i32) as usize, 1, ); @@ -429,7 +120,7 @@ impl WordRender<'_> { } } - self.working.letter.x_offset += font_letter.advance_width as i32; + self.working.x_offset += font_letter.advance_width as i32; group } From f947d82049712580f54850d61f57649a89dca83b Mon Sep 17 00:00:00 2001 From: Corwin Date: Wed, 28 Jun 2023 20:29:09 +0100 Subject: [PATCH 09/24] text rendering that supports different alignments --- agb/examples/object_text_render.rs | 37 ++- agb/src/display/object/font.rs | 301 +++++++++++++++------- agb/src/display/object/font/preprocess.rs | 206 +++++++++++++++ agb/src/display/object/font/renderer.rs | 120 +++++++++ 4 files changed, 556 insertions(+), 108 deletions(-) create mode 100644 agb/src/display/object/font/preprocess.rs create mode 100644 agb/src/display/object/font/renderer.rs diff --git a/agb/examples/object_text_render.rs b/agb/examples/object_text_render.rs index a5f8f5bf..bd2397de 100644 --- a/agb/examples/object_text_render.rs +++ b/agb/examples/object_text_render.rs @@ -4,7 +4,7 @@ use agb::{ display::{ object::{ - font::{BufferedWordRender, Configuration}, + font::{BufferedRender, TextAlignment}, PaletteVram, Size, }, palette16::Palette16, @@ -15,6 +15,9 @@ use agb::{ }; use agb_fixnum::Rect; +extern crate alloc; +use alloc::vec::Vec; + use core::fmt::Write; const FONT: Font = include_font!("examples/font/pixelated.ttf", 8); @@ -32,9 +35,7 @@ fn main(mut gba: agb::Gba) -> ! { let palette = Palette16::new(palette); let palette = PaletteVram::new(&palette).unwrap(); - let config = Configuration::new(Size::S16x8, palette); - - let mut wr = BufferedWordRender::new(&FONT, config); + let mut wr = BufferedRender::new(&FONT, Size::S16x8, palette); let _ = writeln!( wr, "{}", @@ -54,22 +55,40 @@ fn main(mut gba: agb::Gba) -> ! { let mut num_letters = 0; let mut frame = 0; + let mut alignment = TextAlignment::Left; + + let mut text = Vec::new(); + loop { vblank.wait_for_vblank(); input.update(); let oam = &mut unmanaged.iter(); - wr.commit(oam); + for (letter, slot) in text.iter().zip(oam) { + slot.set(letter); + } let start = timer.value(); - wr.update( - Rect::new((WIDTH / 8, 0).into(), (80, 100).into()), - num_letters, - ); wr.process(); + text = wr.layout( + Rect::new((WIDTH / 8, 0).into(), (80, 100).into()), + alignment, + num_letters, + 2, + ); let end = timer.value(); agb::println!("Took {} cycles", 256 * (end.wrapping_sub(start) as u32)); + if input.is_just_pressed(Button::LEFT) { + alignment = TextAlignment::Left; + } + if input.is_just_pressed(Button::RIGHT) { + alignment = TextAlignment::Right; + } + if input.is_just_pressed(Button::UP | Button::DOWN) { + alignment = TextAlignment::Center; + } + frame += 1; // if frame % 2 == 0 { diff --git a/agb/src/display/object/font.rs b/agb/src/display/object/font.rs index 4eea710e..75149d1b 100644 --- a/agb/src/display/object/font.rs +++ b/agb/src/display/object/font.rs @@ -1,127 +1,230 @@ +use core::fmt::Write; + +use agb_fixnum::{Rect, Vector2D}; +use alloc::{collections::VecDeque, vec::Vec}; + use crate::display::Font; -use super::{DynamicSprite, PaletteVram, Size, SpriteVram}; +use self::{ + preprocess::{Line, Preprocessed, PreprocessedElement}, + renderer::{Configuration, WordRender}, +}; -struct LetterGroup { +use super::{DynamicSprite, ObjectUnmanaged, PaletteVram, Size, SpriteVram}; + +mod preprocess; +mod renderer; + +#[derive(Debug, PartialEq, Eq)] +#[non_exhaustive] +pub(crate) enum WhiteSpace { + NewLine, + Space, +} + +impl WhiteSpace { + pub(crate) fn from_char(c: char) -> Self { + match c { + ' ' => WhiteSpace::Space, + '\n' => WhiteSpace::NewLine, + _ => panic!("char not supported whitespace"), + } + } +} + +#[derive(Debug)] +pub(crate) struct LetterGroup { sprite: SpriteVram, // the width of the letter group width: u16, left: i16, } -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: i32, -} - -impl WorkingLetter { - fn new(size: Size) -> Self { - Self { - dynamic: DynamicSprite::new(size), - x_position: 0, - x_offset: 0, - } - } - - fn reset(&mut self) { - self.x_position = 0; - self.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, - } - } -} - -struct WordRender<'font> { +pub struct BufferedRender<'font> { + char_render: WordRender, + preprocessor: Preprocessed, + buffered_chars: VecDeque, + letters: Letters, font: &'font Font, - working: WorkingLetter, - config: Configuration, } -impl<'font> WordRender<'font> { +#[derive(Debug, Default)] +struct Letters { + letters: Vec, + word_lengths: Vec, + current_word_length: usize, + number_of_groups: usize, +} + +impl Write for BufferedRender<'_> { + fn write_str(&mut self, s: &str) -> core::fmt::Result { + for c in s.chars() { + self.input_character(c); + } + + Ok(()) + } +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +#[non_exhaustive] +pub enum TextAlignment { + Left, + Right, + Center, +} + +struct TextAlignmentSettings { + space_width: i32, + start_x: i32, +} + +impl TextAlignment { + fn settings( + self, + line: &Line, + minimum_space_width: i32, + size: Vector2D, + ) -> TextAlignmentSettings { + match self { + TextAlignment::Left => TextAlignmentSettings { + space_width: minimum_space_width, + start_x: 0, + }, + TextAlignment::Right => TextAlignmentSettings { + space_width: minimum_space_width, + start_x: size.x - line.width(), + }, + TextAlignment::Center => TextAlignmentSettings { + space_width: minimum_space_width, + start_x: (size.x - line.width()) / 2, + }, + } + } +} + +impl<'font> BufferedRender<'font> { #[must_use] - fn new(font: &'font Font, config: Configuration) -> Self { - WordRender { + pub fn new(font: &'font Font, sprite_size: Size, palette: PaletteVram) -> Self { + let config = Configuration::new(sprite_size, palette); + BufferedRender { + char_render: WordRender::new(config), + preprocessor: Preprocessed::new(), + buffered_chars: VecDeque::new(), + letters: Default::default(), font, - working: WorkingLetter::new(config.sprite_size), - - config, } } } -impl WordRender<'_> { - #[must_use] - fn finalise_letter(&mut self) -> Option { - if self.working.x_offset == 0 { - return None; - } - - let mut new_sprite = DynamicSprite::new(self.config.sprite_size); - core::mem::swap(&mut self.working.dynamic, &mut new_sprite); - let sprite = new_sprite.to_vram(self.config.palette.clone()); - - let group = LetterGroup { - sprite, - width: self.working.x_offset as u16, - left: self.working.x_position as i16, - }; - self.working.reset(); - - Some(group) +impl BufferedRender<'_> { + fn input_character(&mut self, character: char) { + self.preprocessor.add_character(self.font, character); + self.buffered_chars.push_back(character); } - #[must_use] - fn render_char(&mut self, c: char) -> Option { - let font_letter = self.font.letter(c); - - // uses more than the sprite can hold - let group = if self.working.x_offset + font_letter.width as i32 - > self.config.sprite_size.to_width_height().0 as i32 - { - self.finalise_letter() - } else { - None - }; - - if self.working.x_offset == 0 { - self.working.x_position = font_letter.xmin as i32; - } else { - self.working.x_offset += 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.dynamic.set_pixel( - x + self.working.x_offset as usize, - (y_position + y as i32) as usize, - 1, + pub fn process(&mut self) { + let Some(c) = self.buffered_chars.pop_front() else { return }; + match c { + ' ' | '\n' => { + if let Some(group) = self.char_render.finalise_letter() { + self.letters.letters.push(group); + self.letters.current_word_length += 1; + self.letters.number_of_groups += 1; + } + if self.letters.current_word_length != 0 { + self.letters.word_lengths.push( + self.letters + .current_word_length + .try_into() + .expect("word is too big"), ); } + self.letters.current_word_length = 0; + self.letters.number_of_groups += 1; + } + letter => { + if let Some(group) = self.char_render.render_char(self.font, letter) { + self.letters.letters.push(group); + self.letters.current_word_length += 1; + self.letters.number_of_groups += 1; + } } } + } - self.working.x_offset += font_letter.advance_width as i32; + #[must_use] + pub fn layout( + &mut self, + area: Rect, + alignment: TextAlignment, + number_of_groups: usize, + paragraph_spacing: i32, + ) -> Vec { + let mut objects = Vec::new(); - group + while !self.buffered_chars.is_empty() && self.letters.number_of_groups <= number_of_groups { + self.process(); + } + + let minimum_space_width = self.font.letter(' ').advance_width as i32; + + let lines = self.preprocessor.lines(area.size.x, minimum_space_width); + let mut head_position = area.position; + + let mut processed_depth = 0; + let mut group_depth = 0; + let mut word_depth = 0; + let mut rendered_groups = 0; + + 'outer: for line in lines { + let settings = alignment.settings(&line, minimum_space_width, area.size); + head_position.x += settings.start_x; + + for idx in 0..line.number_of_text_elements() { + let element = self.preprocessor.get(processed_depth + idx); + match element { + PreprocessedElement::Word(_) => { + for _ in 0..self + .letters + .word_lengths + .get(word_depth) + .copied() + .unwrap_or(u8::MAX) + { + let letter_group = &self.letters.letters[group_depth]; + let mut object = ObjectUnmanaged::new(letter_group.sprite.clone()); + head_position.x += letter_group.left as i32; + object.set_position(head_position); + head_position.x += letter_group.width as i32; + object.show(); + objects.push(object); + group_depth += 1; + rendered_groups += 1; + if rendered_groups >= number_of_groups { + break 'outer; + } + } + word_depth += 1; + } + PreprocessedElement::WhiteSpace(space_type) => { + if space_type == WhiteSpace::NewLine { + head_position.y += paragraph_spacing; + } + head_position.x += settings.space_width; + rendered_groups += 1; + if rendered_groups >= number_of_groups { + break 'outer; + } + } + } + } + + processed_depth += line.number_of_text_elements(); + head_position.x = area.position.x; + head_position.y += 9; + } + + objects } } diff --git a/agb/src/display/object/font/preprocess.rs b/agb/src/display/object/font/preprocess.rs new file mode 100644 index 00000000..26023608 --- /dev/null +++ b/agb/src/display/object/font/preprocess.rs @@ -0,0 +1,206 @@ +use alloc::vec::Vec; + +use crate::display::Font; + +use super::WhiteSpace; + +#[derive(Debug, PartialEq, Eq)] +pub(crate) enum PreprocessedElement { + Word(u8), + WhiteSpace(WhiteSpace), +} + +#[derive(Clone, Copy, PartialEq, Eq)] +struct PreprocessedElementStored(u8); + +impl core::fmt::Debug for PreprocessedElementStored { + fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result { + f.debug_tuple("PreprocessedElementStored") + .field(&self.parse()) + .finish() + } +} + +impl PreprocessedElementStored { + fn parse(self) -> PreprocessedElement { + match self.0 { + 255 => PreprocessedElement::WhiteSpace(WhiteSpace::NewLine), + 254 => PreprocessedElement::WhiteSpace(WhiteSpace::Space), + length => PreprocessedElement::Word(length), + } + } + + fn from_element(x: PreprocessedElement) -> Self { + match x { + PreprocessedElement::Word(length) => PreprocessedElementStored(length), + PreprocessedElement::WhiteSpace(space) => PreprocessedElementStored(match space { + WhiteSpace::NewLine => 255, + WhiteSpace::Space => 254, + }), + } + } +} + +#[derive(Default, Debug)] +pub(crate) struct Preprocessed { + widths: Vec, + preprocessor: Preprocessor, +} + +#[derive(Debug, Default)] +struct Preprocessor { + current_word_width: i32, +} + +impl Preprocessor { + fn add_character( + &mut self, + font: &Font, + character: char, + widths: &mut Vec, + ) { + match character { + space @ (' ' | '\n') => { + if self.current_word_width != 0 { + widths.push(PreprocessedElementStored::from_element( + PreprocessedElement::Word( + self.current_word_width + .try_into() + .expect("word should be small and positive"), + ), + )); + self.current_word_width = 0; + } + widths.push(PreprocessedElementStored::from_element( + PreprocessedElement::WhiteSpace(WhiteSpace::from_char(space)), + )); + } + letter => { + let letter = font.letter(letter); + self.current_word_width += letter.advance_width as i32 + letter.xmin as i32; + } + } + } +} + +pub(crate) struct Lines<'preprocess> { + minimum_space_width: i32, + layout_width: i32, + data: &'preprocess [PreprocessedElementStored], + current_start_idx: usize, +} + +pub(crate) struct Line { + width: i32, + number_of_text_elements: usize, + number_of_spaces: usize, + number_of_words: usize, +} + +impl Line { + #[inline(always)] + pub(crate) fn width(&self) -> i32 { + self.width + } + #[inline(always)] + pub(crate) fn number_of_text_elements(&self) -> usize { + self.number_of_text_elements + } + #[inline(always)] + pub(crate) fn number_of_spaces(&self) -> usize { + self.number_of_spaces + } + #[inline(always)] + pub(crate) fn number_of_words(&self) -> usize { + self.number_of_words + } +} + +impl<'pre> Iterator for Lines<'pre> { + type Item = Line; + + fn next(&mut self) -> Option { + if self.current_start_idx >= self.data.len() { + return None; + } + + let mut line_idx_length = 0; + let mut current_line_width = 0; + let mut additional_space_count = 0; + let mut number_of_spaces = 0; + let mut number_of_words = 0; + + while let Some(next) = self.data.get(self.current_start_idx + line_idx_length) { + match next.parse() { + PreprocessedElement::Word(pixels) => { + let additional_space_width = + additional_space_count as i32 * self.minimum_space_width; + let width = pixels as i32; + if width + current_line_width + additional_space_width > self.layout_width { + break; + } + number_of_words += 1; + current_line_width += width + additional_space_width; + number_of_spaces += additional_space_count; + } + PreprocessedElement::WhiteSpace(space) => match space { + WhiteSpace::NewLine => { + line_idx_length += 1; + break; + } + WhiteSpace::Space => { + additional_space_count += 1; + } + }, + }; + + line_idx_length += 1; + } + + self.current_start_idx += line_idx_length; + + Some(Line { + width: current_line_width, + number_of_text_elements: line_idx_length, + number_of_spaces, + number_of_words, + }) + } +} + +impl Preprocessed { + pub(crate) fn new() -> Self { + Default::default() + } + + pub(crate) fn get(&self, idx: usize) -> PreprocessedElement { + self.widths[idx].parse() + } + + pub(crate) fn add_character(&mut self, font: &Font, c: char) { + self.preprocessor.add_character(font, c, &mut self.widths); + } + + pub(crate) fn lines(&self, layout_width: i32, minimum_space_width: i32) -> Lines<'_> { + Lines { + minimum_space_width, + layout_width, + data: &self.widths, + current_start_idx: 0, + } + } + + fn lines_element( + &self, + layout_width: i32, + minimum_space_width: i32, + ) -> impl Iterator { + let mut idx = 0; + self.lines(layout_width, minimum_space_width).map(move |x| { + let length = x.number_of_text_elements; + let d = &self.widths[idx..(idx + length)]; + idx += length; + d + }) + } +} diff --git a/agb/src/display/object/font/renderer.rs b/agb/src/display/object/font/renderer.rs new file mode 100644 index 00000000..1aa3b0e0 --- /dev/null +++ b/agb/src/display/object/font/renderer.rs @@ -0,0 +1,120 @@ +use crate::display::{ + object::{DynamicSprite, PaletteVram, Size}, + Font, +}; + +use super::LetterGroup; + +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: i32, +} + +impl WorkingLetter { + fn new(size: Size) -> Self { + Self { + dynamic: DynamicSprite::new(size), + x_position: 0, + x_offset: 0, + } + } + + fn reset(&mut self) { + self.x_position = 0; + self.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(crate) struct WordRender { + working: WorkingLetter, + config: Configuration, + colour: usize, +} + +impl WordRender { + #[must_use] + pub(crate) fn new(config: Configuration) -> Self { + WordRender { + working: WorkingLetter::new(config.sprite_size), + config, + colour: 1, + } + } + + #[must_use] + pub(crate) fn finalise_letter(&mut self) -> Option { + if self.working.x_offset == 0 { + return None; + } + + let mut new_sprite = DynamicSprite::new(self.config.sprite_size); + core::mem::swap(&mut self.working.dynamic, &mut new_sprite); + let sprite = new_sprite.to_vram(self.config.palette.clone()); + + let group = LetterGroup { + sprite, + width: self.working.x_offset as u16, + left: self.working.x_position as i16, + }; + self.working.reset(); + + Some(group) + } + + #[must_use] + pub(crate) fn render_char(&mut self, font: &Font, c: char) -> Option { + let font_letter: &crate::display::FontLetter = font.letter(c); + + // uses more than the sprite can hold + let group = if self.working.x_offset + font_letter.width as i32 + > self.config.sprite_size.to_width_height().0 as i32 + { + self.finalise_letter() + } else { + None + }; + + if self.working.x_offset == 0 { + self.working.x_position = font_letter.xmin as i32; + } else { + self.working.x_offset += font_letter.xmin as i32; + } + + let y_position = 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.dynamic.set_pixel( + x + self.working.x_offset as usize, + (y_position + y as i32) as usize, + self.colour, + ); + } + } + } + + self.working.x_offset += font_letter.advance_width as i32; + + group + } +} From cf400029f5995ff2a3c28af641bb41b07f3c6c08 Mon Sep 17 00:00:00 2001 From: Corwin Date: Thu, 29 Jun 2023 00:10:21 +0100 Subject: [PATCH 10/24] caching layout --- agb/examples/object_text_render.rs | 20 +-- agb/src/display/font.rs | 4 + agb/src/display/object/font.rs | 199 +++++++++++++++------- agb/src/display/object/font/preprocess.rs | 129 ++++++++------ agb/src/display/object/font/renderer.rs | 4 + 5 files changed, 222 insertions(+), 134 deletions(-) diff --git a/agb/examples/object_text_render.rs b/agb/examples/object_text_render.rs index bd2397de..c8312760 100644 --- a/agb/examples/object_text_render.rs +++ b/agb/examples/object_text_render.rs @@ -4,7 +4,7 @@ use agb::{ display::{ object::{ - font::{BufferedRender, TextAlignment}, + font::{BufferedRender, LayoutCache, TextAlignment}, PaletteVram, Size, }, palette16::Palette16, @@ -53,27 +53,25 @@ fn main(mut gba: agb::Gba) -> ! { timer.set_divider(agb::timer::Divider::Divider256); let mut num_letters = 0; - let mut frame = 0; let mut alignment = TextAlignment::Left; - let mut text = Vec::new(); + let mut cache = LayoutCache::new(); loop { vblank.wait_for_vblank(); input.update(); let oam = &mut unmanaged.iter(); - for (letter, slot) in text.iter().zip(oam) { - slot.set(letter); - } + cache.commit(oam); let start = timer.value(); wr.process(); - text = wr.layout( - Rect::new((WIDTH / 8, 0).into(), (80, 100).into()), + cache.update( + &mut wr, + Rect::new((WIDTH / 3, 0).into(), (WIDTH / 3, 100).into()), alignment, - num_letters, 2, + num_letters, ); let end = timer.value(); @@ -89,11 +87,7 @@ fn main(mut gba: agb::Gba) -> ! { alignment = TextAlignment::Center; } - frame += 1; - - // if frame % 2 == 0 { num_letters += 1; - // } if input.is_just_pressed(Button::A) { break; diff --git a/agb/src/display/font.rs b/agb/src/display/font.rs index f26a75c5..17bd184a 100644 --- a/agb/src/display/font.rs +++ b/agb/src/display/font.rs @@ -69,6 +69,10 @@ impl Font { pub(crate) fn ascent(&self) -> i32 { self.ascent } + + pub(crate) fn line_height(&self) -> i32 { + self.line_height + } } impl Font { diff --git a/agb/src/display/object/font.rs b/agb/src/display/object/font.rs index 75149d1b..7bfd5c96 100644 --- a/agb/src/display/object/font.rs +++ b/agb/src/display/object/font.rs @@ -10,7 +10,7 @@ use self::{ renderer::{Configuration, WordRender}, }; -use super::{DynamicSprite, ObjectUnmanaged, PaletteVram, Size, SpriteVram}; +use super::{DynamicSprite, OamIterator, ObjectUnmanaged, PaletteVram, Size, SpriteVram}; mod preprocess; mod renderer; @@ -48,11 +48,15 @@ pub struct BufferedRender<'font> { font: &'font Font, } +#[derive(Debug)] +struct Word { + index: usize, + length: usize, +} + #[derive(Debug, Default)] struct Letters { letters: Vec, - word_lengths: Vec, - current_word_length: usize, number_of_groups: usize, } @@ -119,7 +123,8 @@ impl<'font> BufferedRender<'font> { impl BufferedRender<'_> { fn input_character(&mut self, character: char) { - self.preprocessor.add_character(self.font, character); + self.preprocessor + .add_character(self.font, character, self.char_render.sprite_width()); self.buffered_chars.push_back(character); } @@ -129,102 +134,164 @@ impl BufferedRender<'_> { ' ' | '\n' => { if let Some(group) = self.char_render.finalise_letter() { self.letters.letters.push(group); - self.letters.current_word_length += 1; self.letters.number_of_groups += 1; } - if self.letters.current_word_length != 0 { - self.letters.word_lengths.push( - self.letters - .current_word_length - .try_into() - .expect("word is too big"), - ); - } - self.letters.current_word_length = 0; + self.letters.number_of_groups += 1; } letter => { if let Some(group) = self.char_render.render_char(self.font, letter) { self.letters.letters.push(group); - self.letters.current_word_length += 1; self.letters.number_of_groups += 1; } } } } +} - #[must_use] - pub fn layout( - &mut self, - area: Rect, - alignment: TextAlignment, - number_of_groups: usize, - paragraph_spacing: i32, - ) -> Vec { - let mut objects = Vec::new(); +pub struct LayoutCache { + objects: Vec, + state: LayoutCacheState, + settings: LayoutSettings, +} - while !self.buffered_chars.is_empty() && self.letters.number_of_groups <= number_of_groups { - self.process(); - } +impl LayoutCache { + fn update_cache(&mut self, number_of_groups: usize, render: &BufferedRender) { + let minimum_space_width = render.font.letter(' ').advance_width as i32; - let minimum_space_width = self.font.letter(' ').advance_width as i32; + let lines = render + .preprocessor + .lines_element(self.settings.area.size.x, minimum_space_width); - let lines = self.preprocessor.lines(area.size.x, minimum_space_width); - let mut head_position = area.position; + 'outer: for (line, line_elements) in lines.skip(self.state.line_depth) { + let settings = self.settings.alignment.settings( + &line, + minimum_space_width, + self.settings.area.size, + ); - let mut processed_depth = 0; - let mut group_depth = 0; - let mut word_depth = 0; - let mut rendered_groups = 0; + if self.state.line_element_depth == 0 { + self.state.head_position.x += settings.start_x; + } - 'outer: for line in lines { - let settings = alignment.settings(&line, minimum_space_width, area.size); - head_position.x += settings.start_x; - - for idx in 0..line.number_of_text_elements() { - let element = self.preprocessor.get(processed_depth + idx); + for element in line_elements.iter().skip(self.state.line_element_depth) { match element { - PreprocessedElement::Word(_) => { - for _ in 0..self - .letters - .word_lengths - .get(word_depth) - .copied() - .unwrap_or(u8::MAX) + PreprocessedElement::Word(word) => { + for letter in (word.sprite_index() + ..(word.sprite_index() + word.number_of_sprites())) + .skip(self.state.word_depth) + .map(|x| &render.letters.letters[x]) { - let letter_group = &self.letters.letters[group_depth]; - let mut object = ObjectUnmanaged::new(letter_group.sprite.clone()); - head_position.x += letter_group.left as i32; - object.set_position(head_position); - head_position.x += letter_group.width as i32; + let mut object = ObjectUnmanaged::new(letter.sprite.clone()); + self.state.head_position.x += letter.left as i32; + object.set_position(self.state.head_position); + self.state.head_position.x += letter.width as i32; object.show(); - objects.push(object); - group_depth += 1; - rendered_groups += 1; - if rendered_groups >= number_of_groups { + self.objects.push(object); + self.state.rendered_groups += 1; + self.state.word_depth += 1; + if self.state.rendered_groups >= number_of_groups { break 'outer; } } - word_depth += 1; + + self.state.word_depth = 0; + self.state.line_element_depth += 1; } PreprocessedElement::WhiteSpace(space_type) => { - if space_type == WhiteSpace::NewLine { - head_position.y += paragraph_spacing; + if *space_type == WhiteSpace::NewLine { + self.state.head_position.y += self.settings.paragraph_spacing; } - head_position.x += settings.space_width; - rendered_groups += 1; - if rendered_groups >= number_of_groups { + self.state.head_position.x += settings.space_width; + self.state.rendered_groups += 1; + self.state.line_element_depth += 1; + if self.state.rendered_groups >= number_of_groups { break 'outer; } } } } - processed_depth += line.number_of_text_elements(); - head_position.x = area.position.x; - head_position.y += 9; + self.state.head_position.y += render.font.line_height(); + self.state.head_position.x = self.settings.area.position.x; + + self.state.line_element_depth = 0; + self.state.line_depth += 1; + } + } + + pub fn update( + &mut self, + r: &mut BufferedRender<'_>, + area: Rect, + alignment: TextAlignment, + paragraph_spacing: i32, + number_of_groups: usize, + ) { + while !r.buffered_chars.is_empty() && r.letters.number_of_groups <= number_of_groups { + r.process(); } - objects + let settings = LayoutSettings { + area, + alignment, + paragraph_spacing, + }; + if settings != self.settings { + self.reset(settings); + } + + self.update_cache(number_of_groups, r); + } + + pub fn commit(&self, oam: &mut OamIterator) { + for (object, slot) in self.objects.iter().zip(oam) { + slot.set(object); + } + } + + #[must_use] + pub fn new() -> Self { + Self { + objects: Vec::new(), + state: Default::default(), + settings: LayoutSettings { + area: Rect::new((0, 0).into(), (0, 0).into()), + alignment: TextAlignment::Right, + paragraph_spacing: -100, + }, + } + } + + fn reset(&mut self, settings: LayoutSettings) { + self.objects.clear(); + self.state = LayoutCacheState { + head_position: settings.area.position, + processed_depth: 0, + group_depth: 0, + word_depth: 0, + rendered_groups: 0, + line_depth: 0, + line_element_depth: 0, + }; + self.settings = settings; } } + +#[derive(PartialEq, Eq)] +struct LayoutSettings { + area: Rect, + alignment: TextAlignment, + paragraph_spacing: i32, +} + +#[derive(Default)] +struct LayoutCacheState { + head_position: Vector2D, + processed_depth: usize, + group_depth: usize, + word_depth: usize, + rendered_groups: usize, + line_depth: usize, + line_element_depth: usize, +} diff --git a/agb/src/display/object/font/preprocess.rs b/agb/src/display/object/font/preprocess.rs index 26023608..2de88e81 100644 --- a/agb/src/display/object/font/preprocess.rs +++ b/agb/src/display/object/font/preprocess.rs @@ -1,3 +1,5 @@ +use core::num::NonZeroU8; + use alloc::vec::Vec; use crate::display::Font; @@ -6,50 +8,53 @@ use super::WhiteSpace; #[derive(Debug, PartialEq, Eq)] pub(crate) enum PreprocessedElement { - Word(u8), + Word(Word), WhiteSpace(WhiteSpace), } +#[test_case] +fn check_size_of_preprocessed_element_is_correct(_: &mut crate::Gba) { + assert_eq!( + core::mem::size_of::(), + core::mem::size_of::() + ); +} + +#[derive(Debug, PartialEq, Eq, Clone, Copy)] +#[repr(align(4))] +pub(crate) struct Word { + pixels: u8, + number_of_sprites: NonZeroU8, + index: u16, +} + +impl Word { + pub fn pixels(self) -> i32 { + self.pixels.into() + } + pub fn number_of_sprites(self) -> usize { + self.number_of_sprites.get().into() + } + pub fn sprite_index(self) -> usize { + self.index.into() + } +} + #[derive(Clone, Copy, PartialEq, Eq)] -struct PreprocessedElementStored(u8); - -impl core::fmt::Debug for PreprocessedElementStored { - fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result { - f.debug_tuple("PreprocessedElementStored") - .field(&self.parse()) - .finish() - } -} - -impl PreprocessedElementStored { - fn parse(self) -> PreprocessedElement { - match self.0 { - 255 => PreprocessedElement::WhiteSpace(WhiteSpace::NewLine), - 254 => PreprocessedElement::WhiteSpace(WhiteSpace::Space), - length => PreprocessedElement::Word(length), - } - } - - fn from_element(x: PreprocessedElement) -> Self { - match x { - PreprocessedElement::Word(length) => PreprocessedElementStored(length), - PreprocessedElement::WhiteSpace(space) => PreprocessedElementStored(match space { - WhiteSpace::NewLine => 255, - WhiteSpace::Space => 254, - }), - } - } -} +pub(crate) struct PreprocessedElementStored(u8); #[derive(Default, Debug)] pub(crate) struct Preprocessed { - widths: Vec, + widths: Vec, preprocessor: Preprocessor, } #[derive(Debug, Default)] struct Preprocessor { current_word_width: i32, + number_of_sprites: usize, + width_in_sprite: i32, + total_number_of_sprites: usize, } impl Preprocessor { @@ -57,27 +62,44 @@ impl Preprocessor { &mut self, font: &Font, character: char, - widths: &mut Vec, + sprite_width: i32, + widths: &mut Vec, ) { match character { space @ (' ' | '\n') => { if self.current_word_width != 0 { - widths.push(PreprocessedElementStored::from_element( - PreprocessedElement::Word( - self.current_word_width - .try_into() - .expect("word should be small and positive"), - ), - )); + self.number_of_sprites += 1; + self.total_number_of_sprites += 1; + widths.push(PreprocessedElement::Word(Word { + pixels: self.current_word_width.try_into().expect("word too wide"), + number_of_sprites: NonZeroU8::new( + self.number_of_sprites.try_into().expect("word too wide"), + ) + .unwrap(), + index: (self.total_number_of_sprites - self.number_of_sprites) + .try_into() + .expect("out of range"), + })); self.current_word_width = 0; + self.number_of_sprites = 0; + self.width_in_sprite = 0; } - widths.push(PreprocessedElementStored::from_element( - PreprocessedElement::WhiteSpace(WhiteSpace::from_char(space)), - )); + widths.push(PreprocessedElement::WhiteSpace(WhiteSpace::from_char( + space, + ))); } letter => { let letter = font.letter(letter); self.current_word_width += letter.advance_width as i32 + letter.xmin as i32; + if self.width_in_sprite + letter.width as i32 > sprite_width { + self.number_of_sprites += 1; + self.total_number_of_sprites += 1; + self.width_in_sprite = 0; + } + if self.width_in_sprite != 0 { + self.width_in_sprite += letter.xmin as i32; + } + self.width_in_sprite += letter.advance_width as i32; } } } @@ -86,7 +108,7 @@ impl Preprocessor { pub(crate) struct Lines<'preprocess> { minimum_space_width: i32, layout_width: i32, - data: &'preprocess [PreprocessedElementStored], + data: &'preprocess [PreprocessedElement], current_start_idx: usize, } @@ -131,11 +153,11 @@ impl<'pre> Iterator for Lines<'pre> { let mut number_of_words = 0; while let Some(next) = self.data.get(self.current_start_idx + line_idx_length) { - match next.parse() { - PreprocessedElement::Word(pixels) => { + match next { + PreprocessedElement::Word(word) => { let additional_space_width = additional_space_count as i32 * self.minimum_space_width; - let width = pixels as i32; + let width = word.pixels(); if width + current_line_width + additional_space_width > self.layout_width { break; } @@ -173,12 +195,9 @@ impl Preprocessed { Default::default() } - pub(crate) fn get(&self, idx: usize) -> PreprocessedElement { - self.widths[idx].parse() - } - - pub(crate) fn add_character(&mut self, font: &Font, c: char) { - self.preprocessor.add_character(font, c, &mut self.widths); + pub(crate) fn add_character(&mut self, font: &Font, c: char, sprite_width: i32) { + self.preprocessor + .add_character(font, c, sprite_width, &mut self.widths); } pub(crate) fn lines(&self, layout_width: i32, minimum_space_width: i32) -> Lines<'_> { @@ -190,17 +209,17 @@ impl Preprocessed { } } - fn lines_element( + pub(crate) fn lines_element( &self, layout_width: i32, minimum_space_width: i32, - ) -> impl Iterator { + ) -> impl Iterator { let mut idx = 0; self.lines(layout_width, minimum_space_width).map(move |x| { let length = x.number_of_text_elements; let d = &self.widths[idx..(idx + length)]; idx += length; - d + (x, d) }) } } diff --git a/agb/src/display/object/font/renderer.rs b/agb/src/display/object/font/renderer.rs index 1aa3b0e0..f364d07c 100644 --- a/agb/src/display/object/font/renderer.rs +++ b/agb/src/display/object/font/renderer.rs @@ -50,6 +50,10 @@ pub(crate) struct WordRender { } impl WordRender { + pub(crate) fn sprite_width(&self) -> i32 { + self.config.sprite_size.to_width_height().0 as i32 + } + #[must_use] pub(crate) fn new(config: Configuration) -> Self { WordRender { From 93024f6bab7fd747b5fdb8107caef99ddf678bbd Mon Sep 17 00:00:00 2001 From: Corwin Date: Thu, 29 Jun 2023 20:04:27 +0100 Subject: [PATCH 11/24] remove some unused bits --- agb/src/display/object/font.rs | 12 +----------- 1 file changed, 1 insertion(+), 11 deletions(-) diff --git a/agb/src/display/object/font.rs b/agb/src/display/object/font.rs index 7bfd5c96..0a52804f 100644 --- a/agb/src/display/object/font.rs +++ b/agb/src/display/object/font.rs @@ -10,7 +10,7 @@ use self::{ renderer::{Configuration, WordRender}, }; -use super::{DynamicSprite, OamIterator, ObjectUnmanaged, PaletteVram, Size, SpriteVram}; +use super::{OamIterator, ObjectUnmanaged, PaletteVram, Size, SpriteVram}; mod preprocess; mod renderer; @@ -48,12 +48,6 @@ pub struct BufferedRender<'font> { font: &'font Font, } -#[derive(Debug)] -struct Word { - index: usize, - length: usize, -} - #[derive(Debug, Default)] struct Letters { letters: Vec, @@ -267,8 +261,6 @@ impl LayoutCache { self.objects.clear(); self.state = LayoutCacheState { head_position: settings.area.position, - processed_depth: 0, - group_depth: 0, word_depth: 0, rendered_groups: 0, line_depth: 0, @@ -288,8 +280,6 @@ struct LayoutSettings { #[derive(Default)] struct LayoutCacheState { head_position: Vector2D, - processed_depth: usize, - group_depth: usize, word_depth: usize, rendered_groups: usize, line_depth: usize, From ec3003c81d0a3452480ce800c50243769809b975 Mon Sep 17 00:00:00 2001 From: Corwin Date: Sat, 1 Jul 2023 19:12:39 +0100 Subject: [PATCH 12/24] garbage renderer --- agb/examples/object_text_render.rs | 59 ++--- agb/src/display/object/font.rs | 280 +++++++++++++++++----- agb/src/display/object/font/preprocess.rs | 38 ++- 3 files changed, 264 insertions(+), 113 deletions(-) diff --git a/agb/examples/object_text_render.rs b/agb/examples/object_text_render.rs index c8312760..aab2ff6e 100644 --- a/agb/examples/object_text_render.rs +++ b/agb/examples/object_text_render.rs @@ -4,7 +4,7 @@ use agb::{ display::{ object::{ - font::{BufferedRender, LayoutCache, TextAlignment}, + font::{ObjectTextRender, TextAlignment}, PaletteVram, Size, }, palette16::Palette16, @@ -35,7 +35,7 @@ fn main(mut gba: agb::Gba) -> ! { let palette = Palette16::new(palette); let palette = PaletteVram::new(&palette).unwrap(); - let mut wr = BufferedRender::new(&FONT, Size::S16x8, palette); + let mut wr = ObjectTextRender::new(&FONT, Size::S16x8, palette); let _ = writeln!( wr, "{}", @@ -52,55 +52,30 @@ fn main(mut gba: agb::Gba) -> ! { timer.set_enabled(true); timer.set_divider(agb::timer::Divider::Divider256); - let mut num_letters = 0; - - let mut alignment = TextAlignment::Left; - - let mut cache = LayoutCache::new(); + wr.set_alignment(TextAlignment::Left); + wr.set_size((WIDTH / 3, 20).into()); + wr.set_paragraph_spacing(2); + wr.layout(); loop { vblank.wait_for_vblank(); input.update(); let oam = &mut unmanaged.iter(); - cache.commit(oam); + wr.commit(oam, (WIDTH / 3, 0).into()); let start = timer.value(); - wr.process(); - cache.update( - &mut wr, - Rect::new((WIDTH / 3, 0).into(), (WIDTH / 3, 100).into()), - alignment, - 2, - num_letters, - ); + let line_done = !wr.next_letter_group(); + if line_done && input.is_just_pressed(Button::A) { + wr.pop_line(); + } + wr.layout(); let end = timer.value(); - agb::println!("Took {} cycles", 256 * (end.wrapping_sub(start) as u32)); - - if input.is_just_pressed(Button::LEFT) { - alignment = TextAlignment::Left; - } - if input.is_just_pressed(Button::RIGHT) { - alignment = TextAlignment::Right; - } - if input.is_just_pressed(Button::UP | Button::DOWN) { - alignment = TextAlignment::Center; - } - - num_letters += 1; - - if input.is_just_pressed(Button::A) { - break; - } + agb::println!( + "Took {} cycles, line done {}", + 256 * (end.wrapping_sub(start) as u32), + line_done + ); } - let start = timer.value(); - drop(wr); - let oam = unmanaged.iter(); - drop(oam); - let end = timer.value(); - agb::println!( - "Drop took {} cycles", - 256 * (end.wrapping_sub(start) as u32) - ); } } diff --git a/agb/src/display/object/font.rs b/agb/src/display/object/font.rs index 0a52804f..4fd38686 100644 --- a/agb/src/display/object/font.rs +++ b/agb/src/display/object/font.rs @@ -3,7 +3,7 @@ use core::fmt::Write; use agb_fixnum::{Rect, Vector2D}; use alloc::{collections::VecDeque, vec::Vec}; -use crate::display::Font; +use crate::display::{object::font::preprocess::Word, Font}; use self::{ preprocess::{Line, Preprocessed, PreprocessedElement}, @@ -15,7 +15,7 @@ use super::{OamIterator, ObjectUnmanaged, PaletteVram, Size, SpriteVram}; mod preprocess; mod renderer; -#[derive(Debug, PartialEq, Eq)] +#[derive(Debug, PartialEq, Eq, Clone, Copy)] #[non_exhaustive] pub(crate) enum WhiteSpace { NewLine, @@ -50,23 +50,14 @@ pub struct BufferedRender<'font> { #[derive(Debug, Default)] struct Letters { - letters: Vec, + letters: VecDeque, number_of_groups: usize, } -impl Write for BufferedRender<'_> { - fn write_str(&mut self, s: &str) -> core::fmt::Result { - for c in s.chars() { - self.input_character(c); - } - - Ok(()) - } -} - -#[derive(Debug, Clone, Copy, PartialEq, Eq)] +#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)] #[non_exhaustive] pub enum TextAlignment { + #[default] Left, Right, Center, @@ -103,7 +94,7 @@ impl TextAlignment { impl<'font> BufferedRender<'font> { #[must_use] - pub fn new(font: &'font Font, sprite_size: Size, palette: PaletteVram) -> Self { + fn new(font: &'font Font, sprite_size: Size, palette: PaletteVram) -> Self { let config = Configuration::new(sprite_size, palette); BufferedRender { char_render: WordRender::new(config), @@ -115,19 +106,25 @@ impl<'font> BufferedRender<'font> { } } +fn is_private_use(c: char) -> bool { + ('\u{E000}'..'\u{F8FF}').contains(&c) +} + impl BufferedRender<'_> { fn input_character(&mut self, character: char) { - self.preprocessor - .add_character(self.font, character, self.char_render.sprite_width()); + if !is_private_use(character) { + self.preprocessor + .add_character(self.font, character, self.char_render.sprite_width()); + } self.buffered_chars.push_back(character); } - pub fn process(&mut self) { + fn process(&mut self) { let Some(c) = self.buffered_chars.pop_front() else { return }; match c { ' ' | '\n' => { if let Some(group) = self.char_render.finalise_letter() { - self.letters.letters.push(group); + self.letters.letters.push_back(group); self.letters.number_of_groups += 1; } @@ -135,7 +132,7 @@ impl BufferedRender<'_> { } letter => { if let Some(group) = self.char_render.render_char(self.font, letter) { - self.letters.letters.push(group); + self.letters.letters.push_back(group); self.letters.number_of_groups += 1; } } @@ -143,32 +140,173 @@ impl BufferedRender<'_> { } } -pub struct LayoutCache { - objects: Vec, - state: LayoutCacheState, +pub struct ObjectTextRender<'font> { + buffer: BufferedRender<'font>, + layout: LayoutCache, settings: LayoutSettings, } +impl<'font> ObjectTextRender<'font> { + #[must_use] + pub fn new(font: &'font Font, sprite_size: Size, palette: PaletteVram) -> Self { + Self { + buffer: BufferedRender::new(font, sprite_size, palette), + layout: LayoutCache::new(), + settings: Default::default(), + } + } +} + +impl Write for ObjectTextRender<'_> { + fn write_str(&mut self, s: &str) -> core::fmt::Result { + for c in s.chars() { + self.buffer.input_character(c); + } + + Ok(()) + } +} + +impl ObjectTextRender<'_> { + /// Remove a line from the render and shift everything up one line. + /// A full complete line must be rendered for this to do anything, incomplete lines won't be popped. Returns whether a line could be popped. + pub fn pop_line(&mut self) -> bool { + let width = self.layout.settings.area.x; + let space = self.buffer.font.letter(' ').advance_width as i32; + let Some(line) = self.buffer.preprocessor.lines(width, space).next() else { + return false; + }; + + let number_of_elements = line.number_of_letter_groups(); + if self.layout.state.line_depth >= 1 && self.layout.objects.len() >= number_of_elements { + for _ in 0..number_of_elements { + // self.buffer.letters.letters.pop_front(); + self.layout.objects.pop_front(); + } + self.buffer.preprocessor.pop(&line); + + self.layout.state.head_offset.y -= self.buffer.font.line_height(); + for obj in self.layout.objects.iter_mut() { + obj.offset.y -= self.buffer.font.line_height() as i16; + let object_offset = obj.offset.change_base(); + obj.object + .set_position(self.layout.position + object_offset); + } + + self.layout.state.line_depth -= 1; + + true + } else { + false + } + } + + /// On next update, the next unit of letters will be rendered. Returns whether the next element could be added. + /// Can only be called once per layout. + pub fn next_letter_group(&mut self) -> bool { + self.layout.next_letter_group(&self.buffer) + } + /// Commits work already done to screen. You can commit to multiple places in the same frame. + pub fn commit(&mut self, oam: &mut OamIterator, position: Vector2D) { + self.layout.commit(oam, position); + } + /// Updates the internal state based on the chosen render settings. Best + /// effort is made to reuse previous layouts, but a full rerender may be + /// required if certain settings are changed. + pub fn layout(&mut self) { + self.layout.update( + &mut self.buffer, + self.settings.area, + self.settings.alignment, + self.settings.paragraph_spacing, + ); + } + /// Causes a change to the area that text is rendered. This will cause a relayout. + pub fn set_size(&mut self, size: Vector2D) { + self.settings.area = size; + } + /// Causes a change to the text alignment. This will cause a relayout. + pub fn set_alignment(&mut self, alignment: TextAlignment) { + self.settings.alignment = alignment; + } + /// Sets the paragraph spacing. This will cause a relayout. + pub fn set_paragraph_spacing(&mut self, paragraph_spacing: i32) { + self.settings.paragraph_spacing = paragraph_spacing; + } +} + +struct LayoutObject { + object: ObjectUnmanaged, + offset: Vector2D, +} + +struct LayoutCache { + objects: VecDeque, + state: LayoutCacheState, + settings: LayoutSettings, + desired_number_of_groups: usize, + position: Vector2D, +} + impl LayoutCache { - fn update_cache(&mut self, number_of_groups: usize, render: &BufferedRender) { + fn next_letter_group(&mut self, buffer: &BufferedRender) -> bool { + let width = self.settings.area.x; + let space = buffer.font.letter(' ').advance_width as i32; + let line_height = buffer.font.line_height(); + + if self.state.head_offset.y + line_height > self.settings.area.y { + return false; + } + + if let Some((_line, mut line_elements)) = buffer + .preprocessor + .lines_element(width, space) + .nth(self.state.line_depth) + { + match line_elements.nth(self.state.line_element_depth) { + Some(PreprocessedElement::Word(_)) => { + self.desired_number_of_groups += 1; + } + Some(PreprocessedElement::WhiteSpace(WhiteSpace::Space)) => { + self.desired_number_of_groups += 1; + } + Some(PreprocessedElement::WhiteSpace(WhiteSpace::NewLine)) => { + self.desired_number_of_groups += 1; + } + None => { + if self.state.head_offset.y + line_height * 2 > self.settings.area.y { + return false; + } + self.desired_number_of_groups += 1; + } + } + } + + true + } + + fn update_cache(&mut self, render: &BufferedRender) { + if self.state.rendered_groups >= self.desired_number_of_groups { + return; + } + let minimum_space_width = render.font.letter(' ').advance_width as i32; let lines = render .preprocessor - .lines_element(self.settings.area.size.x, minimum_space_width); + .lines_element(self.settings.area.x, minimum_space_width); 'outer: for (line, line_elements) in lines.skip(self.state.line_depth) { - let settings = self.settings.alignment.settings( - &line, - minimum_space_width, - self.settings.area.size, - ); + let settings = + self.settings + .alignment + .settings(&line, minimum_space_width, self.settings.area); if self.state.line_element_depth == 0 { - self.state.head_position.x += settings.start_x; + self.state.head_offset.x += settings.start_x; } - for element in line_elements.iter().skip(self.state.line_element_depth) { + for element in line_elements.skip(self.state.line_element_depth) { match element { PreprocessedElement::Word(word) => { for letter in (word.sprite_index() @@ -177,14 +315,24 @@ impl LayoutCache { .map(|x| &render.letters.letters[x]) { let mut object = ObjectUnmanaged::new(letter.sprite.clone()); - self.state.head_position.x += letter.left as i32; - object.set_position(self.state.head_position); - self.state.head_position.x += letter.width as i32; - object.show(); - self.objects.push(object); + self.state.head_offset.x += letter.left as i32; + object + .set_position(self.state.head_offset + self.position) + .show(); + + let layout_object = LayoutObject { + object, + offset: ( + self.state.head_offset.x as i16, + self.state.head_offset.y as i16, + ) + .into(), + }; + self.state.head_offset.x += letter.width as i32; + self.objects.push_back(layout_object); self.state.rendered_groups += 1; self.state.word_depth += 1; - if self.state.rendered_groups >= number_of_groups { + if self.state.rendered_groups >= self.desired_number_of_groups { break 'outer; } } @@ -193,36 +341,39 @@ impl LayoutCache { self.state.line_element_depth += 1; } PreprocessedElement::WhiteSpace(space_type) => { - if *space_type == WhiteSpace::NewLine { - self.state.head_position.y += self.settings.paragraph_spacing; + if space_type == WhiteSpace::NewLine { + self.state.head_offset.y += self.settings.paragraph_spacing; } - self.state.head_position.x += settings.space_width; + self.state.head_offset.x += settings.space_width; self.state.rendered_groups += 1; self.state.line_element_depth += 1; - if self.state.rendered_groups >= number_of_groups { + if self.state.rendered_groups >= self.desired_number_of_groups { break 'outer; } } } } - self.state.head_position.y += render.font.line_height(); - self.state.head_position.x = self.settings.area.position.x; + self.state.head_offset.y += render.font.line_height(); + self.state.head_offset.x = 0; self.state.line_element_depth = 0; self.state.line_depth += 1; } } - pub fn update( + fn update( &mut self, r: &mut BufferedRender<'_>, - area: Rect, + area: Vector2D, alignment: TextAlignment, paragraph_spacing: i32, - number_of_groups: usize, ) { - while !r.buffered_chars.is_empty() && r.letters.number_of_groups <= number_of_groups { + r.process(); + + while !r.buffered_chars.is_empty() + && r.letters.number_of_groups <= self.desired_number_of_groups + { r.process(); } @@ -235,32 +386,43 @@ impl LayoutCache { self.reset(settings); } - self.update_cache(number_of_groups, r); + self.update_cache(r); } - pub fn commit(&self, oam: &mut OamIterator) { - for (object, slot) in self.objects.iter().zip(oam) { - slot.set(object); + fn commit(&mut self, oam: &mut OamIterator, position: Vector2D) { + if self.position != position { + for (object, slot) in self.objects.iter_mut().zip(oam) { + let object_offset = object.offset.change_base(); + object.object.set_position(position + object_offset); + slot.set(&object.object); + } + self.position = position; + } else { + for (object, slot) in self.objects.iter().zip(oam) { + slot.set(&object.object); + } } } #[must_use] - pub fn new() -> Self { + fn new() -> Self { Self { - objects: Vec::new(), + objects: VecDeque::new(), state: Default::default(), settings: LayoutSettings { - area: Rect::new((0, 0).into(), (0, 0).into()), + area: (0, 0).into(), alignment: TextAlignment::Right, paragraph_spacing: -100, }, + desired_number_of_groups: 0, + position: (0, 0).into(), } } fn reset(&mut self, settings: LayoutSettings) { self.objects.clear(); self.state = LayoutCacheState { - head_position: settings.area.position, + head_offset: (0, 0).into(), word_depth: 0, rendered_groups: 0, line_depth: 0, @@ -270,16 +432,16 @@ impl LayoutCache { } } -#[derive(PartialEq, Eq)] +#[derive(PartialEq, Eq, Default)] struct LayoutSettings { - area: Rect, + area: Vector2D, alignment: TextAlignment, paragraph_spacing: i32, } #[derive(Default)] struct LayoutCacheState { - head_position: Vector2D, + head_offset: Vector2D, word_depth: usize, rendered_groups: usize, line_depth: usize, diff --git a/agb/src/display/object/font/preprocess.rs b/agb/src/display/object/font/preprocess.rs index 2de88e81..f5ab2fbb 100644 --- a/agb/src/display/object/font/preprocess.rs +++ b/agb/src/display/object/font/preprocess.rs @@ -1,12 +1,12 @@ use core::num::NonZeroU8; -use alloc::vec::Vec; +use alloc::{collections::VecDeque, vec::Vec}; use crate::display::Font; use super::WhiteSpace; -#[derive(Debug, PartialEq, Eq)] +#[derive(Debug, PartialEq, Eq, Clone, Copy)] pub(crate) enum PreprocessedElement { Word(Word), WhiteSpace(WhiteSpace), @@ -40,12 +40,9 @@ impl Word { } } -#[derive(Clone, Copy, PartialEq, Eq)] -pub(crate) struct PreprocessedElementStored(u8); - #[derive(Default, Debug)] pub(crate) struct Preprocessed { - widths: Vec, + widths: VecDeque, preprocessor: Preprocessor, } @@ -63,14 +60,14 @@ impl Preprocessor { font: &Font, character: char, sprite_width: i32, - widths: &mut Vec, + widths: &mut VecDeque, ) { match character { space @ (' ' | '\n') => { if self.current_word_width != 0 { self.number_of_sprites += 1; self.total_number_of_sprites += 1; - widths.push(PreprocessedElement::Word(Word { + widths.push_back(PreprocessedElement::Word(Word { pixels: self.current_word_width.try_into().expect("word too wide"), number_of_sprites: NonZeroU8::new( self.number_of_sprites.try_into().expect("word too wide"), @@ -84,7 +81,7 @@ impl Preprocessor { self.number_of_sprites = 0; self.width_in_sprite = 0; } - widths.push(PreprocessedElement::WhiteSpace(WhiteSpace::from_char( + widths.push_back(PreprocessedElement::WhiteSpace(WhiteSpace::from_char( space, ))); } @@ -108,7 +105,7 @@ impl Preprocessor { pub(crate) struct Lines<'preprocess> { minimum_space_width: i32, layout_width: i32, - data: &'preprocess [PreprocessedElement], + data: &'preprocess VecDeque, current_start_idx: usize, } @@ -117,6 +114,7 @@ pub(crate) struct Line { number_of_text_elements: usize, number_of_spaces: usize, number_of_words: usize, + number_of_letter_groups: usize, } impl Line { @@ -136,6 +134,11 @@ impl Line { pub(crate) fn number_of_words(&self) -> usize { self.number_of_words } + + #[inline(always)] + pub(crate) fn number_of_letter_groups(&self) -> usize { + self.number_of_letter_groups + } } impl<'pre> Iterator for Lines<'pre> { @@ -151,6 +154,7 @@ impl<'pre> Iterator for Lines<'pre> { let mut additional_space_count = 0; let mut number_of_spaces = 0; let mut number_of_words = 0; + let mut number_of_letter_groups = 0; while let Some(next) = self.data.get(self.current_start_idx + line_idx_length) { match next { @@ -161,6 +165,7 @@ impl<'pre> Iterator for Lines<'pre> { if width + current_line_width + additional_space_width > self.layout_width { break; } + number_of_letter_groups += word.number_of_sprites.get() as usize; number_of_words += 1; current_line_width += width + additional_space_width; number_of_spaces += additional_space_count; @@ -186,6 +191,7 @@ impl<'pre> Iterator for Lines<'pre> { number_of_text_elements: line_idx_length, number_of_spaces, number_of_words, + number_of_letter_groups, }) } } @@ -200,6 +206,13 @@ impl Preprocessed { .add_character(font, c, sprite_width, &mut self.widths); } + pub(crate) fn pop(&mut self, line: &Line) { + let elements = line.number_of_text_elements(); + for _ in 0..elements { + self.widths.pop_front(); + } + } + pub(crate) fn lines(&self, layout_width: i32, minimum_space_width: i32) -> Lines<'_> { Lines { minimum_space_width, @@ -213,11 +226,12 @@ impl Preprocessed { &self, layout_width: i32, minimum_space_width: i32, - ) -> impl Iterator { + ) -> impl Iterator + '_)> { let mut idx = 0; self.lines(layout_width, minimum_space_width).map(move |x| { let length = x.number_of_text_elements; - let d = &self.widths[idx..(idx + length)]; + + let d = self.widths.range(idx..(idx + length)).copied(); idx += length; (x, d) }) From 5f120407529f7efef495ac35b7829a28a2afadd7 Mon Sep 17 00:00:00 2001 From: Corwin Date: Sat, 1 Jul 2023 23:36:58 +0100 Subject: [PATCH 13/24] the proper nice okay working text rendering --- agb-image-converter/src/font_loader.rs | 50 ++- agb/examples/object_text_render.rs | 36 +- agb/src/display/object/font.rs | 497 ++++++++++------------ agb/src/display/object/font/preprocess.rs | 163 +++---- agb/src/display/object/font/renderer.rs | 24 +- 5 files changed, 362 insertions(+), 408 deletions(-) diff --git a/agb-image-converter/src/font_loader.rs b/agb-image-converter/src/font_loader.rs index 2c61f664..2e59c83f 100644 --- a/agb-image-converter/src/font_loader.rs +++ b/agb-image-converter/src/font_loader.rs @@ -25,9 +25,9 @@ pub fn load_font(font_data: &[u8], pixels_per_em: f32) -> TokenStream { let line_metrics = font.horizontal_line_metrics(pixels_per_em).unwrap(); let line_height = line_metrics.new_line_size as i32; - let ascent = line_metrics.ascent as i32; + let mut ascent = line_metrics.ascent as i32; - let font = (0..128) + let letters: Vec<_> = (0..128) .map(|i| font.rasterize(char::from_u32(i).unwrap(), pixels_per_em)) .map(|(metrics, bitmap)| { let width = metrics.width; @@ -56,25 +56,37 @@ pub fn load_font(font_data: &[u8], pixels_per_em: f32) -> TokenStream { advance_width: metrics.advance_width, } }) - .map(|letter_data| { - let data_raw = ByteString(&letter_data.rendered); - let height = letter_data.height as u8; - let width = letter_data.width as u8; - let xmin = letter_data.xmin as i8; - let ymin = letter_data.ymin as i8; - let advance_width = letter_data.advance_width.ceil() as u8; + .collect(); - quote!( - display::FontLetter::new( - #width, - #height, - #data_raw, - #xmin, - #ymin, - #advance_width, - ) + let maximum_above_line = letters + .iter() + .map(|x| (x.height as i32 + x.ymin)) + .max() + .unwrap(); + + if (ascent - maximum_above_line) < 0 { + ascent = maximum_above_line; + } + + let font = letters.iter().map(|letter_data| { + let data_raw = ByteString(&letter_data.rendered); + let height = letter_data.height as u8; + let width = letter_data.width as u8; + let xmin = letter_data.xmin as i8; + let ymin = letter_data.ymin as i8; + let advance_width = letter_data.advance_width.ceil() as u8; + + quote!( + display::FontLetter::new( + #width, + #height, + #data_raw, + #xmin, + #ymin, + #advance_width, ) - }); + ) + }); quote![ display::Font::new(&[#(#font),*], #line_height, #ascent) diff --git a/agb/examples/object_text_render.rs b/agb/examples/object_text_render.rs index aab2ff6e..8bc1729f 100644 --- a/agb/examples/object_text_render.rs +++ b/agb/examples/object_text_render.rs @@ -8,19 +8,17 @@ use agb::{ PaletteVram, Size, }, palette16::Palette16, - Font, WIDTH, + Font, HEIGHT, WIDTH, }, include_font, input::Button, }; -use agb_fixnum::Rect; extern crate alloc; -use alloc::vec::Vec; use core::fmt::Write; -const FONT: Font = include_font!("examples/font/pixelated.ttf", 8); +const FONT: Font = include_font!("examples/font/yoster.ttf", 12); #[agb::entry] fn entry(gba: agb::Gba) -> ! { main(gba); @@ -35,12 +33,11 @@ fn main(mut gba: agb::Gba) -> ! { let palette = Palette16::new(palette); let palette = PaletteVram::new(&palette).unwrap(); - let mut wr = ObjectTextRender::new(&FONT, Size::S16x8, palette); + let mut wr = ObjectTextRender::new(&FONT, Size::S16x16, palette); let _ = writeln!( wr, - "{}", - "counts for three shoot dice for damage calculation\nmalfunctions all dice after use" - .to_ascii_uppercase() + "Woah! Hey there! I have a bunch of text I want to show you. However, you will find that the amount of text I can display is limited. Who'd have thought! Good thing that my text system supports scrolling! It only took around 20 jank versions to get here!" + ); let vblank = agb::interrupt::VBlank::get(); @@ -52,25 +49,32 @@ fn main(mut gba: agb::Gba) -> ! { timer.set_enabled(true); timer.set_divider(agb::timer::Divider::Divider256); - wr.set_alignment(TextAlignment::Left); - wr.set_size((WIDTH / 3, 20).into()); - wr.set_paragraph_spacing(2); - wr.layout(); + wr.layout((WIDTH, 40).into(), TextAlignment::Left, 2); + + let mut line_done = false; + let mut frame = 0; loop { vblank.wait_for_vblank(); input.update(); let oam = &mut unmanaged.iter(); - wr.commit(oam, (WIDTH / 3, 0).into()); + wr.commit(oam); let start = timer.value(); - let line_done = !wr.next_letter_group(); - if line_done && input.is_just_pressed(Button::A) { + if frame % 4 == 0 { + line_done = !wr.next_letter_group(); + } + if line_done + && input.is_just_pressed(Button::A) + { + line_done = false; wr.pop_line(); } - wr.layout(); + wr.update((0, HEIGHT - 40).into()); let end = timer.value(); + frame += 1; + agb::println!( "Took {} cycles, line done {}", 256 * (end.wrapping_sub(start) as u32), diff --git a/agb/src/display/object/font.rs b/agb/src/display/object/font.rs index 4fd38686..9cea9054 100644 --- a/agb/src/display/object/font.rs +++ b/agb/src/display/object/font.rs @@ -1,9 +1,9 @@ use core::fmt::Write; -use agb_fixnum::{Rect, Vector2D}; +use agb_fixnum::Vector2D; use alloc::{collections::VecDeque, vec::Vec}; -use crate::display::{object::font::preprocess::Word, Font}; +use crate::display::Font; use self::{ preprocess::{Line, Preprocessed, PreprocessedElement}, @@ -32,14 +32,6 @@ impl WhiteSpace { } } -#[derive(Debug)] -pub(crate) struct LetterGroup { - sprite: SpriteVram, - // the width of the letter group - width: u16, - left: i16, -} - pub struct BufferedRender<'font> { char_render: WordRender, preprocessor: Preprocessed, @@ -50,7 +42,7 @@ pub struct BufferedRender<'font> { #[derive(Debug, Default)] struct Letters { - letters: VecDeque, + letters: VecDeque, number_of_groups: usize, } @@ -69,12 +61,7 @@ struct TextAlignmentSettings { } impl TextAlignment { - fn settings( - self, - line: &Line, - minimum_space_width: i32, - size: Vector2D, - ) -> TextAlignmentSettings { + fn settings(self, line: &Line, minimum_space_width: i32, width: i32) -> TextAlignmentSettings { match self { TextAlignment::Left => TextAlignmentSettings { space_width: minimum_space_width, @@ -82,11 +69,11 @@ impl TextAlignment { }, TextAlignment::Right => TextAlignmentSettings { space_width: minimum_space_width, - start_x: size.x - line.width(), + start_x: width - line.width(), }, TextAlignment::Center => TextAlignmentSettings { space_width: minimum_space_width, - start_x: (size.x - line.width()) / 2, + start_x: (width - line.width()) / 2, }, } } @@ -143,7 +130,7 @@ impl BufferedRender<'_> { pub struct ObjectTextRender<'font> { buffer: BufferedRender<'font>, layout: LayoutCache, - settings: LayoutSettings, + number_of_objects: usize, } impl<'font> ObjectTextRender<'font> { @@ -151,8 +138,14 @@ impl<'font> ObjectTextRender<'font> { pub fn new(font: &'font Font, sprite_size: Size, palette: PaletteVram) -> Self { Self { buffer: BufferedRender::new(font, sprite_size, palette), - layout: LayoutCache::new(), - settings: Default::default(), + number_of_objects: 0, + layout: LayoutCache { + positions: VecDeque::new(), + line_capacity: VecDeque::new(), + objects: Vec::new(), + objects_are_at_origin: (0, 0).into(), + area: (0, 0).into(), + }, } } } @@ -168,267 +161,222 @@ impl Write for ObjectTextRender<'_> { } impl ObjectTextRender<'_> { - /// Remove a line from the render and shift everything up one line. - /// A full complete line must be rendered for this to do anything, incomplete lines won't be popped. Returns whether a line could be popped. - pub fn pop_line(&mut self) -> bool { - let width = self.layout.settings.area.x; - let space = self.buffer.font.letter(' ').advance_width as i32; - let Some(line) = self.buffer.preprocessor.lines(width, space).next() else { - return false; - }; - - let number_of_elements = line.number_of_letter_groups(); - if self.layout.state.line_depth >= 1 && self.layout.objects.len() >= number_of_elements { - for _ in 0..number_of_elements { - // self.buffer.letters.letters.pop_front(); - self.layout.objects.pop_front(); - } - self.buffer.preprocessor.pop(&line); - - self.layout.state.head_offset.y -= self.buffer.font.line_height(); - for obj in self.layout.objects.iter_mut() { - obj.offset.y -= self.buffer.font.line_height() as i16; - let object_offset = obj.offset.change_base(); - obj.object - .set_position(self.layout.position + object_offset); - } - - self.layout.state.line_depth -= 1; - - true - } else { - false - } - } - - /// On next update, the next unit of letters will be rendered. Returns whether the next element could be added. - /// Can only be called once per layout. - pub fn next_letter_group(&mut self) -> bool { - self.layout.next_letter_group(&self.buffer) - } /// Commits work already done to screen. You can commit to multiple places in the same frame. - pub fn commit(&mut self, oam: &mut OamIterator, position: Vector2D) { - self.layout.commit(oam, position); - } - /// Updates the internal state based on the chosen render settings. Best - /// effort is made to reuse previous layouts, but a full rerender may be - /// required if certain settings are changed. - pub fn layout(&mut self) { - self.layout.update( - &mut self.buffer, - self.settings.area, - self.settings.alignment, - self.settings.paragraph_spacing, - ); - } - /// Causes a change to the area that text is rendered. This will cause a relayout. - pub fn set_size(&mut self, size: Vector2D) { - self.settings.area = size; - } - /// Causes a change to the text alignment. This will cause a relayout. - pub fn set_alignment(&mut self, alignment: TextAlignment) { - self.settings.alignment = alignment; - } - /// Sets the paragraph spacing. This will cause a relayout. - pub fn set_paragraph_spacing(&mut self, paragraph_spacing: i32) { - self.settings.paragraph_spacing = paragraph_spacing; - } -} - -struct LayoutObject { - object: ObjectUnmanaged, - offset: Vector2D, -} - -struct LayoutCache { - objects: VecDeque, - state: LayoutCacheState, - settings: LayoutSettings, - desired_number_of_groups: usize, - position: Vector2D, -} - -impl LayoutCache { - fn next_letter_group(&mut self, buffer: &BufferedRender) -> bool { - let width = self.settings.area.x; - let space = buffer.font.letter(' ').advance_width as i32; - let line_height = buffer.font.line_height(); - - if self.state.head_offset.y + line_height > self.settings.area.y { - return false; - } - - if let Some((_line, mut line_elements)) = buffer - .preprocessor - .lines_element(width, space) - .nth(self.state.line_depth) - { - match line_elements.nth(self.state.line_element_depth) { - Some(PreprocessedElement::Word(_)) => { - self.desired_number_of_groups += 1; - } - Some(PreprocessedElement::WhiteSpace(WhiteSpace::Space)) => { - self.desired_number_of_groups += 1; - } - Some(PreprocessedElement::WhiteSpace(WhiteSpace::NewLine)) => { - self.desired_number_of_groups += 1; - } - None => { - if self.state.head_offset.y + line_height * 2 > self.settings.area.y { - return false; - } - self.desired_number_of_groups += 1; - } - } - } - - true - } - - fn update_cache(&mut self, render: &BufferedRender) { - if self.state.rendered_groups >= self.desired_number_of_groups { - return; - } - - let minimum_space_width = render.font.letter(' ').advance_width as i32; - - let lines = render - .preprocessor - .lines_element(self.settings.area.x, minimum_space_width); - - 'outer: for (line, line_elements) in lines.skip(self.state.line_depth) { - let settings = - self.settings - .alignment - .settings(&line, minimum_space_width, self.settings.area); - - if self.state.line_element_depth == 0 { - self.state.head_offset.x += settings.start_x; - } - - for element in line_elements.skip(self.state.line_element_depth) { - match element { - PreprocessedElement::Word(word) => { - for letter in (word.sprite_index() - ..(word.sprite_index() + word.number_of_sprites())) - .skip(self.state.word_depth) - .map(|x| &render.letters.letters[x]) - { - let mut object = ObjectUnmanaged::new(letter.sprite.clone()); - self.state.head_offset.x += letter.left as i32; - object - .set_position(self.state.head_offset + self.position) - .show(); - - let layout_object = LayoutObject { - object, - offset: ( - self.state.head_offset.x as i16, - self.state.head_offset.y as i16, - ) - .into(), - }; - self.state.head_offset.x += letter.width as i32; - self.objects.push_back(layout_object); - self.state.rendered_groups += 1; - self.state.word_depth += 1; - if self.state.rendered_groups >= self.desired_number_of_groups { - break 'outer; - } - } - - self.state.word_depth = 0; - self.state.line_element_depth += 1; - } - PreprocessedElement::WhiteSpace(space_type) => { - if space_type == WhiteSpace::NewLine { - self.state.head_offset.y += self.settings.paragraph_spacing; - } - self.state.head_offset.x += settings.space_width; - self.state.rendered_groups += 1; - self.state.line_element_depth += 1; - if self.state.rendered_groups >= self.desired_number_of_groups { - break 'outer; - } - } - } - } - - self.state.head_offset.y += render.font.line_height(); - self.state.head_offset.x = 0; - - self.state.line_element_depth = 0; - self.state.line_depth += 1; + pub fn commit(&mut self, oam: &mut OamIterator) { + for (object, slot) in self.layout.objects.iter().zip(oam) { + slot.set(object); } } - fn update( + /// Force a relayout, must be called after writing. + pub fn layout( &mut self, - r: &mut BufferedRender<'_>, area: Vector2D, alignment: TextAlignment, paragraph_spacing: i32, ) { - r.process(); - - while !r.buffered_chars.is_empty() - && r.letters.number_of_groups <= self.desired_number_of_groups - { - r.process(); - } - - let settings = LayoutSettings { - area, - alignment, - paragraph_spacing, - }; - if settings != self.settings { - self.reset(settings); - } - - self.update_cache(r); - } - - fn commit(&mut self, oam: &mut OamIterator, position: Vector2D) { - if self.position != position { - for (object, slot) in self.objects.iter_mut().zip(oam) { - let object_offset = object.offset.change_base(); - object.object.set_position(position + object_offset); - slot.set(&object.object); - } - self.position = position; - } else { - for (object, slot) in self.objects.iter().zip(oam) { - slot.set(&object.object); - } - } - } - - #[must_use] - fn new() -> Self { - Self { - objects: VecDeque::new(), - state: Default::default(), - settings: LayoutSettings { - area: (0, 0).into(), - alignment: TextAlignment::Right, - paragraph_spacing: -100, + self.layout.create_positions( + self.buffer.font, + &self.buffer.preprocessor, + &LayoutSettings { + area, + alignment, + paragraph_spacing, }, - desired_number_of_groups: 0, - position: (0, 0).into(), + ); + } + + /// Removes one complete line. + pub fn pop_line(&mut self) -> bool { + let width = self.layout.area.x; + let space = self.buffer.font.letter(' ').advance_width as i32; + let line_height = self.buffer.font.line_height(); + if let Some(line) = self.buffer.preprocessor.lines(width, space).next() { + // there is a line + if self.layout.objects.len() >= line.number_of_letter_groups() { + // we have enough rendered letter groups to count + self.number_of_objects -= line.number_of_letter_groups(); + for _ in 0..line.number_of_letter_groups() { + self.buffer.letters.letters.pop_front(); + self.layout.positions.pop_front(); + } + self.layout.line_capacity.pop_front(); + self.layout.objects.clear(); + self.buffer.preprocessor.pop(&line); + for position in self.layout.positions.iter_mut() { + position.y -= line_height as i16; + } + return true; + } + } + false + } + + pub fn update(&mut self, position: Vector2D) { + if !self.buffer.buffered_chars.is_empty() + && self.buffer.letters.letters.len() <= self.number_of_objects + 5 + { + self.buffer.process(); + } + + self.layout.update_objects_to_display_at_position( + position, + self.buffer.letters.letters.iter(), + self.number_of_objects, + ); + } + + pub fn next_letter_group(&mut self) -> bool { + if !self.can_render_another_element() { + return false; + } + self.number_of_objects += 1; + self.at_least_n_letter_groups(self.number_of_objects); + + true + } + + fn can_render_another_element(&self) -> bool { + let max_number_of_lines = (self.layout.area.y / self.buffer.font.line_height()) as usize; + + let max_number_of_objects = self + .layout + .line_capacity + .iter() + .take(max_number_of_lines) + .sum::(); + + max_number_of_objects > self.number_of_objects + } + + pub fn next_line(&mut self) -> bool { + let max_number_of_lines = (self.layout.area.y / self.buffer.font.line_height()) as usize; + + // find current line + + for (start, end) in self + .layout + .line_capacity + .iter() + .scan(0, |count, line_size| { + let start = *count; + *count += line_size; + Some((start, *count)) + }) + .take(max_number_of_lines) + { + if self.number_of_objects >= start && self.number_of_objects < end { + self.number_of_objects = end; + self.at_least_n_letter_groups(end); + return true; + } + } + + false + } + + fn at_least_n_letter_groups(&mut self, n: usize) { + while !self.buffer.buffered_chars.is_empty() && self.buffer.letters.letters.len() <= n { + self.buffer.process(); + } + } +} + +struct LayoutCache { + positions: VecDeque>, + line_capacity: VecDeque, + objects: Vec, + objects_are_at_origin: Vector2D, + area: Vector2D, +} + +impl LayoutCache { + fn update_objects_to_display_at_position<'a>( + &mut self, + position: Vector2D, + letters: impl Iterator, + number_of_objects: usize, + ) { + let already_done = if position == self.objects_are_at_origin { + self.objects.len() + } else { + self.objects.clear(); + 0 + }; + self.objects.extend( + self.positions + .iter() + .zip(letters) + .take(number_of_objects) + .skip(already_done) + .map(|(offset, letter)| { + let position = offset.change_base() + position; + let mut object = ObjectUnmanaged::new(letter.clone()); + object.show().set_position(position); + object + }), + ); + self.objects.truncate(number_of_objects); + self.objects_are_at_origin = position; + } + + fn create_positions( + &mut self, + font: &Font, + preprocessed: &Preprocessed, + settings: &LayoutSettings, + ) { + self.area = settings.area; + self.line_capacity.clear(); + self.positions.clear(); + for (line, line_positions) in Self::create_layout(font, preprocessed, settings) { + self.line_capacity.push_back(line.number_of_letter_groups()); + self.positions + .extend(line_positions.map(|x| Vector2D::new(x.x as i16, x.y as i16))); } } - fn reset(&mut self, settings: LayoutSettings) { - self.objects.clear(); - self.state = LayoutCacheState { - head_offset: (0, 0).into(), - word_depth: 0, - rendered_groups: 0, - line_depth: 0, - line_element_depth: 0, - }; - self.settings = settings; + fn create_layout<'a>( + font: &Font, + preprocessed: &'a Preprocessed, + settings: &'a LayoutSettings, + ) -> impl Iterator> + 'a)> + 'a { + let minimum_space_width = font.letter(' ').advance_width as i32; + let width = settings.area.x; + let line_height = font.line_height(); + + let mut head_position: Vector2D = (0, -line_height).into(); + + preprocessed + .lines_element(width, minimum_space_width) + .map(move |(line, line_elements)| { + let line_settings = settings + .alignment + .settings(&line, minimum_space_width, width); + + head_position.y += line_height; + head_position.x = line_settings.start_x; + + ( + line, + line_elements.filter_map(move |element| match element.decode() { + PreprocessedElement::LetterGroup { width } => { + let this_position = head_position; + head_position.x += width as i32; + Some(this_position) + } + PreprocessedElement::WhiteSpace(space) => { + match space { + WhiteSpace::NewLine => { + head_position.y += settings.paragraph_spacing; + } + WhiteSpace::Space => head_position.x += line_settings.space_width, + } + None + } + }), + ) + }) } } @@ -438,12 +386,3 @@ struct LayoutSettings { alignment: TextAlignment, paragraph_spacing: i32, } - -#[derive(Default)] -struct LayoutCacheState { - head_offset: Vector2D, - word_depth: usize, - rendered_groups: usize, - line_depth: usize, - line_element_depth: usize, -} diff --git a/agb/src/display/object/font/preprocess.rs b/agb/src/display/object/font/preprocess.rs index f5ab2fbb..ca1e95e5 100644 --- a/agb/src/display/object/font/preprocess.rs +++ b/agb/src/display/object/font/preprocess.rs @@ -6,52 +6,47 @@ use crate::display::Font; use super::WhiteSpace; -#[derive(Debug, PartialEq, Eq, Clone, Copy)] +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub(crate) struct PreprocessedElementEncoded(u8); + +impl PreprocessedElementEncoded { + pub(crate) fn decode(self) -> PreprocessedElement { + match self.0 { + 255 => PreprocessedElement::WhiteSpace(WhiteSpace::NewLine), + 254 => PreprocessedElement::WhiteSpace(WhiteSpace::Space), + width => PreprocessedElement::LetterGroup { width }, + } + } +} + +#[derive(Clone, Copy, Debug, PartialEq, Eq)] + pub(crate) enum PreprocessedElement { - Word(Word), + LetterGroup { width: u8 }, WhiteSpace(WhiteSpace), } -#[test_case] -fn check_size_of_preprocessed_element_is_correct(_: &mut crate::Gba) { - assert_eq!( - core::mem::size_of::(), - core::mem::size_of::() - ); -} - -#[derive(Debug, PartialEq, Eq, Clone, Copy)] -#[repr(align(4))] -pub(crate) struct Word { - pixels: u8, - number_of_sprites: NonZeroU8, - index: u16, -} - -impl Word { - pub fn pixels(self) -> i32 { - self.pixels.into() - } - pub fn number_of_sprites(self) -> usize { - self.number_of_sprites.get().into() - } - pub fn sprite_index(self) -> usize { - self.index.into() +impl PreprocessedElement { + fn encode(self) -> PreprocessedElementEncoded { + PreprocessedElementEncoded(match self { + PreprocessedElement::LetterGroup { width } => width, + PreprocessedElement::WhiteSpace(space) => match space { + WhiteSpace::NewLine => 255, + WhiteSpace::Space => 254, + }, + }) } } #[derive(Default, Debug)] pub(crate) struct Preprocessed { - widths: VecDeque, + widths: VecDeque, preprocessor: Preprocessor, } #[derive(Debug, Default)] struct Preprocessor { - current_word_width: i32, - number_of_sprites: usize, width_in_sprite: i32, - total_number_of_sprites: usize, } impl Preprocessor { @@ -60,37 +55,32 @@ impl Preprocessor { font: &Font, character: char, sprite_width: i32, - widths: &mut VecDeque, + widths: &mut VecDeque, ) { match character { space @ (' ' | '\n') => { - if self.current_word_width != 0 { - self.number_of_sprites += 1; - self.total_number_of_sprites += 1; - widths.push_back(PreprocessedElement::Word(Word { - pixels: self.current_word_width.try_into().expect("word too wide"), - number_of_sprites: NonZeroU8::new( - self.number_of_sprites.try_into().expect("word too wide"), - ) - .unwrap(), - index: (self.total_number_of_sprites - self.number_of_sprites) - .try_into() - .expect("out of range"), - })); - self.current_word_width = 0; - self.number_of_sprites = 0; + if self.width_in_sprite != 0 { + widths.push_back( + PreprocessedElement::LetterGroup { + width: self.width_in_sprite as u8, + } + .encode(), + ); self.width_in_sprite = 0; } - widths.push_back(PreprocessedElement::WhiteSpace(WhiteSpace::from_char( - space, - ))); + widths.push_back( + PreprocessedElement::WhiteSpace(WhiteSpace::from_char(space)).encode(), + ); } letter => { let letter = font.letter(letter); - self.current_word_width += letter.advance_width as i32 + letter.xmin as i32; if self.width_in_sprite + letter.width as i32 > sprite_width { - self.number_of_sprites += 1; - self.total_number_of_sprites += 1; + widths.push_back( + PreprocessedElement::LetterGroup { + width: self.width_in_sprite as u8, + } + .encode(), + ); self.width_in_sprite = 0; } if self.width_in_sprite != 0 { @@ -105,7 +95,7 @@ impl Preprocessor { pub(crate) struct Lines<'preprocess> { minimum_space_width: i32, layout_width: i32, - data: &'preprocess VecDeque, + data: &'preprocess VecDeque, current_start_idx: usize, } @@ -150,35 +140,58 @@ impl<'pre> Iterator for Lines<'pre> { } let mut line_idx_length = 0; - let mut current_line_width = 0; - let mut additional_space_count = 0; + let mut current_line_width_pixels = 0; + let mut spaces_after_last_word_count = 0usize; + let mut start_of_current_word = usize::MAX; + let mut length_of_current_word_pixels = 0; + let mut length_of_current_word = 0; let mut number_of_spaces = 0; let mut number_of_words = 0; let mut number_of_letter_groups = 0; while let Some(next) = self.data.get(self.current_start_idx + line_idx_length) { - match next { - PreprocessedElement::Word(word) => { - let additional_space_width = - additional_space_count as i32 * self.minimum_space_width; - let width = word.pixels(); - if width + current_line_width + additional_space_width > self.layout_width { + match next.decode() { + PreprocessedElement::LetterGroup { width } => { + if start_of_current_word == usize::MAX { + start_of_current_word = line_idx_length; + } + length_of_current_word_pixels += width as i32; + length_of_current_word += 1; + if current_line_width_pixels + + length_of_current_word_pixels + + spaces_after_last_word_count as i32 * self.minimum_space_width + >= self.layout_width + { + line_idx_length = start_of_current_word; break; } - number_of_letter_groups += word.number_of_sprites.get() as usize; - number_of_words += 1; - current_line_width += width + additional_space_width; - number_of_spaces += additional_space_count; } - PreprocessedElement::WhiteSpace(space) => match space { - WhiteSpace::NewLine => { - line_idx_length += 1; - break; + PreprocessedElement::WhiteSpace(space) => { + if start_of_current_word != usize::MAX { + // flush word + current_line_width_pixels += length_of_current_word_pixels + + spaces_after_last_word_count as i32 * self.minimum_space_width; + number_of_spaces += spaces_after_last_word_count; + number_of_words += 1; + number_of_letter_groups += length_of_current_word; + + // reset parser + length_of_current_word_pixels = 0; + length_of_current_word = 0; + start_of_current_word = usize::MAX; + spaces_after_last_word_count = 0; } - WhiteSpace::Space => { - additional_space_count += 1; + + match space { + WhiteSpace::NewLine => { + line_idx_length += 1; + break; + } + WhiteSpace::Space => { + spaces_after_last_word_count += 1; + } } - }, + } }; line_idx_length += 1; @@ -187,7 +200,7 @@ impl<'pre> Iterator for Lines<'pre> { self.current_start_idx += line_idx_length; Some(Line { - width: current_line_width, + width: current_line_width_pixels, number_of_text_elements: line_idx_length, number_of_spaces, number_of_words, @@ -226,7 +239,7 @@ impl Preprocessed { &self, layout_width: i32, minimum_space_width: i32, - ) -> impl Iterator + '_)> { + ) -> impl Iterator + '_)> { let mut idx = 0; self.lines(layout_width, minimum_space_width).map(move |x| { let length = x.number_of_text_elements; diff --git a/agb/src/display/object/font/renderer.rs b/agb/src/display/object/font/renderer.rs index f364d07c..d08ee8e0 100644 --- a/agb/src/display/object/font/renderer.rs +++ b/agb/src/display/object/font/renderer.rs @@ -1,14 +1,10 @@ use crate::display::{ - object::{DynamicSprite, PaletteVram, Size}, + object::{DynamicSprite, PaletteVram, Size, SpriteVram}, Font, }; -use super::LetterGroup; - 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: i32, } @@ -17,13 +13,11 @@ impl WorkingLetter { fn new(size: Size) -> Self { Self { dynamic: DynamicSprite::new(size), - x_position: 0, x_offset: 0, } } fn reset(&mut self) { - self.x_position = 0; self.x_offset = 0; } } @@ -64,7 +58,7 @@ impl WordRender { } #[must_use] - pub(crate) fn finalise_letter(&mut self) -> Option { + pub(crate) fn finalise_letter(&mut self) -> Option { if self.working.x_offset == 0 { return None; } @@ -72,19 +66,13 @@ impl WordRender { let mut new_sprite = DynamicSprite::new(self.config.sprite_size); core::mem::swap(&mut self.working.dynamic, &mut new_sprite); let sprite = new_sprite.to_vram(self.config.palette.clone()); - - let group = LetterGroup { - sprite, - width: self.working.x_offset as u16, - left: self.working.x_position as i16, - }; self.working.reset(); - Some(group) + Some(sprite) } #[must_use] - pub(crate) fn render_char(&mut self, font: &Font, c: char) -> Option { + pub(crate) fn render_char(&mut self, font: &Font, c: char) -> Option { let font_letter: &crate::display::FontLetter = font.letter(c); // uses more than the sprite can hold @@ -96,9 +84,7 @@ impl WordRender { None }; - if self.working.x_offset == 0 { - self.working.x_position = font_letter.xmin as i32; - } else { + if self.working.x_offset != 0 { self.working.x_offset += font_letter.xmin as i32; } From 9edbca582ebf2986504754a0a5b2d322a87dda29 Mon Sep 17 00:00:00 2001 From: Corwin Date: Sun, 2 Jul 2023 00:24:54 +0100 Subject: [PATCH 14/24] magic colour changing --- agb/examples/object_text_render.rs | 13 ++++----- agb/src/display/object/font.rs | 33 ++++++++++++++++++++++- agb/src/display/object/font/preprocess.rs | 4 +-- agb/src/display/object/font/renderer.rs | 7 +++++ 4 files changed, 47 insertions(+), 10 deletions(-) diff --git a/agb/examples/object_text_render.rs b/agb/examples/object_text_render.rs index 8bc1729f..e05c0041 100644 --- a/agb/examples/object_text_render.rs +++ b/agb/examples/object_text_render.rs @@ -4,7 +4,7 @@ use agb::{ display::{ object::{ - font::{ObjectTextRender, TextAlignment}, + font::{ChangeColour, ObjectTextRender, TextAlignment}, PaletteVram, Size, }, palette16::Palette16, @@ -30,14 +30,17 @@ fn main(mut gba: agb::Gba) -> ! { loop { let mut palette = [0x0; 16]; palette[1] = 0xFF_FF; + palette[2] = 0x00_FF; let palette = Palette16::new(palette); let palette = PaletteVram::new(&palette).unwrap(); let mut wr = ObjectTextRender::new(&FONT, Size::S16x16, palette); + let player_name = "You"; let _ = writeln!( wr, - "Woah! Hey there! I have a bunch of text I want to show you. However, you will find that the amount of text I can display is limited. Who'd have thought! Good thing that my text system supports scrolling! It only took around 20 jank versions to get here!" - + "Woah!{change2} {player_name}! {change1}Hey there! I have a bunch of text I want to show you. However, you will find that the amount of text I can display is limited. Who'd have thought! Good thing that my text system supports scrolling! It only took around 20 jank versions to get here!", + change2 = ChangeColour::new(2), + change1 = ChangeColour::new(1), ); let vblank = agb::interrupt::VBlank::get(); @@ -64,9 +67,7 @@ fn main(mut gba: agb::Gba) -> ! { if frame % 4 == 0 { line_done = !wr.next_letter_group(); } - if line_done - && input.is_just_pressed(Button::A) - { + if line_done && input.is_just_pressed(Button::A) { line_done = false; wr.pop_line(); } diff --git a/agb/src/display/object/font.rs b/agb/src/display/object/font.rs index 9cea9054..417e71ea 100644 --- a/agb/src/display/object/font.rs +++ b/agb/src/display/object/font.rs @@ -1,4 +1,4 @@ -use core::fmt::Write; +use core::fmt::{Display, Write}; use agb_fixnum::Vector2D; use alloc::{collections::VecDeque, vec::Vec}; @@ -97,6 +97,37 @@ fn is_private_use(c: char) -> bool { ('\u{E000}'..'\u{F8FF}').contains(&c) } +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub struct ChangeColour(u8); + +impl ChangeColour { + #[must_use] + pub fn new(colour: u32) -> Self { + assert!(colour < 16, "paletted colour must be valid (0..=15)"); + + Self(colour as u8) + } + + fn try_from_char(c: char) -> Option { + let c = c as u32; + if c >= 0xE000 && c < 0xE000 + 16 { + Some(ChangeColour::new(c - 0xE000)) + } else { + None + } + } + + fn to_char(self) -> char { + char::from_u32(self.0 as u32 + 0xE000).unwrap() + } +} + +impl Display for ChangeColour { + fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result { + f.write_char(self.to_char()) + } +} + impl BufferedRender<'_> { fn input_character(&mut self, character: char) { if !is_private_use(character) { diff --git a/agb/src/display/object/font/preprocess.rs b/agb/src/display/object/font/preprocess.rs index ca1e95e5..9955bf51 100644 --- a/agb/src/display/object/font/preprocess.rs +++ b/agb/src/display/object/font/preprocess.rs @@ -1,6 +1,4 @@ -use core::num::NonZeroU8; - -use alloc::{collections::VecDeque, vec::Vec}; +use alloc::collections::VecDeque; use crate::display::Font; diff --git a/agb/src/display/object/font/renderer.rs b/agb/src/display/object/font/renderer.rs index d08ee8e0..b59b6549 100644 --- a/agb/src/display/object/font/renderer.rs +++ b/agb/src/display/object/font/renderer.rs @@ -3,6 +3,8 @@ use crate::display::{ Font, }; +use super::ChangeColour; + struct WorkingLetter { dynamic: DynamicSprite, // where to render the letter from x_min to x_max @@ -73,6 +75,11 @@ impl WordRender { #[must_use] pub(crate) fn render_char(&mut self, font: &Font, c: char) -> Option { + if let Some(next_colour) = ChangeColour::try_from_char(c) { + self.colour = next_colour.0 as usize; + return None; + } + let font_letter: &crate::display::FontLetter = font.letter(c); // uses more than the sprite can hold From 938809831848afe874e9843a06713829f8c0ee53 Mon Sep 17 00:00:00 2001 From: Corwin Date: Sun, 2 Jul 2023 00:38:52 +0100 Subject: [PATCH 15/24] JUSTIFY --- agb/examples/object_text_render.rs | 2 +- agb/src/display/object/font.rs | 30 ++++++++++++++++++++++-------- 2 files changed, 23 insertions(+), 9 deletions(-) diff --git a/agb/examples/object_text_render.rs b/agb/examples/object_text_render.rs index e05c0041..0962a60b 100644 --- a/agb/examples/object_text_render.rs +++ b/agb/examples/object_text_render.rs @@ -52,7 +52,7 @@ fn main(mut gba: agb::Gba) -> ! { timer.set_enabled(true); timer.set_divider(agb::timer::Divider::Divider256); - wr.layout((WIDTH, 40).into(), TextAlignment::Left, 2); + wr.layout((WIDTH, 40).into(), TextAlignment::Justify, 2); let mut line_done = false; let mut frame = 0; diff --git a/agb/src/display/object/font.rs b/agb/src/display/object/font.rs index 417e71ea..bf8baaf3 100644 --- a/agb/src/display/object/font.rs +++ b/agb/src/display/object/font.rs @@ -1,6 +1,6 @@ use core::fmt::{Display, Write}; -use agb_fixnum::Vector2D; +use agb_fixnum::{Num, Vector2D}; use alloc::{collections::VecDeque, vec::Vec}; use crate::display::Font; @@ -53,10 +53,11 @@ pub enum TextAlignment { Left, Right, Center, + Justify, } struct TextAlignmentSettings { - space_width: i32, + space_width: Num, start_x: i32, } @@ -64,17 +65,30 @@ impl TextAlignment { fn settings(self, line: &Line, minimum_space_width: i32, width: i32) -> TextAlignmentSettings { match self { TextAlignment::Left => TextAlignmentSettings { - space_width: minimum_space_width, + space_width: minimum_space_width.into(), start_x: 0, }, TextAlignment::Right => TextAlignmentSettings { - space_width: minimum_space_width, + space_width: minimum_space_width.into(), start_x: width - line.width(), }, TextAlignment::Center => TextAlignmentSettings { - space_width: minimum_space_width, + space_width: minimum_space_width.into(), start_x: (width - line.width()) / 2, }, + TextAlignment::Justify => { + let space_width = if line.number_of_spaces() != 0 { + Num::new( + width - line.width() + line.number_of_spaces() as i32 * minimum_space_width, + ) / line.number_of_spaces() as i32 + } else { + minimum_space_width.into() + }; + TextAlignmentSettings { + space_width, + start_x: 0, + } + } } } } @@ -376,7 +390,7 @@ impl LayoutCache { let width = settings.area.x; let line_height = font.line_height(); - let mut head_position: Vector2D = (0, -line_height).into(); + let mut head_position: Vector2D> = (0, -line_height).into(); preprocessed .lines_element(width, minimum_space_width) @@ -386,7 +400,7 @@ impl LayoutCache { .settings(&line, minimum_space_width, width); head_position.y += line_height; - head_position.x = line_settings.start_x; + head_position.x = line_settings.start_x.into(); ( line, @@ -394,7 +408,7 @@ impl LayoutCache { PreprocessedElement::LetterGroup { width } => { let this_position = head_position; head_position.x += width as i32; - Some(this_position) + Some(this_position.floor()) } PreprocessedElement::WhiteSpace(space) => { match space { From 66a212f29a2bcade6941b85ef7c2df584c2ba1fa Mon Sep 17 00:00:00 2001 From: Corwin Date: Sun, 2 Jul 2023 13:08:44 +0100 Subject: [PATCH 16/24] remove unused --- agb/examples/object_text_render.rs | 5 +---- agb/src/display/object/font/preprocess.rs | 9 --------- 2 files changed, 1 insertion(+), 13 deletions(-) diff --git a/agb/examples/object_text_render.rs b/agb/examples/object_text_render.rs index 0962a60b..1cec3763 100644 --- a/agb/examples/object_text_render.rs +++ b/agb/examples/object_text_render.rs @@ -3,10 +3,7 @@ use agb::{ display::{ - object::{ - font::{ChangeColour, ObjectTextRender, TextAlignment}, - PaletteVram, Size, - }, + object::{ChangeColour, ObjectTextRender, PaletteVram, Size, TextAlignment}, palette16::Palette16, Font, HEIGHT, WIDTH, }, diff --git a/agb/src/display/object/font/preprocess.rs b/agb/src/display/object/font/preprocess.rs index 9955bf51..f899071f 100644 --- a/agb/src/display/object/font/preprocess.rs +++ b/agb/src/display/object/font/preprocess.rs @@ -101,7 +101,6 @@ pub(crate) struct Line { width: i32, number_of_text_elements: usize, number_of_spaces: usize, - number_of_words: usize, number_of_letter_groups: usize, } @@ -118,11 +117,6 @@ impl Line { pub(crate) fn number_of_spaces(&self) -> usize { self.number_of_spaces } - #[inline(always)] - pub(crate) fn number_of_words(&self) -> usize { - self.number_of_words - } - #[inline(always)] pub(crate) fn number_of_letter_groups(&self) -> usize { self.number_of_letter_groups @@ -144,7 +138,6 @@ impl<'pre> Iterator for Lines<'pre> { let mut length_of_current_word_pixels = 0; let mut length_of_current_word = 0; let mut number_of_spaces = 0; - let mut number_of_words = 0; let mut number_of_letter_groups = 0; while let Some(next) = self.data.get(self.current_start_idx + line_idx_length) { @@ -170,7 +163,6 @@ impl<'pre> Iterator for Lines<'pre> { current_line_width_pixels += length_of_current_word_pixels + spaces_after_last_word_count as i32 * self.minimum_space_width; number_of_spaces += spaces_after_last_word_count; - number_of_words += 1; number_of_letter_groups += length_of_current_word; // reset parser @@ -201,7 +193,6 @@ impl<'pre> Iterator for Lines<'pre> { width: current_line_width_pixels, number_of_text_elements: line_idx_length, number_of_spaces, - number_of_words, number_of_letter_groups, }) } From fa7a71e0d3a7b388522f3d7c95446ef1d7d71e8d Mon Sep 17 00:00:00 2001 From: Corwin Date: Sun, 2 Jul 2023 13:08:54 +0100 Subject: [PATCH 17/24] add docs and export --- agb/src/display/object.rs | 4 +- agb/src/display/object/font.rs | 86 ++++++++++++++++++++++++++++++++-- 2 files changed, 84 insertions(+), 6 deletions(-) diff --git a/agb/src/display/object.rs b/agb/src/display/object.rs index ce9c5ac4..85df2a55 100644 --- a/agb/src/display/object.rs +++ b/agb/src/display/object.rs @@ -9,7 +9,7 @@ //! harder to integrate into your games depending on how they are architectured. mod affine; -pub mod font; +mod font; mod managed; mod sprites; mod unmanaged; @@ -23,6 +23,8 @@ pub use affine::AffineMatrixInstance; pub use managed::{OamManaged, Object}; pub use unmanaged::{AffineMode, OamIterator, OamSlot, OamUnmanaged, ObjectUnmanaged}; +pub use font::{ChangeColour, ObjectTextRender, TextAlignment}; + use super::DISPLAY_CONTROL; const OBJECT_ATTRIBUTE_MEMORY: *mut u16 = 0x0700_0000 as *mut u16; diff --git a/agb/src/display/object/font.rs b/agb/src/display/object/font.rs index bf8baaf3..780fd323 100644 --- a/agb/src/display/object/font.rs +++ b/agb/src/display/object/font.rs @@ -32,7 +32,7 @@ impl WhiteSpace { } } -pub struct BufferedRender<'font> { +struct BufferedRender<'font> { char_render: WordRender, preprocessor: Preprocessed, buffered_chars: VecDeque, @@ -48,11 +48,16 @@ struct Letters { #[derive(Debug, Clone, Copy, PartialEq, Eq, Default)] #[non_exhaustive] +/// The text alignment of the layout pub enum TextAlignment { #[default] + /// Left aligned, the left edge of the text lines up Left, + /// Right aligned, the right edge of the text lines up Right, + /// Center aligned, the center of the text lines up Center, + /// Justified, both the left and right edges line up with space width adapted to make it so. Justify, } @@ -112,19 +117,43 @@ fn is_private_use(c: char) -> bool { } #[derive(Clone, Copy, Debug, PartialEq, Eq)] +/// Changes the palette to use to draw characters. +/// ```rust,no_run +/// # #![no_std] +/// # #![no_main] +/// use agb::display::object::{ObjectTextRender, PaletteVram, ChangeColour, Size}; +/// use agb::display::palette16::Palette16; +/// use agb::display::Font; +/// +/// use core::fmt::Write; +/// +/// const EXAMPLE_FONT: Font = agb::include_font!("examples/font/yoster.ttf", 12); +/// +/// # fn foo() { +/// let mut palette = [0x0; 16]; +/// palette[1] = 0xFF_FF; +/// palette[2] = 0x00_FF; +/// let palette = Palette16::new(palette); +/// let palette = PaletteVram::new(&palette).unwrap(); +/// let mut writer = ObjectTextRender::new(&EXAMPLE_FONT, Size::S16x16, palette); +/// +/// let _ = writeln!(writer, "Hello, {}World{}!", ChangeColour::new(2), ChangeColour::new(1)); +/// # } +/// ``` pub struct ChangeColour(u8); impl ChangeColour { #[must_use] - pub fn new(colour: u32) -> Self { + /// Creates the colour changer. Colour is a palette index and must be in the range 0..16. + pub fn new(colour: usize) -> Self { assert!(colour < 16, "paletted colour must be valid (0..=15)"); Self(colour as u8) } fn try_from_char(c: char) -> Option { - let c = c as u32; - if c >= 0xE000 && c < 0xE000 + 16 { + let c = c as u32 as usize; + if (0xE000..0xE000 + 16).contains(&c) { Some(ChangeColour::new(c - 0xE000)) } else { None @@ -172,6 +201,43 @@ impl BufferedRender<'_> { } } +/// The object text renderer. Uses objects to render and layout text. It's use is non trivial. +/// Changes the palette to use to draw characters. +/// ```rust,no_run +/// #![no_std] +/// #![no_main] +/// use agb::display::object::{ObjectTextRender, PaletteVram, TextAlignment, Size}; +/// use agb::display::palette16::Palette16; +/// use agb::display::{Font, WIDTH}; +/// +/// use core::fmt::Write; +/// +/// const EXAMPLE_FONT: Font = agb::include_font!("examples/font/yoster.ttf", 12); +/// +/// #[agb::entry] +/// fn main(gba: &mut agb::Gba) -> ! { +/// let (mut unmanaged, _) = gba.display.object.get_unmanaged(); +/// let vblank = agb::interrupt::VBlank::get(); +/// +/// let mut palette = [0x0; 16]; +/// palette[1] = 0xFF_FF; +/// let palette = Palette16::new(palette); +/// let palette = PaletteVram::new(&palette).unwrap(); +/// +/// let mut writer = ObjectTextRender::new(&EXAMPLE_FONT, Size::S16x16, palette); +/// +/// let _ = writeln!(writer, "Hello, World!"); +/// writer.layout((WIDTH, 40).into(), TextAlignment::Left, 2); +/// +/// loop { +/// writer.next_letter_group(); +/// writer.update((0, 0).into()); +/// vblank.wait_for_vblank(); +/// let oam = &mut unmanaged.iter(); +/// writer.commit(oam); +/// } +/// } +/// ``` pub struct ObjectTextRender<'font> { buffer: BufferedRender<'font>, layout: LayoutCache, @@ -180,6 +246,9 @@ pub struct ObjectTextRender<'font> { impl<'font> ObjectTextRender<'font> { #[must_use] + /// Creates a new text renderer with a given font, sprite size, and palette. + /// You must ensure that the sprite size can accomodate the letters from the + /// font otherwise it will panic at render time. pub fn new(font: &'font Font, sprite_size: Size, palette: PaletteVram) -> Self { Self { buffer: BufferedRender::new(font, sprite_size, palette), @@ -231,7 +300,7 @@ impl ObjectTextRender<'_> { ); } - /// Removes one complete line. + /// Removes one complete line. Returns whether a line could be removed. You must call [`update`] after this pub fn pop_line(&mut self) -> bool { let width = self.layout.area.x; let space = self.buffer.font.letter(' ').advance_width as i32; @@ -257,6 +326,9 @@ impl ObjectTextRender<'_> { false } + /// Updates the internal state of the number of letters to write and popped + /// line. Should be called in the same frame as and after + /// [`next_letter_group`], [`next_line`], and [`pop_line`]. pub fn update(&mut self, position: Vector2D) { if !self.buffer.buffered_chars.is_empty() && self.buffer.letters.letters.len() <= self.number_of_objects + 5 @@ -271,6 +343,8 @@ impl ObjectTextRender<'_> { ); } + /// Causes the next letter group to be shown on the next update. Returns + /// whether another letter could be added in the space given. pub fn next_letter_group(&mut self) -> bool { if !self.can_render_another_element() { return false; @@ -294,6 +368,8 @@ impl ObjectTextRender<'_> { max_number_of_objects > self.number_of_objects } + /// Causes the next line to be shown on the next update. Returns + /// whether another line could be added in the space given. pub fn next_line(&mut self) -> bool { let max_number_of_lines = (self.layout.area.y / self.buffer.font.line_height()) as usize; From 5c0f855e1b6546fb209cf944c0d37bcc43cc5d34 Mon Sep 17 00:00:00 2001 From: Corwin Date: Sun, 2 Jul 2023 14:32:09 +0100 Subject: [PATCH 18/24] use newer rustfmt --- agb/src/display/object/font.rs | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/agb/src/display/object/font.rs b/agb/src/display/object/font.rs index 780fd323..fe23a0b9 100644 --- a/agb/src/display/object/font.rs +++ b/agb/src/display/object/font.rs @@ -181,7 +181,9 @@ impl BufferedRender<'_> { } fn process(&mut self) { - let Some(c) = self.buffered_chars.pop_front() else { return }; + let Some(c) = self.buffered_chars.pop_front() else { + return; + }; match c { ' ' | '\n' => { if let Some(group) = self.char_render.finalise_letter() { From b82ed16cc2c17613116659114fa129b578cedace Mon Sep 17 00:00:00 2001 From: Corwin Date: Sun, 2 Jul 2023 14:55:30 +0100 Subject: [PATCH 19/24] update test image --- agb/examples/font/font-test-output.png | Bin 2670 -> 1691 bytes 1 file changed, 0 insertions(+), 0 deletions(-) diff --git a/agb/examples/font/font-test-output.png b/agb/examples/font/font-test-output.png index be7bce8b6513106006c624f0e76b176fbb1b603b..e79165d409579a98b7cad01434e087f63114669a 100644 GIT binary patch literal 1691 zcmds&?^jX>7{|%PJXtP19oNaBYp%`H`gOF;bfVc0Z`QJ@C`}4XYyK*!2t%%+Yn{r> zsGU{I0xPGbBsWuHk{8HaR8C0~QAA8RLJ1eFgnQ)2W%h1=K(F@Vc|PA4&v{-v=X<_S z#gECr!ud<*<8ZizNn1Ck&T?wDZut7l_K4xJ{WzRYcGBhq66-oL^60QOaq-E$eSd%X z+1b8(X>WT0hk;~XxA^Si3%wLopFR(_AV{7~amX5P^GYI@WFYFEj3SZyY$g4qWWF8*o!lc!La^N9;FyXjo+B9t}Mr?@d2f#;>8+}LK z0$wZl~0to^Cg!SMoZtCVX5~?!}-i9=#WF>l;i5l4zWkC}Uwu9!DvYn-Nw=&K% zw8xFbyL{o!N2xDJ<9ymMhb|066Qjc!(0Fr4{j6lsK}5TIUB(;Ikb1RusE8djPG*7C*q&1S!3j!qkGQSke^2dHT$drc;G2N8Wh3+Oe2KmBia_v z#TDkB;iO&vaaC_3*J*j$hEKLVBf+8JEU34N!vNVklBc zjX?2J;efX6O-|GiX}n^)QM<`l6wNSU(J{c$0q3-2pOtoqo_2g97;11%Uoc?1cjjDr zvdUH`RPmpzs50h_T(&2TP!u*vo0PS~9V66!${i+Vs7-^~NT>}eA&A?*f|(6qr&FtL zKfgbpsvuHBRTi|(0Uc^47;HW|__48S)nJf8Xka$ItmV{IoQx&S$ zRJ4`0++t(x0Mk>!@iy8B)Sp&?+WJK?MbJSbk@}K)=`S%kN)9xBa#xexCCWU8OhlHJ z$t0ui94UsLm+o|iK`0oozb;i>7nuZgF!d!o>%%}KT~yknj)htcnd61-nB0SKowM=$ z^ce7ZmFx2`dIxy4>iPQsoxnumlP4?Hu{#ZDs*Mc_e{)p(#ImDRlOd91k&39k`}QhD2XcE-kh(|`x&%&kl5%^8RtWR~8t8u8VWq|q z&a3@x6)jkj$UHfQ6T9TL1PzrZ=!7pjk+|;kacY;lxW>pZ*j3VP+G~2IRs6s@6C_Q{ zz4Kv2_w_-Dw;gHdgqQ2|g_$suD>4M4;N*uh<_QCBqzq?Y51?|#JWemMz|2{*+a>N0 z78@T@k8aVLh_$sf3@dbzdix9c`tIq4x4IJUux}1J@lvwW#VLw6sgywZ;l}T9ls$wC zBBRVPW5MWIICYqO!&r)0imMg@(2$NBwa2D7#>AhDDPY#fnG=mLFIWzm4nSIv&_RRj~ z?uiiGHUl#Q7!0=USfJl2&?>=OhTH_cE@S_VhQSPu9P{(}CIv1p{w2ZuE0n)uVcxa< zU%w3Lz*)(Q@pXeI_8a~nvs$hxI@_}~Mn5v%h>%|dGn;_DD#b^*ZcoQvfRJtp zsuXtwN?9V%uYR`|bo|YZ?WUMug_PCOCx?7taAL*%2hI*~ga>JgRQv7}&sGw|%exR` zOQ(|-+};whU{L!v7{{0|A!JGu6Tei6G~c1DjH@z5Ryfes$!l3kawsJ4>@OG@Qw;Fj%s4`yDyd`sRx@EaQTIMa-sLzpppJ2bXGPWc*$X%aX z$|!VB9~sR~NZ61@L^@|;iIBG!W1kx#18L6C zu5lYKopq~)XaXiAs@K7L6Qkcw=_52@U6#LY(}lAWXPZ+zN4ag5rw#fZ)eHD9wUvS< zFOvwds4!nh5oli$e{%ZVGxd0+b3j7wm)Qd|Bib}%gJ?Ht!8#)Zvhrf&THg~!;}Xpm z_-mOAs*5Q8-_LH=bw$iHE8c`f2j@13UBix+7fnOA>( zeppcXM#MMWu6wo2UQej?EY=ao-|#zMo7d>vl1pfW10fVGk4cgHtJqX>opWb`nVc)_ zXlpww$5Co&?o+DKmYr8KdbpaT$GRd>$}_PiH!AW?YVA1BRNj5z=@4^D>s)-HIbL%h zX}9EMkYISUF!r2RaQhKFGyZahnClTn?wNcs9ac0(ZC6PP`^ml0N8X3$%#@a2kDbgW zr6^81yGR3wk%g)jg4ii)XqrmO8TJTg)wDgV;`MlSh#!TjUN=pP*G(hXFH+@SN($4MTn1abCqnzK7`CuN#?DB~{eef_5ko)i1q4}&ozgaUJ!g>Z^iJGL z@hs31mgJbeDDedIvPIz%BjSgs63g=ILB`CJ%pS6Ii4~RapmLXWb9rih!J(Sgg0+pJ zLkji7=J5fO!Ihn*Glba5qJ~{(Fg1M9LR-W4is8xYtLw7R^Sh|C#;@*Ar~;+kOxa{! zLvm)n$Eh@VaH(yL++z99tt0o%MNKlt5E*~6ystnr?$&%2aiU%g zXqoEtKh>jJr*cYCQ!sip*0MKU6w7^+zu?l@${gG%LU*F0BQH1* zV=ZX}9A}1KHPCMAkWqssF1x>c%scMYYq4jzGBkm-_)_4@LH{cf*=SO&v#-^J_orPP zS7401WQzjdDV?YCmt+$|h85z?T|1kubhjz{6~VT3cdSGlvA9sYyahr921*o5jO|0O z@@%f5A>>-3Q)L+o>5)$-L7~r3^FA=Rx~ywA%G3`AH-h?kA0tG9*bGvmutN0-TYV@L z6o2nywkews3L83o>8Aig1p5}K*gppJtZ$7Ia6sYbSg=s0A;8&HxDj2b1L~n$?7Ahq z2DBh$ONy`u(Kp1T_nX*RL7xESVJKV8AQI>_Ai6V!STM<&T6;$r2C?+0O*d9-?y+G| z$ds?aLlz8Tw`zZ5e_;2G&1o%k*eNg z75Yl<2>qb;pDpweuYorG84(C~1+8l`XLRrb8=u74%l#T?B(LGOBYHq1{fiw}o&XxY z#RQ$h10wN_sj|J?a~P=oM#j&IX)w~UCpdK&C?a=)_~eBf96?>)grDSXj^DC2%EMdy zS6#nS!8z5|FH{LIpT5dq>V z*ldZc0C5dAW@Y+H3P9STZ<1o5?l_#V+pLpfg92m*{(wgO2poO{%zq}i$WulBqm*N= zbE2%jxCw(SeM&JlzutvG3dE0pLlR|C)iiU!5YzLkqmsiQZZjKw?D>z8p5XIIa%4)9 zev1|40CKxs8F*WG3G9@Q5nDZv=Qk9Y*zds&hPK7=d<>W)Q{dA+w1d^HpF-z=mxHEg z1QZ22&f|YaNbUKf7;xXQ_%3k=!tyxS^F?lIpE6rGV%*ps_m`HTl;w9RQ$2|1!%X@M zTPYUl!34*^oP6abHW@;;U``p{63zhcqB@y9GRfgypm2gbhy6#=z{bJ(mVuKq+$y<^ zH2^{Q7pK@E{s)p{WXEvE6?b UU~Nu-dnD|be~4eL@A;em2h<+#`Tzg` From 318ee03c1227ed19222413910f49e6556b5d3b7e Mon Sep 17 00:00:00 2001 From: Corwin Date: Sun, 2 Jul 2023 14:57:01 +0100 Subject: [PATCH 20/24] fix doclinks --- agb/src/display/object/font.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/agb/src/display/object/font.rs b/agb/src/display/object/font.rs index fe23a0b9..a9d05da5 100644 --- a/agb/src/display/object/font.rs +++ b/agb/src/display/object/font.rs @@ -302,7 +302,7 @@ impl ObjectTextRender<'_> { ); } - /// Removes one complete line. Returns whether a line could be removed. You must call [`update`] after this + /// Removes one complete line. Returns whether a line could be removed. You must call [`update`][ObjectTextRender::update] after this pub fn pop_line(&mut self) -> bool { let width = self.layout.area.x; let space = self.buffer.font.letter(' ').advance_width as i32; @@ -330,7 +330,7 @@ impl ObjectTextRender<'_> { /// Updates the internal state of the number of letters to write and popped /// line. Should be called in the same frame as and after - /// [`next_letter_group`], [`next_line`], and [`pop_line`]. + /// [`next_letter_group`][ObjectTextRender::next_letter_group], [`next_line`][ObjectTextRender::next_line], and [`pop_line`][ObjectTextRender::pop_line]. pub fn update(&mut self, position: Vector2D) { if !self.buffer.buffered_chars.is_empty() && self.buffer.letters.letters.len() <= self.number_of_objects + 5 From 991bb644f9e283016323ac9eabdd3393cbd10df3 Mon Sep 17 00:00:00 2001 From: Corwin Date: Tue, 4 Jul 2023 21:34:51 +0100 Subject: [PATCH 21/24] timers around area of interest --- agb/examples/object_text_render.rs | 36 +++++++++++++++++++++--------- 1 file changed, 26 insertions(+), 10 deletions(-) diff --git a/agb/examples/object_text_render.rs b/agb/examples/object_text_render.rs index 1cec3763..75e839fb 100644 --- a/agb/examples/object_text_render.rs +++ b/agb/examples/object_text_render.rs @@ -31,7 +31,15 @@ fn main(mut gba: agb::Gba) -> ! { let palette = Palette16::new(palette); let palette = PaletteVram::new(&palette).unwrap(); + let timer = gba.timers.timers(); + let mut timer: agb::timer::Timer = timer.timer2; + + timer.set_enabled(true); + timer.set_divider(agb::timer::Divider::Divider256); + let mut wr = ObjectTextRender::new(&FONT, Size::S16x16, palette); + let start = timer.value(); + let player_name = "You"; let _ = writeln!( wr, @@ -39,17 +47,25 @@ fn main(mut gba: agb::Gba) -> ! { change2 = ChangeColour::new(2), change1 = ChangeColour::new(1), ); + let end = timer.value(); + + agb::println!( + "Write took {} cycles", + 256 * (end.wrapping_sub(start) as u32) + ); let vblank = agb::interrupt::VBlank::get(); let mut input = agb::input::ButtonController::new(); - let timer = gba.timers.timers(); - let mut timer: agb::timer::Timer = timer.timer2; - - timer.set_enabled(true); - timer.set_divider(agb::timer::Divider::Divider256); + let start = timer.value(); wr.layout((WIDTH, 40).into(), TextAlignment::Justify, 2); + let end = timer.value(); + + agb::println!( + "Layout took {} cycles", + 256 * (end.wrapping_sub(start) as u32) + ); let mut line_done = false; let mut frame = 0; @@ -73,11 +89,11 @@ fn main(mut gba: agb::Gba) -> ! { frame += 1; - agb::println!( - "Took {} cycles, line done {}", - 256 * (end.wrapping_sub(start) as u32), - line_done - ); + // agb::println!( + // "Took {} cycles, line done {}", + // 256 * (end.wrapping_sub(start) as u32), + // line_done + // ); } } } From 0cd469866ca42c26d3d6f16a2ca3ee0f17945d7b Mon Sep 17 00:00:00 2001 From: Corwin Date: Tue, 4 Jul 2023 21:35:00 +0100 Subject: [PATCH 22/24] font get optimisation --- agb/src/display/font.rs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/agb/src/display/font.rs b/agb/src/display/font.rs index 17bd184a..e37a77a2 100644 --- a/agb/src/display/font.rs +++ b/agb/src/display/font.rs @@ -47,14 +47,14 @@ impl FontLetter { } pub struct Font { - letters: &'static [FontLetter], + letters: &'static [FontLetter; 128], line_height: i32, ascent: i32, } impl Font { #[must_use] - pub const fn new(letters: &'static [FontLetter], line_height: i32, ascent: i32) -> Self { + pub const fn new(letters: &'static [FontLetter; 128], line_height: i32, ascent: i32) -> Self { Self { letters, line_height, @@ -63,7 +63,7 @@ impl Font { } pub(crate) fn letter(&self, letter: char) -> &'static FontLetter { - &self.letters[letter as usize] + &self.letters[letter as usize & (128 - 1)] } pub(crate) fn ascent(&self) -> i32 { From 551fbfab4a59a37b31908fb684f35329e52e3240 Mon Sep 17 00:00:00 2001 From: Corwin Date: Tue, 4 Jul 2023 22:29:28 +0100 Subject: [PATCH 23/24] update changelog --- CHANGELOG.md | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index daa6a377..558f9674 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,10 +8,11 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Added - New `include_palette` macro for including every colour in an image as a `u16` slice. +- New object based text renderer. ### Changed - Changed the default template game. -- `DynamicSprite` has a new API which changes the constructor and adds a `set_pixel` method. +- `DynamicSprite` has a new API which changes the constructor and adds a `set_pixel` and `clear` methods. - You no longer need to install arm-none-eabi-binutils. In order to write games using `agb`, you now only need to install rust nightly. - 10% performance improvement with the software mixer. From 636e31aa91f9fe0da29e70dad4767aa5b62dd276 Mon Sep 17 00:00:00 2001 From: Corwin Date: Tue, 4 Jul 2023 22:35:12 +0100 Subject: [PATCH 24/24] uncomment out code --- agb/examples/object_text_render.rs | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/agb/examples/object_text_render.rs b/agb/examples/object_text_render.rs index 75e839fb..d5323724 100644 --- a/agb/examples/object_text_render.rs +++ b/agb/examples/object_text_render.rs @@ -89,11 +89,11 @@ fn main(mut gba: agb::Gba) -> ! { frame += 1; - // agb::println!( - // "Took {} cycles, line done {}", - // 256 * (end.wrapping_sub(start) as u32), - // line_done - // ); + agb::println!( + "Took {} cycles, line done {}", + 256 * (end.wrapping_sub(start) as u32), + line_done + ); } } }