mirror of
https://github.com/italicsjenga/agb.git
synced 2025-01-22 15:16:40 +11:00
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:
commit
99ce2f73d5
6 changed files with 130 additions and 58 deletions
|
@ -6,6 +6,9 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
||||||
|
|
||||||
## [Unreleased]
|
## [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
|
## [0.12.2] - 2022/10/22
|
||||||
|
|
||||||
This is a minor release to fix an alignment issue with background tiles.
|
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 |
|
@ -27,7 +27,8 @@ fn main(mut gba: Gba) -> ! {
|
||||||
|
|
||||||
init_background(&mut bg, &mut vram);
|
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();
|
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 frame_counter = 0i32;
|
||||||
let mut has_written_frame_time = false;
|
let mut has_written_frame_time = false;
|
||||||
|
|
||||||
|
let mut stats_renderer = FONT.render_text((0u16, 6u16).into());
|
||||||
loop {
|
loop {
|
||||||
vblank_provider.wait_for_vblank();
|
vblank_provider.wait_for_vblank();
|
||||||
bg.commit(&mut vram);
|
bg.commit(&mut vram);
|
||||||
|
@ -66,7 +68,9 @@ fn main(mut gba: Gba) -> ! {
|
||||||
|
|
||||||
let percent = (total_cycles * 100) / 280896;
|
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, "{total_cycles} cycles").unwrap();
|
||||||
writeln!(&mut writer, "{percent} percent").unwrap();
|
writeln!(&mut writer, "{percent} percent").unwrap();
|
||||||
|
|
||||||
|
|
|
@ -27,10 +27,12 @@ fn main(mut gba: Gba) -> ! {
|
||||||
|
|
||||||
init_background(&mut bg, &mut vram);
|
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();
|
writeln!(&mut writer, "Let it in by Josh Woodward").unwrap();
|
||||||
|
|
||||||
|
|
||||||
writer.commit();
|
writer.commit();
|
||||||
|
|
||||||
bg.commit(&mut vram);
|
bg.commit(&mut vram);
|
||||||
|
@ -49,6 +51,8 @@ fn main(mut gba: Gba) -> ! {
|
||||||
|
|
||||||
let mut frame_counter = 0i32;
|
let mut frame_counter = 0i32;
|
||||||
let mut has_written_frame_time = false;
|
let mut has_written_frame_time = false;
|
||||||
|
|
||||||
|
let mut stats_renderer = FONT.render_text((0u16, 6u16).into());
|
||||||
loop {
|
loop {
|
||||||
vblank_provider.wait_for_vblank();
|
vblank_provider.wait_for_vblank();
|
||||||
bg.commit(&mut vram);
|
bg.commit(&mut vram);
|
||||||
|
@ -65,7 +69,7 @@ fn main(mut gba: Gba) -> ! {
|
||||||
|
|
||||||
let percent = (total_cycles * 100) / 280896;
|
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, "{total_cycles} cycles").unwrap();
|
||||||
writeln!(&mut writer, "{percent} percent").unwrap();
|
writeln!(&mut writer, "{percent} percent").unwrap();
|
||||||
|
|
||||||
|
|
|
@ -40,7 +40,8 @@ fn main(mut gba: agb::Gba) -> ! {
|
||||||
|
|
||||||
vram.remove_dynamic_tile(background_tile);
|
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, "Hello, World!").unwrap();
|
||||||
writeln!(&mut writer, "This is a font rendering example").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;
|
let mut frame = 0;
|
||||||
|
|
||||||
loop {
|
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();
|
writeln!(&mut writer, "Frame {}", frame).unwrap();
|
||||||
writer.commit();
|
writer.commit();
|
||||||
|
@ -62,5 +64,7 @@ fn main(mut gba: agb::Gba) -> ! {
|
||||||
|
|
||||||
vblank.wait_for_vblank();
|
vblank.wait_for_vblank();
|
||||||
bg.commit(&mut vram);
|
bg.commit(&mut vram);
|
||||||
|
|
||||||
|
renderer.clear(&mut vram);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -5,6 +5,10 @@ use crate::hash_map::HashMap;
|
||||||
|
|
||||||
use super::tiled::{DynamicTile, RegularMap, TileSetting, VRamManager};
|
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 {
|
pub struct FontLetter {
|
||||||
width: u8,
|
width: u8,
|
||||||
height: u8,
|
height: u8,
|
||||||
|
@ -57,69 +61,91 @@ impl Font {
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Font {
|
impl Font {
|
||||||
pub fn render_text<'a>(
|
#[must_use]
|
||||||
&'a self,
|
/// Create renderer starting at the given tile co-ordinates.
|
||||||
tile_pos: Vector2D<u16>,
|
pub fn render_text(&self, tile_pos: Vector2D<u16>) -> TextRenderer<'_> {
|
||||||
foreground_colour: u8,
|
|
||||||
background_colour: u8,
|
|
||||||
bg: &'a mut RegularMap,
|
|
||||||
vram_manager: &'a mut VRamManager,
|
|
||||||
) -> TextRenderer<'a> {
|
|
||||||
TextRenderer {
|
TextRenderer {
|
||||||
current_x_pos: 0,
|
current_x_pos: 0,
|
||||||
current_y_pos: 0,
|
current_y_pos: 0,
|
||||||
font: self,
|
font: self,
|
||||||
tile_pos,
|
tile_pos,
|
||||||
vram_manager,
|
|
||||||
bg,
|
|
||||||
background_colour,
|
|
||||||
foreground_colour,
|
|
||||||
tiles: Default::default(),
|
tiles: Default::default(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Keeps track of the cursor and manages rendered tiles.
|
||||||
pub struct TextRenderer<'a> {
|
pub struct TextRenderer<'a> {
|
||||||
current_x_pos: i32,
|
current_x_pos: i32,
|
||||||
current_y_pos: i32,
|
current_y_pos: i32,
|
||||||
font: &'a Font,
|
font: &'a Font,
|
||||||
tile_pos: Vector2D<u16>,
|
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>>,
|
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> {
|
fn write_str(&mut self, text: &str) -> Result<(), Error> {
|
||||||
for c in text.chars() {
|
for c in text.chars() {
|
||||||
if c == '\n' {
|
self.text_renderer.write_char(
|
||||||
self.current_y_pos += self.font.line_height;
|
c,
|
||||||
self.current_x_pos = 0;
|
self.vram_manager,
|
||||||
continue;
|
self.foreground_colour,
|
||||||
}
|
self.background_colour,
|
||||||
|
);
|
||||||
let letter = self.font.letter(c);
|
|
||||||
|
|
||||||
self.render_letter(letter);
|
|
||||||
|
|
||||||
self.current_x_pos += i32::from(letter.advance_width);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
Ok(())
|
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 {
|
fn div_ceil(quotient: i32, divisor: i32) -> i32 {
|
||||||
(quotient + divisor - 1) / divisor
|
(quotient + divisor - 1) / divisor
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<'a> TextRenderer<'a> {
|
impl<'a, 'b> TextRenderer<'b> {
|
||||||
fn render_letter(&mut self, letter: &FontLetter) {
|
pub fn writer(
|
||||||
let vram_manager = &mut self.vram_manager;
|
&'a mut self,
|
||||||
let foreground_colour = self.foreground_colour;
|
foreground_colour: u8,
|
||||||
let background_colour = self.background_colour;
|
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 x_start = (self.current_x_pos + i32::from(letter.xmin)).max(0);
|
||||||
let y_start = self.current_y_pos + self.font.ascent
|
let y_start = self.current_y_pos + self.font.ascent
|
||||||
|
@ -182,27 +208,44 @@ impl<'a> TextRenderer<'a> {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn commit(mut self) {
|
/// Commit the dynamic tiles that contain the text to the background.
|
||||||
let tiles = core::mem::take(&mut self.tiles);
|
pub fn commit(&self, bg: &'a mut RegularMap, vram_manager: &'a mut VRamManager) {
|
||||||
|
for ((x, y), tile) in self.tiles.iter() {
|
||||||
for ((x, y), tile) in tiles.into_iter() {
|
bg.set_tile(
|
||||||
self.bg.set_tile(
|
vram_manager,
|
||||||
self.vram_manager,
|
(self.tile_pos.x + *x as u16, self.tile_pos.y + *y as u16).into(),
|
||||||
(self.tile_pos.x + x as u16, self.tile_pos.y + y as u16).into(),
|
|
||||||
&tile.tile_set(),
|
&tile.tile_set(),
|
||||||
TileSetting::from_raw(tile.tile_index()),
|
TileSetting::from_raw(tile.tile_index()),
|
||||||
);
|
);
|
||||||
self.vram_manager.remove_dynamic_tile(tile);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
impl<'a> Drop for TextRenderer<'a> {
|
/// Write another char into the text, moving the cursor as appropriate.
|
||||||
fn drop(&mut self) {
|
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);
|
let tiles = core::mem::take(&mut self.tiles);
|
||||||
|
|
||||||
for (_, tile) in tiles.into_iter() {
|
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);
|
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();
|
||||||
|
|
||||||
|
// 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.commit(&mut vram);
|
||||||
bg.show();
|
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);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
Loading…
Add table
Reference in a new issue