mirror of
https://github.com/italicsjenga/agb.git
synced 2024-12-23 08:11:33 +11:00
Object based font rendering (#450)
* Pulled out of something else I was working on. - [x] Changelog updated / no changelog update needed
This commit is contained in:
commit
1eb0505e50
|
@ -8,10 +8,11 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
||||||
|
|
||||||
### Added
|
### Added
|
||||||
- New `include_palette` macro for including every colour in an image as a `u16` slice.
|
- New `include_palette` macro for including every colour in an image as a `u16` slice.
|
||||||
|
- New object based text renderer.
|
||||||
|
|
||||||
### Changed
|
### Changed
|
||||||
- Changed the default template game.
|
- Changed the default template game.
|
||||||
- `DynamicSprite` has a new API which changes the constructor and adds a `set_pixel` method.
|
- `DynamicSprite` has a new API which changes the constructor and adds a `set_pixel` and `clear` methods.
|
||||||
- You no longer need to install arm-none-eabi-binutils. In order to write games using `agb`, you now only need to install rust nightly.
|
- You no longer need to install arm-none-eabi-binutils. In order to write games using `agb`, you now only need to install rust nightly.
|
||||||
- 10% performance improvement with the software mixer.
|
- 10% performance improvement with the software mixer.
|
||||||
|
|
||||||
|
|
|
@ -25,9 +25,9 @@ pub fn load_font(font_data: &[u8], pixels_per_em: f32) -> TokenStream {
|
||||||
let line_metrics = font.horizontal_line_metrics(pixels_per_em).unwrap();
|
let line_metrics = font.horizontal_line_metrics(pixels_per_em).unwrap();
|
||||||
|
|
||||||
let line_height = line_metrics.new_line_size as i32;
|
let line_height = line_metrics.new_line_size as i32;
|
||||||
let ascent = line_metrics.ascent as i32;
|
let mut ascent = line_metrics.ascent as i32;
|
||||||
|
|
||||||
let font = (0..128)
|
let letters: Vec<_> = (0..128)
|
||||||
.map(|i| font.rasterize(char::from_u32(i).unwrap(), pixels_per_em))
|
.map(|i| font.rasterize(char::from_u32(i).unwrap(), pixels_per_em))
|
||||||
.map(|(metrics, bitmap)| {
|
.map(|(metrics, bitmap)| {
|
||||||
let width = metrics.width;
|
let width = metrics.width;
|
||||||
|
@ -56,7 +56,19 @@ pub fn load_font(font_data: &[u8], pixels_per_em: f32) -> TokenStream {
|
||||||
advance_width: metrics.advance_width,
|
advance_width: metrics.advance_width,
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
.map(|letter_data| {
|
.collect();
|
||||||
|
|
||||||
|
let maximum_above_line = letters
|
||||||
|
.iter()
|
||||||
|
.map(|x| (x.height as i32 + x.ymin))
|
||||||
|
.max()
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
if (ascent - maximum_above_line) < 0 {
|
||||||
|
ascent = maximum_above_line;
|
||||||
|
}
|
||||||
|
|
||||||
|
let font = letters.iter().map(|letter_data| {
|
||||||
let data_raw = ByteString(&letter_data.rendered);
|
let data_raw = ByteString(&letter_data.rendered);
|
||||||
let height = letter_data.height as u8;
|
let height = letter_data.height as u8;
|
||||||
let width = letter_data.width as u8;
|
let width = letter_data.width as u8;
|
||||||
|
|
Binary file not shown.
Before Width: | Height: | Size: 2.6 KiB After Width: | Height: | Size: 1.7 KiB |
BIN
agb/examples/font/pixelated.ttf
Normal file
BIN
agb/examples/font/pixelated.ttf
Normal file
Binary file not shown.
99
agb/examples/object_text_render.rs
Normal file
99
agb/examples/object_text_render.rs
Normal file
|
@ -0,0 +1,99 @@
|
||||||
|
#![no_std]
|
||||||
|
#![no_main]
|
||||||
|
|
||||||
|
use agb::{
|
||||||
|
display::{
|
||||||
|
object::{ChangeColour, ObjectTextRender, PaletteVram, Size, TextAlignment},
|
||||||
|
palette16::Palette16,
|
||||||
|
Font, HEIGHT, WIDTH,
|
||||||
|
},
|
||||||
|
include_font,
|
||||||
|
input::Button,
|
||||||
|
};
|
||||||
|
|
||||||
|
extern crate alloc;
|
||||||
|
|
||||||
|
use core::fmt::Write;
|
||||||
|
|
||||||
|
const FONT: Font = include_font!("examples/font/yoster.ttf", 12);
|
||||||
|
#[agb::entry]
|
||||||
|
fn entry(gba: agb::Gba) -> ! {
|
||||||
|
main(gba);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn main(mut gba: agb::Gba) -> ! {
|
||||||
|
let (mut unmanaged, _sprites) = gba.display.object.get_unmanaged();
|
||||||
|
|
||||||
|
loop {
|
||||||
|
let mut palette = [0x0; 16];
|
||||||
|
palette[1] = 0xFF_FF;
|
||||||
|
palette[2] = 0x00_FF;
|
||||||
|
let palette = Palette16::new(palette);
|
||||||
|
let palette = PaletteVram::new(&palette).unwrap();
|
||||||
|
|
||||||
|
let timer = gba.timers.timers();
|
||||||
|
let mut timer: agb::timer::Timer = timer.timer2;
|
||||||
|
|
||||||
|
timer.set_enabled(true);
|
||||||
|
timer.set_divider(agb::timer::Divider::Divider256);
|
||||||
|
|
||||||
|
let mut wr = ObjectTextRender::new(&FONT, Size::S16x16, palette);
|
||||||
|
let start = timer.value();
|
||||||
|
|
||||||
|
let player_name = "You";
|
||||||
|
let _ = writeln!(
|
||||||
|
wr,
|
||||||
|
"Woah!{change2} {player_name}! {change1}Hey there! I have a bunch of text I want to show you. However, you will find that the amount of text I can display is limited. Who'd have thought! Good thing that my text system supports scrolling! It only took around 20 jank versions to get here!",
|
||||||
|
change2 = ChangeColour::new(2),
|
||||||
|
change1 = ChangeColour::new(1),
|
||||||
|
);
|
||||||
|
let end = timer.value();
|
||||||
|
|
||||||
|
agb::println!(
|
||||||
|
"Write took {} cycles",
|
||||||
|
256 * (end.wrapping_sub(start) as u32)
|
||||||
|
);
|
||||||
|
|
||||||
|
let vblank = agb::interrupt::VBlank::get();
|
||||||
|
let mut input = agb::input::ButtonController::new();
|
||||||
|
|
||||||
|
let start = timer.value();
|
||||||
|
|
||||||
|
wr.layout((WIDTH, 40).into(), TextAlignment::Justify, 2);
|
||||||
|
let end = timer.value();
|
||||||
|
|
||||||
|
agb::println!(
|
||||||
|
"Layout took {} cycles",
|
||||||
|
256 * (end.wrapping_sub(start) as u32)
|
||||||
|
);
|
||||||
|
|
||||||
|
let mut line_done = false;
|
||||||
|
let mut frame = 0;
|
||||||
|
|
||||||
|
loop {
|
||||||
|
vblank.wait_for_vblank();
|
||||||
|
input.update();
|
||||||
|
let oam = &mut unmanaged.iter();
|
||||||
|
wr.commit(oam);
|
||||||
|
|
||||||
|
let start = timer.value();
|
||||||
|
if frame % 4 == 0 {
|
||||||
|
line_done = !wr.next_letter_group();
|
||||||
|
}
|
||||||
|
if line_done && input.is_just_pressed(Button::A) {
|
||||||
|
line_done = false;
|
||||||
|
wr.pop_line();
|
||||||
|
}
|
||||||
|
wr.update((0, HEIGHT - 40).into());
|
||||||
|
let end = timer.value();
|
||||||
|
|
||||||
|
frame += 1;
|
||||||
|
|
||||||
|
agb::println!(
|
||||||
|
"Took {} cycles, line done {}",
|
||||||
|
256 * (end.wrapping_sub(start) as u32),
|
||||||
|
line_done
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,4 +1,3 @@
|
||||||
use core::alloc::{Allocator, Layout};
|
|
||||||
use core::ops::{Deref, DerefMut};
|
use core::ops::{Deref, DerefMut};
|
||||||
use core::ptr::NonNull;
|
use core::ptr::NonNull;
|
||||||
|
|
||||||
|
@ -45,18 +44,23 @@ static GLOBAL_ALLOC: BlockAllocator = unsafe {
|
||||||
|
|
||||||
macro_rules! impl_zst_allocator {
|
macro_rules! impl_zst_allocator {
|
||||||
($name_of_struct: ty, $name_of_static: ident) => {
|
($name_of_struct: ty, $name_of_static: ident) => {
|
||||||
unsafe impl Allocator for $name_of_struct {
|
unsafe impl core::alloc::Allocator for $name_of_struct {
|
||||||
fn allocate(&self, layout: Layout) -> Result<NonNull<[u8]>, core::alloc::AllocError> {
|
fn allocate(
|
||||||
|
&self,
|
||||||
|
layout: core::alloc::Layout,
|
||||||
|
) -> Result<core::ptr::NonNull<[u8]>, core::alloc::AllocError> {
|
||||||
$name_of_static.allocate(layout)
|
$name_of_static.allocate(layout)
|
||||||
}
|
}
|
||||||
|
|
||||||
unsafe fn deallocate(&self, ptr: NonNull<u8>, layout: Layout) {
|
unsafe fn deallocate(&self, ptr: core::ptr::NonNull<u8>, layout: core::alloc::Layout) {
|
||||||
$name_of_static.deallocate(ptr, layout)
|
$name_of_static.deallocate(ptr, layout)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub(crate) use impl_zst_allocator;
|
||||||
|
|
||||||
/// This is the allocator for the External Working Ram. This is currently
|
/// This is the allocator for the External Working Ram. This is currently
|
||||||
/// equivalent to the Global Allocator (where things are allocated if no allocator is provided). This implements the allocator trait, so
|
/// equivalent to the Global Allocator (where things are allocated if no allocator is provided). This implements the allocator trait, so
|
||||||
/// is meant to be used in specifying where certain structures should be
|
/// is meant to be used in specifying where certain structures should be
|
||||||
|
|
|
@ -30,4 +30,19 @@ impl Bitmap3<'_> {
|
||||||
let y = y.try_into().unwrap();
|
let y = y.try_into().unwrap();
|
||||||
BITMAP_MODE_3.set(x, y, colour);
|
BITMAP_MODE_3.set(x, y, colour);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[must_use]
|
||||||
|
pub fn read_point(&self, x: i32, y: i32) -> u16 {
|
||||||
|
let x = x.try_into().unwrap();
|
||||||
|
let y = y.try_into().unwrap();
|
||||||
|
BITMAP_MODE_3.get(x, y)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn clear(&mut self, colour: u16) {
|
||||||
|
for y in 0..(HEIGHT as usize) {
|
||||||
|
for x in 0..(WIDTH as usize) {
|
||||||
|
BITMAP_MODE_3.set(x, y, colour);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -10,12 +10,12 @@ use super::tiled::{DynamicTile, RegularMap, TileSetting, VRamManager};
|
||||||
/// Does not support any unicode features.
|
/// Does not support any unicode features.
|
||||||
/// For usage see the `text_render.rs` example
|
/// For usage see the `text_render.rs` example
|
||||||
pub struct FontLetter {
|
pub struct FontLetter {
|
||||||
width: u8,
|
pub(crate) width: u8,
|
||||||
height: u8,
|
pub(crate) height: u8,
|
||||||
data: &'static [u8],
|
pub(crate) data: &'static [u8],
|
||||||
xmin: i8,
|
pub(crate) xmin: i8,
|
||||||
ymin: i8,
|
pub(crate) ymin: i8,
|
||||||
advance_width: u8,
|
pub(crate) advance_width: u8,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl FontLetter {
|
impl FontLetter {
|
||||||
|
@ -37,17 +37,24 @@ impl FontLetter {
|
||||||
advance_width,
|
advance_width,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub(crate) const fn bit_absolute(&self, x: usize, y: usize) -> bool {
|
||||||
|
let position = x + y * self.width as usize;
|
||||||
|
let byte = self.data[position / 8];
|
||||||
|
let bit = position % 8;
|
||||||
|
((byte >> bit) & 1) != 0
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub struct Font {
|
pub struct Font {
|
||||||
letters: &'static [FontLetter],
|
letters: &'static [FontLetter; 128],
|
||||||
line_height: i32,
|
line_height: i32,
|
||||||
ascent: i32,
|
ascent: i32,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Font {
|
impl Font {
|
||||||
#[must_use]
|
#[must_use]
|
||||||
pub const fn new(letters: &'static [FontLetter], line_height: i32, ascent: i32) -> Self {
|
pub const fn new(letters: &'static [FontLetter; 128], line_height: i32, ascent: i32) -> Self {
|
||||||
Self {
|
Self {
|
||||||
letters,
|
letters,
|
||||||
line_height,
|
line_height,
|
||||||
|
@ -55,8 +62,16 @@ impl Font {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn letter(&self, letter: char) -> &'static FontLetter {
|
pub(crate) fn letter(&self, letter: char) -> &'static FontLetter {
|
||||||
&self.letters[letter as usize]
|
&self.letters[letter as usize & (128 - 1)]
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn ascent(&self) -> i32 {
|
||||||
|
self.ascent
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn line_height(&self) -> i32 {
|
||||||
|
self.line_height
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -9,6 +9,7 @@
|
||||||
//! harder to integrate into your games depending on how they are architectured.
|
//! harder to integrate into your games depending on how they are architectured.
|
||||||
|
|
||||||
mod affine;
|
mod affine;
|
||||||
|
mod font;
|
||||||
mod managed;
|
mod managed;
|
||||||
mod sprites;
|
mod sprites;
|
||||||
mod unmanaged;
|
mod unmanaged;
|
||||||
|
@ -22,6 +23,8 @@ pub use affine::AffineMatrixInstance;
|
||||||
pub use managed::{OamManaged, Object};
|
pub use managed::{OamManaged, Object};
|
||||||
pub use unmanaged::{AffineMode, OamIterator, OamSlot, OamUnmanaged, ObjectUnmanaged};
|
pub use unmanaged::{AffineMode, OamIterator, OamSlot, OamUnmanaged, ObjectUnmanaged};
|
||||||
|
|
||||||
|
pub use font::{ChangeColour, ObjectTextRender, TextAlignment};
|
||||||
|
|
||||||
use super::DISPLAY_CONTROL;
|
use super::DISPLAY_CONTROL;
|
||||||
|
|
||||||
const OBJECT_ATTRIBUTE_MEMORY: *mut u16 = 0x0700_0000 as *mut u16;
|
const OBJECT_ATTRIBUTE_MEMORY: *mut u16 = 0x0700_0000 as *mut u16;
|
||||||
|
|
511
agb/src/display/object/font.rs
Normal file
511
agb/src/display/object/font.rs
Normal file
|
@ -0,0 +1,511 @@
|
||||||
|
use core::fmt::{Display, Write};
|
||||||
|
|
||||||
|
use agb_fixnum::{Num, Vector2D};
|
||||||
|
use alloc::{collections::VecDeque, vec::Vec};
|
||||||
|
|
||||||
|
use crate::display::Font;
|
||||||
|
|
||||||
|
use self::{
|
||||||
|
preprocess::{Line, Preprocessed, PreprocessedElement},
|
||||||
|
renderer::{Configuration, WordRender},
|
||||||
|
};
|
||||||
|
|
||||||
|
use super::{OamIterator, ObjectUnmanaged, PaletteVram, Size, SpriteVram};
|
||||||
|
|
||||||
|
mod preprocess;
|
||||||
|
mod renderer;
|
||||||
|
|
||||||
|
#[derive(Debug, PartialEq, Eq, Clone, Copy)]
|
||||||
|
#[non_exhaustive]
|
||||||
|
pub(crate) enum WhiteSpace {
|
||||||
|
NewLine,
|
||||||
|
Space,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl WhiteSpace {
|
||||||
|
pub(crate) fn from_char(c: char) -> Self {
|
||||||
|
match c {
|
||||||
|
' ' => WhiteSpace::Space,
|
||||||
|
'\n' => WhiteSpace::NewLine,
|
||||||
|
_ => panic!("char not supported whitespace"),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
struct BufferedRender<'font> {
|
||||||
|
char_render: WordRender,
|
||||||
|
preprocessor: Preprocessed,
|
||||||
|
buffered_chars: VecDeque<char>,
|
||||||
|
letters: Letters,
|
||||||
|
font: &'font Font,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Default)]
|
||||||
|
struct Letters {
|
||||||
|
letters: VecDeque<SpriteVram>,
|
||||||
|
number_of_groups: usize,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
|
||||||
|
#[non_exhaustive]
|
||||||
|
/// The text alignment of the layout
|
||||||
|
pub enum TextAlignment {
|
||||||
|
#[default]
|
||||||
|
/// Left aligned, the left edge of the text lines up
|
||||||
|
Left,
|
||||||
|
/// Right aligned, the right edge of the text lines up
|
||||||
|
Right,
|
||||||
|
/// Center aligned, the center of the text lines up
|
||||||
|
Center,
|
||||||
|
/// Justified, both the left and right edges line up with space width adapted to make it so.
|
||||||
|
Justify,
|
||||||
|
}
|
||||||
|
|
||||||
|
struct TextAlignmentSettings {
|
||||||
|
space_width: Num<i32, 10>,
|
||||||
|
start_x: i32,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl TextAlignment {
|
||||||
|
fn settings(self, line: &Line, minimum_space_width: i32, width: i32) -> TextAlignmentSettings {
|
||||||
|
match self {
|
||||||
|
TextAlignment::Left => TextAlignmentSettings {
|
||||||
|
space_width: minimum_space_width.into(),
|
||||||
|
start_x: 0,
|
||||||
|
},
|
||||||
|
TextAlignment::Right => TextAlignmentSettings {
|
||||||
|
space_width: minimum_space_width.into(),
|
||||||
|
start_x: width - line.width(),
|
||||||
|
},
|
||||||
|
TextAlignment::Center => TextAlignmentSettings {
|
||||||
|
space_width: minimum_space_width.into(),
|
||||||
|
start_x: (width - line.width()) / 2,
|
||||||
|
},
|
||||||
|
TextAlignment::Justify => {
|
||||||
|
let space_width = if line.number_of_spaces() != 0 {
|
||||||
|
Num::new(
|
||||||
|
width - line.width() + line.number_of_spaces() as i32 * minimum_space_width,
|
||||||
|
) / line.number_of_spaces() as i32
|
||||||
|
} else {
|
||||||
|
minimum_space_width.into()
|
||||||
|
};
|
||||||
|
TextAlignmentSettings {
|
||||||
|
space_width,
|
||||||
|
start_x: 0,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<'font> BufferedRender<'font> {
|
||||||
|
#[must_use]
|
||||||
|
fn new(font: &'font Font, sprite_size: Size, palette: PaletteVram) -> Self {
|
||||||
|
let config = Configuration::new(sprite_size, palette);
|
||||||
|
BufferedRender {
|
||||||
|
char_render: WordRender::new(config),
|
||||||
|
preprocessor: Preprocessed::new(),
|
||||||
|
buffered_chars: VecDeque::new(),
|
||||||
|
letters: Default::default(),
|
||||||
|
font,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn is_private_use(c: char) -> bool {
|
||||||
|
('\u{E000}'..'\u{F8FF}').contains(&c)
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
|
||||||
|
/// Changes the palette to use to draw characters.
|
||||||
|
/// ```rust,no_run
|
||||||
|
/// # #![no_std]
|
||||||
|
/// # #![no_main]
|
||||||
|
/// use agb::display::object::{ObjectTextRender, PaletteVram, ChangeColour, Size};
|
||||||
|
/// use agb::display::palette16::Palette16;
|
||||||
|
/// use agb::display::Font;
|
||||||
|
///
|
||||||
|
/// use core::fmt::Write;
|
||||||
|
///
|
||||||
|
/// const EXAMPLE_FONT: Font = agb::include_font!("examples/font/yoster.ttf", 12);
|
||||||
|
///
|
||||||
|
/// # fn foo() {
|
||||||
|
/// let mut palette = [0x0; 16];
|
||||||
|
/// palette[1] = 0xFF_FF;
|
||||||
|
/// palette[2] = 0x00_FF;
|
||||||
|
/// let palette = Palette16::new(palette);
|
||||||
|
/// let palette = PaletteVram::new(&palette).unwrap();
|
||||||
|
/// let mut writer = ObjectTextRender::new(&EXAMPLE_FONT, Size::S16x16, palette);
|
||||||
|
///
|
||||||
|
/// let _ = writeln!(writer, "Hello, {}World{}!", ChangeColour::new(2), ChangeColour::new(1));
|
||||||
|
/// # }
|
||||||
|
/// ```
|
||||||
|
pub struct ChangeColour(u8);
|
||||||
|
|
||||||
|
impl ChangeColour {
|
||||||
|
#[must_use]
|
||||||
|
/// Creates the colour changer. Colour is a palette index and must be in the range 0..16.
|
||||||
|
pub fn new(colour: usize) -> Self {
|
||||||
|
assert!(colour < 16, "paletted colour must be valid (0..=15)");
|
||||||
|
|
||||||
|
Self(colour as u8)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn try_from_char(c: char) -> Option<Self> {
|
||||||
|
let c = c as u32 as usize;
|
||||||
|
if (0xE000..0xE000 + 16).contains(&c) {
|
||||||
|
Some(ChangeColour::new(c - 0xE000))
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn to_char(self) -> char {
|
||||||
|
char::from_u32(self.0 as u32 + 0xE000).unwrap()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Display for ChangeColour {
|
||||||
|
fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
|
||||||
|
f.write_char(self.to_char())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl BufferedRender<'_> {
|
||||||
|
fn input_character(&mut self, character: char) {
|
||||||
|
if !is_private_use(character) {
|
||||||
|
self.preprocessor
|
||||||
|
.add_character(self.font, character, self.char_render.sprite_width());
|
||||||
|
}
|
||||||
|
self.buffered_chars.push_back(character);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn process(&mut self) {
|
||||||
|
let Some(c) = self.buffered_chars.pop_front() else {
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
match c {
|
||||||
|
' ' | '\n' => {
|
||||||
|
if let Some(group) = self.char_render.finalise_letter() {
|
||||||
|
self.letters.letters.push_back(group);
|
||||||
|
self.letters.number_of_groups += 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
self.letters.number_of_groups += 1;
|
||||||
|
}
|
||||||
|
letter => {
|
||||||
|
if let Some(group) = self.char_render.render_char(self.font, letter) {
|
||||||
|
self.letters.letters.push_back(group);
|
||||||
|
self.letters.number_of_groups += 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The object text renderer. Uses objects to render and layout text. It's use is non trivial.
|
||||||
|
/// Changes the palette to use to draw characters.
|
||||||
|
/// ```rust,no_run
|
||||||
|
/// #![no_std]
|
||||||
|
/// #![no_main]
|
||||||
|
/// use agb::display::object::{ObjectTextRender, PaletteVram, TextAlignment, Size};
|
||||||
|
/// use agb::display::palette16::Palette16;
|
||||||
|
/// use agb::display::{Font, WIDTH};
|
||||||
|
///
|
||||||
|
/// use core::fmt::Write;
|
||||||
|
///
|
||||||
|
/// const EXAMPLE_FONT: Font = agb::include_font!("examples/font/yoster.ttf", 12);
|
||||||
|
///
|
||||||
|
/// #[agb::entry]
|
||||||
|
/// fn main(gba: &mut agb::Gba) -> ! {
|
||||||
|
/// let (mut unmanaged, _) = gba.display.object.get_unmanaged();
|
||||||
|
/// let vblank = agb::interrupt::VBlank::get();
|
||||||
|
///
|
||||||
|
/// let mut palette = [0x0; 16];
|
||||||
|
/// palette[1] = 0xFF_FF;
|
||||||
|
/// let palette = Palette16::new(palette);
|
||||||
|
/// let palette = PaletteVram::new(&palette).unwrap();
|
||||||
|
///
|
||||||
|
/// let mut writer = ObjectTextRender::new(&EXAMPLE_FONT, Size::S16x16, palette);
|
||||||
|
///
|
||||||
|
/// let _ = writeln!(writer, "Hello, World!");
|
||||||
|
/// writer.layout((WIDTH, 40).into(), TextAlignment::Left, 2);
|
||||||
|
///
|
||||||
|
/// loop {
|
||||||
|
/// writer.next_letter_group();
|
||||||
|
/// writer.update((0, 0).into());
|
||||||
|
/// vblank.wait_for_vblank();
|
||||||
|
/// let oam = &mut unmanaged.iter();
|
||||||
|
/// writer.commit(oam);
|
||||||
|
/// }
|
||||||
|
/// }
|
||||||
|
/// ```
|
||||||
|
pub struct ObjectTextRender<'font> {
|
||||||
|
buffer: BufferedRender<'font>,
|
||||||
|
layout: LayoutCache,
|
||||||
|
number_of_objects: usize,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<'font> ObjectTextRender<'font> {
|
||||||
|
#[must_use]
|
||||||
|
/// Creates a new text renderer with a given font, sprite size, and palette.
|
||||||
|
/// You must ensure that the sprite size can accomodate the letters from the
|
||||||
|
/// font otherwise it will panic at render time.
|
||||||
|
pub fn new(font: &'font Font, sprite_size: Size, palette: PaletteVram) -> Self {
|
||||||
|
Self {
|
||||||
|
buffer: BufferedRender::new(font, sprite_size, palette),
|
||||||
|
number_of_objects: 0,
|
||||||
|
layout: LayoutCache {
|
||||||
|
positions: VecDeque::new(),
|
||||||
|
line_capacity: VecDeque::new(),
|
||||||
|
objects: Vec::new(),
|
||||||
|
objects_are_at_origin: (0, 0).into(),
|
||||||
|
area: (0, 0).into(),
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Write for ObjectTextRender<'_> {
|
||||||
|
fn write_str(&mut self, s: &str) -> core::fmt::Result {
|
||||||
|
for c in s.chars() {
|
||||||
|
self.buffer.input_character(c);
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl ObjectTextRender<'_> {
|
||||||
|
/// Commits work already done to screen. You can commit to multiple places in the same frame.
|
||||||
|
pub fn commit(&mut self, oam: &mut OamIterator) {
|
||||||
|
for (object, slot) in self.layout.objects.iter().zip(oam) {
|
||||||
|
slot.set(object);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Force a relayout, must be called after writing.
|
||||||
|
pub fn layout(
|
||||||
|
&mut self,
|
||||||
|
area: Vector2D<i32>,
|
||||||
|
alignment: TextAlignment,
|
||||||
|
paragraph_spacing: i32,
|
||||||
|
) {
|
||||||
|
self.layout.create_positions(
|
||||||
|
self.buffer.font,
|
||||||
|
&self.buffer.preprocessor,
|
||||||
|
&LayoutSettings {
|
||||||
|
area,
|
||||||
|
alignment,
|
||||||
|
paragraph_spacing,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Removes one complete line. Returns whether a line could be removed. You must call [`update`][ObjectTextRender::update] after this
|
||||||
|
pub fn pop_line(&mut self) -> bool {
|
||||||
|
let width = self.layout.area.x;
|
||||||
|
let space = self.buffer.font.letter(' ').advance_width as i32;
|
||||||
|
let line_height = self.buffer.font.line_height();
|
||||||
|
if let Some(line) = self.buffer.preprocessor.lines(width, space).next() {
|
||||||
|
// there is a line
|
||||||
|
if self.layout.objects.len() >= line.number_of_letter_groups() {
|
||||||
|
// we have enough rendered letter groups to count
|
||||||
|
self.number_of_objects -= line.number_of_letter_groups();
|
||||||
|
for _ in 0..line.number_of_letter_groups() {
|
||||||
|
self.buffer.letters.letters.pop_front();
|
||||||
|
self.layout.positions.pop_front();
|
||||||
|
}
|
||||||
|
self.layout.line_capacity.pop_front();
|
||||||
|
self.layout.objects.clear();
|
||||||
|
self.buffer.preprocessor.pop(&line);
|
||||||
|
for position in self.layout.positions.iter_mut() {
|
||||||
|
position.y -= line_height as i16;
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
false
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Updates the internal state of the number of letters to write and popped
|
||||||
|
/// line. Should be called in the same frame as and after
|
||||||
|
/// [`next_letter_group`][ObjectTextRender::next_letter_group], [`next_line`][ObjectTextRender::next_line], and [`pop_line`][ObjectTextRender::pop_line].
|
||||||
|
pub fn update(&mut self, position: Vector2D<i32>) {
|
||||||
|
if !self.buffer.buffered_chars.is_empty()
|
||||||
|
&& self.buffer.letters.letters.len() <= self.number_of_objects + 5
|
||||||
|
{
|
||||||
|
self.buffer.process();
|
||||||
|
}
|
||||||
|
|
||||||
|
self.layout.update_objects_to_display_at_position(
|
||||||
|
position,
|
||||||
|
self.buffer.letters.letters.iter(),
|
||||||
|
self.number_of_objects,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Causes the next letter group to be shown on the next update. Returns
|
||||||
|
/// whether another letter could be added in the space given.
|
||||||
|
pub fn next_letter_group(&mut self) -> bool {
|
||||||
|
if !self.can_render_another_element() {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
self.number_of_objects += 1;
|
||||||
|
self.at_least_n_letter_groups(self.number_of_objects);
|
||||||
|
|
||||||
|
true
|
||||||
|
}
|
||||||
|
|
||||||
|
fn can_render_another_element(&self) -> bool {
|
||||||
|
let max_number_of_lines = (self.layout.area.y / self.buffer.font.line_height()) as usize;
|
||||||
|
|
||||||
|
let max_number_of_objects = self
|
||||||
|
.layout
|
||||||
|
.line_capacity
|
||||||
|
.iter()
|
||||||
|
.take(max_number_of_lines)
|
||||||
|
.sum::<usize>();
|
||||||
|
|
||||||
|
max_number_of_objects > self.number_of_objects
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Causes the next line to be shown on the next update. Returns
|
||||||
|
/// whether another line could be added in the space given.
|
||||||
|
pub fn next_line(&mut self) -> bool {
|
||||||
|
let max_number_of_lines = (self.layout.area.y / self.buffer.font.line_height()) as usize;
|
||||||
|
|
||||||
|
// find current line
|
||||||
|
|
||||||
|
for (start, end) in self
|
||||||
|
.layout
|
||||||
|
.line_capacity
|
||||||
|
.iter()
|
||||||
|
.scan(0, |count, line_size| {
|
||||||
|
let start = *count;
|
||||||
|
*count += line_size;
|
||||||
|
Some((start, *count))
|
||||||
|
})
|
||||||
|
.take(max_number_of_lines)
|
||||||
|
{
|
||||||
|
if self.number_of_objects >= start && self.number_of_objects < end {
|
||||||
|
self.number_of_objects = end;
|
||||||
|
self.at_least_n_letter_groups(end);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
false
|
||||||
|
}
|
||||||
|
|
||||||
|
fn at_least_n_letter_groups(&mut self, n: usize) {
|
||||||
|
while !self.buffer.buffered_chars.is_empty() && self.buffer.letters.letters.len() <= n {
|
||||||
|
self.buffer.process();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
struct LayoutCache {
|
||||||
|
positions: VecDeque<Vector2D<i16>>,
|
||||||
|
line_capacity: VecDeque<usize>,
|
||||||
|
objects: Vec<ObjectUnmanaged>,
|
||||||
|
objects_are_at_origin: Vector2D<i32>,
|
||||||
|
area: Vector2D<i32>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl LayoutCache {
|
||||||
|
fn update_objects_to_display_at_position<'a>(
|
||||||
|
&mut self,
|
||||||
|
position: Vector2D<i32>,
|
||||||
|
letters: impl Iterator<Item = &'a SpriteVram>,
|
||||||
|
number_of_objects: usize,
|
||||||
|
) {
|
||||||
|
let already_done = if position == self.objects_are_at_origin {
|
||||||
|
self.objects.len()
|
||||||
|
} else {
|
||||||
|
self.objects.clear();
|
||||||
|
0
|
||||||
|
};
|
||||||
|
self.objects.extend(
|
||||||
|
self.positions
|
||||||
|
.iter()
|
||||||
|
.zip(letters)
|
||||||
|
.take(number_of_objects)
|
||||||
|
.skip(already_done)
|
||||||
|
.map(|(offset, letter)| {
|
||||||
|
let position = offset.change_base() + position;
|
||||||
|
let mut object = ObjectUnmanaged::new(letter.clone());
|
||||||
|
object.show().set_position(position);
|
||||||
|
object
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
self.objects.truncate(number_of_objects);
|
||||||
|
self.objects_are_at_origin = position;
|
||||||
|
}
|
||||||
|
|
||||||
|
fn create_positions(
|
||||||
|
&mut self,
|
||||||
|
font: &Font,
|
||||||
|
preprocessed: &Preprocessed,
|
||||||
|
settings: &LayoutSettings,
|
||||||
|
) {
|
||||||
|
self.area = settings.area;
|
||||||
|
self.line_capacity.clear();
|
||||||
|
self.positions.clear();
|
||||||
|
for (line, line_positions) in Self::create_layout(font, preprocessed, settings) {
|
||||||
|
self.line_capacity.push_back(line.number_of_letter_groups());
|
||||||
|
self.positions
|
||||||
|
.extend(line_positions.map(|x| Vector2D::new(x.x as i16, x.y as i16)));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn create_layout<'a>(
|
||||||
|
font: &Font,
|
||||||
|
preprocessed: &'a Preprocessed,
|
||||||
|
settings: &'a LayoutSettings,
|
||||||
|
) -> impl Iterator<Item = (Line, impl Iterator<Item = Vector2D<i32>> + 'a)> + 'a {
|
||||||
|
let minimum_space_width = font.letter(' ').advance_width as i32;
|
||||||
|
let width = settings.area.x;
|
||||||
|
let line_height = font.line_height();
|
||||||
|
|
||||||
|
let mut head_position: Vector2D<Num<i32, 10>> = (0, -line_height).into();
|
||||||
|
|
||||||
|
preprocessed
|
||||||
|
.lines_element(width, minimum_space_width)
|
||||||
|
.map(move |(line, line_elements)| {
|
||||||
|
let line_settings = settings
|
||||||
|
.alignment
|
||||||
|
.settings(&line, minimum_space_width, width);
|
||||||
|
|
||||||
|
head_position.y += line_height;
|
||||||
|
head_position.x = line_settings.start_x.into();
|
||||||
|
|
||||||
|
(
|
||||||
|
line,
|
||||||
|
line_elements.filter_map(move |element| match element.decode() {
|
||||||
|
PreprocessedElement::LetterGroup { width } => {
|
||||||
|
let this_position = head_position;
|
||||||
|
head_position.x += width as i32;
|
||||||
|
Some(this_position.floor())
|
||||||
|
}
|
||||||
|
PreprocessedElement::WhiteSpace(space) => {
|
||||||
|
match space {
|
||||||
|
WhiteSpace::NewLine => {
|
||||||
|
head_position.y += settings.paragraph_spacing;
|
||||||
|
}
|
||||||
|
WhiteSpace::Space => head_position.x += line_settings.space_width,
|
||||||
|
}
|
||||||
|
None
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(PartialEq, Eq, Default)]
|
||||||
|
struct LayoutSettings {
|
||||||
|
area: Vector2D<i32>,
|
||||||
|
alignment: TextAlignment,
|
||||||
|
paragraph_spacing: i32,
|
||||||
|
}
|
241
agb/src/display/object/font/preprocess.rs
Normal file
241
agb/src/display/object/font/preprocess.rs
Normal file
|
@ -0,0 +1,241 @@
|
||||||
|
use alloc::collections::VecDeque;
|
||||||
|
|
||||||
|
use crate::display::Font;
|
||||||
|
|
||||||
|
use super::WhiteSpace;
|
||||||
|
|
||||||
|
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
|
||||||
|
pub(crate) struct PreprocessedElementEncoded(u8);
|
||||||
|
|
||||||
|
impl PreprocessedElementEncoded {
|
||||||
|
pub(crate) fn decode(self) -> PreprocessedElement {
|
||||||
|
match self.0 {
|
||||||
|
255 => PreprocessedElement::WhiteSpace(WhiteSpace::NewLine),
|
||||||
|
254 => PreprocessedElement::WhiteSpace(WhiteSpace::Space),
|
||||||
|
width => PreprocessedElement::LetterGroup { width },
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
|
||||||
|
|
||||||
|
pub(crate) enum PreprocessedElement {
|
||||||
|
LetterGroup { width: u8 },
|
||||||
|
WhiteSpace(WhiteSpace),
|
||||||
|
}
|
||||||
|
|
||||||
|
impl PreprocessedElement {
|
||||||
|
fn encode(self) -> PreprocessedElementEncoded {
|
||||||
|
PreprocessedElementEncoded(match self {
|
||||||
|
PreprocessedElement::LetterGroup { width } => width,
|
||||||
|
PreprocessedElement::WhiteSpace(space) => match space {
|
||||||
|
WhiteSpace::NewLine => 255,
|
||||||
|
WhiteSpace::Space => 254,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Default, Debug)]
|
||||||
|
pub(crate) struct Preprocessed {
|
||||||
|
widths: VecDeque<PreprocessedElementEncoded>,
|
||||||
|
preprocessor: Preprocessor,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Default)]
|
||||||
|
struct Preprocessor {
|
||||||
|
width_in_sprite: i32,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Preprocessor {
|
||||||
|
fn add_character(
|
||||||
|
&mut self,
|
||||||
|
font: &Font,
|
||||||
|
character: char,
|
||||||
|
sprite_width: i32,
|
||||||
|
widths: &mut VecDeque<PreprocessedElementEncoded>,
|
||||||
|
) {
|
||||||
|
match character {
|
||||||
|
space @ (' ' | '\n') => {
|
||||||
|
if self.width_in_sprite != 0 {
|
||||||
|
widths.push_back(
|
||||||
|
PreprocessedElement::LetterGroup {
|
||||||
|
width: self.width_in_sprite as u8,
|
||||||
|
}
|
||||||
|
.encode(),
|
||||||
|
);
|
||||||
|
self.width_in_sprite = 0;
|
||||||
|
}
|
||||||
|
widths.push_back(
|
||||||
|
PreprocessedElement::WhiteSpace(WhiteSpace::from_char(space)).encode(),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
letter => {
|
||||||
|
let letter = font.letter(letter);
|
||||||
|
if self.width_in_sprite + letter.width as i32 > sprite_width {
|
||||||
|
widths.push_back(
|
||||||
|
PreprocessedElement::LetterGroup {
|
||||||
|
width: self.width_in_sprite as u8,
|
||||||
|
}
|
||||||
|
.encode(),
|
||||||
|
);
|
||||||
|
self.width_in_sprite = 0;
|
||||||
|
}
|
||||||
|
if self.width_in_sprite != 0 {
|
||||||
|
self.width_in_sprite += letter.xmin as i32;
|
||||||
|
}
|
||||||
|
self.width_in_sprite += letter.advance_width as i32;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) struct Lines<'preprocess> {
|
||||||
|
minimum_space_width: i32,
|
||||||
|
layout_width: i32,
|
||||||
|
data: &'preprocess VecDeque<PreprocessedElementEncoded>,
|
||||||
|
current_start_idx: usize,
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) struct Line {
|
||||||
|
width: i32,
|
||||||
|
number_of_text_elements: usize,
|
||||||
|
number_of_spaces: usize,
|
||||||
|
number_of_letter_groups: usize,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Line {
|
||||||
|
#[inline(always)]
|
||||||
|
pub(crate) fn width(&self) -> i32 {
|
||||||
|
self.width
|
||||||
|
}
|
||||||
|
#[inline(always)]
|
||||||
|
pub(crate) fn number_of_text_elements(&self) -> usize {
|
||||||
|
self.number_of_text_elements
|
||||||
|
}
|
||||||
|
#[inline(always)]
|
||||||
|
pub(crate) fn number_of_spaces(&self) -> usize {
|
||||||
|
self.number_of_spaces
|
||||||
|
}
|
||||||
|
#[inline(always)]
|
||||||
|
pub(crate) fn number_of_letter_groups(&self) -> usize {
|
||||||
|
self.number_of_letter_groups
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<'pre> Iterator for Lines<'pre> {
|
||||||
|
type Item = Line;
|
||||||
|
|
||||||
|
fn next(&mut self) -> Option<Self::Item> {
|
||||||
|
if self.current_start_idx >= self.data.len() {
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
|
||||||
|
let mut line_idx_length = 0;
|
||||||
|
let mut current_line_width_pixels = 0;
|
||||||
|
let mut spaces_after_last_word_count = 0usize;
|
||||||
|
let mut start_of_current_word = usize::MAX;
|
||||||
|
let mut length_of_current_word_pixels = 0;
|
||||||
|
let mut length_of_current_word = 0;
|
||||||
|
let mut number_of_spaces = 0;
|
||||||
|
let mut number_of_letter_groups = 0;
|
||||||
|
|
||||||
|
while let Some(next) = self.data.get(self.current_start_idx + line_idx_length) {
|
||||||
|
match next.decode() {
|
||||||
|
PreprocessedElement::LetterGroup { width } => {
|
||||||
|
if start_of_current_word == usize::MAX {
|
||||||
|
start_of_current_word = line_idx_length;
|
||||||
|
}
|
||||||
|
length_of_current_word_pixels += width as i32;
|
||||||
|
length_of_current_word += 1;
|
||||||
|
if current_line_width_pixels
|
||||||
|
+ length_of_current_word_pixels
|
||||||
|
+ spaces_after_last_word_count as i32 * self.minimum_space_width
|
||||||
|
>= self.layout_width
|
||||||
|
{
|
||||||
|
line_idx_length = start_of_current_word;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
PreprocessedElement::WhiteSpace(space) => {
|
||||||
|
if start_of_current_word != usize::MAX {
|
||||||
|
// flush word
|
||||||
|
current_line_width_pixels += length_of_current_word_pixels
|
||||||
|
+ spaces_after_last_word_count as i32 * self.minimum_space_width;
|
||||||
|
number_of_spaces += spaces_after_last_word_count;
|
||||||
|
number_of_letter_groups += length_of_current_word;
|
||||||
|
|
||||||
|
// reset parser
|
||||||
|
length_of_current_word_pixels = 0;
|
||||||
|
length_of_current_word = 0;
|
||||||
|
start_of_current_word = usize::MAX;
|
||||||
|
spaces_after_last_word_count = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
match space {
|
||||||
|
WhiteSpace::NewLine => {
|
||||||
|
line_idx_length += 1;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
WhiteSpace::Space => {
|
||||||
|
spaces_after_last_word_count += 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
line_idx_length += 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
self.current_start_idx += line_idx_length;
|
||||||
|
|
||||||
|
Some(Line {
|
||||||
|
width: current_line_width_pixels,
|
||||||
|
number_of_text_elements: line_idx_length,
|
||||||
|
number_of_spaces,
|
||||||
|
number_of_letter_groups,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Preprocessed {
|
||||||
|
pub(crate) fn new() -> Self {
|
||||||
|
Default::default()
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn add_character(&mut self, font: &Font, c: char, sprite_width: i32) {
|
||||||
|
self.preprocessor
|
||||||
|
.add_character(font, c, sprite_width, &mut self.widths);
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn pop(&mut self, line: &Line) {
|
||||||
|
let elements = line.number_of_text_elements();
|
||||||
|
for _ in 0..elements {
|
||||||
|
self.widths.pop_front();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn lines(&self, layout_width: i32, minimum_space_width: i32) -> Lines<'_> {
|
||||||
|
Lines {
|
||||||
|
minimum_space_width,
|
||||||
|
layout_width,
|
||||||
|
data: &self.widths,
|
||||||
|
current_start_idx: 0,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn lines_element(
|
||||||
|
&self,
|
||||||
|
layout_width: i32,
|
||||||
|
minimum_space_width: i32,
|
||||||
|
) -> impl Iterator<Item = (Line, impl Iterator<Item = PreprocessedElementEncoded> + '_)> {
|
||||||
|
let mut idx = 0;
|
||||||
|
self.lines(layout_width, minimum_space_width).map(move |x| {
|
||||||
|
let length = x.number_of_text_elements;
|
||||||
|
|
||||||
|
let d = self.widths.range(idx..(idx + length)).copied();
|
||||||
|
idx += length;
|
||||||
|
(x, d)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
117
agb/src/display/object/font/renderer.rs
Normal file
117
agb/src/display/object/font/renderer.rs
Normal file
|
@ -0,0 +1,117 @@
|
||||||
|
use crate::display::{
|
||||||
|
object::{DynamicSprite, PaletteVram, Size, SpriteVram},
|
||||||
|
Font,
|
||||||
|
};
|
||||||
|
|
||||||
|
use super::ChangeColour;
|
||||||
|
|
||||||
|
struct WorkingLetter {
|
||||||
|
dynamic: DynamicSprite,
|
||||||
|
// where to render the letter from x_min to x_max
|
||||||
|
x_offset: i32,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl WorkingLetter {
|
||||||
|
fn new(size: Size) -> Self {
|
||||||
|
Self {
|
||||||
|
dynamic: DynamicSprite::new(size),
|
||||||
|
x_offset: 0,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn reset(&mut self) {
|
||||||
|
self.x_offset = 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct Configuration {
|
||||||
|
sprite_size: Size,
|
||||||
|
palette: PaletteVram,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Configuration {
|
||||||
|
#[must_use]
|
||||||
|
pub fn new(sprite_size: Size, palette: PaletteVram) -> Self {
|
||||||
|
Self {
|
||||||
|
sprite_size,
|
||||||
|
palette,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) struct WordRender {
|
||||||
|
working: WorkingLetter,
|
||||||
|
config: Configuration,
|
||||||
|
colour: usize,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl WordRender {
|
||||||
|
pub(crate) fn sprite_width(&self) -> i32 {
|
||||||
|
self.config.sprite_size.to_width_height().0 as i32
|
||||||
|
}
|
||||||
|
|
||||||
|
#[must_use]
|
||||||
|
pub(crate) fn new(config: Configuration) -> Self {
|
||||||
|
WordRender {
|
||||||
|
working: WorkingLetter::new(config.sprite_size),
|
||||||
|
config,
|
||||||
|
colour: 1,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[must_use]
|
||||||
|
pub(crate) fn finalise_letter(&mut self) -> Option<SpriteVram> {
|
||||||
|
if self.working.x_offset == 0 {
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
|
||||||
|
let mut new_sprite = DynamicSprite::new(self.config.sprite_size);
|
||||||
|
core::mem::swap(&mut self.working.dynamic, &mut new_sprite);
|
||||||
|
let sprite = new_sprite.to_vram(self.config.palette.clone());
|
||||||
|
self.working.reset();
|
||||||
|
|
||||||
|
Some(sprite)
|
||||||
|
}
|
||||||
|
|
||||||
|
#[must_use]
|
||||||
|
pub(crate) fn render_char(&mut self, font: &Font, c: char) -> Option<SpriteVram> {
|
||||||
|
if let Some(next_colour) = ChangeColour::try_from_char(c) {
|
||||||
|
self.colour = next_colour.0 as usize;
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
|
||||||
|
let font_letter: &crate::display::FontLetter = font.letter(c);
|
||||||
|
|
||||||
|
// uses more than the sprite can hold
|
||||||
|
let group = if self.working.x_offset + font_letter.width as i32
|
||||||
|
> self.config.sprite_size.to_width_height().0 as i32
|
||||||
|
{
|
||||||
|
self.finalise_letter()
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
};
|
||||||
|
|
||||||
|
if self.working.x_offset != 0 {
|
||||||
|
self.working.x_offset += font_letter.xmin as i32;
|
||||||
|
}
|
||||||
|
|
||||||
|
let y_position = font.ascent() - font_letter.height as i32 - font_letter.ymin as i32;
|
||||||
|
|
||||||
|
for y in 0..font_letter.height as usize {
|
||||||
|
for x in 0..font_letter.width as usize {
|
||||||
|
let rendered = font_letter.bit_absolute(x, y);
|
||||||
|
if rendered {
|
||||||
|
self.working.dynamic.set_pixel(
|
||||||
|
x + self.working.x_offset as usize,
|
||||||
|
(y_position + y as i32) as usize,
|
||||||
|
self.colour,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
self.working.x_offset += font_letter.advance_width as i32;
|
||||||
|
|
||||||
|
group
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,14 +1,12 @@
|
||||||
use core::{alloc::Allocator, ptr::NonNull};
|
use core::{alloc::Allocator, ptr::NonNull};
|
||||||
|
|
||||||
use alloc::{
|
use alloc::{
|
||||||
alloc::Global,
|
|
||||||
boxed::Box,
|
boxed::Box,
|
||||||
rc::{Rc, Weak},
|
rc::{Rc, Weak},
|
||||||
vec::Vec,
|
|
||||||
};
|
};
|
||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
agb_alloc::{block_allocator::BlockAllocator, bump_allocator::StartEnd},
|
agb_alloc::{block_allocator::BlockAllocator, bump_allocator::StartEnd, impl_zst_allocator},
|
||||||
display::palette16::Palette16,
|
display::palette16::Palette16,
|
||||||
hash_map::HashMap,
|
hash_map::HashMap,
|
||||||
};
|
};
|
||||||
|
@ -18,8 +16,8 @@ use super::{
|
||||||
BYTES_PER_TILE_4BPP,
|
BYTES_PER_TILE_4BPP,
|
||||||
};
|
};
|
||||||
|
|
||||||
const PALETTE_SPRITE: usize = 0x0500_0200;
|
pub const PALETTE_SPRITE: usize = 0x0500_0200;
|
||||||
const TILE_SPRITE: usize = 0x06010000;
|
pub const TILE_SPRITE: usize = 0x06010000;
|
||||||
|
|
||||||
static SPRITE_ALLOCATOR: BlockAllocator = unsafe {
|
static SPRITE_ALLOCATOR: BlockAllocator = unsafe {
|
||||||
BlockAllocator::new(StartEnd {
|
BlockAllocator::new(StartEnd {
|
||||||
|
@ -28,6 +26,10 @@ static SPRITE_ALLOCATOR: BlockAllocator = unsafe {
|
||||||
})
|
})
|
||||||
};
|
};
|
||||||
|
|
||||||
|
pub struct SpriteAllocator;
|
||||||
|
|
||||||
|
impl_zst_allocator!(SpriteAllocator, SPRITE_ALLOCATOR);
|
||||||
|
|
||||||
static PALETTE_ALLOCATOR: BlockAllocator = unsafe {
|
static PALETTE_ALLOCATOR: BlockAllocator = unsafe {
|
||||||
BlockAllocator::new(StartEnd {
|
BlockAllocator::new(StartEnd {
|
||||||
start: || PALETTE_SPRITE,
|
start: || PALETTE_SPRITE,
|
||||||
|
@ -35,6 +37,10 @@ static PALETTE_ALLOCATOR: BlockAllocator = unsafe {
|
||||||
})
|
})
|
||||||
};
|
};
|
||||||
|
|
||||||
|
pub struct PaletteAllocator;
|
||||||
|
|
||||||
|
impl_zst_allocator!(PaletteAllocator, PALETTE_ALLOCATOR);
|
||||||
|
|
||||||
/// The Sprite Id is a thin wrapper around the pointer to the sprite in
|
/// The Sprite Id is a thin wrapper around the pointer to the sprite in
|
||||||
/// rom and is therefore a unique identifier to a sprite
|
/// rom and is therefore a unique identifier to a sprite
|
||||||
#[derive(Clone, Copy, PartialEq, Eq, Hash)]
|
#[derive(Clone, Copy, PartialEq, Eq, Hash)]
|
||||||
|
@ -162,13 +168,21 @@ impl SpriteVram {
|
||||||
.as_ptr()
|
.as_ptr()
|
||||||
.copy_from_nonoverlapping(data.as_ptr(), data.len());
|
.copy_from_nonoverlapping(data.as_ptr(), data.len());
|
||||||
}
|
}
|
||||||
Ok(SpriteVram {
|
Ok(unsafe { Self::from_location_size(allocated, size, palette) })
|
||||||
|
}
|
||||||
|
|
||||||
|
unsafe fn from_location_size(
|
||||||
|
data: NonNull<u8>,
|
||||||
|
size: Size,
|
||||||
|
palette: PaletteVram,
|
||||||
|
) -> SpriteVram {
|
||||||
|
SpriteVram {
|
||||||
data: Rc::new(SpriteVramData {
|
data: Rc::new(SpriteVramData {
|
||||||
location: Location::from_sprite_ptr(allocated),
|
location: Location::from_sprite_ptr(data),
|
||||||
size,
|
size,
|
||||||
palette,
|
palette,
|
||||||
}),
|
}),
|
||||||
})
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub(crate) fn location(&self) -> u16 {
|
pub(crate) fn location(&self) -> u16 {
|
||||||
|
@ -290,31 +304,54 @@ impl Default for SpriteLoader {
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Sprite data that can be used to create sprites in vram.
|
/// Sprite data that can be used to create sprites in vram.
|
||||||
pub struct DynamicSprite<A: Allocator = Global> {
|
pub struct DynamicSprite {
|
||||||
data: Box<[u8], A>,
|
data: Box<[u16], SpriteAllocator>,
|
||||||
size: Size,
|
size: Size,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl DynamicSprite {
|
impl Clone for DynamicSprite {
|
||||||
#[must_use]
|
fn clone(&self) -> Self {
|
||||||
/// Creates a new dynamic sprite.
|
let allocation = SpriteAllocator
|
||||||
pub fn new(size: Size) -> Self {
|
.allocate(self.size.layout())
|
||||||
Self::new_in(size, Global)
|
.expect("cannot allocate dynamic sprite");
|
||||||
|
|
||||||
|
let allocation = core::ptr::slice_from_raw_parts_mut(
|
||||||
|
allocation.as_ptr() as *mut _,
|
||||||
|
allocation.len() / 2,
|
||||||
|
);
|
||||||
|
|
||||||
|
let mut data = unsafe { Box::from_raw_in(allocation, SpriteAllocator) };
|
||||||
|
|
||||||
|
data.clone_from_slice(&self.data);
|
||||||
|
|
||||||
|
Self {
|
||||||
|
data,
|
||||||
|
size: self.size,
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<A: Allocator> DynamicSprite<A> {
|
impl DynamicSprite {
|
||||||
|
/// Creates a new dynamic sprite of a given size
|
||||||
|
pub fn try_new(size: Size) -> Result<Self, LoaderError> {
|
||||||
|
let allocation = SpriteAllocator
|
||||||
|
.allocate_zeroed(size.layout())
|
||||||
|
.map_err(|_| LoaderError::SpriteFull)?;
|
||||||
|
|
||||||
|
let allocation = core::ptr::slice_from_raw_parts_mut(
|
||||||
|
allocation.as_ptr() as *mut _,
|
||||||
|
allocation.len() / 2,
|
||||||
|
);
|
||||||
|
|
||||||
|
let data = unsafe { Box::from_raw_in(allocation, SpriteAllocator) };
|
||||||
|
|
||||||
|
Ok(DynamicSprite { data, size })
|
||||||
|
}
|
||||||
|
|
||||||
#[must_use]
|
#[must_use]
|
||||||
/// Creates a new dynamic sprite of a given size in a given allocator.
|
/// Creates a new dynamic sprite of a given size
|
||||||
pub fn new_in(size: Size, allocator: A) -> Self {
|
pub fn new(size: Size) -> Self {
|
||||||
let num_bytes = size.number_of_tiles() * BYTES_PER_TILE_4BPP;
|
Self::try_new(size).expect("couldn't allocate dynamic sprite")
|
||||||
let mut data = Vec::with_capacity_in(num_bytes, allocator);
|
|
||||||
|
|
||||||
data.resize(num_bytes, 0);
|
|
||||||
|
|
||||||
let data = data.into_boxed_slice();
|
|
||||||
|
|
||||||
DynamicSprite { data, size }
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Set the pixel of a sprite to a given paletted pixel. Panics if the
|
/// Set the pixel of a sprite to a given paletted pixel. Panics if the
|
||||||
|
@ -333,24 +370,36 @@ impl<A: Allocator> DynamicSprite<A> {
|
||||||
|
|
||||||
let tile_number_to_modify = adjust_tile_x + adjust_tile_y * sprite_tile_x;
|
let tile_number_to_modify = adjust_tile_x + adjust_tile_y * sprite_tile_x;
|
||||||
|
|
||||||
let byte_to_modify_in_tile = x / 2 + y * 4;
|
let (x_in_tile, y_in_tile) = (x % 8, y % 8);
|
||||||
let byte_to_modify = tile_number_to_modify * BYTES_PER_TILE_4BPP + byte_to_modify_in_tile;
|
|
||||||
let mut byte = self.data[byte_to_modify];
|
|
||||||
let parity = (x & 0b1) * 4;
|
|
||||||
|
|
||||||
byte = (byte & !(0b1111 << parity)) | ((paletted_pixel as u8) << parity);
|
let half_word_to_modify_in_tile = x_in_tile / 4 + y_in_tile * 2;
|
||||||
self.data[byte_to_modify] = byte;
|
|
||||||
|
let half_word_to_modify =
|
||||||
|
tile_number_to_modify * BYTES_PER_TILE_4BPP / 2 + half_word_to_modify_in_tile;
|
||||||
|
let mut half_word = self.data[half_word_to_modify];
|
||||||
|
|
||||||
|
let nibble_to_modify = (x % 4) * 4;
|
||||||
|
|
||||||
|
half_word = (half_word & !(0b1111 << nibble_to_modify))
|
||||||
|
| ((paletted_pixel as u16) << nibble_to_modify);
|
||||||
|
self.data[half_word_to_modify] = half_word;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Tries to copy the sprite to vram to be used to set object sprites.
|
/// Wipes the sprite
|
||||||
pub fn try_vram(&self, palette: PaletteVram) -> Result<SpriteVram, LoaderError> {
|
pub fn clear(&mut self, paletted_pixel: usize) {
|
||||||
SpriteVram::new(&self.data, self.size, palette)
|
assert!(paletted_pixel < 0x10);
|
||||||
|
let reset =
|
||||||
|
(paletted_pixel | paletted_pixel << 4 | paletted_pixel << 8 | paletted_pixel << 12)
|
||||||
|
as u16;
|
||||||
|
self.data.fill(reset);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[must_use]
|
#[must_use]
|
||||||
/// Tries to copy the sprite to vram to be used to set object sprites.
|
/// Tries to copy the sprite to vram to be used to set object sprites.
|
||||||
/// Panics if it cannot be allocated.
|
/// Panics if it cannot be allocated.
|
||||||
pub fn to_vram(&self, palette: PaletteVram) -> SpriteVram {
|
pub fn to_vram(self, palette: PaletteVram) -> SpriteVram {
|
||||||
self.try_vram(palette).expect("cannot create sprite")
|
let data = unsafe { NonNull::new_unchecked(Box::leak(self.data).as_mut_ptr()) };
|
||||||
|
|
||||||
|
unsafe { SpriteVram::from_location_size(data.cast(), self.size, palette) }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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].
|
||||||
|
|
|
@ -129,13 +129,10 @@ fn generate_sprites() -> Box<[SpriteVram]> {
|
||||||
.collect();
|
.collect();
|
||||||
|
|
||||||
// generate sprites
|
// generate sprites
|
||||||
let mut sprite = DynamicSprite::new(Size::S8x8);
|
|
||||||
for (palette, colour) in (0..PALETTE.len()).map(|x| (x / 15, x % 15)) {
|
for (palette, colour) in (0..PALETTE.len()).map(|x| (x / 15, x % 15)) {
|
||||||
for y in 0..8 {
|
let mut sprite = DynamicSprite::new(Size::S8x8);
|
||||||
for x in 0..8 {
|
sprite.clear(colour + 1);
|
||||||
sprite.set_pixel(x, y, colour + 1);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
sprites.push(sprite.to_vram(palettes[palette].clone()));
|
sprites.push(sprite.to_vram(palettes[palette].clone()));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
Loading…
Reference in a new issue