caching layout

This commit is contained in:
Corwin 2023-06-29 00:10:21 +01:00
parent f947d82049
commit cf400029f5
No known key found for this signature in database
5 changed files with 222 additions and 134 deletions

View file

@ -4,7 +4,7 @@
use agb::{ use agb::{
display::{ display::{
object::{ object::{
font::{BufferedRender, TextAlignment}, font::{BufferedRender, LayoutCache, TextAlignment},
PaletteVram, Size, PaletteVram, Size,
}, },
palette16::Palette16, palette16::Palette16,
@ -53,27 +53,25 @@ fn main(mut gba: agb::Gba) -> ! {
timer.set_divider(agb::timer::Divider::Divider256); timer.set_divider(agb::timer::Divider::Divider256);
let mut num_letters = 0; let mut num_letters = 0;
let mut frame = 0;
let mut alignment = TextAlignment::Left; let mut alignment = TextAlignment::Left;
let mut text = Vec::new(); let mut cache = LayoutCache::new();
loop { loop {
vblank.wait_for_vblank(); vblank.wait_for_vblank();
input.update(); input.update();
let oam = &mut unmanaged.iter(); let oam = &mut unmanaged.iter();
for (letter, slot) in text.iter().zip(oam) { cache.commit(oam);
slot.set(letter);
}
let start = timer.value(); let start = timer.value();
wr.process(); wr.process();
text = wr.layout( cache.update(
Rect::new((WIDTH / 8, 0).into(), (80, 100).into()), &mut wr,
Rect::new((WIDTH / 3, 0).into(), (WIDTH / 3, 100).into()),
alignment, alignment,
num_letters,
2, 2,
num_letters,
); );
let end = timer.value(); let end = timer.value();
@ -89,11 +87,7 @@ fn main(mut gba: agb::Gba) -> ! {
alignment = TextAlignment::Center; alignment = TextAlignment::Center;
} }
frame += 1;
// if frame % 2 == 0 {
num_letters += 1; num_letters += 1;
// }
if input.is_just_pressed(Button::A) { if input.is_just_pressed(Button::A) {
break; break;

View file

@ -69,6 +69,10 @@ impl Font {
pub(crate) fn ascent(&self) -> i32 { pub(crate) fn ascent(&self) -> i32 {
self.ascent self.ascent
} }
pub(crate) fn line_height(&self) -> i32 {
self.line_height
}
} }
impl Font { impl Font {

View file

@ -10,7 +10,7 @@ use self::{
renderer::{Configuration, WordRender}, renderer::{Configuration, WordRender},
}; };
use super::{DynamicSprite, ObjectUnmanaged, PaletteVram, Size, SpriteVram}; use super::{DynamicSprite, OamIterator, ObjectUnmanaged, PaletteVram, Size, SpriteVram};
mod preprocess; mod preprocess;
mod renderer; mod renderer;
@ -48,11 +48,15 @@ pub struct BufferedRender<'font> {
font: &'font Font, font: &'font Font,
} }
#[derive(Debug)]
struct Word {
index: usize,
length: usize,
}
#[derive(Debug, Default)] #[derive(Debug, Default)]
struct Letters { struct Letters {
letters: Vec<LetterGroup>, letters: Vec<LetterGroup>,
word_lengths: Vec<u8>,
current_word_length: usize,
number_of_groups: usize, number_of_groups: usize,
} }
@ -119,7 +123,8 @@ impl<'font> BufferedRender<'font> {
impl BufferedRender<'_> { impl BufferedRender<'_> {
fn input_character(&mut self, character: char) { 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); self.buffered_chars.push_back(character);
} }
@ -129,102 +134,164 @@ impl BufferedRender<'_> {
' ' | '\n' => { ' ' | '\n' => {
if let Some(group) = self.char_render.finalise_letter() { if let Some(group) = self.char_render.finalise_letter() {
self.letters.letters.push(group); self.letters.letters.push(group);
self.letters.current_word_length += 1;
self.letters.number_of_groups += 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; self.letters.number_of_groups += 1;
} }
letter => { letter => {
if let Some(group) = self.char_render.render_char(self.font, letter) { if let Some(group) = self.char_render.render_char(self.font, letter) {
self.letters.letters.push(group); self.letters.letters.push(group);
self.letters.current_word_length += 1;
self.letters.number_of_groups += 1; self.letters.number_of_groups += 1;
} }
} }
} }
} }
}
#[must_use] pub struct LayoutCache {
pub fn layout( objects: Vec<ObjectUnmanaged>,
&mut self, state: LayoutCacheState,
area: Rect<i32>, settings: LayoutSettings,
alignment: TextAlignment, }
number_of_groups: usize,
paragraph_spacing: i32,
) -> Vec<ObjectUnmanaged> {
let mut objects = Vec::new();
while !self.buffered_chars.is_empty() && self.letters.number_of_groups <= number_of_groups { impl LayoutCache {
self.process(); fn update_cache(&mut self, number_of_groups: usize, render: &BufferedRender) {
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);
'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,
);
if self.state.line_element_depth == 0 {
self.state.head_position.x += settings.start_x;
} }
let minimum_space_width = self.font.letter(' ').advance_width as i32; for element in line_elements.iter().skip(self.state.line_element_depth) {
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 { match element {
PreprocessedElement::Word(_) => { PreprocessedElement::Word(word) => {
for _ in 0..self for letter in (word.sprite_index()
.letters ..(word.sprite_index() + word.number_of_sprites()))
.word_lengths .skip(self.state.word_depth)
.get(word_depth) .map(|x| &render.letters.letters[x])
.copied()
.unwrap_or(u8::MAX)
{ {
let letter_group = &self.letters.letters[group_depth]; let mut object = ObjectUnmanaged::new(letter.sprite.clone());
let mut object = ObjectUnmanaged::new(letter_group.sprite.clone()); self.state.head_position.x += letter.left as i32;
head_position.x += letter_group.left as i32; object.set_position(self.state.head_position);
object.set_position(head_position); self.state.head_position.x += letter.width as i32;
head_position.x += letter_group.width as i32;
object.show(); object.show();
objects.push(object); self.objects.push(object);
group_depth += 1; self.state.rendered_groups += 1;
rendered_groups += 1; self.state.word_depth += 1;
if rendered_groups >= number_of_groups { if self.state.rendered_groups >= number_of_groups {
break 'outer; break 'outer;
} }
} }
word_depth += 1;
self.state.word_depth = 0;
self.state.line_element_depth += 1;
} }
PreprocessedElement::WhiteSpace(space_type) => { PreprocessedElement::WhiteSpace(space_type) => {
if space_type == WhiteSpace::NewLine { if *space_type == WhiteSpace::NewLine {
head_position.y += paragraph_spacing; self.state.head_position.y += self.settings.paragraph_spacing;
} }
head_position.x += settings.space_width; self.state.head_position.x += settings.space_width;
rendered_groups += 1; self.state.rendered_groups += 1;
if rendered_groups >= number_of_groups { self.state.line_element_depth += 1;
if self.state.rendered_groups >= number_of_groups {
break 'outer; break 'outer;
} }
} }
} }
} }
processed_depth += line.number_of_text_elements(); self.state.head_position.y += render.font.line_height();
head_position.x = area.position.x; self.state.head_position.x = self.settings.area.position.x;
head_position.y += 9;
self.state.line_element_depth = 0;
self.state.line_depth += 1;
}
} }
objects pub fn update(
&mut self,
r: &mut BufferedRender<'_>,
area: Rect<i32>,
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();
}
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<i32>,
alignment: TextAlignment,
paragraph_spacing: i32,
}
#[derive(Default)]
struct LayoutCacheState {
head_position: Vector2D<i32>,
processed_depth: usize,
group_depth: usize,
word_depth: usize,
rendered_groups: usize,
line_depth: usize,
line_element_depth: usize,
}

View file

@ -1,3 +1,5 @@
use core::num::NonZeroU8;
use alloc::vec::Vec; use alloc::vec::Vec;
use crate::display::Font; use crate::display::Font;
@ -6,50 +8,53 @@ use super::WhiteSpace;
#[derive(Debug, PartialEq, Eq)] #[derive(Debug, PartialEq, Eq)]
pub(crate) enum PreprocessedElement { pub(crate) enum PreprocessedElement {
Word(u8), Word(Word),
WhiteSpace(WhiteSpace), WhiteSpace(WhiteSpace),
} }
#[test_case]
fn check_size_of_preprocessed_element_is_correct(_: &mut crate::Gba) {
assert_eq!(
core::mem::size_of::<PreprocessedElement>(),
core::mem::size_of::<Word>()
);
}
#[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)] #[derive(Clone, Copy, PartialEq, Eq)]
struct PreprocessedElementStored(u8); pub(crate) 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)] #[derive(Default, Debug)]
pub(crate) struct Preprocessed { pub(crate) struct Preprocessed {
widths: Vec<PreprocessedElementStored>, widths: Vec<PreprocessedElement>,
preprocessor: Preprocessor, preprocessor: Preprocessor,
} }
#[derive(Debug, Default)] #[derive(Debug, Default)]
struct Preprocessor { struct Preprocessor {
current_word_width: i32, current_word_width: i32,
number_of_sprites: usize,
width_in_sprite: i32,
total_number_of_sprites: usize,
} }
impl Preprocessor { impl Preprocessor {
@ -57,27 +62,44 @@ impl Preprocessor {
&mut self, &mut self,
font: &Font, font: &Font,
character: char, character: char,
widths: &mut Vec<PreprocessedElementStored>, sprite_width: i32,
widths: &mut Vec<PreprocessedElement>,
) { ) {
match character { match character {
space @ (' ' | '\n') => { space @ (' ' | '\n') => {
if self.current_word_width != 0 { if self.current_word_width != 0 {
widths.push(PreprocessedElementStored::from_element( self.number_of_sprites += 1;
PreprocessedElement::Word( self.total_number_of_sprites += 1;
self.current_word_width 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() .try_into()
.expect("word should be small and positive"), .expect("out of range"),
), }));
));
self.current_word_width = 0; self.current_word_width = 0;
self.number_of_sprites = 0;
self.width_in_sprite = 0;
} }
widths.push(PreprocessedElementStored::from_element( widths.push(PreprocessedElement::WhiteSpace(WhiteSpace::from_char(
PreprocessedElement::WhiteSpace(WhiteSpace::from_char(space)), space,
)); )));
} }
letter => { letter => {
let letter = font.letter(letter); let letter = font.letter(letter);
self.current_word_width += letter.advance_width as i32 + letter.xmin as i32; 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> { pub(crate) struct Lines<'preprocess> {
minimum_space_width: i32, minimum_space_width: i32,
layout_width: i32, layout_width: i32,
data: &'preprocess [PreprocessedElementStored], data: &'preprocess [PreprocessedElement],
current_start_idx: usize, current_start_idx: usize,
} }
@ -131,11 +153,11 @@ impl<'pre> Iterator for Lines<'pre> {
let mut number_of_words = 0; let mut number_of_words = 0;
while let Some(next) = self.data.get(self.current_start_idx + line_idx_length) { while let Some(next) = self.data.get(self.current_start_idx + line_idx_length) {
match next.parse() { match next {
PreprocessedElement::Word(pixels) => { PreprocessedElement::Word(word) => {
let additional_space_width = let additional_space_width =
additional_space_count as i32 * self.minimum_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 { if width + current_line_width + additional_space_width > self.layout_width {
break; break;
} }
@ -173,12 +195,9 @@ impl Preprocessed {
Default::default() Default::default()
} }
pub(crate) fn get(&self, idx: usize) -> PreprocessedElement { pub(crate) fn add_character(&mut self, font: &Font, c: char, sprite_width: i32) {
self.widths[idx].parse() self.preprocessor
} .add_character(font, c, sprite_width, &mut self.widths);
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<'_> { 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, &self,
layout_width: i32, layout_width: i32,
minimum_space_width: i32, minimum_space_width: i32,
) -> impl Iterator<Item = &[PreprocessedElementStored]> { ) -> impl Iterator<Item = (Line, &[PreprocessedElement])> {
let mut idx = 0; let mut idx = 0;
self.lines(layout_width, minimum_space_width).map(move |x| { self.lines(layout_width, minimum_space_width).map(move |x| {
let length = x.number_of_text_elements; let length = x.number_of_text_elements;
let d = &self.widths[idx..(idx + length)]; let d = &self.widths[idx..(idx + length)];
idx += length; idx += length;
d (x, d)
}) })
} }
} }

View file

@ -50,6 +50,10 @@ pub(crate) struct WordRender {
} }
impl WordRender { impl WordRender {
pub(crate) fn sprite_width(&self) -> i32 {
self.config.sprite_size.to_width_height().0 as i32
}
#[must_use] #[must_use]
pub(crate) fn new(config: Configuration) -> Self { pub(crate) fn new(config: Configuration) -> Self {
WordRender { WordRender {