From fec401597834a22bf9d8554bcbe38278bf9734f1 Mon Sep 17 00:00:00 2001 From: Gwilym Kuiper Date: Sat, 23 Apr 2022 14:30:46 +0100 Subject: [PATCH 1/5] Remove hash map lookup for every pixel rendered --- agb/src/display/font.rs | 78 ++++++++++++++++++++++++++++------------- 1 file changed, 53 insertions(+), 25 deletions(-) diff --git a/agb/src/display/font.rs b/agb/src/display/font.rs index badafc5..c5a71ac 100644 --- a/agb/src/display/font.rs +++ b/agb/src/display/font.rs @@ -86,7 +86,7 @@ pub struct TextRenderer<'a> { bg: &'a mut RegularMap, background_colour: u8, foreground_colour: u8, - tiles: HashMap<(usize, usize), DynamicTile<'a>>, + tiles: HashMap<(i32, i32), DynamicTile<'a>>, } impl<'a> Write for TextRenderer<'a> { @@ -96,23 +96,6 @@ impl<'a> Write for TextRenderer<'a> { let foreground_colour = self.foreground_colour; let background_colour = self.background_colour; - let mut render_pixel = |x: u16, y: u16| { - let tile_x = (x / 8) as usize; - let tile_y = (y / 8) as usize; - let inner_x = x % 8; - let inner_y = y % 8; - - let colour = foreground_colour as u32; - - let index = (inner_x + inner_y * 8) as usize; - - let tile = tiles - .entry((tile_x, tile_y)) - .or_insert_with(|| vram_manager.new_dynamic_tile().fill_with(background_colour)); - - tile.tile_data[index / 8] |= colour << ((index % 8) * 4); - }; - for c in text.chars() { if c == '\n' { self.current_y_pos += self.font.line_height; @@ -126,15 +109,56 @@ impl<'a> Write for TextRenderer<'a> { let y_start = self.current_y_pos + self.font.ascent - letter.height as i32 - letter.ymin as i32; - for letter_y in 0..(letter.height as i32) { - for letter_x in 0..(letter.width as i32) { - let x = x_start + letter_x; - let y = y_start + letter_y; + let x_tile_start = x_start / 8; + let y_tile_start = y_start / 8; - let px = letter.data[(letter_x + letter_y * letter.width as i32) as usize]; + let letter_offset_x = x_start.rem_euclid(8); + let letter_offset_y = y_start.rem_euclid(8); - if px > 100 { - render_pixel(x as u16, y as u16); + let x_tiles = div_ceil(letter.width as i32 + letter_offset_x, 8); + let y_tiles = div_ceil(letter.height as i32 + letter_offset_y, 8); + + for letter_y_tile in 0..(y_tiles + 1) { + let letter_y_start = 0.max(letter_offset_y - 8 * letter_y_tile) + 8 * letter_y_tile; + let letter_y_end = + (letter_offset_y + letter.height as i32).min((letter_y_tile + 1) * 8); + + let tile_y = y_tile_start + letter_y_tile; + + for letter_x_tile in 0..(x_tiles + 1) { + let letter_x_start = + 0.max(letter_offset_x - 8 * letter_x_tile) + 8 * letter_x_tile; + let letter_x_end = + (letter_offset_x + letter.width as i32).min((letter_x_tile + 1) * 8); + + let tile_x = x_tile_start + letter_x_tile; + + let mut masks = [0u32; 8]; + let mut zero = true; + + for letter_y in letter_y_start..letter_y_end { + let y = letter_y - letter_offset_y; + + for letter_x in letter_x_start..letter_x_end { + let x = letter_x - letter_offset_x; + let px = letter.data[(x + y * letter.width as i32) as usize]; + + if px > 100 { + masks[(letter_y & 7) as usize] |= + (foreground_colour as u32) << ((letter_x & 7) * 4); + zero = false; + } + } + } + + if !zero { + let tile = tiles.entry((tile_x, tile_y)).or_insert_with(|| { + vram_manager.new_dynamic_tile().fill_with(background_colour) + }); + + for (i, tile_data_line) in tile.tile_data.iter_mut().enumerate() { + *tile_data_line |= masks[i]; + } } } } @@ -146,6 +170,10 @@ impl<'a> Write for TextRenderer<'a> { } } +fn div_ceil(quotient: i32, divisor: i32) -> i32 { + (quotient + divisor - 1) / divisor +} + impl<'a> TextRenderer<'a> { pub fn commit(mut self) { let tiles = core::mem::take(&mut self.tiles); From 6485cc895a7cdcf373c38e93003f0562bab96342 Mon Sep 17 00:00:00 2001 From: Gwilym Kuiper Date: Sat, 23 Apr 2022 14:34:34 +0100 Subject: [PATCH 2/5] Extract method --- agb/src/display/font.rs | 126 ++++++++++++++++++++-------------------- 1 file changed, 64 insertions(+), 62 deletions(-) diff --git a/agb/src/display/font.rs b/agb/src/display/font.rs index c5a71ac..eccde3e 100644 --- a/agb/src/display/font.rs +++ b/agb/src/display/font.rs @@ -91,11 +91,6 @@ pub struct TextRenderer<'a> { impl<'a> Write for TextRenderer<'a> { fn write_str(&mut self, text: &str) -> Result<(), Error> { - let vram_manager = &mut self.vram_manager; - let tiles = &mut self.tiles; - let foreground_colour = self.foreground_colour; - let background_colour = self.background_colour; - for c in text.chars() { if c == '\n' { self.current_y_pos += self.font.line_height; @@ -105,63 +100,7 @@ impl<'a> Write for TextRenderer<'a> { let letter = self.font.letter(c); - let x_start = (self.current_x_pos + letter.xmin as i32).max(0); - let y_start = - self.current_y_pos + self.font.ascent - letter.height as i32 - letter.ymin as i32; - - let x_tile_start = x_start / 8; - let y_tile_start = y_start / 8; - - let letter_offset_x = x_start.rem_euclid(8); - let letter_offset_y = y_start.rem_euclid(8); - - let x_tiles = div_ceil(letter.width as i32 + letter_offset_x, 8); - let y_tiles = div_ceil(letter.height as i32 + letter_offset_y, 8); - - for letter_y_tile in 0..(y_tiles + 1) { - let letter_y_start = 0.max(letter_offset_y - 8 * letter_y_tile) + 8 * letter_y_tile; - let letter_y_end = - (letter_offset_y + letter.height as i32).min((letter_y_tile + 1) * 8); - - let tile_y = y_tile_start + letter_y_tile; - - for letter_x_tile in 0..(x_tiles + 1) { - let letter_x_start = - 0.max(letter_offset_x - 8 * letter_x_tile) + 8 * letter_x_tile; - let letter_x_end = - (letter_offset_x + letter.width as i32).min((letter_x_tile + 1) * 8); - - let tile_x = x_tile_start + letter_x_tile; - - let mut masks = [0u32; 8]; - let mut zero = true; - - for letter_y in letter_y_start..letter_y_end { - let y = letter_y - letter_offset_y; - - for letter_x in letter_x_start..letter_x_end { - let x = letter_x - letter_offset_x; - let px = letter.data[(x + y * letter.width as i32) as usize]; - - if px > 100 { - masks[(letter_y & 7) as usize] |= - (foreground_colour as u32) << ((letter_x & 7) * 4); - zero = false; - } - } - } - - if !zero { - let tile = tiles.entry((tile_x, tile_y)).or_insert_with(|| { - vram_manager.new_dynamic_tile().fill_with(background_colour) - }); - - for (i, tile_data_line) in tile.tile_data.iter_mut().enumerate() { - *tile_data_line |= masks[i]; - } - } - } - } + self.render_letter(letter); self.current_x_pos += letter.advance_width as i32; } @@ -175,6 +114,69 @@ fn div_ceil(quotient: i32, divisor: i32) -> i32 { } 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; + + let x_start = (self.current_x_pos + letter.xmin as i32).max(0); + let y_start = + self.current_y_pos + self.font.ascent - letter.height as i32 - letter.ymin as i32; + + let x_tile_start = x_start / 8; + let y_tile_start = y_start / 8; + + let letter_offset_x = x_start.rem_euclid(8); + let letter_offset_y = y_start.rem_euclid(8); + + let x_tiles = div_ceil(letter.width as i32 + letter_offset_x, 8); + let y_tiles = div_ceil(letter.height as i32 + letter_offset_y, 8); + + for letter_y_tile in 0..(y_tiles + 1) { + let letter_y_start = 0.max(letter_offset_y - 8 * letter_y_tile) + 8 * letter_y_tile; + let letter_y_end = + (letter_offset_y + letter.height as i32).min((letter_y_tile + 1) * 8); + + let tile_y = y_tile_start + letter_y_tile; + + for letter_x_tile in 0..(x_tiles + 1) { + let letter_x_start = 0.max(letter_offset_x - 8 * letter_x_tile) + 8 * letter_x_tile; + let letter_x_end = + (letter_offset_x + letter.width as i32).min((letter_x_tile + 1) * 8); + + let tile_x = x_tile_start + letter_x_tile; + + let mut masks = [0u32; 8]; + let mut zero = true; + + for letter_y in letter_y_start..letter_y_end { + let y = letter_y - letter_offset_y; + + for letter_x in letter_x_start..letter_x_end { + let x = letter_x - letter_offset_x; + let px = letter.data[(x + y * letter.width as i32) as usize]; + + if px > 100 { + masks[(letter_y & 7) as usize] |= + (foreground_colour as u32) << ((letter_x & 7) * 4); + zero = false; + } + } + } + + if !zero { + let tile = self.tiles.entry((tile_x, tile_y)).or_insert_with(|| { + vram_manager.new_dynamic_tile().fill_with(background_colour) + }); + + for (i, tile_data_line) in tile.tile_data.iter_mut().enumerate() { + *tile_data_line |= masks[i]; + } + } + } + } + } + pub fn commit(mut self) { let tiles = core::mem::take(&mut self.tiles); From 1b71ef64732e193ad2bd9ff3047614b484b5d635 Mon Sep 17 00:00:00 2001 From: Gwilym Kuiper Date: Sat, 23 Apr 2022 15:33:57 +0100 Subject: [PATCH 3/5] Add test for font output --- agb-image-converter/src/font_loader.rs | 4 +-- agb-image-converter/src/lib.rs | 11 ++++-- agb/examples/font/font-test-output.png | Bin 0 -> 1300 bytes agb/src/display/font.rs | 45 +++++++++++++++++++++++++ agb/src/display/object.rs | 32 ++++++++++-------- agb/src/lib.rs | 12 ++++++- mgba-test-runner/src/main.rs | 35 ++++++++++++++++--- 7 files changed, 116 insertions(+), 23 deletions(-) create mode 100644 agb/examples/font/font-test-output.png diff --git a/agb-image-converter/src/font_loader.rs b/agb-image-converter/src/font_loader.rs index 97dd521..e1554d0 100644 --- a/agb-image-converter/src/font_loader.rs +++ b/agb-image-converter/src/font_loader.rs @@ -51,7 +51,7 @@ pub fn load_font(font_data: &[u8], pixels_per_em: f32) -> TokenStream { let advance_width = letter_data.advance_width.ceil() as u8; quote!( - agb::display::FontLetter::new( + display::FontLetter::new( #width, #height, #data_raw, @@ -63,6 +63,6 @@ pub fn load_font(font_data: &[u8], pixels_per_em: f32) -> TokenStream { }); quote![ - agb::display::Font::new(&[#(#font),*], #line_height, #ascent) + display::Font::new(&[#(#font),*], #line_height, #ascent) ] } diff --git a/agb-image-converter/src/lib.rs b/agb-image-converter/src/lib.rs index e788509..c82cbd0 100644 --- a/agb-image-converter/src/lib.rs +++ b/agb-image-converter/src/lib.rs @@ -311,6 +311,13 @@ fn palete_tile_data( (palette_data, tile_data, assignments) } +fn flatten_group(expr: &Expr) -> &Expr { + match expr { + Expr::Group(group) => &group.expr, + _ => expr, + } +} + #[proc_macro] pub fn include_font(input: TokenStream) -> TokenStream { let parser = Punctuated::::parse_separated_nonempty; @@ -324,7 +331,7 @@ pub fn include_font(input: TokenStream) -> TokenStream { panic!("Include_font requires 2 arguments, got {}", all_args.len()); } - let filename = match &all_args[0] { + let filename = match flatten_group(&all_args[0]) { Expr::Lit(ExprLit { lit: Lit::Str(str_lit), .. @@ -332,7 +339,7 @@ pub fn include_font(input: TokenStream) -> TokenStream { _ => panic!("Expected literal string as first argument to include_font"), }; - let font_size = match &all_args[1] { + let font_size = match flatten_group(&all_args[1]) { Expr::Lit(ExprLit { lit: Lit::Float(value), .. diff --git a/agb/examples/font/font-test-output.png b/agb/examples/font/font-test-output.png new file mode 100644 index 0000000000000000000000000000000000000000..b07074d53101600643231d4b5a6198c488caf8a2 GIT binary patch literal 1300 zcmbtSeK^wz9RF3A=9ILq$ZLm^m6!Esb4g6t=2$(`GH<2i=8C+{%QCKXUTX4kZOU$1 zX^5$rRGSeKH;(SkTQ;`{EhDoP#>BZl`};o6^Z9(9&p+Si`#hgd9yY+&5Nrwt0KgFC z=Y4YB=hk^of8)9eUyxh?U_&+$iNvCiNUO^UaTkaeqX1xEQAUxwpXiv`s|XKo`yh}3 zI{tU;Z_wO)c+8G`taU$_<5n8Ftows$F=VH2>(kNE&d7n*SHXA?-qUCWh)+s5nN(!F zMu!mEQ!kI)N?Ei>Q39C{+Hf%Vma4l^6(yUk%R+YWBeu0$EgN~^BH2gx=G&(hJSJ;2 z={x1YGWn{R5G^~O6==nb;-B3XXA~GmaFb`e(|16DRR~q7=S~~S1b;@oTYg_eVVXF* zOr^ck=VE8hyto!D#^zP#p38r9qS<4*zAfzrk5J7WbYC(MrCo};GkqwhsC^I3$}8)p zX;;-tFUBb~m<$Hp(t8> zY%{0J2HArnGJ+PK0l4%BDfHlQ9iJP&3TIWb%wE~5d!IrHa%@VtxsS;h1%OQ$l(#1? zmGMy$l2GOZ0kFklJAMhYxBR25sRa7q%RO71AAYM7iIK>LB^ytnbAK}O>eE@T^D9(@ zQ(A7}1fd>m;_}k!cS~0yhM9**%E@#AC7wC7)Y3hF?x;#E55Y+I#|6`IVT~u4;_3d( zJmJ)28hpS)?$c^648qjHYHh=4>YRyFu99Mbr!k#BCRrH?ANO!H;R)kACquw-4z4~J z3Xc;a%z0kuQS&;R8BKfG9wY8eR(vz@`B+0tIC@0+KCz3-(Ei*sOpJ|(YC+3(-Q}mF zy10W+;e{g~YLdroc2h>bU+WfU2v;btZQd?)1~>X2>FenTUH^aZ?6aqhf1x+<+)+$( za?5UCP9S|yOCDeBSW--q4VzD!6xvJ+{XRSnYQ|>_&l+tl?kTbFvph7gk+cd?wml z+7YPGqtXhvgl7#R8;cxwR(&}}^roH*{qZnjVdg}Gr9`c@(lo+^-Dy+Evr|u`i+c~) zhUi?cIdE`?ytoxpp1d?eW82`WITf)a`KdqAbCk-%xcOYP^Y#G}FXA?>v~SHm+^tHs z1#jNi#Z9{Z&~l)St`4YvlirUWub00s6M`>jO&W|tp=6}4Zn5WN+Zl~JTfoMLvh?Lb(}=5Wrg{{6Z{M4{%n zq!Oe$vLDqx-*?>U4odP*ZxlINl$Sg|VgJy Drop for TextRenderer<'a> { } } } + +#[cfg(test)] +mod tests { + use super::*; + const FONT: Font = crate::include_font!("examples/font/yoster.ttf", 12); + + #[test_case] + fn font_display(gba: &mut crate::Gba) { + let (gfx, mut vram) = gba.display.video.tiled0(); + + let mut bg = gfx.background(crate::display::Priority::P0); + + vram.set_background_palette_raw(&[ + 0x0000, 0x0ff0, 0x00ff, 0xf00f, 0xf0f0, 0x0f0f, 0xaaaa, 0x5555, 0x0000, 0x0000, 0x0000, + 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, + ]); + + let background_tile = vram.new_dynamic_tile().fill_with(0); + + for y in 0..20u16 { + for x in 0..30u16 { + bg.set_tile( + &mut vram, + (x, y).into(), + &background_tile.tile_set(), + TileSetting::from_raw(background_tile.tile_index()), + ); + } + } + + vram.remove_dynamic_tile(background_tile); + + let mut writer = FONT.render_text((0u16, 3u16).into(), 1, 2, &mut bg, &mut vram); + + writeln!(&mut writer, "Hello, World!").unwrap(); + writeln!(&mut writer, "This is a font rendering example").unwrap(); + + writer.commit(); + + bg.commit(); + bg.show(); + + crate::test_runner::assert_image_output("examples/font/font-test-output.png"); + } +} diff --git a/agb/src/display/object.rs b/agb/src/display/object.rs index c57b280..7336e91 100644 --- a/agb/src/display/object.rs +++ b/agb/src/display/object.rs @@ -961,22 +961,26 @@ mod tests { let object = gba.display.object.get(); - let mut objects: Vec<_> = alloc::vec![ - object.object(object.sprite(BOSS.sprite(0))), - object.object(object.sprite(EMU.sprite(0))), - ] - .into_iter() - .map(Some) - .collect(); + { + let mut objects: Vec<_> = alloc::vec![ + object.object(object.sprite(BOSS.sprite(0))), + object.object(object.sprite(EMU.sprite(0))), + ] + .into_iter() + .map(Some) + .collect(); - object.commit(); + object.commit(); - let x = objects[0].as_mut().unwrap(); - x.set_hflip(true); - x.set_vflip(true); - x.set_position((1, 1).into()); - x.set_z(100); - x.set_sprite(object.sprite(BOSS.sprite(2))); + let x = objects[0].as_mut().unwrap(); + x.set_hflip(true); + x.set_vflip(true); + x.set_position((1, 1).into()); + x.set_z(100); + x.set_sprite(object.sprite(BOSS.sprite(2))); + + object.commit(); + } object.commit(); } diff --git a/agb/src/lib.rs b/agb/src/lib.rs index 2ea42dd..9eb473c 100644 --- a/agb/src/lib.rs +++ b/agb/src/lib.rs @@ -112,9 +112,19 @@ /// ``` pub use agb_image_converter::include_gfx; +#[doc(hidden)] pub use agb_image_converter::include_aseprite_inner; -pub use agb_image_converter::include_font; +#[doc(hidden)] +pub use agb_image_converter::include_font as include_font_inner; + +#[macro_export] +macro_rules! include_font { + ($font_path: literal, $font_size: literal) => {{ + use $crate::display; + $crate::include_font_inner!($font_path, $font_size) + }}; +} /// This macro declares the entry point to your game written using `agb`. /// diff --git a/mgba-test-runner/src/main.rs b/mgba-test-runner/src/main.rs index 97ead86..e50ad5d 100644 --- a/mgba-test-runner/src/main.rs +++ b/mgba-test-runner/src/main.rs @@ -3,6 +3,7 @@ mod runner; use anyhow::{anyhow, Error}; use image::io::Reader; +use image::GenericImage; use io::Write; use regex::Regex; use runner::VideoBuffer; @@ -135,15 +136,14 @@ fn rgba_to_gba_to_rgba(c: [u8; 4]) -> [u8; 4] { fn check_image_match(image_path: &str, video_buffer: &VideoBuffer) -> Result<(), Error> { let expected_image = Reader::open(image_path)?.decode()?; - let expected = expected_image - .as_rgba8() - .ok_or(anyhow!("cannot convert to rgba8"))?; + let expected = expected_image.to_rgba8(); let (buf_dim_x, buf_dim_y) = video_buffer.get_size(); let (exp_dim_x, exp_dim_y) = expected.dimensions(); if (buf_dim_x != exp_dim_x) || (buf_dim_y != exp_dim_y) { return Err(anyhow!("image sizes do not match")); } + for y in 0..buf_dim_y { for x in 0..buf_dim_x { let video_pixel = video_buffer.get_pixel(x, y); @@ -151,10 +151,37 @@ fn check_image_match(image_path: &str, video_buffer: &VideoBuffer) -> Result<(), let video_pixel = gba_colour_to_rgba(video_pixel); let image_pixel = rgba_to_gba_to_rgba(image_pixel.0); if image_pixel != video_pixel { - return Err(anyhow!("images do not match")); + let output_file = write_video_buffer(video_buffer); + + return Err(anyhow!( + "images do not match, actual output written to {}", + output_file + )); } } } Ok(()) } + +fn write_video_buffer(video_buffer: &VideoBuffer) -> String { + let (width, height) = video_buffer.get_size(); + let mut output_image = image::DynamicImage::new_rgba8(width, height); + + for y in 0..height { + for x in 0..width { + let pixel = video_buffer.get_pixel(x, y); + let pixel_as_rgba = gba_colour_to_rgba(pixel); + + output_image.put_pixel(x, y, pixel_as_rgba.into()) + } + } + + let output_folder = std::env::temp_dir(); + let output_file = "mgba-test-runner-output.png"; // TODO make this random + + let output_file = output_folder.join(output_file); + let _ = output_image.save_with_format(&output_file, image::ImageFormat::Png); + + output_file.to_string_lossy().into_owned() +} From fb197d3e79301f231c891cc590bfc56c3bc8c920 Mon Sep 17 00:00:00 2001 From: Gwilym Kuiper Date: Sat, 23 Apr 2022 15:42:08 +0100 Subject: [PATCH 4/5] Store font data more compressed --- agb-image-converter/src/font_loader.rs | 16 +++++++++++++++- agb/src/display/font.rs | 6 ++++-- 2 files changed, 19 insertions(+), 3 deletions(-) diff --git a/agb-image-converter/src/font_loader.rs b/agb-image-converter/src/font_loader.rs index e1554d0..2c61f66 100644 --- a/agb-image-converter/src/font_loader.rs +++ b/agb-image-converter/src/font_loader.rs @@ -33,10 +33,24 @@ pub fn load_font(font_data: &[u8], pixels_per_em: f32) -> TokenStream { let width = metrics.width; let height = metrics.height; + let rendered = bitmap + .chunks(8) + .map(|chunk| { + let mut output = 0u8; + for (i, &value) in chunk.iter().enumerate() { + if value > 100 { + output |= 1 << i; + } + } + + output + }) + .collect(); + LetterData { width, height, - rendered: bitmap, + rendered, xmin: metrics.xmin, ymin: metrics.ymin, advance_width: metrics.advance_width, diff --git a/agb/src/display/font.rs b/agb/src/display/font.rs index 62bac78..d6f8605 100644 --- a/agb/src/display/font.rs +++ b/agb/src/display/font.rs @@ -154,9 +154,11 @@ impl<'a> TextRenderer<'a> { for letter_x in letter_x_start..letter_x_end { let x = letter_x - letter_offset_x; - let px = letter.data[(x + y * letter.width as i32) as usize]; + let pos = x + y * letter.width as i32; + let px_line = letter.data[(pos / 8) as usize]; + let px = (px_line >> (pos & 7)) & 1; - if px > 100 { + if px != 0 { masks[(letter_y & 7) as usize] |= (foreground_colour as u32) << ((letter_x & 7) * 4); zero = false; From 4f6f981ce7f310394db38dca0dff1d74556ee0f3 Mon Sep 17 00:00:00 2001 From: Gwilym Kuiper Date: Sat, 23 Apr 2022 15:56:07 +0100 Subject: [PATCH 5/5] Usage of group now requires 'full' --- agb-image-converter/Cargo.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/agb-image-converter/Cargo.toml b/agb-image-converter/Cargo.toml index 6f845b2..05af511 100644 --- a/agb-image-converter/Cargo.toml +++ b/agb-image-converter/Cargo.toml @@ -13,7 +13,7 @@ proc-macro = true image = { version = "0.23", default-features = false, features = [ "png", "bmp" ] } toml = "0.5" serde = { version = "1", features = ["derive"] } -syn = "1" +syn = { version = "1", features = ["full"] } proc-macro2 = "1" quote = "1" asefile = "0.3.4"