diff --git a/CHANGELOG.md b/CHANGELOG.md index b0764120..450a5b70 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,9 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] +### Changed +- Changed the default template game + ## [0.15.0] - 2023/04/25 ### Added diff --git a/agb-image-converter/src/lib.rs b/agb-image-converter/src/lib.rs index c63c4656..26c9a582 100644 --- a/agb-image-converter/src/lib.rs +++ b/agb-image-converter/src/lib.rs @@ -239,9 +239,36 @@ impl ToTokens for ByteString<'_> { } } +#[proc_macro] +pub fn include_colours_inner(input: TokenStream) -> TokenStream { + let input_filename = parse_macro_input!(input as LitStr); + let input_filename = input_filename.value(); + + let root = std::env::var("CARGO_MANIFEST_DIR").expect("Failed to get cargo manifest dir"); + let input_filename = Path::new(&root).join(input_filename); + + let image = Image::load_from_file(Path::new(&input_filename)); + + let mut palette_data = Vec::with_capacity(image.width * image.height); + for y in 0..image.height { + for x in 0..image.width { + palette_data.push(image.colour(x, y).to_rgb15()) + } + } + + let filename = input_filename.to_string_lossy(); + + TokenStream::from(quote! { + { + const _: &[u8] = include_bytes!(#filename); + [#(#palette_data),*] + } + }) +} + #[proc_macro] pub fn include_aseprite_inner(input: TokenStream) -> TokenStream { - let parser = Punctuated::::parse_separated_nonempty; + let parser = Punctuated::::parse_terminated; let parsed = match parser.parse(input) { Ok(e) => e, Err(e) => return e.to_compile_error().into(), @@ -432,8 +459,8 @@ fn palette_tile_data( let mut tile_data = Vec::new(); - for image in images { - add_image_to_tile_data(&mut tile_data, image, optimiser, 0) + for (image_idx, image) in images.iter().enumerate() { + add_image_to_tile_data(&mut tile_data, image, optimiser, image_idx) } let tile_data = collapse_to_4bpp(&tile_data); diff --git a/agb-image-converter/src/palette16.rs b/agb-image-converter/src/palette16.rs index 2d384c69..03ae3a57 100644 --- a/agb-image-converter/src/palette16.rs +++ b/agb-image-converter/src/palette16.rs @@ -138,17 +138,7 @@ impl Palette16Optimiser { while !unsatisfied_palettes.is_empty() { let palette = self.find_maximal_palette_for(&unsatisfied_palettes); - for test_palette in unsatisfied_palettes.clone() { - if test_palette.is_satisfied_by(&palette) { - unsatisfied_palettes.remove(&test_palette); - } - } - - for (i, overall_palette) in self.palettes.iter().enumerate() { - if overall_palette.is_satisfied_by(&palette) { - assignments[i] = optimised_palettes.len(); - } - } + unsatisfied_palettes.retain(|test_palette| !test_palette.is_satisfied_by(&palette)); optimised_palettes.push(palette); @@ -157,6 +147,13 @@ impl Palette16Optimiser { } } + for (i, overall_palette) in self.palettes.iter().enumerate() { + assignments[i] = optimised_palettes + .iter() + .position(|palette| overall_palette.is_satisfied_by(palette)) + .unwrap(); + } + Palette16OptimisationResults { optimised_palettes, assignments, diff --git a/agb/examples/no_game.rs b/agb/examples/no_game.rs new file mode 100644 index 00000000..38aa29b5 --- /dev/null +++ b/agb/examples/no_game.rs @@ -0,0 +1,7 @@ +#![no_std] +#![no_main] + +#[agb::entry] +fn main(gba: agb::Gba) -> ! { + agb::no_game(gba); +} diff --git a/agb/gfx/pastel.png b/agb/gfx/pastel.png new file mode 100644 index 00000000..7ece9c36 Binary files /dev/null and b/agb/gfx/pastel.png differ diff --git a/agb/src/display/object/sprites/sprite.rs b/agb/src/display/object/sprites/sprite.rs index 6751c47d..0857e283 100644 --- a/agb/src/display/object/sprites/sprite.rs +++ b/agb/src/display/object/sprites/sprite.rs @@ -94,6 +94,7 @@ macro_rules! align_bytes { #[macro_export] macro_rules! include_aseprite { ($($aseprite_path: expr),*) => {{ + #[allow(unused_imports)] use $crate::display::object::{Size, Sprite, Tag, TagMap, Graphics}; use $crate::display::palette16::Palette16; use $crate::align_bytes; @@ -367,4 +368,11 @@ impl Size { Size::S32x64 => (32, 64), } } + + #[must_use] + /// Returns the width and height of the size in pixels. + pub const fn to_tiles_width_height(self) -> (usize, usize) { + let wh = self.to_width_height(); + (wh.0 / 8, wh.1 / 8) + } } diff --git a/agb/src/display/object/sprites/sprite_allocator.rs b/agb/src/display/object/sprites/sprite_allocator.rs index ae28f908..1d9c9a00 100644 --- a/agb/src/display/object/sprites/sprite_allocator.rs +++ b/agb/src/display/object/sprites/sprite_allocator.rs @@ -1,6 +1,11 @@ -use core::ptr::NonNull; +use core::{alloc::Allocator, ptr::NonNull}; -use alloc::rc::{Rc, Weak}; +use alloc::{ + alloc::Global, + boxed::Box, + rc::{Rc, Weak}, + vec::Vec, +}; use crate::{ agb_alloc::{block_allocator::BlockAllocator, bump_allocator::StartEnd}, @@ -285,34 +290,61 @@ impl Default for SpriteLoader { } /// Sprite data that can be used to create sprites in vram. -pub struct DynamicSprite<'a> { - data: &'a [u8], +pub struct DynamicSprite { + data: Box<[u8], A>, size: Size, } -impl DynamicSprite<'_> { +impl DynamicSprite { #[must_use] - /// Creates a new dynamic sprite from underlying bytes. Note that despite - /// being an array of u8, this must be aligned to at least a 2 byte - /// boundary. - pub fn new(data: &[u8], size: Size) -> DynamicSprite { - let ptr = &data[0] as *const _ as usize; - if ptr % 2 != 0 { - panic!("data is not aligned to a 2 byte boundary"); - } - if data.len() != size.number_of_tiles() * BYTES_PER_TILE_4BPP { - panic!( - "data is not of expected length, got {} expected {}", - data.len(), - size.number_of_tiles() * BYTES_PER_TILE_4BPP - ); - } + /// Creates a new dynamic sprite. + pub fn new(size: Size) -> Self { + Self::new_in(size, Global) + } +} + +impl DynamicSprite { + #[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 } } + /// Set the pixel of a sprite to a given paletted pixel. Panics if the + /// coordinate is out of range of the sprite or if the paletted pixel is + /// greater than 4 bits. + pub fn set_pixel(&mut self, x: usize, y: usize, paletted_pixel: usize) { + assert!(paletted_pixel < 0x10); + + let (sprite_pixel_x, sprite_pixel_y) = self.size.to_width_height(); + assert!(x < sprite_pixel_x, "x too big for sprite size"); + assert!(y < sprite_pixel_y, "y too big for sprite size"); + + let (sprite_tile_x, _) = self.size.to_tiles_width_height(); + + let (adjust_tile_x, adjust_tile_y) = (x / 8, y / 8); + + 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; + + byte = (byte & !(0b1111 << parity)) | ((paletted_pixel as u8) << parity); + self.data[byte_to_modify] = byte; + } + /// 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) + SpriteVram::new(&self.data, self.size, palette) } #[must_use] diff --git a/agb/src/display/palette16.rs b/agb/src/display/palette16.rs index 7296b5bd..06383f93 100644 --- a/agb/src/display/palette16.rs +++ b/agb/src/display/palette16.rs @@ -27,3 +27,12 @@ impl Palette16 { Layout::new::() } } + +#[macro_export] +macro_rules! include_palette { + ($palette:literal) => { + $crate::include_colours_inner!($palette) + }; +} + +pub use include_palette; diff --git a/agb/src/lib.rs b/agb/src/lib.rs index cda67dae..5849a192 100644 --- a/agb/src/lib.rs +++ b/agb/src/lib.rs @@ -104,6 +104,9 @@ pub use agb_image_converter::include_aseprite_inner; #[doc(hidden)] pub use agb_image_converter::include_font as include_font_inner; +#[doc(hidden)] +pub use agb_image_converter::include_colours_inner; + #[macro_export] macro_rules! include_font { ($font_path: literal, $font_size: literal) => {{ @@ -165,6 +168,11 @@ pub mod syscall; /// Interactions with the internal timers pub mod timer; +mod no_game; + +/// Default game +pub use no_game::no_game; + pub(crate) mod arena; pub use {agb_alloc::ExternalAllocator, agb_alloc::InternalAllocator}; diff --git a/agb/src/no_game.rs b/agb/src/no_game.rs new file mode 100644 index 00000000..ed82f880 --- /dev/null +++ b/agb/src/no_game.rs @@ -0,0 +1,213 @@ +//! The no game screen is what is displayed if there isn't a game made yet. + +use agb_fixnum::{num, Num, Vector2D}; +use alloc::vec::Vec; +use alloc::{boxed::Box, vec}; + +use crate::display::object::{DynamicSprite, PaletteVram, Size, SpriteVram}; +use crate::display::palette16::Palette16; +use crate::{ + display::{object::ObjectUnmanaged, HEIGHT, WIDTH}, + include_palette, + interrupt::VBlank, +}; + +const PALETTE: &[u16] = &include_palette!("gfx/pastel.png"); + +fn letters() -> Vec>>> { + vec![ + // N + vec![ + (num!(0.), num!(0.)).into(), + (num!(1.), num!(1.)).into(), + (num!(2.), num!(2.)).into(), + (num!(3.), num!(3.)).into(), + (num!(0.), num!(1.)).into(), + (num!(0.), num!(2.)).into(), + (num!(0.), num!(3.)).into(), + (num!(3.), num!(0.)).into(), + (num!(3.), num!(1.)).into(), + (num!(3.), num!(2.)).into(), + (num!(3.), num!(3.)).into(), + ], + // O + vec![ + (num!(0.), num!(0.)).into(), + (num!(0.), num!(1.)).into(), + (num!(0.), num!(2.)).into(), + (num!(0.), num!(3.)).into(), + (num!(1.), num!(3.)).into(), + (num!(2.), num!(3.)).into(), + (num!(3.), num!(3.)).into(), + (num!(3.), num!(2.)).into(), + (num!(3.), num!(1.)).into(), + (num!(3.), num!(0.)).into(), + (num!(2.), num!(0.)).into(), + (num!(1.), num!(0.)).into(), + ], + // G + vec![ + (num!(3.), num!(0.)).into(), + (num!(2.), num!(0.)).into(), + (num!(1.), num!(0.)).into(), + (num!(0.), num!(0.)).into(), + (num!(0.), num!(1.)).into(), + (num!(0.), num!(2.)).into(), + (num!(0.), num!(3.)).into(), + (num!(1.), num!(3.)).into(), + (num!(2.), num!(3.)).into(), + (num!(3.), num!(3.)).into(), + (num!(3.), num!(2.25)).into(), + (num!(3.), num!(1.5)).into(), + (num!(2.), num!(1.5)).into(), + ], + // A + vec![ + (num!(0.), num!(0.)).into(), + (num!(0.), num!(1.)).into(), + (num!(0.), num!(2.)).into(), + (num!(0.), num!(3.)).into(), + (num!(3.), num!(3.)).into(), + (num!(3.), num!(2.)).into(), + (num!(3.), num!(1.)).into(), + (num!(3.), num!(0.)).into(), + (num!(2.), num!(0.)).into(), + (num!(1.), num!(0.)).into(), + (num!(1.), num!(1.5)).into(), + (num!(2.), num!(1.5)).into(), + ], + // M + vec![ + (num!(0.), num!(0.)).into(), + (num!(0.), num!(1.)).into(), + (num!(0.), num!(2.)).into(), + (num!(0.), num!(3.)).into(), + (num!(3.), num!(3.)).into(), + (num!(3.), num!(2.)).into(), + (num!(3.), num!(1.)).into(), + (num!(3.), num!(0.)).into(), + (num!(1.5), num!(1.5)).into(), + (num!(0.75), num!(0.75)).into(), + (num!(2.25), num!(0.75)).into(), + ], + // E + vec![ + (num!(0.), num!(0.)).into(), + (num!(0.), num!(1.)).into(), + (num!(0.), num!(2.)).into(), + (num!(0.), num!(3.)).into(), + (num!(1.), num!(3.)).into(), + (num!(2.), num!(3.)).into(), + (num!(3.), num!(3.)).into(), + (num!(3.), num!(0.)).into(), + (num!(2.), num!(0.)).into(), + (num!(1.), num!(0.)).into(), + (num!(1.), num!(1.5)).into(), + (num!(2.), num!(1.5)).into(), + ], + ] +} + +fn generate_sprites() -> Box<[SpriteVram]> { + let mut sprites = Vec::new(); + + // generate palettes + + let palettes: Vec = PALETTE + .chunks(15) + .map(|x| { + core::iter::once(0) + .chain(x.iter().copied()) + .chain(core::iter::repeat(0)) + .take(16) + .collect::>() + }) + .map(|palette| { + let palette = Palette16::new(palette.try_into().unwrap()); + PaletteVram::new(&palette).unwrap() + }) + .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); + } + } + sprites.push(sprite.to_vram(palettes[palette].clone())); + } + + sprites.into_boxed_slice() +} + +pub fn no_game(mut gba: crate::Gba) -> ! { + let (mut oam, _) = gba.display.object.get_unmanaged(); + + let squares = generate_sprites(); + + let mut letter_positons = Vec::new(); + + let square_positions = { + let mut s = letters(); + for letter in s.iter_mut() { + letter.sort_by_key(|a| a.magnitude_squared()); + } + s + }; + for (letter_idx, letter_parts) in square_positions.iter().enumerate() { + for part in letter_parts.iter() { + let position = part + .hadamard((8, 10).into()) + .hadamard((num!(3.) / 2, num!(3.) / 2).into()); + + let letter_pos = Vector2D::new( + 60 * (1 + letter_idx as i32 - ((letter_idx >= 2) as i32 * 3)), + 70 * ((letter_idx >= 2) as i32), + ); + + letter_positons.push(position + letter_pos.change_base()); + } + } + + let bottom_right = letter_positons + .iter() + .copied() + .max_by_key(|x| x.manhattan_distance()) + .unwrap(); + + let difference = (Vector2D::new(WIDTH - 8, HEIGHT - 8).change_base() - bottom_right) / 2; + + for pos in letter_positons.iter_mut() { + *pos += difference; + } + + let mut time: Num = num!(0.); + let time_delta: Num = num!(0.025); + + let vblank = VBlank::get(); + + loop { + time += time_delta; + time %= 1; + let letters: Vec = letter_positons + .iter() + .enumerate() + .map(|(idx, position)| { + let time = time + Num::::new(idx as i32) / 128; + (idx, *position + Vector2D::new(time.sin(), time.cos()) * 10) + }) + .map(|(idx, pos)| { + let mut obj = ObjectUnmanaged::new(squares[idx % squares.len()].clone()); + obj.show().set_position(pos.floor()); + obj + }) + .collect(); + + vblank.wait_for_vblank(); + for (obj, slot) in letters.iter().zip(oam.iter()) { + slot.set(obj); + } + } +} diff --git a/template/src/main.rs b/template/src/main.rs index 84683dc9..e58c652d 100644 --- a/template/src/main.rs +++ b/template/src/main.rs @@ -14,22 +14,10 @@ #![cfg_attr(test, reexport_test_harness_main = "test_main")] #![cfg_attr(test, test_runner(agb::test_runner::test_runner))] -use agb::{display, syscall}; - // The main function must take 1 arguments and never return. The agb::entry decorator // ensures that everything is in order. `agb` will call this after setting up the stack // and interrupt handlers correctly. It will also handle creating the `Gba` struct for you. #[agb::entry] fn main(mut gba: agb::Gba) -> ! { - let mut bitmap = gba.display.video.bitmap3(); - - for x in 0..display::WIDTH { - let y = syscall::sqrt(x << 6); - let y = (display::HEIGHT - y).clamp(0, display::HEIGHT - 1); - bitmap.draw_point(x, y, 0x001F); - } - - loop { - syscall::halt(); - } + agb::no_game(gba); }