mirror of
https://github.com/italicsjenga/agb.git
synced 2024-12-23 08:11:33 +11:00
caching layout
This commit is contained in:
parent
f947d82049
commit
cf400029f5
|
@ -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;
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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 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;
|
||||
|
||||
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);
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
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,
|
||||
}
|
||||
|
|
|
@ -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
|
||||
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("word should be small and positive"),
|
||||
),
|
||||
));
|
||||
.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)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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 {
|
||||
|
|
Loading…
Reference in a new issue