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::{
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;

View file

@ -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 {

View file

@ -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<LetterGroup>,
word_lengths: Vec<u8>,
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<i32>,
alignment: TextAlignment,
number_of_groups: usize,
paragraph_spacing: i32,
) -> Vec<ObjectUnmanaged> {
let mut objects = Vec::new();
pub struct LayoutCache {
objects: Vec<ObjectUnmanaged>,
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<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();
}
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<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 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::<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)]
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<PreprocessedElementStored>,
widths: Vec<PreprocessedElement>,
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<PreprocessedElementStored>,
sprite_width: i32,
widths: &mut Vec<PreprocessedElement>,
) {
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<Item = &[PreprocessedElementStored]> {
) -> impl Iterator<Item = (Line, &[PreprocessedElement])> {
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)
})
}
}

View file

@ -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 {