mirror of
https://github.com/italicsjenga/agb.git
synced 2024-12-23 08:11:33 +11:00
buffered text render
This commit is contained in:
parent
87ac2fe53c
commit
b75303863d
|
@ -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
|
|
||||||
}
|
|
||||||
|
|
|
@ -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
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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].
|
||||||
|
|
Loading…
Reference in a new issue