buffered text render

This commit is contained in:
Corwin 2023-06-26 17:07:50 +01:00
parent 87ac2fe53c
commit b75303863d
No known key found for this signature in database
3 changed files with 224 additions and 153 deletions

View file

@ -4,17 +4,15 @@
use agb::{ use agb::{
display::{ display::{
object::{ object::{
font::{Configuration, WordRender}, font::{BufferedWordRender, Configuration},
PaletteVram, Size, PaletteVram, Size,
}, },
palette16::Palette16, palette16::Palette16,
Font, Font,
}, },
fixnum::num,
include_font, include_font,
timer::Divider, input::Button,
}; };
use agb_fixnum::Num;
use core::fmt::Write; use core::fmt::Write;
@ -25,54 +23,55 @@ fn entry(gba: agb::Gba) -> ! {
} }
fn main(mut gba: agb::Gba) -> ! { fn main(mut gba: agb::Gba) -> ! {
let (mut unmanaged, mut sprites) = gba.display.object.get_unmanaged(); let (mut unmanaged, _sprites) = gba.display.object.get_unmanaged();
let mut palette = [0x0; 16];
palette[1] = 0xFF_FF;
let palette = Palette16::new(palette);
let palette = PaletteVram::new(&palette).unwrap();
let config = Configuration::new(Size::S32x16, palette);
let mut wr = WordRender::new(&FONT, config);
let mut number: Num<i32, 8> = num!(1.25235);
let vblank = agb::interrupt::VBlank::get();
let mut input = agb::input::ButtonController::new();
let timer = gba.timers.timers();
let mut timer = timer.timer2;
timer.set_enabled(true);
timer.set_divider(agb::timer::Divider::Divider64);
loop { loop {
vblank.wait_for_vblank(); let mut palette = [0x0; 16];
input.update(); palette[1] = 0xFF_FF;
let palette = Palette16::new(palette);
let palette = PaletteVram::new(&palette).unwrap();
number += num!(0.01) * input.y_tri() as i32; let config = Configuration::new(Size::S32x16, palette);
let start = timer.value(); let mut wr = BufferedWordRender::new(&FONT, config);
let _ = writeln!(
wr,
"Hello there!\nI spent this weekend\nwriting this text system!\nIs it any good?\n\nOh, by the way, you can\npress A to restart!"
);
let _ = writeln!(wr, "abcdefgh ijklmnopq rstuvwxyz"); let vblank = agb::interrupt::VBlank::get();
let line = wr.get_line(); let mut input = agb::input::ButtonController::new();
let rasterised = timer.value();
let oam_frmae = &mut unmanaged.iter(); let timer = gba.timers.timers();
line.unwrap().draw(oam_frmae); let mut timer: agb::timer::Timer = timer.timer2;
let drawn = timer.value();
let start_to_end = to_ms(drawn.wrapping_sub(start)); timer.set_enabled(true);
let raster = to_ms(rasterised.wrapping_sub(start)); timer.set_divider(agb::timer::Divider::Divider64);
let object = to_ms(drawn.wrapping_sub(rasterised));
agb::println!("Start: {start_to_end:.3}"); let mut num_letters = 0;
agb::println!("Raster: {raster:.3}"); let mut frame = 0;
agb::println!("Object: {object:.3}");
loop {
vblank.wait_for_vblank();
input.update();
let oam_frmae = &mut unmanaged.iter();
let start = timer.value();
wr.draw_partial(oam_frmae, (0, 0).into(), num_letters);
let end = timer.value();
agb::println!("Took {} cycles", 64 * (end.wrapping_sub(start) as u32));
frame += 1;
if frame % 4 == 0 {
num_letters += 1;
}
wr.process();
if input.is_just_pressed(Button::A) {
break;
}
}
} }
} }
fn to_ms(time: u16) -> Num<i32, 8> {
Num::new(time as i32) * num!(3.815) / 1000
}

View file

@ -9,60 +9,91 @@ use super::{DynamicSprite, OamIterator, PaletteVram, Size, SpriteVram};
struct LetterGroup { struct LetterGroup {
sprite: SpriteVram, sprite: SpriteVram,
/// x offset from the *start* of the *word* // the width of the letter group
offset: i32, width: u16,
left: i16,
} }
struct Word { enum WhiteSpace {
start_index: usize, Space,
end_index: usize, NewLine,
size: i32,
} }
impl Word { enum TextElement {
fn number_of_letter_groups(&self) -> usize { LetterGroup(LetterGroup),
self.end_index - self.start_index WhiteSpace(WhiteSpace),
}
#[cfg(test)]
mod tests {
use super::*;
#[test_case]
fn check_size_of_text_element_is_expected(_: &mut crate::Gba) {
assert_eq!(
core::mem::size_of::<TextElement>(),
core::mem::size_of::<LetterGroup>()
);
} }
} }
pub struct MetaWords { pub struct TextBlock {
letters: Vec<LetterGroup>, elements: Vec<TextElement>,
words: Vec<Word>, cache: CachedRender,
} }
impl MetaWords { pub struct CachedRender {
const fn new_empty() -> Self { objects: Vec<ObjectUnmanaged>,
Self { up_to: usize,
letters: Vec::new(), head_position: Vector2D<i32>,
words: Vec::new(), origin: Vector2D<i32>,
} }
impl TextBlock {
fn reset_cache(&mut self, position: Vector2D<i32>) {
self.cache.objects.clear();
self.cache.up_to = 0;
self.cache.head_position = position;
self.cache.origin = position;
} }
fn word_iter(&self) -> impl Iterator<Item = (i32, &[LetterGroup])> { fn generate_cache(&mut self, up_to: usize) {
self.words let mut head_position = self.cache.head_position;
.iter()
.map(|x| (x.size, &self.letters[x.start_index..x.end_index]))
}
pub fn draw(&self, oam: &mut OamIterator) { for element in self.elements.iter().take(up_to).skip(self.cache.up_to) {
fn inner_draw(mw: &MetaWords, oam: &mut OamIterator) -> Option<()> { match element {
let mut word_offset = 0; TextElement::LetterGroup(group) => {
let mut object = ObjectUnmanaged::new(group.sprite.clone());
for (size, word) in mw.word_iter() {
for letter_group in word.iter() {
let mut object = ObjectUnmanaged::new(letter_group.sprite.clone());
object.set_position((word_offset + letter_group.offset, 0).into());
object.show(); object.show();
oam.next()?.set(&object); head_position.x += group.left as i32;
object.set_position(head_position);
head_position.x += group.width as i32;
self.cache.objects.push(object);
} }
TextElement::WhiteSpace(white) => match white {
word_offset += size + 10; WhiteSpace::Space => head_position.x += 10,
WhiteSpace::NewLine => {
head_position.x = self.cache.origin.x;
head_position.y += 15;
}
},
} }
Some(())
} }
let _ = inner_draw(self, oam); self.cache.head_position = head_position;
self.cache.up_to = up_to.min(self.elements.len());
}
fn draw(&mut self, oam: &mut OamIterator, position: Vector2D<i32>, up_to: usize) {
if position != self.cache.origin {
self.reset_cache(position);
}
self.generate_cache(up_to);
for (obj, slot) in self.cache.objects.iter().zip(oam) {
slot.set(obj);
}
} }
} }
@ -71,7 +102,7 @@ struct WorkingLetter {
// the x offset of the current letter with respect to the start of the current letter group // the x offset of the current letter with respect to the start of the current letter group
x_position: i32, x_position: i32,
// where to render the letter from x_min to x_max // where to render the letter from x_min to x_max
x_offset: usize, x_offset: i32,
} }
impl WorkingLetter { impl WorkingLetter {
@ -105,118 +136,159 @@ impl Configuration {
} }
} }
pub struct WordRender<'font> { pub struct BufferedWordRender<'font> {
font: &'font Font, word_render: WordRender<'font>,
working: Working, block: TextBlock,
finalised_metas: VecDeque<MetaWords>, buffered_chars: VecDeque<char>,
config: Configuration,
} }
struct Working { impl Write for BufferedWordRender<'_> {
letter: WorkingLetter,
meta: MetaWords,
word_offset: i32,
}
impl<'font> Write for WordRender<'font> {
fn write_str(&mut self, s: &str) -> core::fmt::Result { fn write_str(&mut self, s: &str) -> core::fmt::Result {
for c in s.chars() { for char in s.chars() {
self.write_char(c); self.buffered_chars.push_back(char);
} }
Ok(()) Ok(())
} }
} }
impl<'font> WordRender<'font> { impl<'font> BufferedWordRender<'font> {
#[must_use] #[must_use]
pub fn new(font: &'font Font, config: Configuration) -> Self { pub fn new(font: &'font Font, config: Configuration) -> Self {
BufferedWordRender {
word_render: WordRender::new(font, config),
block: TextBlock {
elements: Vec::new(),
cache: CachedRender {
objects: Vec::new(),
up_to: 0,
head_position: (0, 0).into(),
origin: (0, 0).into(),
},
},
buffered_chars: VecDeque::new(),
}
}
}
impl BufferedWordRender<'_> {
pub fn process(&mut self) {
if let Some(char) = self.buffered_chars.pop_front() {
if char == '\n' {
if let Some(group) = self.word_render.finalise_letter() {
self.block.elements.push(TextElement::LetterGroup(group));
}
self.block
.elements
.push(TextElement::WhiteSpace(WhiteSpace::NewLine));
} else if char == ' ' {
if let Some(group) = self.word_render.finalise_letter() {
self.block.elements.push(TextElement::LetterGroup(group));
}
self.block
.elements
.push(TextElement::WhiteSpace(WhiteSpace::Space));
} else if let Some(group) = self.word_render.render_char(char) {
self.block.elements.push(TextElement::LetterGroup(group));
}
}
}
pub fn draw_partial(
&mut self,
oam: &mut OamIterator,
position: Vector2D<i32>,
num_groups: usize,
) {
while self.block.elements.len() < num_groups && !self.buffered_chars.is_empty() {
self.process();
}
self.block.draw(oam, position, num_groups);
}
pub fn draw(&mut self, oam: &mut OamIterator, position: Vector2D<i32>) {
while !self.buffered_chars.is_empty() {
self.process();
}
self.block.draw(oam, position, usize::MAX);
}
}
struct WordRender<'font> {
font: &'font Font,
working: Working,
config: Configuration,
}
struct Working {
letter: WorkingLetter,
}
impl<'font> WordRender<'font> {
#[must_use]
fn new(font: &'font Font, config: Configuration) -> Self {
WordRender { WordRender {
font, font,
working: Working { working: Working {
letter: WorkingLetter::new(config.sprite_size), letter: WorkingLetter::new(config.sprite_size),
meta: MetaWords::new_empty(),
word_offset: 0,
}, },
finalised_metas: VecDeque::new(),
config, config,
} }
} }
} }
impl WordRender<'_> { impl WordRender<'_> {
pub fn get_line(&mut self) -> Option<MetaWords> { #[must_use]
self.finalised_metas.pop_front() fn finalise_letter(&mut self) -> Option<LetterGroup> {
} if self.working.letter.x_offset == 0 {
return None;
fn write_char(&mut self, c: char) {
if c == '\n' {
self.finalise_line();
} else if c == ' ' {
self.finalise_word();
} else {
self.render_char(c);
} }
}
fn finalise_line(&mut self) {
self.finalise_word();
let mut final_meta = MetaWords::new_empty();
core::mem::swap(&mut self.working.meta, &mut final_meta);
self.finalised_metas.push_back(final_meta);
}
fn finalise_word(&mut self) {
self.finalise_letter();
let start_index = self.working.meta.words.last().map_or(0, |x| x.end_index);
let end_index = self.working.meta.letters.len();
let word = Word {
start_index,
end_index,
size: self.working.word_offset,
};
self.working.meta.words.push(word);
self.working.word_offset = 0;
}
fn finalise_letter(&mut self) {
let sprite = self let sprite = self
.working .working
.letter .letter
.dynamic .dynamic
.to_vram(self.config.palette.clone()); .to_vram(self.config.palette.clone());
self.working.meta.letters.push(LetterGroup { let group = LetterGroup {
sprite, sprite,
offset: self.working.word_offset, width: self.working.letter.x_offset as u16,
}); left: self.working.letter.x_position as i16,
self.working.word_offset += self.working.letter.x_position; };
self.working.letter.reset(); self.working.letter.reset();
Some(group)
} }
fn render_char(&mut self, c: char) { #[must_use]
fn render_char(&mut self, c: char) -> Option<LetterGroup> {
let font_letter = self.font.letter(c); let font_letter = self.font.letter(c);
// uses more than the sprite can hold // uses more than the sprite can hold
if self.working.letter.x_offset + font_letter.width as usize let group = if self.working.letter.x_offset + font_letter.width as i32
> self.config.sprite_size.to_width_height().0 > self.config.sprite_size.to_width_height().0 as i32
{ {
self.finalise_letter(); self.finalise_letter()
} else {
None
};
if self.working.letter.x_offset == 0 {
self.working.letter.x_position = font_letter.xmin as i32;
} else {
self.working.letter.x_offset += font_letter.xmin as i32;
} }
self.working.letter.x_position += font_letter.xmin as i32; let y_position =
self.font.ascent() - font_letter.height as i32 - font_letter.ymin as i32 + 4;
let y_position = self.font.ascent() - font_letter.height as i32 - font_letter.ymin as i32;
for y in 0..font_letter.height as usize { for y in 0..font_letter.height as usize {
for x in 0..font_letter.width as usize { for x in 0..font_letter.width as usize {
let rendered = font_letter.bit_absolute(x, y); let rendered = font_letter.bit_absolute(x, y);
if rendered { if rendered {
self.working.letter.dynamic.set_pixel( self.working.letter.dynamic.set_pixel(
x + self.working.letter.x_offset, x + self.working.letter.x_offset as usize,
(y_position + y as i32) as usize, (y_position + y as i32) as usize,
1, 1,
); );
@ -224,7 +296,8 @@ impl WordRender<'_> {
} }
} }
self.working.letter.x_position += font_letter.advance_width as i32; self.working.letter.x_offset += font_letter.advance_width as i32;
self.working.letter.x_offset += font_letter.advance_width as usize;
group
} }
} }

View file

@ -101,7 +101,6 @@ impl OamSlot<'_> {
/// By writing these as two separate functions, one inlined and one not, the /// By writing these as two separate functions, one inlined and one not, the
/// compiler doesn't have to copy around the slot structure while still /// compiler doesn't have to copy around the slot structure while still
/// keeping move semantics. This is slightly faster in benchmarks. /// keeping move semantics. This is slightly faster in benchmarks.
#[inline(never)]
fn set_inner(&self, object: &ObjectUnmanaged) { fn set_inner(&self, object: &ObjectUnmanaged) {
let mut attributes = object.attributes; let mut attributes = object.attributes;
// SAFETY: This function is not reentrant and we currently hold a mutable borrow of the [UnmanagedOAM]. // SAFETY: This function is not reentrant and we currently hold a mutable borrow of the [UnmanagedOAM].