diff --git a/CHANGELOG.md b/CHANGELOG.md index daa6a377..558f9674 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,10 +8,11 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Added - New `include_palette` macro for including every colour in an image as a `u16` slice. +- New object based text renderer. ### Changed - 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. - 10% performance improvement with the software mixer. diff --git a/agb-image-converter/src/font_loader.rs b/agb-image-converter/src/font_loader.rs index 2c61f664..2e59c83f 100644 --- a/agb-image-converter/src/font_loader.rs +++ b/agb-image-converter/src/font_loader.rs @@ -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_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(|(metrics, bitmap)| { let width = metrics.width; @@ -56,25 +56,37 @@ pub fn load_font(font_data: &[u8], pixels_per_em: f32) -> TokenStream { advance_width: metrics.advance_width, } }) - .map(|letter_data| { - let data_raw = ByteString(&letter_data.rendered); - let height = letter_data.height as u8; - let width = letter_data.width as u8; - let xmin = letter_data.xmin as i8; - let ymin = letter_data.ymin as i8; - let advance_width = letter_data.advance_width.ceil() as u8; + .collect(); - quote!( - display::FontLetter::new( - #width, - #height, - #data_raw, - #xmin, - #ymin, - #advance_width, - ) + 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 height = letter_data.height as u8; + let width = letter_data.width as u8; + let xmin = letter_data.xmin as i8; + let ymin = letter_data.ymin as i8; + let advance_width = letter_data.advance_width.ceil() as u8; + + quote!( + display::FontLetter::new( + #width, + #height, + #data_raw, + #xmin, + #ymin, + #advance_width, ) - }); + ) + }); quote![ display::Font::new(&[#(#font),*], #line_height, #ascent) diff --git a/agb/examples/font/font-test-output.png b/agb/examples/font/font-test-output.png index be7bce8b..e79165d4 100644 Binary files a/agb/examples/font/font-test-output.png and b/agb/examples/font/font-test-output.png differ diff --git a/agb/examples/font/pixelated.ttf b/agb/examples/font/pixelated.ttf new file mode 100644 index 00000000..29c265a1 Binary files /dev/null and b/agb/examples/font/pixelated.ttf differ diff --git a/agb/examples/object_text_render.rs b/agb/examples/object_text_render.rs new file mode 100644 index 00000000..d5323724 --- /dev/null +++ b/agb/examples/object_text_render.rs @@ -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 + ); + } + } +} diff --git a/agb/src/agb_alloc/mod.rs b/agb/src/agb_alloc/mod.rs index bcbc8bd3..53f3d5f8 100644 --- a/agb/src/agb_alloc/mod.rs +++ b/agb/src/agb_alloc/mod.rs @@ -1,4 +1,3 @@ -use core::alloc::{Allocator, Layout}; use core::ops::{Deref, DerefMut}; use core::ptr::NonNull; @@ -45,18 +44,23 @@ static GLOBAL_ALLOC: BlockAllocator = unsafe { macro_rules! impl_zst_allocator { ($name_of_struct: ty, $name_of_static: ident) => { - unsafe impl Allocator for $name_of_struct { - fn allocate(&self, layout: Layout) -> Result, core::alloc::AllocError> { + unsafe impl core::alloc::Allocator for $name_of_struct { + fn allocate( + &self, + layout: core::alloc::Layout, + ) -> Result, core::alloc::AllocError> { $name_of_static.allocate(layout) } - unsafe fn deallocate(&self, ptr: NonNull, layout: Layout) { + unsafe fn deallocate(&self, ptr: core::ptr::NonNull, layout: core::alloc::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 /// 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 diff --git a/agb/src/display/bitmap3.rs b/agb/src/display/bitmap3.rs index 2674d47e..b29b3177 100644 --- a/agb/src/display/bitmap3.rs +++ b/agb/src/display/bitmap3.rs @@ -30,4 +30,19 @@ impl Bitmap3<'_> { let y = y.try_into().unwrap(); 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); + } + } + } } diff --git a/agb/src/display/font.rs b/agb/src/display/font.rs index b4349887..e37a77a2 100644 --- a/agb/src/display/font.rs +++ b/agb/src/display/font.rs @@ -10,12 +10,12 @@ use super::tiled::{DynamicTile, RegularMap, TileSetting, VRamManager}; /// Does not support any unicode features. /// For usage see the `text_render.rs` example pub struct FontLetter { - width: u8, - height: u8, - data: &'static [u8], - xmin: i8, - ymin: i8, - advance_width: u8, + pub(crate) width: u8, + pub(crate) height: u8, + pub(crate) data: &'static [u8], + pub(crate) xmin: i8, + pub(crate) ymin: i8, + pub(crate) advance_width: u8, } impl FontLetter { @@ -37,17 +37,24 @@ impl FontLetter { 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 { - letters: &'static [FontLetter], + letters: &'static [FontLetter; 128], line_height: i32, ascent: i32, } impl Font { #[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 { letters, line_height, @@ -55,8 +62,16 @@ impl Font { } } - fn letter(&self, letter: char) -> &'static FontLetter { - &self.letters[letter as usize] + pub(crate) fn letter(&self, letter: char) -> &'static FontLetter { + &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 } } diff --git a/agb/src/display/object.rs b/agb/src/display/object.rs index 64c13b76..85df2a55 100644 --- a/agb/src/display/object.rs +++ b/agb/src/display/object.rs @@ -9,6 +9,7 @@ //! harder to integrate into your games depending on how they are architectured. mod affine; +mod font; mod managed; mod sprites; mod unmanaged; @@ -22,6 +23,8 @@ pub use affine::AffineMatrixInstance; pub use managed::{OamManaged, Object}; pub use unmanaged::{AffineMode, OamIterator, OamSlot, OamUnmanaged, ObjectUnmanaged}; +pub use font::{ChangeColour, ObjectTextRender, TextAlignment}; + use super::DISPLAY_CONTROL; const OBJECT_ATTRIBUTE_MEMORY: *mut u16 = 0x0700_0000 as *mut u16; diff --git a/agb/src/display/object/font.rs b/agb/src/display/object/font.rs new file mode 100644 index 00000000..a9d05da5 --- /dev/null +++ b/agb/src/display/object/font.rs @@ -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, + letters: Letters, + font: &'font Font, +} + +#[derive(Debug, Default)] +struct Letters { + letters: VecDeque, + 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, + 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 { + 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, + 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) { + 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::(); + + 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>, + line_capacity: VecDeque, + objects: Vec, + objects_are_at_origin: Vector2D, + area: Vector2D, +} + +impl LayoutCache { + fn update_objects_to_display_at_position<'a>( + &mut self, + position: Vector2D, + letters: impl Iterator, + 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> + '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> = (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, + alignment: TextAlignment, + paragraph_spacing: i32, +} diff --git a/agb/src/display/object/font/preprocess.rs b/agb/src/display/object/font/preprocess.rs new file mode 100644 index 00000000..f899071f --- /dev/null +++ b/agb/src/display/object/font/preprocess.rs @@ -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, + 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, + ) { + 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, + 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 { + 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 + '_)> { + 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) + }) + } +} diff --git a/agb/src/display/object/font/renderer.rs b/agb/src/display/object/font/renderer.rs new file mode 100644 index 00000000..b59b6549 --- /dev/null +++ b/agb/src/display/object/font/renderer.rs @@ -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 { + 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 { + 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 + } +} diff --git a/agb/src/display/object/sprites/sprite_allocator.rs b/agb/src/display/object/sprites/sprite_allocator.rs index 1d9c9a00..8050347b 100644 --- a/agb/src/display/object/sprites/sprite_allocator.rs +++ b/agb/src/display/object/sprites/sprite_allocator.rs @@ -1,14 +1,12 @@ use core::{alloc::Allocator, ptr::NonNull}; use alloc::{ - alloc::Global, boxed::Box, rc::{Rc, Weak}, - vec::Vec, }; use crate::{ - agb_alloc::{block_allocator::BlockAllocator, bump_allocator::StartEnd}, + agb_alloc::{block_allocator::BlockAllocator, bump_allocator::StartEnd, impl_zst_allocator}, display::palette16::Palette16, hash_map::HashMap, }; @@ -18,8 +16,8 @@ use super::{ BYTES_PER_TILE_4BPP, }; -const PALETTE_SPRITE: usize = 0x0500_0200; -const TILE_SPRITE: usize = 0x06010000; +pub const PALETTE_SPRITE: usize = 0x0500_0200; +pub const TILE_SPRITE: usize = 0x06010000; static SPRITE_ALLOCATOR: BlockAllocator = unsafe { 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 { BlockAllocator::new(StartEnd { 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 /// rom and is therefore a unique identifier to a sprite #[derive(Clone, Copy, PartialEq, Eq, Hash)] @@ -162,13 +168,21 @@ impl SpriteVram { .as_ptr() .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, + size: Size, + palette: PaletteVram, + ) -> SpriteVram { + SpriteVram { data: Rc::new(SpriteVramData { - location: Location::from_sprite_ptr(allocated), + location: Location::from_sprite_ptr(data), size, palette, }), - }) + } } 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. -pub struct DynamicSprite { - data: Box<[u8], A>, +pub struct DynamicSprite { + data: Box<[u16], SpriteAllocator>, size: Size, } -impl DynamicSprite { - #[must_use] - /// Creates a new dynamic sprite. - pub fn new(size: Size) -> Self { - Self::new_in(size, Global) +impl Clone for DynamicSprite { + fn clone(&self) -> Self { + let allocation = SpriteAllocator + .allocate(self.size.layout()) + .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 DynamicSprite { +impl DynamicSprite { + /// Creates a new dynamic sprite of a given size + pub fn try_new(size: Size) -> Result { + 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] - /// Creates a new dynamic sprite of a given size in a given allocator. - pub fn new_in(size: Size, allocator: A) -> Self { - let num_bytes = size.number_of_tiles() * BYTES_PER_TILE_4BPP; - let mut data = Vec::with_capacity_in(num_bytes, allocator); - - data.resize(num_bytes, 0); - - let data = data.into_boxed_slice(); - - DynamicSprite { data, size } + /// Creates a new dynamic sprite of a given size + pub fn new(size: Size) -> Self { + Self::try_new(size).expect("couldn't allocate dynamic sprite") } /// Set the pixel of a sprite to a given paletted pixel. Panics if the @@ -333,24 +370,36 @@ impl DynamicSprite { 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 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; + let (x_in_tile, y_in_tile) = (x % 8, y % 8); - byte = (byte & !(0b1111 << parity)) | ((paletted_pixel as u8) << parity); - self.data[byte_to_modify] = byte; + let half_word_to_modify_in_tile = x_in_tile / 4 + y_in_tile * 2; + + 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. - pub fn try_vram(&self, palette: PaletteVram) -> Result { - SpriteVram::new(&self.data, self.size, palette) + /// Wipes the sprite + pub fn clear(&mut self, paletted_pixel: usize) { + 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] /// Tries to copy the sprite to vram to be used to set object sprites. /// Panics if it cannot be allocated. - pub fn to_vram(&self, palette: PaletteVram) -> SpriteVram { - self.try_vram(palette).expect("cannot create sprite") + pub fn to_vram(self, palette: PaletteVram) -> SpriteVram { + let data = unsafe { NonNull::new_unchecked(Box::leak(self.data).as_mut_ptr()) }; + + unsafe { SpriteVram::from_location_size(data.cast(), self.size, palette) } } } diff --git a/agb/src/display/object/unmanaged/object.rs b/agb/src/display/object/unmanaged/object.rs index 8d65e31a..98fc2a65 100644 --- a/agb/src/display/object/unmanaged/object.rs +++ b/agb/src/display/object/unmanaged/object.rs @@ -101,7 +101,6 @@ impl OamSlot<'_> { /// 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 /// keeping move semantics. This is slightly faster in benchmarks. - #[inline(never)] fn set_inner(&self, object: &ObjectUnmanaged) { let mut attributes = object.attributes; // SAFETY: This function is not reentrant and we currently hold a mutable borrow of the [UnmanagedOAM]. diff --git a/agb/src/no_game.rs b/agb/src/no_game.rs index ed82f880..0b84e5f1 100644 --- a/agb/src/no_game.rs +++ b/agb/src/no_game.rs @@ -129,13 +129,10 @@ fn generate_sprites() -> Box<[SpriteVram]> { .collect(); // generate sprites - let mut sprite = DynamicSprite::new(Size::S8x8); + for (palette, colour) in (0..PALETTE.len()).map(|x| (x / 15, x % 15)) { - for y in 0..8 { - for x in 0..8 { - sprite.set_pixel(x, y, colour + 1); - } - } + let mut sprite = DynamicSprite::new(Size::S8x8); + sprite.clear(colour + 1); sprites.push(sprite.to_vram(palettes[palette].clone())); }