From cf400029f5995ff2a3c28af641bb41b07f3c6c08 Mon Sep 17 00:00:00 2001 From: Corwin Date: Thu, 29 Jun 2023 00:10:21 +0100 Subject: [PATCH] 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 {