mirror of
https://github.com/italicsjenga/agb.git
synced 2024-12-24 00:31:34 +11:00
Merge pull request #215 from gwilymk/optimise-font-rendering
Optimise font rendering
This commit is contained in:
commit
4cf92fa18a
|
@ -13,7 +13,7 @@ proc-macro = true
|
||||||
image = { version = "0.23", default-features = false, features = [ "png", "bmp" ] }
|
image = { version = "0.23", default-features = false, features = [ "png", "bmp" ] }
|
||||||
toml = "0.5"
|
toml = "0.5"
|
||||||
serde = { version = "1", features = ["derive"] }
|
serde = { version = "1", features = ["derive"] }
|
||||||
syn = "1"
|
syn = { version = "1", features = ["full"] }
|
||||||
proc-macro2 = "1"
|
proc-macro2 = "1"
|
||||||
quote = "1"
|
quote = "1"
|
||||||
asefile = "0.3.4"
|
asefile = "0.3.4"
|
||||||
|
|
|
@ -33,10 +33,24 @@ pub fn load_font(font_data: &[u8], pixels_per_em: f32) -> TokenStream {
|
||||||
let width = metrics.width;
|
let width = metrics.width;
|
||||||
let height = metrics.height;
|
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 {
|
LetterData {
|
||||||
width,
|
width,
|
||||||
height,
|
height,
|
||||||
rendered: bitmap,
|
rendered,
|
||||||
xmin: metrics.xmin,
|
xmin: metrics.xmin,
|
||||||
ymin: metrics.ymin,
|
ymin: metrics.ymin,
|
||||||
advance_width: metrics.advance_width,
|
advance_width: metrics.advance_width,
|
||||||
|
@ -51,7 +65,7 @@ pub fn load_font(font_data: &[u8], pixels_per_em: f32) -> TokenStream {
|
||||||
let advance_width = letter_data.advance_width.ceil() as u8;
|
let advance_width = letter_data.advance_width.ceil() as u8;
|
||||||
|
|
||||||
quote!(
|
quote!(
|
||||||
agb::display::FontLetter::new(
|
display::FontLetter::new(
|
||||||
#width,
|
#width,
|
||||||
#height,
|
#height,
|
||||||
#data_raw,
|
#data_raw,
|
||||||
|
@ -63,6 +77,6 @@ pub fn load_font(font_data: &[u8], pixels_per_em: f32) -> TokenStream {
|
||||||
});
|
});
|
||||||
|
|
||||||
quote![
|
quote![
|
||||||
agb::display::Font::new(&[#(#font),*], #line_height, #ascent)
|
display::Font::new(&[#(#font),*], #line_height, #ascent)
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|
|
@ -311,6 +311,13 @@ fn palete_tile_data(
|
||||||
(palette_data, tile_data, assignments)
|
(palette_data, tile_data, assignments)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn flatten_group(expr: &Expr) -> &Expr {
|
||||||
|
match expr {
|
||||||
|
Expr::Group(group) => &group.expr,
|
||||||
|
_ => expr,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
#[proc_macro]
|
#[proc_macro]
|
||||||
pub fn include_font(input: TokenStream) -> TokenStream {
|
pub fn include_font(input: TokenStream) -> TokenStream {
|
||||||
let parser = Punctuated::<Expr, syn::Token![,]>::parse_separated_nonempty;
|
let parser = Punctuated::<Expr, syn::Token![,]>::parse_separated_nonempty;
|
||||||
|
@ -324,7 +331,7 @@ pub fn include_font(input: TokenStream) -> TokenStream {
|
||||||
panic!("Include_font requires 2 arguments, got {}", all_args.len());
|
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 {
|
Expr::Lit(ExprLit {
|
||||||
lit: Lit::Str(str_lit),
|
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"),
|
_ => 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 {
|
Expr::Lit(ExprLit {
|
||||||
lit: Lit::Float(value),
|
lit: Lit::Float(value),
|
||||||
..
|
..
|
||||||
|
|
BIN
agb/examples/font/font-test-output.png
Normal file
BIN
agb/examples/font/font-test-output.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 1.3 KiB |
|
@ -86,33 +86,11 @@ pub struct TextRenderer<'a> {
|
||||||
bg: &'a mut RegularMap,
|
bg: &'a mut RegularMap,
|
||||||
background_colour: u8,
|
background_colour: u8,
|
||||||
foreground_colour: u8,
|
foreground_colour: u8,
|
||||||
tiles: HashMap<(usize, usize), DynamicTile<'a>>,
|
tiles: HashMap<(i32, i32), DynamicTile<'a>>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<'a> Write for TextRenderer<'a> {
|
impl<'a> Write for TextRenderer<'a> {
|
||||||
fn write_str(&mut self, text: &str) -> Result<(), Error> {
|
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;
|
|
||||||
|
|
||||||
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() {
|
for c in text.chars() {
|
||||||
if c == '\n' {
|
if c == '\n' {
|
||||||
self.current_y_pos += self.font.line_height;
|
self.current_y_pos += self.font.line_height;
|
||||||
|
@ -122,22 +100,7 @@ impl<'a> Write for TextRenderer<'a> {
|
||||||
|
|
||||||
let letter = self.font.letter(c);
|
let letter = self.font.letter(c);
|
||||||
|
|
||||||
let x_start = (self.current_x_pos + letter.xmin as i32).max(0);
|
self.render_letter(letter);
|
||||||
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 px = letter.data[(letter_x + letter_y * letter.width as i32) as usize];
|
|
||||||
|
|
||||||
if px > 100 {
|
|
||||||
render_pixel(x as u16, y as u16);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
self.current_x_pos += letter.advance_width as i32;
|
self.current_x_pos += letter.advance_width as i32;
|
||||||
}
|
}
|
||||||
|
@ -146,7 +109,76 @@ impl<'a> Write for TextRenderer<'a> {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn div_ceil(quotient: i32, divisor: i32) -> i32 {
|
||||||
|
(quotient + divisor - 1) / divisor
|
||||||
|
}
|
||||||
|
|
||||||
impl<'a> TextRenderer<'a> {
|
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 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 != 0 {
|
||||||
|
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) {
|
pub fn commit(mut self) {
|
||||||
let tiles = core::mem::take(&mut self.tiles);
|
let tiles = core::mem::take(&mut self.tiles);
|
||||||
|
|
||||||
|
@ -171,3 +203,48 @@ impl<'a> 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");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -961,22 +961,26 @@ mod tests {
|
||||||
|
|
||||||
let object = gba.display.object.get();
|
let object = gba.display.object.get();
|
||||||
|
|
||||||
let mut objects: Vec<_> = alloc::vec![
|
{
|
||||||
object.object(object.sprite(BOSS.sprite(0))),
|
let mut objects: Vec<_> = alloc::vec![
|
||||||
object.object(object.sprite(EMU.sprite(0))),
|
object.object(object.sprite(BOSS.sprite(0))),
|
||||||
]
|
object.object(object.sprite(EMU.sprite(0))),
|
||||||
.into_iter()
|
]
|
||||||
.map(Some)
|
.into_iter()
|
||||||
.collect();
|
.map(Some)
|
||||||
|
.collect();
|
||||||
|
|
||||||
object.commit();
|
object.commit();
|
||||||
|
|
||||||
let x = objects[0].as_mut().unwrap();
|
let x = objects[0].as_mut().unwrap();
|
||||||
x.set_hflip(true);
|
x.set_hflip(true);
|
||||||
x.set_vflip(true);
|
x.set_vflip(true);
|
||||||
x.set_position((1, 1).into());
|
x.set_position((1, 1).into());
|
||||||
x.set_z(100);
|
x.set_z(100);
|
||||||
x.set_sprite(object.sprite(BOSS.sprite(2)));
|
x.set_sprite(object.sprite(BOSS.sprite(2)));
|
||||||
|
|
||||||
|
object.commit();
|
||||||
|
}
|
||||||
|
|
||||||
object.commit();
|
object.commit();
|
||||||
}
|
}
|
||||||
|
|
|
@ -112,9 +112,19 @@
|
||||||
/// ```
|
/// ```
|
||||||
pub use agb_image_converter::include_gfx;
|
pub use agb_image_converter::include_gfx;
|
||||||
|
|
||||||
|
#[doc(hidden)]
|
||||||
pub use agb_image_converter::include_aseprite_inner;
|
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`.
|
/// This macro declares the entry point to your game written using `agb`.
|
||||||
///
|
///
|
||||||
|
|
|
@ -3,6 +3,7 @@
|
||||||
mod runner;
|
mod runner;
|
||||||
use anyhow::{anyhow, Error};
|
use anyhow::{anyhow, Error};
|
||||||
use image::io::Reader;
|
use image::io::Reader;
|
||||||
|
use image::GenericImage;
|
||||||
use io::Write;
|
use io::Write;
|
||||||
use regex::Regex;
|
use regex::Regex;
|
||||||
use runner::VideoBuffer;
|
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> {
|
fn check_image_match(image_path: &str, video_buffer: &VideoBuffer) -> Result<(), Error> {
|
||||||
let expected_image = Reader::open(image_path)?.decode()?;
|
let expected_image = Reader::open(image_path)?.decode()?;
|
||||||
let expected = expected_image
|
let expected = expected_image.to_rgba8();
|
||||||
.as_rgba8()
|
|
||||||
.ok_or(anyhow!("cannot convert to rgba8"))?;
|
|
||||||
|
|
||||||
let (buf_dim_x, buf_dim_y) = video_buffer.get_size();
|
let (buf_dim_x, buf_dim_y) = video_buffer.get_size();
|
||||||
let (exp_dim_x, exp_dim_y) = expected.dimensions();
|
let (exp_dim_x, exp_dim_y) = expected.dimensions();
|
||||||
if (buf_dim_x != exp_dim_x) || (buf_dim_y != exp_dim_y) {
|
if (buf_dim_x != exp_dim_x) || (buf_dim_y != exp_dim_y) {
|
||||||
return Err(anyhow!("image sizes do not match"));
|
return Err(anyhow!("image sizes do not match"));
|
||||||
}
|
}
|
||||||
|
|
||||||
for y in 0..buf_dim_y {
|
for y in 0..buf_dim_y {
|
||||||
for x in 0..buf_dim_x {
|
for x in 0..buf_dim_x {
|
||||||
let video_pixel = video_buffer.get_pixel(x, y);
|
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 video_pixel = gba_colour_to_rgba(video_pixel);
|
||||||
let image_pixel = rgba_to_gba_to_rgba(image_pixel.0);
|
let image_pixel = rgba_to_gba_to_rgba(image_pixel.0);
|
||||||
if image_pixel != video_pixel {
|
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(())
|
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()
|
||||||
|
}
|
||||||
|
|
Loading…
Reference in a new issue