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::{
display::{
object::{
font::{BufferedRender, LayoutCache, TextAlignment},
font::{ObjectTextRender, TextAlignment},
PaletteVram, Size,
},
palette16::Palette16,
@ -35,7 +35,7 @@ fn main(mut gba: agb::Gba) -> ! {
let palette = Palette16::new(palette);
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!(
wr,
"{}",
@ -52,55 +52,30 @@ fn main(mut gba: agb::Gba) -> ! {
timer.set_enabled(true);
timer.set_divider(agb::timer::Divider::Divider256);
let mut num_letters = 0;
let mut alignment = TextAlignment::Left;
let mut cache = LayoutCache::new();
wr.set_alignment(TextAlignment::Left);
wr.set_size((WIDTH / 3, 20).into());
wr.set_paragraph_spacing(2);
wr.layout();
loop {
vblank.wait_for_vblank();
input.update();
let oam = &mut unmanaged.iter();
cache.commit(oam);
wr.commit(oam, (WIDTH / 3, 0).into());
let start = timer.value();
wr.process();
cache.update(
&mut wr,
Rect::new((WIDTH / 3, 0).into(), (WIDTH / 3, 100).into()),
alignment,
2,
num_letters,
);
let line_done = !wr.next_letter_group();
if line_done && input.is_just_pressed(Button::A) {
wr.pop_line();
}
wr.layout();
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!(
"Drop took {} cycles",
256 * (end.wrapping_sub(start) as u32)
"Took {} cycles, line done {}",
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 alloc::{collections::VecDeque, vec::Vec};
use crate::display::Font;
use crate::display::{object::font::preprocess::Word, Font};
use self::{
preprocess::{Line, Preprocessed, PreprocessedElement},
@ -15,7 +15,7 @@ use super::{OamIterator, ObjectUnmanaged, PaletteVram, Size, SpriteVram};
mod preprocess;
mod renderer;
#[derive(Debug, PartialEq, Eq)]
#[derive(Debug, PartialEq, Eq, Clone, Copy)]
#[non_exhaustive]
pub(crate) enum WhiteSpace {
NewLine,
@ -50,23 +50,14 @@ pub struct BufferedRender<'font> {
#[derive(Debug, Default)]
struct Letters {
letters: Vec<LetterGroup>,
letters: VecDeque<LetterGroup>,
number_of_groups: usize,
}
impl Write for BufferedRender<'_> {
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)]
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
#[non_exhaustive]
pub enum TextAlignment {
#[default]
Left,
Right,
Center,
@ -103,7 +94,7 @@ impl TextAlignment {
impl<'font> BufferedRender<'font> {
#[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);
BufferedRender {
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<'_> {
fn input_character(&mut self, character: char) {
if !is_private_use(character) {
self.preprocessor
.add_character(self.font, character, self.char_render.sprite_width());
}
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 };
match c {
' ' | '\n' => {
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;
}
@ -135,7 +132,7 @@ impl BufferedRender<'_> {
}
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;
}
}
@ -143,32 +140,173 @@ impl BufferedRender<'_> {
}
}
pub struct LayoutCache {
objects: Vec<ObjectUnmanaged>,
state: LayoutCacheState,
pub struct ObjectTextRender<'font> {
buffer: BufferedRender<'font>,
layout: LayoutCache,
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 {
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 lines = render
.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) {
let settings = self.settings.alignment.settings(
&line,
minimum_space_width,
self.settings.area.size,
);
let settings =
self.settings
.alignment
.settings(&line, minimum_space_width, self.settings.area);
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 {
PreprocessedElement::Word(word) => {
for letter in (word.sprite_index()
@ -177,14 +315,24 @@ impl LayoutCache {
.map(|x| &render.letters.letters[x])
{
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();
self.objects.push(object);
self.state.head_offset.x += letter.left as i32;
object
.set_position(self.state.head_offset + self.position)
.show();
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.word_depth += 1;
if self.state.rendered_groups >= number_of_groups {
if self.state.rendered_groups >= self.desired_number_of_groups {
break 'outer;
}
}
@ -193,36 +341,39 @@ impl LayoutCache {
self.state.line_element_depth += 1;
}
PreprocessedElement::WhiteSpace(space_type) => {
if *space_type == WhiteSpace::NewLine {
self.state.head_position.y += self.settings.paragraph_spacing;
if space_type == WhiteSpace::NewLine {
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.line_element_depth += 1;
if self.state.rendered_groups >= number_of_groups {
if self.state.rendered_groups >= self.desired_number_of_groups {
break 'outer;
}
}
}
}
self.state.head_position.y += render.font.line_height();
self.state.head_position.x = self.settings.area.position.x;
self.state.head_offset.y += render.font.line_height();
self.state.head_offset.x = 0;
self.state.line_element_depth = 0;
self.state.line_depth += 1;
}
}
pub fn update(
fn update(
&mut self,
r: &mut BufferedRender<'_>,
area: Rect<i32>,
area: Vector2D<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();
while !r.buffered_chars.is_empty()
&& r.letters.number_of_groups <= self.desired_number_of_groups
{
r.process();
}
@ -235,32 +386,43 @@ impl LayoutCache {
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) {
slot.set(object);
slot.set(&object.object);
}
}
}
#[must_use]
pub fn new() -> Self {
fn new() -> Self {
Self {
objects: Vec::new(),
objects: VecDeque::new(),
state: Default::default(),
settings: LayoutSettings {
area: Rect::new((0, 0).into(), (0, 0).into()),
area: (0, 0).into(),
alignment: TextAlignment::Right,
paragraph_spacing: -100,
},
desired_number_of_groups: 0,
position: (0, 0).into(),
}
}
fn reset(&mut self, settings: LayoutSettings) {
self.objects.clear();
self.state = LayoutCacheState {
head_position: settings.area.position,
head_offset: (0, 0).into(),
word_depth: 0,
rendered_groups: 0,
line_depth: 0,
@ -270,16 +432,16 @@ impl LayoutCache {
}
}
#[derive(PartialEq, Eq)]
#[derive(PartialEq, Eq, Default)]
struct LayoutSettings {
area: Rect<i32>,
area: Vector2D<i32>,
alignment: TextAlignment,
paragraph_spacing: i32,
}
#[derive(Default)]
struct LayoutCacheState {
head_position: Vector2D<i32>,
head_offset: Vector2D<i32>,
word_depth: usize,
rendered_groups: usize,
line_depth: usize,

View file

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