Textrender rework (#352)

This reworks the text rendering system so the tiles in some rendered
text can be persisted across frames. This is important for performance
in situations where you want to incrementally write to a text box for
example in a Pokemon/Phoenix Wright style dialogue system. Previously
the only way to do this was to render all the chars in the text every
frame, which is currently prohibitively slow (though this could be
potentially sped up).

In addition with this implementation it is easy to individually colour
each character, so I added the ability to do that to.

I expanded the tests to test keeping the renderer instance (which
contains the tiles) over multiple frames, and also added in some
coloured text.

If you are happy with this implementation then I will change the
examples which use text, and also add some more documentation. Thanks
for taking a look.
This commit is contained in:
Gwilym Kuiper 2022-11-21 23:14:40 +00:00 committed by GitHub
commit 99ce2f73d5
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
6 changed files with 130 additions and 58 deletions

View file

@ -6,6 +6,9 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
## [Unreleased]
### Changed
- Text renderer can now be re-used which is useful for rpg style character/word at a time text boxes.
## [0.12.2] - 2022/10/22
This is a minor release to fix an alignment issue with background tiles.

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.3 KiB

After

Width:  |  Height:  |  Size: 2.6 KiB

View file

@ -27,7 +27,8 @@ fn main(mut gba: Gba) -> ! {
init_background(&mut bg, &mut vram);
let mut writer = FONT.render_text((0u16, 3u16).into(), 1, 0, &mut bg, &mut vram);
let mut title_renderer = FONT.render_text((0u16, 3u16).into());
let mut writer = title_renderer.writer(1, 0, &mut bg, &mut vram);
writeln!(&mut writer, "Crazy Glue by Josh Woodward").unwrap();
@ -51,6 +52,7 @@ fn main(mut gba: Gba) -> ! {
let mut frame_counter = 0i32;
let mut has_written_frame_time = false;
let mut stats_renderer = FONT.render_text((0u16, 6u16).into());
loop {
vblank_provider.wait_for_vblank();
bg.commit(&mut vram);
@ -66,7 +68,9 @@ fn main(mut gba: Gba) -> ! {
let percent = (total_cycles * 100) / 280896;
let mut writer = FONT.render_text((0u16, 6u16).into(), 2, 0, &mut bg, &mut vram);
stats_renderer.clear(&mut vram);
let mut writer = stats_renderer.writer(1, 0, &mut bg, &mut vram);
writeln!(&mut writer, "{total_cycles} cycles").unwrap();
writeln!(&mut writer, "{percent} percent").unwrap();

View file

@ -27,10 +27,12 @@ fn main(mut gba: Gba) -> ! {
init_background(&mut bg, &mut vram);
let mut writer = FONT.render_text((0u16, 3u16).into(), 1, 0, &mut bg, &mut vram);
let mut title_renderer = FONT.render_text((0u16, 3u16).into());
let mut writer = title_renderer.writer(1, 0, &mut bg, &mut vram);
writeln!(&mut writer, "Let it in by Josh Woodward").unwrap();
writer.commit();
bg.commit(&mut vram);
@ -49,6 +51,8 @@ fn main(mut gba: Gba) -> ! {
let mut frame_counter = 0i32;
let mut has_written_frame_time = false;
let mut stats_renderer = FONT.render_text((0u16, 6u16).into());
loop {
vblank_provider.wait_for_vblank();
bg.commit(&mut vram);
@ -65,7 +69,7 @@ fn main(mut gba: Gba) -> ! {
let percent = (total_cycles * 100) / 280896;
let mut writer = FONT.render_text((0u16, 6u16).into(), 2, 0, &mut bg, &mut vram);
let mut writer = stats_renderer.writer(1, 0, &mut bg, &mut vram);
writeln!(&mut writer, "{total_cycles} cycles").unwrap();
writeln!(&mut writer, "{percent} percent").unwrap();

View file

@ -40,7 +40,8 @@ fn main(mut gba: agb::Gba) -> ! {
vram.remove_dynamic_tile(background_tile);
let mut writer = FONT.render_text((0u16, 3u16).into(), 1, 2, &mut bg, &mut vram);
let mut renderer = FONT.render_text((0u16, 3u16).into());
let mut writer = renderer.writer(1, 2, &mut bg, &mut vram);
writeln!(&mut writer, "Hello, World!").unwrap();
writeln!(&mut writer, "This is a font rendering example").unwrap();
@ -53,7 +54,8 @@ fn main(mut gba: agb::Gba) -> ! {
let mut frame = 0;
loop {
let mut writer = FONT.render_text((4u16, 0u16).into(), 1, 2, &mut bg, &mut vram);
let mut renderer = FONT.render_text((4u16, 0u16).into());
let mut writer = renderer.writer(1, 2, &mut bg, &mut vram);
writeln!(&mut writer, "Frame {}", frame).unwrap();
writer.commit();
@ -62,5 +64,7 @@ fn main(mut gba: agb::Gba) -> ! {
vblank.wait_for_vblank();
bg.commit(&mut vram);
renderer.clear(&mut vram);
}
}

View file

@ -5,6 +5,10 @@ use crate::hash_map::HashMap;
use super::tiled::{DynamicTile, RegularMap, TileSetting, VRamManager};
/// The text renderer renders a variable width fixed size
/// bitmap font using dynamic tiles as a rendering surface.
/// Does not support any unicode features.
/// For usage see the `text_render.rs` example
pub struct FontLetter {
width: u8,
height: u8,
@ -57,69 +61,91 @@ impl Font {
}
impl Font {
pub fn render_text<'a>(
&'a self,
tile_pos: Vector2D<u16>,
foreground_colour: u8,
background_colour: u8,
bg: &'a mut RegularMap,
vram_manager: &'a mut VRamManager,
) -> TextRenderer<'a> {
#[must_use]
/// Create renderer starting at the given tile co-ordinates.
pub fn render_text(&self, tile_pos: Vector2D<u16>) -> TextRenderer<'_> {
TextRenderer {
current_x_pos: 0,
current_y_pos: 0,
font: self,
tile_pos,
vram_manager,
bg,
background_colour,
foreground_colour,
tiles: Default::default(),
}
}
}
/// Keeps track of the cursor and manages rendered tiles.
pub struct TextRenderer<'a> {
current_x_pos: i32,
current_y_pos: i32,
font: &'a Font,
tile_pos: Vector2D<u16>,
vram_manager: &'a mut VRamManager,
bg: &'a mut RegularMap,
background_colour: u8,
foreground_colour: u8,
tiles: HashMap<(i32, i32), DynamicTile<'a>>,
}
impl<'a> Write for TextRenderer<'a> {
/// Generated from the renderer for use
/// with `Write` trait methods.
pub struct TextWriter<'a, 'b> {
foreground_colour: u8,
background_colour: u8,
text_renderer: &'a mut TextRenderer<'b>,
vram_manager: &'a mut VRamManager,
bg: &'a mut RegularMap,
}
impl<'a, 'b> Write for TextWriter<'a, 'b> {
fn write_str(&mut self, text: &str) -> Result<(), Error> {
for c in text.chars() {
if c == '\n' {
self.current_y_pos += self.font.line_height;
self.current_x_pos = 0;
continue;
}
let letter = self.font.letter(c);
self.render_letter(letter);
self.current_x_pos += i32::from(letter.advance_width);
self.text_renderer.write_char(
c,
self.vram_manager,
self.foreground_colour,
self.background_colour,
);
}
Ok(())
}
}
impl<'a, 'b> TextWriter<'a, 'b> {
pub fn commit(self) {
self.text_renderer.commit(self.bg, self.vram_manager);
}
}
fn div_ceil(quotient: i32, divisor: i32) -> i32 {
(quotient + divisor - 1) / divisor
}
impl<'a> TextRenderer<'a> {
fn render_letter(&mut self, letter: &FontLetter) {
let vram_manager = &mut self.vram_manager;
let foreground_colour = self.foreground_colour;
let background_colour = self.background_colour;
impl<'a, 'b> TextRenderer<'b> {
pub fn writer(
&'a mut self,
foreground_colour: u8,
background_colour: u8,
bg: &'a mut RegularMap,
vram_manager: &'a mut VRamManager,
) -> TextWriter<'a, 'b> {
TextWriter {
text_renderer: self,
foreground_colour,
background_colour,
bg,
vram_manager,
}
}
/// Renders a single character creating as many dynamic tiles as needed.
/// The foreground and background colour are palette indicies.
fn render_letter(
&mut self,
letter: &FontLetter,
vram_manager: &mut VRamManager,
foreground_colour: u8,
background_colour: u8,
) {
assert!(foreground_colour < 16);
assert!(background_colour < 16);
let x_start = (self.current_x_pos + i32::from(letter.xmin)).max(0);
let y_start = self.current_y_pos + self.font.ascent
@ -182,27 +208,44 @@ impl<'a> TextRenderer<'a> {
}
}
pub fn commit(mut self) {
let tiles = core::mem::take(&mut self.tiles);
for ((x, y), tile) in tiles.into_iter() {
self.bg.set_tile(
self.vram_manager,
(self.tile_pos.x + x as u16, self.tile_pos.y + y as u16).into(),
/// Commit the dynamic tiles that contain the text to the background.
pub fn commit(&self, bg: &'a mut RegularMap, vram_manager: &'a mut VRamManager) {
for ((x, y), tile) in self.tiles.iter() {
bg.set_tile(
vram_manager,
(self.tile_pos.x + *x as u16, self.tile_pos.y + *y as u16).into(),
&tile.tile_set(),
TileSetting::from_raw(tile.tile_index()),
);
self.vram_manager.remove_dynamic_tile(tile);
}
}
}
impl<'a> Drop for TextRenderer<'a> {
fn drop(&mut self) {
/// Write another char into the text, moving the cursor as appropriate.
pub fn write_char(
&mut self,
c: char,
vram_manager: &mut VRamManager,
foreground_colour: u8,
background_colour: u8,
) {
if c == '\n' {
self.current_y_pos += self.font.line_height;
self.current_x_pos = 0;
} else {
let letter = self.font.letter(c);
self.render_letter(letter, vram_manager, foreground_colour, background_colour);
self.current_x_pos += i32::from(letter.advance_width);
}
}
/// Clear the text, removing the tiles from vram and resetting the cursor.
pub fn clear(&mut self, vram_manager: &mut VRamManager) {
self.current_x_pos = 0;
self.current_y_pos = 0;
let tiles = core::mem::take(&mut self.tiles);
for (_, tile) in tiles.into_iter() {
self.vram_manager.remove_dynamic_tile(tile);
vram_manager.remove_dynamic_tile(tile);
}
}
}
@ -242,16 +285,30 @@ mod tests {
vram.remove_dynamic_tile(background_tile);
let mut writer = FONT.render_text((0u16, 3u16).into(), 1, 2, &mut bg, &mut vram);
let mut renderer = FONT.render_text((0u16, 3u16).into());
writeln!(&mut writer, "Hello, World!").unwrap();
writeln!(&mut writer, "This is a font rendering example").unwrap();
// Test twice to ensure that clearing works
for _ in 0..2 {
let mut writer = renderer.writer(1, 2, &mut bg, &mut vram);
write!(&mut writer, "Hello, ").unwrap();
writer.commit();
writer.commit();
// Test changing color
let mut writer = renderer.writer(4, 2, &mut bg, &mut vram);
writeln!(&mut writer, "World!").unwrap();
writer.commit();
bg.commit(&mut vram);
bg.show();
bg.commit(&mut vram);
bg.show();
// Test writing with same renderer after showing background
let mut writer = renderer.writer(1, 2, &mut bg, &mut vram);
writeln!(&mut writer, "This is a font rendering example").unwrap();
writer.commit();
bg.commit(&mut vram);
bg.show();
crate::test_runner::assert_image_output("examples/font/font-test-output.png");
crate::test_runner::assert_image_output("examples/font/font-test-output.png");
renderer.clear(&mut vram);
}
}
}