garbage renderer

This commit is contained in:
Corwin 2023-07-01 19:12:39 +01:00
parent 93024f6bab
commit ec3003c81d
No known key found for this signature in database
3 changed files with 264 additions and 113 deletions

View file

@ -4,7 +4,7 @@
use agb::{ use agb::{
display::{ display::{
object::{ object::{
font::{BufferedRender, LayoutCache, TextAlignment}, font::{ObjectTextRender, TextAlignment},
PaletteVram, Size, PaletteVram, Size,
}, },
palette16::Palette16, palette16::Palette16,
@ -35,7 +35,7 @@ fn main(mut gba: agb::Gba) -> ! {
let palette = Palette16::new(palette); let palette = Palette16::new(palette);
let palette = PaletteVram::new(&palette).unwrap(); 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!( let _ = writeln!(
wr, wr,
"{}", "{}",
@ -52,55 +52,30 @@ fn main(mut gba: agb::Gba) -> ! {
timer.set_enabled(true); timer.set_enabled(true);
timer.set_divider(agb::timer::Divider::Divider256); timer.set_divider(agb::timer::Divider::Divider256);
let mut num_letters = 0; wr.set_alignment(TextAlignment::Left);
wr.set_size((WIDTH / 3, 20).into());
let mut alignment = TextAlignment::Left; wr.set_paragraph_spacing(2);
wr.layout();
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();
cache.commit(oam); wr.commit(oam, (WIDTH / 3, 0).into());
let start = timer.value(); let start = timer.value();
wr.process(); let line_done = !wr.next_letter_group();
cache.update( if line_done && input.is_just_pressed(Button::A) {
&mut wr, wr.pop_line();
Rect::new((WIDTH / 3, 0).into(), (WIDTH / 3, 100).into()), }
alignment, wr.layout();
2,
num_letters,
);
let end = timer.value(); 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;
}
}
let start = timer.value();
drop(wr);
let oam = unmanaged.iter();
drop(oam);
let end = timer.value();
agb::println!( agb::println!(
"Drop took {} cycles", "Took {} cycles, line done {}",
256 * (end.wrapping_sub(start) as u32) 256 * (end.wrapping_sub(start) as u32),
line_done
); );
} }
}
} }

View file

@ -3,7 +3,7 @@ use core::fmt::Write;
use agb_fixnum::{Rect, Vector2D}; use agb_fixnum::{Rect, Vector2D};
use alloc::{collections::VecDeque, vec::Vec}; use alloc::{collections::VecDeque, vec::Vec};
use crate::display::Font; use crate::display::{object::font::preprocess::Word, Font};
use self::{ use self::{
preprocess::{Line, Preprocessed, PreprocessedElement}, preprocess::{Line, Preprocessed, PreprocessedElement},
@ -15,7 +15,7 @@ use super::{OamIterator, ObjectUnmanaged, PaletteVram, Size, SpriteVram};
mod preprocess; mod preprocess;
mod renderer; mod renderer;
#[derive(Debug, PartialEq, Eq)] #[derive(Debug, PartialEq, Eq, Clone, Copy)]
#[non_exhaustive] #[non_exhaustive]
pub(crate) enum WhiteSpace { pub(crate) enum WhiteSpace {
NewLine, NewLine,
@ -50,23 +50,14 @@ pub struct BufferedRender<'font> {
#[derive(Debug, Default)] #[derive(Debug, Default)]
struct Letters { struct Letters {
letters: Vec<LetterGroup>, letters: VecDeque<LetterGroup>,
number_of_groups: usize, number_of_groups: usize,
} }
impl Write for BufferedRender<'_> { #[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
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] #[non_exhaustive]
pub enum TextAlignment { pub enum TextAlignment {
#[default]
Left, Left,
Right, Right,
Center, Center,
@ -103,7 +94,7 @@ impl TextAlignment {
impl<'font> BufferedRender<'font> { impl<'font> BufferedRender<'font> {
#[must_use] #[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); let config = Configuration::new(sprite_size, palette);
BufferedRender { BufferedRender {
char_render: WordRender::new(config), 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<'_> { impl BufferedRender<'_> {
fn input_character(&mut self, character: char) { fn input_character(&mut self, character: char) {
if !is_private_use(character) {
self.preprocessor self.preprocessor
.add_character(self.font, character, self.char_render.sprite_width()); .add_character(self.font, character, self.char_render.sprite_width());
}
self.buffered_chars.push_back(character); 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 }; let Some(c) = self.buffered_chars.pop_front() else { return };
match c { match c {
' ' | '\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_back(group);
self.letters.number_of_groups += 1; self.letters.number_of_groups += 1;
} }
@ -135,7 +132,7 @@ impl BufferedRender<'_> {
} }
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_back(group);
self.letters.number_of_groups += 1; self.letters.number_of_groups += 1;
} }
} }
@ -143,32 +140,173 @@ impl BufferedRender<'_> {
} }
} }
pub struct LayoutCache { pub struct ObjectTextRender<'font> {
objects: Vec<ObjectUnmanaged>, buffer: BufferedRender<'font>,
state: LayoutCacheState, layout: LayoutCache,
settings: LayoutSettings, 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<i32>) {
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<i32>) {
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<i16>,
}
struct LayoutCache {
objects: VecDeque<LayoutObject>,
state: LayoutCacheState,
settings: LayoutSettings,
desired_number_of_groups: usize,
position: Vector2D<i32>,
}
impl LayoutCache { 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 minimum_space_width = render.font.letter(' ').advance_width as i32;
let lines = render let lines = render
.preprocessor .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) { 'outer: for (line, line_elements) in lines.skip(self.state.line_depth) {
let settings = self.settings.alignment.settings( let settings =
&line, self.settings
minimum_space_width, .alignment
self.settings.area.size, .settings(&line, minimum_space_width, self.settings.area);
);
if self.state.line_element_depth == 0 { 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 { match element {
PreprocessedElement::Word(word) => { PreprocessedElement::Word(word) => {
for letter in (word.sprite_index() for letter in (word.sprite_index()
@ -177,14 +315,24 @@ impl LayoutCache {
.map(|x| &render.letters.letters[x]) .map(|x| &render.letters.letters[x])
{ {
let mut object = ObjectUnmanaged::new(letter.sprite.clone()); let mut object = ObjectUnmanaged::new(letter.sprite.clone());
self.state.head_position.x += letter.left as i32; self.state.head_offset.x += letter.left as i32;
object.set_position(self.state.head_position); object
self.state.head_position.x += letter.width as i32; .set_position(self.state.head_offset + self.position)
object.show(); .show();
self.objects.push(object);
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.rendered_groups += 1;
self.state.word_depth += 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; break 'outer;
} }
} }
@ -193,36 +341,39 @@ impl LayoutCache {
self.state.line_element_depth += 1; self.state.line_element_depth += 1;
} }
PreprocessedElement::WhiteSpace(space_type) => { PreprocessedElement::WhiteSpace(space_type) => {
if *space_type == WhiteSpace::NewLine { if space_type == WhiteSpace::NewLine {
self.state.head_position.y += self.settings.paragraph_spacing; 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.rendered_groups += 1;
self.state.line_element_depth += 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; break 'outer;
} }
} }
} }
} }
self.state.head_position.y += render.font.line_height(); self.state.head_offset.y += render.font.line_height();
self.state.head_position.x = self.settings.area.position.x; self.state.head_offset.x = 0;
self.state.line_element_depth = 0; self.state.line_element_depth = 0;
self.state.line_depth += 1; self.state.line_depth += 1;
} }
} }
pub fn update( fn update(
&mut self, &mut self,
r: &mut BufferedRender<'_>, r: &mut BufferedRender<'_>,
area: Rect<i32>, area: Vector2D<i32>,
alignment: TextAlignment, alignment: TextAlignment,
paragraph_spacing: i32, 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(); r.process();
} }
@ -235,32 +386,43 @@ impl LayoutCache {
self.reset(settings); self.reset(settings);
} }
self.update_cache(number_of_groups, r); self.update_cache(r);
} }
pub fn commit(&self, oam: &mut OamIterator) { fn commit(&mut self, oam: &mut OamIterator, position: Vector2D<i32>) {
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) { for (object, slot) in self.objects.iter().zip(oam) {
slot.set(object); slot.set(&object.object);
}
} }
} }
#[must_use] #[must_use]
pub fn new() -> Self { fn new() -> Self {
Self { Self {
objects: Vec::new(), objects: VecDeque::new(),
state: Default::default(), state: Default::default(),
settings: LayoutSettings { settings: LayoutSettings {
area: Rect::new((0, 0).into(), (0, 0).into()), area: (0, 0).into(),
alignment: TextAlignment::Right, alignment: TextAlignment::Right,
paragraph_spacing: -100, paragraph_spacing: -100,
}, },
desired_number_of_groups: 0,
position: (0, 0).into(),
} }
} }
fn reset(&mut self, settings: LayoutSettings) { fn reset(&mut self, settings: LayoutSettings) {
self.objects.clear(); self.objects.clear();
self.state = LayoutCacheState { self.state = LayoutCacheState {
head_position: settings.area.position, head_offset: (0, 0).into(),
word_depth: 0, word_depth: 0,
rendered_groups: 0, rendered_groups: 0,
line_depth: 0, line_depth: 0,
@ -270,16 +432,16 @@ impl LayoutCache {
} }
} }
#[derive(PartialEq, Eq)] #[derive(PartialEq, Eq, Default)]
struct LayoutSettings { struct LayoutSettings {
area: Rect<i32>, area: Vector2D<i32>,
alignment: TextAlignment, alignment: TextAlignment,
paragraph_spacing: i32, paragraph_spacing: i32,
} }
#[derive(Default)] #[derive(Default)]
struct LayoutCacheState { struct LayoutCacheState {
head_position: Vector2D<i32>, head_offset: Vector2D<i32>,
word_depth: usize, word_depth: usize,
rendered_groups: usize, rendered_groups: usize,
line_depth: usize, line_depth: usize,

View file

@ -1,12 +1,12 @@
use core::num::NonZeroU8; use core::num::NonZeroU8;
use alloc::vec::Vec; use alloc::{collections::VecDeque, vec::Vec};
use crate::display::Font; use crate::display::Font;
use super::WhiteSpace; use super::WhiteSpace;
#[derive(Debug, PartialEq, Eq)] #[derive(Debug, PartialEq, Eq, Clone, Copy)]
pub(crate) enum PreprocessedElement { pub(crate) enum PreprocessedElement {
Word(Word), Word(Word),
WhiteSpace(WhiteSpace), WhiteSpace(WhiteSpace),
@ -40,12 +40,9 @@ impl Word {
} }
} }
#[derive(Clone, Copy, PartialEq, Eq)]
pub(crate) struct PreprocessedElementStored(u8);
#[derive(Default, Debug)] #[derive(Default, Debug)]
pub(crate) struct Preprocessed { pub(crate) struct Preprocessed {
widths: Vec<PreprocessedElement>, widths: VecDeque<PreprocessedElement>,
preprocessor: Preprocessor, preprocessor: Preprocessor,
} }
@ -63,14 +60,14 @@ impl Preprocessor {
font: &Font, font: &Font,
character: char, character: char,
sprite_width: i32, sprite_width: i32,
widths: &mut Vec<PreprocessedElement>, widths: &mut VecDeque<PreprocessedElement>,
) { ) {
match character { match character {
space @ (' ' | '\n') => { space @ (' ' | '\n') => {
if self.current_word_width != 0 { if self.current_word_width != 0 {
self.number_of_sprites += 1; self.number_of_sprites += 1;
self.total_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"), pixels: self.current_word_width.try_into().expect("word too wide"),
number_of_sprites: NonZeroU8::new( number_of_sprites: NonZeroU8::new(
self.number_of_sprites.try_into().expect("word too wide"), self.number_of_sprites.try_into().expect("word too wide"),
@ -84,7 +81,7 @@ impl Preprocessor {
self.number_of_sprites = 0; self.number_of_sprites = 0;
self.width_in_sprite = 0; self.width_in_sprite = 0;
} }
widths.push(PreprocessedElement::WhiteSpace(WhiteSpace::from_char( widths.push_back(PreprocessedElement::WhiteSpace(WhiteSpace::from_char(
space, space,
))); )));
} }
@ -108,7 +105,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 [PreprocessedElement], data: &'preprocess VecDeque<PreprocessedElement>,
current_start_idx: usize, current_start_idx: usize,
} }
@ -117,6 +114,7 @@ pub(crate) struct Line {
number_of_text_elements: usize, number_of_text_elements: usize,
number_of_spaces: usize, number_of_spaces: usize,
number_of_words: usize, number_of_words: usize,
number_of_letter_groups: usize,
} }
impl Line { impl Line {
@ -136,6 +134,11 @@ impl Line {
pub(crate) fn number_of_words(&self) -> usize { pub(crate) fn number_of_words(&self) -> usize {
self.number_of_words 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> { impl<'pre> Iterator for Lines<'pre> {
@ -151,6 +154,7 @@ impl<'pre> Iterator for Lines<'pre> {
let mut additional_space_count = 0; let mut additional_space_count = 0;
let mut number_of_spaces = 0; let mut number_of_spaces = 0;
let mut number_of_words = 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) { while let Some(next) = self.data.get(self.current_start_idx + line_idx_length) {
match next { match next {
@ -161,6 +165,7 @@ impl<'pre> Iterator for Lines<'pre> {
if width + current_line_width + additional_space_width > self.layout_width { if width + current_line_width + additional_space_width > self.layout_width {
break; break;
} }
number_of_letter_groups += word.number_of_sprites.get() as usize;
number_of_words += 1; number_of_words += 1;
current_line_width += width + additional_space_width; current_line_width += width + additional_space_width;
number_of_spaces += additional_space_count; 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_text_elements: line_idx_length,
number_of_spaces, number_of_spaces,
number_of_words, number_of_words,
number_of_letter_groups,
}) })
} }
} }
@ -200,6 +206,13 @@ impl Preprocessed {
.add_character(font, c, sprite_width, &mut self.widths); .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<'_> { pub(crate) fn lines(&self, layout_width: i32, minimum_space_width: i32) -> Lines<'_> {
Lines { Lines {
minimum_space_width, minimum_space_width,
@ -213,11 +226,12 @@ impl Preprocessed {
&self, &self,
layout_width: i32, layout_width: i32,
minimum_space_width: i32, minimum_space_width: i32,
) -> impl Iterator<Item = (Line, &[PreprocessedElement])> { ) -> impl Iterator<Item = (Line, impl Iterator<Item = 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.range(idx..(idx + length)).copied();
idx += length; idx += length;
(x, d) (x, d)
}) })