diff --git a/examples/tetris/.gitignore b/examples/tetris/.gitignore new file mode 100644 index 00000000..eb5a316c --- /dev/null +++ b/examples/tetris/.gitignore @@ -0,0 +1 @@ +target diff --git a/examples/tetris/Cargo.toml b/examples/tetris/Cargo.toml new file mode 100644 index 00000000..f8454c44 --- /dev/null +++ b/examples/tetris/Cargo.toml @@ -0,0 +1,14 @@ +[package] +name = "rust-psp-tetris" +version = "0.1.0" +authors = ["Paul Sajna "] +edition = "2018" + +[dependencies] +psp = { path = "../../psp" } +rand = { version = "0.7", default-features=false } +rand_chacha = { version = "0.2", default-features=false } + +[profile.release] +opt-level = 3 +lto = true diff --git a/examples/tetris/assets/block.bin b/examples/tetris/assets/block.bin new file mode 100644 index 00000000..f29e58b5 --- /dev/null +++ b/examples/tetris/assets/block.bin @@ -0,0 +1 @@ +aaa|||BBBPPPBBBPPPBBBPPPBBBPPPBBBPPPBBBPPPBBBzzzPPPBBBzzzPPPBBBzzzPPP666zzz999&&&zzzzzzqqq///&&&qqqqqq///&&&|||PPPPPPPPPPPPPPPPPPPPPPPPPPP999/////////&&&aaaBBBBBBBBBBBBBBBBBBBBBBBBBBB222&&&&&&&&&&&& \ No newline at end of file diff --git a/examples/tetris/assets/block.png b/examples/tetris/assets/block.png new file mode 100644 index 00000000..13ecda8c Binary files /dev/null and b/examples/tetris/assets/block.png differ diff --git a/examples/tetris/assets/tetris.pcm.raw b/examples/tetris/assets/tetris.pcm.raw new file mode 100644 index 00000000..e902791e Binary files /dev/null and b/examples/tetris/assets/tetris.pcm.raw differ diff --git a/examples/tetris/src/audio.rs b/examples/tetris/src/audio.rs new file mode 100644 index 00000000..0e6d7a97 --- /dev/null +++ b/examples/tetris/src/audio.rs @@ -0,0 +1,49 @@ +use psp::sys; + +use crate::TETRIS_SONG; + +const MAX_VOL: i32 = 0x8000; +pub const MAX_SAMPLES: usize = 65472; + +/// Called once per loop of the game, handles audio. +/// +/// # Parameters +/// - `channel`: An audio channel initialized by `sceAudioChReserve` +/// - `start_pos`: The starting position from which to play audio +/// - `restlen`: How much audio remains to be played +/// +/// # Return Value +/// +/// `(restlen, start_pos)` +pub fn process_audio_loop(channel: i32, mut start_pos: usize, mut restlen: i32) -> (i32, usize) { + unsafe { + if (start_pos+MAX_SAMPLES*2) < TETRIS_SONG.len() { + if restlen == 0 { + sys::sceAudioOutput( + channel, + MAX_VOL, + TETRIS_SONG.as_ptr().add(start_pos) as *mut _ + ); + start_pos += MAX_SAMPLES*2; + } + } else { + let remainder: i32 = (((TETRIS_SONG.len() % (MAX_SAMPLES*2)/2)+63) & !63) as i32; + if restlen == 0 { + sys::sceAudioSetChannelDataLen(channel, remainder); + sys::sceAudioOutput( + channel, + MAX_VOL, + TETRIS_SONG.as_ptr().add(start_pos) as *mut _ + ); + start_pos += (remainder*2) as usize; + } + if start_pos >= TETRIS_SONG.len() { + start_pos = 0; + sys::sceAudioSetChannelDataLen(channel, MAX_SAMPLES as i32); + } + } + + restlen = sys::sceAudioGetChannelRestLen(channel); + (restlen, start_pos) + } +} diff --git a/examples/tetris/src/game.rs b/examples/tetris/src/game.rs new file mode 100644 index 00000000..aad13c6a --- /dev/null +++ b/examples/tetris/src/game.rs @@ -0,0 +1,296 @@ +use crate::gameboard::Gameboard; +use crate::tetromino::Tetromino; +use crate::{BLOCK_SIZE, GAMEBOARD_OFFSET, GAMEBOARD_WIDTH, GAMEBOARD_HEIGHT, BLOCK}; +use crate::graphics::{Align4, sprite::Vertex, self}; + +use psp::{sys, sys::{CtrlButtons, SceCtrlData}}; + +use rand_chacha::ChaChaRng; +use rand::prelude::*; + +/// Stores the state of our entire game +pub struct Game { + score: usize, + board: Gameboard, + next_shape: Tetromino, + current_shape: Tetromino, + next_shape_offset: (usize, usize), + seconds_per_tick: f64, + seconds_since_tick: f64, + shape_placed: bool, + rng: ChaChaRng, + last_input: CtrlButtons, +} + +impl Game { + /// Creates a new `Game` + pub fn new() -> Self { + let mut seed: u64 = 0; + unsafe { + sys::sceRtcGetCurrentTick(&mut seed as *mut u64); + } + let mut rng = ChaChaRng::seed_from_u64(seed); + + let gameboard = Gameboard::new(); + + let mut next_shape = Tetromino::new_random(&mut rng); + next_shape.set_pos(30, 7); + + let mut current_shape = Tetromino::new_random(&mut rng); + let spawn_loc = gameboard.get_spawn_loc(); + current_shape.set_pos(spawn_loc.0 as i32, spawn_loc.1 as i32); + + Self { + score: 0, + board: gameboard, + next_shape, + current_shape, + next_shape_offset: (30, 7), + seconds_per_tick: 0.25, + seconds_since_tick: 0.0, + shape_placed: false, + rng, + last_input: CtrlButtons::default(), + } + } + + /// Handles user input + pub fn process_input(&mut self) { + let mut pad_data = SceCtrlData::default(); + unsafe { + sys::sceCtrlReadBufferPositive(&mut pad_data, 1); + } + if self.last_input.bits() == pad_data.buttons.bits() { + // no change in input, and I don't feel like doing held down buttons + return; + } + if pad_data.buttons.contains(CtrlButtons::LEFT) && !self.last_input.contains(CtrlButtons::LEFT) { + self.attempt_move(-1, 0); + } + if pad_data.buttons.contains(CtrlButtons::RIGHT) && !self.last_input.contains(CtrlButtons::RIGHT) { + self.attempt_move(1, 0); + } + if pad_data.buttons.contains(CtrlButtons::DOWN) && !self.last_input.contains(CtrlButtons::DOWN) { + self.drop(); + self.current_shape.lock_to_gameboard(&mut self.board); + self.shape_placed = true; + } + if pad_data.buttons.contains(CtrlButtons::CROSS) && !self.last_input.contains(CtrlButtons::CROSS) { + self.attempt_rotate_ccw(); + } + if pad_data.buttons.contains(CtrlButtons::CIRCLE) && !self.last_input.contains(CtrlButtons::CIRCLE) { + self.attempt_rotate_cw(); + } + self.last_input = pad_data.buttons; + } + + /// Called once per loop of the game, does all the biz. + /// + /// # Parameters + /// + /// - `seconds_since_last_loop`: Seconds that have passed since the last loop + /// + /// # Return Value + /// + /// `true` if game is over + pub fn process_game_loop(&mut self, seconds_since_last_loop: f32) -> bool { + self.process_input(); + self.seconds_since_tick += seconds_since_last_loop as f64; + if self.seconds_since_tick > self.seconds_per_tick { + self.tick(); + self.seconds_since_tick -= self.seconds_per_tick; + } + if self.shape_placed { + if !self.spawn_next_shape() { + // game over + return true; + } else { + self.pick_next_shape(); + let rows_complete = self.board.remove_completed_rows(); + self.set_score(self.score + 400 * rows_complete); + } + self.shape_placed = false; + } + false + } + + /// Moves `current_shape` down 1 unit and locks to board if it collides. + pub fn tick(&mut self) { + if !self.attempt_move(0, 1) { + self.current_shape.lock_to_gameboard(&mut self.board); + self.shape_placed = true; + } + } + + /// Setter for `score` + /// + /// # Parameters + /// + /// - `score`: Score to set. + pub fn set_score(&mut self, score: usize) { + self.score = score; + } + + /// Moves the `next_shape` into the `current_shape` and sets position accordingly. + pub fn spawn_next_shape(&mut self) -> bool { + self.current_shape = self.next_shape; + let spawn_loc = self.board.get_spawn_loc(); + self.current_shape.set_pos(spawn_loc.0 as i32, spawn_loc.1 as i32); + self.is_position_legal(&self.current_shape) + } + + /// Picks the next Tetromino, sets it's position on the screen to be in the + /// "Next Shape:" section + pub fn pick_next_shape(&mut self) { + self.next_shape = Tetromino::new_random(&mut self.rng); + self.next_shape.set_pos(self.next_shape_offset.0 as i32, self.next_shape_offset.1 as i32); + } + + /// Draws everything for the game + /// + /// # Parameters + /// + /// - `vertex_buffer`: Mutable reference to the main vertex buffer. + /// - `texture_buffer`: Mutable reference to the main texture buffer. + pub fn draw( + &self, + vertex_buffer: &mut [Align4], + texture_buffer: &mut [u8], + ) { + + // background + vertex_buffer[0] = Align4(Vertex { + u: 0.0, + v: 0.0, + color: 0x7f34_3434, + x: BLOCK_SIZE as f32 * GAMEBOARD_OFFSET.0 as f32, + y: BLOCK_SIZE as f32 * GAMEBOARD_OFFSET.1 as f32, + z: -1.0, + }); + vertex_buffer[1] = Align4(Vertex { + u: BLOCK_SIZE as f32 * GAMEBOARD_WIDTH as f32, + v: BLOCK_SIZE as f32 * GAMEBOARD_HEIGHT as f32, + color: 0x7f34_3434, + x: BLOCK_SIZE as f32 * GAMEBOARD_OFFSET.0 as f32 + BLOCK_SIZE as f32 * GAMEBOARD_WIDTH as f32, + y: BLOCK_SIZE as f32 * GAMEBOARD_OFFSET.1 as f32 + BLOCK_SIZE as f32 * GAMEBOARD_HEIGHT as f32, + z: -1.0, + }); + + (*texture_buffer).copy_from_slice(&BLOCK); + (*vertex_buffer)[2..402].copy_from_slice(&self.board.as_vertices()); + (*vertex_buffer)[402..410].copy_from_slice(&self.current_shape.as_vertices()); + (*vertex_buffer)[410..418].copy_from_slice(&self.next_shape.as_vertices()); + + graphics::draw_vertices(vertex_buffer, texture_buffer, BLOCK_SIZE, BLOCK_SIZE, 0.75, 0.75); + let score_string = alloc::format!("Score: {}", self.score); + graphics::draw_text_at(327, 40, 0xffff_ffff, score_string.as_str()); + graphics::draw_text_at(327, 60, 0xffff_ffff, "Next Shape:"); + } + + /// Attempts to add to the `current_shape` position, returns true if successful. + /// + /// # Parameters + /// + /// - `x`: horizontal position to add + /// - `y`: vertical position to add + /// + /// # Return Value + /// + /// `true` if successful + pub fn attempt_move(&mut self, x: i32, y: i32) -> bool { + let mut temp: Tetromino = self.current_shape.clone(); + temp.add_pos(x, y); + if self.is_position_legal(&temp) { + self.current_shape.add_pos(x, y); + return true; + } + false + } + + /// Attempts to rotate `current_shape` clockwise, returns true if successful. + /// + /// # Return Value + /// + /// `true` if successful + pub fn attempt_rotate_cw(&mut self) -> bool { + let mut temp: Tetromino = self.current_shape.clone(); + temp.rotate_cw(); + if self.is_position_legal(&temp) { + self.current_shape.rotate_cw(); + return true; + } + false + } + + /// Attempts to rotate `current_shape` counterclockwise, returns true if successful. + /// + /// # Return Value + /// + /// `true` if successful + pub fn attempt_rotate_ccw(&mut self) -> bool { + let mut temp: Tetromino = self.current_shape.clone(); + temp.rotate_ccw(); + if self.is_position_legal(&temp) { + self.current_shape.rotate_ccw(); + return true; + } + false + } + + /// Checks if the position of the given tetromino is within boundaries and does + /// not collide. + /// + /// # Parameters + /// + /// - `shape`: `Tetromino` to check + /// + /// # Return Value + /// + /// `true` if position is in bounds and does not collide + pub fn is_position_legal(&self, shape: &Tetromino) -> bool { + self.is_shape_within_borders(shape) + && !self.does_shape_intersect_locked_blocks(shape) + } + + /// Checks if the position of the given tetromino is within boundaries of the + /// gameboard + /// + /// # Parameters + /// + /// - `shape`: `Tetromino` to check + /// + /// # Return Value + /// + /// `true` if within boundaries of `board` + pub fn is_shape_within_borders(&self, shape: &Tetromino) -> bool { + let mapped_locs = shape.get_mapped_locs(); + for p in mapped_locs.iter() { + if !(p.0 < GAMEBOARD_WIDTH + && p.1 < GAMEBOARD_HEIGHT) { + return false + } + } + true + } + + /// Checks if the given tetromino's position collides with a block in the gameboard + /// + /// # Parameters + /// + /// `shape`: `Tetromino` to check + /// + /// # Return Value + /// + /// `true` if shape collides + pub fn does_shape_intersect_locked_blocks(&self, shape: &Tetromino) -> bool { + let mapped_locs = shape.get_mapped_locs(); + !self.board.are_locs_empty(mapped_locs.to_vec()) + } + + /// Hard drop function + pub fn drop(&mut self) { + while self.attempt_move(0, 1) {} + } +} + + diff --git a/examples/tetris/src/gameboard.rs b/examples/tetris/src/gameboard.rs new file mode 100644 index 00000000..c6124424 --- /dev/null +++ b/examples/tetris/src/gameboard.rs @@ -0,0 +1,250 @@ +use crate::graphics::sprite::Vertex; +use crate::graphics::Align4; +use crate::BLOCK_SIZE; +use crate::{GAMEBOARD_OFFSET, GAMEBOARD_WIDTH, GAMEBOARD_HEIGHT}; +use alloc::vec::Vec; + +/// The playing field of tetris. +#[derive(Debug)] +pub struct Gameboard { + blocks: [Option; 200], + width: usize, + height: usize, + block_spawn_loc: (usize, usize), +} + +impl Gameboard { + /// Creates a new `Gameboard`. + pub fn new() -> Self { + Self { + blocks: [None; 200], + width: GAMEBOARD_WIDTH, + height: GAMEBOARD_HEIGHT, + block_spawn_loc: (GAMEBOARD_WIDTH / 2, 1), + } + } + + #[inline] + const fn point_to_index(&self, x: usize, y: usize) -> Option { + if x < self.width && y < self.height { + return Some((x + y * self.width) as usize); + } + None + } + + #[inline] + const fn index_to_point(&self, index: usize) -> (usize, usize) { + let y = index / self.width; + let x = index % self.width; + (x, y) + } + + /// Gets the colour of the block at position (x, y), + /// or None if the position is empty. + /// + /// # Parameters + /// + /// `x`: Horizontal position within the gameboard + /// `y`: Vertical position within the gameboard + /// + /// # Return Value + /// The colour of the block at position (x, y), + /// or None if the position is empty. + pub fn get_content(&self, x: usize, y: usize) -> Option { + self.blocks[self.point_to_index(x, y)?] + } + + /// Sets the colour of the block at position (x, y), + /// or None if the position is empty. + /// + /// # Parameters + /// + /// - `x`: Horizontal position within the gameboard + /// - `y`: Vertical position within the gameboard + /// - `content`: Colour of the block at position (x, y), or None if the position is + /// empty. + /// + /// # Return Value + /// + /// Ok(()) if the position is valid, Err(()) otherwise. + pub fn set_content(&mut self, x: usize, y: usize, content: Option) -> Result<(), ()>{ + self.blocks[self.point_to_index(x, y).ok_or(())?] = content; + Ok(()) + } + + /// Checks if the given block positions are empty. + /// + /// # Parameters + /// + /// - `locs`: `Vec` of position tuples to check. + /// + /// # Return Value + /// + /// `true` if all block positions are empty, `false` otherwise. + pub fn are_locs_empty(&self, locs: Vec<(usize, usize)>) -> bool { + for point in locs { + if self.get_content(point.0, point.1).is_some() { + return false + } + } + true + } + + /// Removes all rows which are full along the horizontal axis. + /// + /// # Return Value + /// + /// The number of rows removed. + pub fn remove_completed_rows(&mut self) -> usize { + let row_indices = self.get_completed_row_indices(); + self.remove_rows(&row_indices).unwrap(); + row_indices.len() + } + + /// Returns the position within the `Gameboard` at which new blocks are spawned. + /// + /// # Return Value + /// + /// The position within the `Gameboard` at which new blocks are spawned. + pub fn get_spawn_loc(&self) -> (usize, usize) { + (self.block_spawn_loc.0 + GAMEBOARD_OFFSET.0, self.block_spawn_loc.1 + GAMEBOARD_OFFSET.1) + } + + + /// Returns `true` if the given `row_index` is horizontally full. + /// + /// # Parameters + /// + /// - `row_index`: The index of the row to check, from 0 to `GAMEBOARD_HEIGHT-1` + /// + /// # Return Value + /// + /// `true` if the row is horizontally full, `false` otherwise. + pub fn is_row_completed(&self, row_index: usize) -> bool { + for x in 0..self.width { + if self.get_content(x, row_index).is_none() { + return false + } + } + true + } + + /// Returns all horizontally full rows within the Gameboard. + /// + /// # Return Value + /// + /// A `Vec` of rows which are horizontally full. + pub fn get_completed_row_indices(&self) -> Vec { + let mut ret = Vec::new(); + for y in 0..self.height { + if self.is_row_completed(y) { + ret.push(y); + } + } + ret + } + + /// Removes a row and moves down the rows above it to fill. + /// + /// # Parameters + /// + /// - `row_index`: The index of the row to remove. + /// + /// # Return Value + /// + /// Ok(()) if the row index is valid and the operation is successful, Err(()) + /// otherwise. + pub fn remove_row(&mut self, row_index: usize) -> Result<(), ()> { + for y in (1..=row_index).rev() { + self.copy_row_into_row(y-1, y)?; + } + self.fill_row(0, None)?; + Ok(()) + } + + /// Removes multiple rows and moves down the rows above to fill. + /// + /// # Parameters + /// + /// - `row_indices`: The indices of the row to remove. + /// + /// # Return Value + /// + /// Ok(()) if the row indices are valid and the operation is successful, Err(()) + /// otherwise. + pub fn remove_rows(&mut self, row_indices: &Vec) -> Result<(), ()> { + for index in row_indices { + self.remove_row(*index)? + } + Ok(()) + } + + /// Fills a row with blocks of the given colour, or empties it if `content` is None. + /// + /// # Parameters + /// + /// - `row_index`: Index of the row to fill, from 0 to `GAMEBOARD_HEIGHT-1`. + /// - `content`: Colour of the block to fill with, or None to empty. + /// + /// # Return value + /// + /// Ok(()) if row_index is valid, Err(()) otherwise. + pub fn fill_row(&mut self, row_index: usize, content: Option) -> Result<(), ()> { + for x in 0..self.width { + self.set_content(x, row_index, content.clone())?; + } + Ok(()) + } + + /// Copies a row into another row. + /// Used to move rows above down when a row is completed. + /// + /// # Parameters: + /// + /// - `src_row_index`: row index to copy from + /// - `dst_row_index`: row index to copy to + /// + /// # Return Value + /// + /// Ok(()) if both indices are valid, Err(()) otherwise. + pub fn copy_row_into_row(&mut self, src_row_index: usize, dst_row_index: usize) -> Result<(), ()>{ + for x in 0..self.width { + self.set_content(x, dst_row_index, self.get_content(x, src_row_index))? + } + Ok(()) + } + + /// Returns a representation of the Gameboard as vertices which can be drawn. + /// See `Sprite::Vertex` and `graphics::draw_vertices` + /// + /// # Return Value + /// + /// A representation of the Gameboard as vertices which can be drawn. + pub fn as_vertices(&self) -> [Align4; 400] { + let mut ret = [Align4(Vertex::default()); 400]; + for (index, block) in self.blocks.iter().enumerate() { + let (x, y) = self.index_to_point(index); + let index = index * 2; + if block.is_some() { + let color = block.unwrap(); + ret[index] = Align4(Vertex { + u: 0.0, + v: 0.0, + color, + x: ((x+GAMEBOARD_OFFSET.0) as u32 * BLOCK_SIZE) as f32, + y: ((y+GAMEBOARD_OFFSET.1) as u32 * BLOCK_SIZE) as f32, + z: 0.0, + }); + ret[index+1] = Align4(Vertex { + u: BLOCK_SIZE as f32, + v: BLOCK_SIZE as f32, + color, + x: ((x+GAMEBOARD_OFFSET.0) as u32 * BLOCK_SIZE) as f32 + BLOCK_SIZE as f32, + y: ((y+GAMEBOARD_OFFSET.1) as u32 * BLOCK_SIZE) as f32 + BLOCK_SIZE as f32, + z: 0.0, + }); + } + } + ret + } +} diff --git a/examples/tetris/src/graphics/mod.rs b/examples/tetris/src/graphics/mod.rs new file mode 100644 index 00000000..0661a30e --- /dev/null +++ b/examples/tetris/src/graphics/mod.rs @@ -0,0 +1,150 @@ +use core::ptr; +use alloc::string::ToString; + +use psp::sys::{ + self, DisplayPixelFormat, GuContextType, GuSyncMode, GuSyncBehavior, + GuState, TexturePixelFormat, TextureEffect, TextureColorComponent, + ClearBuffer, ScePspFVector3, VertexType, MipmapLevel, GuPrimitive, + BlendOp, BlendFactor, MatrixMode, AlphaFunc, GuTexWrapMode, +}; + +use psp::Align16; +use psp::{BUF_WIDTH, SCREEN_WIDTH, SCREEN_HEIGHT}; + +use self::sprite::Vertex; + +pub mod sprite; + +#[derive(Debug, Clone, Copy)] +#[repr(align(4))] +pub struct Align4(pub T); + +static mut LIST: Align16<[u32; 0x40000]> = Align16([0; 0x40000]); + +/// Setup the GU Library with all of the configuration we need +/// +/// # Parameters +/// +/// - `allocator`: A reference to a `SimpleVramAllocator`. +pub fn setup(allocator: &mut psp::vram_alloc::SimpleVramAllocator) { + unsafe { + let fbp0 = allocator.alloc_texture_pixels(BUF_WIDTH, SCREEN_HEIGHT, TexturePixelFormat::Psm8888).as_mut_ptr_from_zero(); + let fbp1 = allocator.alloc_texture_pixels(BUF_WIDTH, SCREEN_HEIGHT, TexturePixelFormat::Psm8888).as_mut_ptr_from_zero(); + + sys::sceGumLoadIdentity(); + sys::sceGuInit(); + + sys::sceGuStart(GuContextType::Direct, &mut LIST.0 as *mut [u32; 0x40000] as *mut _); + sys::sceGuDrawBuffer(DisplayPixelFormat::Psm8888, fbp0 as _, BUF_WIDTH as i32); + sys::sceGuDispBuffer(SCREEN_WIDTH as i32, SCREEN_HEIGHT as i32, fbp1 as _, BUF_WIDTH as i32); + sys::sceGuOffset(2048 - (SCREEN_WIDTH / 2), 2048 - (SCREEN_HEIGHT / 2)); + sys::sceGuViewport(2048, 2048, SCREEN_WIDTH as i32, SCREEN_HEIGHT as i32); + sys::sceGuScissor(0, 0, SCREEN_WIDTH as i32, SCREEN_HEIGHT as i32); + sys::sceGuEnable(GuState::ScissorTest); + sys::sceGuEnable(GuState::Texture2D); + + sys::sceGuTexMode(TexturePixelFormat::Psm8888, 0, 0, 0); + sys::sceGuTexFunc(TextureEffect::Modulate, TextureColorComponent::Rgb); + sys::sceGuTexWrap(GuTexWrapMode::Repeat, GuTexWrapMode::Repeat); + + sys::sceGuEnable(GuState::Blend); + sys::sceGuBlendFunc(BlendOp::Add, BlendFactor::SrcAlpha, BlendFactor::OneMinusSrcAlpha, 0, 0); + sys::sceGuAlphaFunc(AlphaFunc::Greater, 0, 0xff); + + sys::sceGumMatrixMode(MatrixMode::View); + sys::sceGumLoadIdentity(); + + sys::sceGumMatrixMode(MatrixMode::Projection); + sys::sceGumLoadIdentity(); + sys::sceGumOrtho(0.0,480.0,272.0,0.0,-30.0,30.0); + + sys::sceDisplayWaitVblankStart(); + sys::sceGuFinish(); + sys::sceGuSync(GuSyncMode::Finish, GuSyncBehavior::Wait); + sys::sceGuDisplay(true); + } +} + +/// Clear the screen a particular colour. +/// +/// # Parameters +/// +/// - `color`: The colour to clear with, in big-endian ABGR, little endian RGBA. +pub fn clear_color(color: u32) { + unsafe { + sys::sceGuStart(GuContextType::Direct, &mut LIST.0 as *mut [u32; 0x40000] as *mut _); + sys::sceGuClearColor(color); + sys::sceGuClear(ClearBuffer::COLOR_BUFFER_BIT | ClearBuffer::FAST_CLEAR_BIT); + sys::sceGuFinish(); + sys::sceGuSync(GuSyncMode::Finish, GuSyncBehavior::Wait); + } +} + +/// Draw vertices to the screen. +/// +/// # Parameters +/// +/// - `vertices`: Reference to buffer of 4-byte aligned vertices. The buffer must be +/// 16-byte aligned . +/// - `texture`: Reference to buffer of texture. The buffer must be 16-byte aligned. +/// - `texture_width`: Width of texture, must be power of 2. +/// - `texture_height`: Height of texture, must be power of 2. +/// - `scale_x`: Horizontal scale factor. +/// - `scale_y`: Vertical scale factor. +pub fn draw_vertices( + vertices: &[Align4], + texture: &[u8], + texture_width: u32, + texture_height: u32, + scale_x: f32, + scale_y: f32, +) { + unsafe { + sys::sceGuStart(GuContextType::Direct, LIST.0.as_mut_ptr() as *mut _); + + sys::sceGumMatrixMode(MatrixMode::Model); + sys::sceGumLoadIdentity(); + sys::sceGumScale(&ScePspFVector3 { x: scale_x, y: scale_y, z: 1.0 }); + + // setup texture + sys::sceGuTexImage(MipmapLevel::None, texture_width as i32, texture_height as i32, texture_width as i32, (*texture).as_ptr() as _); + sys::sceGuTexScale(1.0/texture_width as f32, 1.0/texture_height as f32); + + sys::sceKernelDcacheWritebackInvalidateAll(); + + // draw + sys::sceGumDrawArray( + GuPrimitive::Sprites, + VertexType::TEXTURE_32BITF | VertexType::COLOR_8888 | VertexType::VERTEX_32BITF | VertexType::TRANSFORM_3D, + (*vertices).len() as i32, + ptr::null_mut(), + (*vertices).as_ptr() as _, + ); + sys::sceGuFinish(); + sys::sceGuSync(GuSyncMode::Finish, GuSyncBehavior::Wait); + } +} + +/// Draws text at a given point on the screen in a given colour. +/// +/// # Parameters +/// +/// - `x`: horizontal position +/// - `y`: vertical position +/// - `color`: Colour of text, in big-endian ABGR, little-endian RGBA. +/// - `text`: ASCII text as an &str. +pub fn draw_text_at(x: i32, y: i32, color: u32, text: &str) { + unsafe { + sys::sceGuDebugPrint(x, y, color, (text.to_string() + "\0").as_bytes().as_ptr()); + sys::sceGuDebugFlush(); + } +} + +/// Finishes drawing by waiting for VBlank and swapping the Draw and Display buffer +/// pointers. +pub fn finish_frame() { + unsafe { + sys::sceDisplayWaitVblankStart(); + sys::sceGuSwapBuffers(); + } +} diff --git a/examples/tetris/src/graphics/sprite.rs b/examples/tetris/src/graphics/sprite.rs new file mode 100644 index 00000000..7dc03ae2 --- /dev/null +++ b/examples/tetris/src/graphics/sprite.rs @@ -0,0 +1,235 @@ +use core::{ptr, f32::consts::PI}; +use psp::Align16; +use psp::sys::{ + self, ScePspFVector3, + GuPrimitive, MipmapLevel, VertexType, + GuSyncMode, GuSyncBehavior, GuContextType +}; + +use crate::graphics::Align4; + +#[repr(C, packed)] +#[derive(Debug, Clone, Copy, Default)] +pub struct Vertex { + pub u: f32, + pub v: f32, + pub color: u32, + pub x: f32, + pub y: f32, + pub z: f32, +} + +#[repr(C)] +pub struct Sprite<'a, T> { + texture: &'a T, + color: u32, + x: i32, + y: i32, + width: u32, + height: u32, + rotation_radians: f32, + scale: f32, +} + +impl<'a, T> Sprite<'a, T> where T: AsRef<[u8]> { + /// Creates a new `Sprite`. + /// + /// # Parameters + /// + /// - `texture`: A reference to a 16-byte aligned `u8` buffer of texture pixels. + /// Only used by the `draw` function. + /// - `color`: A 32-bit color in Big-Endian ABGR format (little-endian RGBA). + /// - `x`: Starting horizontal position, in screen coordinates. + /// - `y`: Starting vertical position, in screen coordinates. + /// - `width`: Width of the `Sprite`. Must be a power of 2. + /// - `height`: Height of the `Sprite`. Must be a power of 2. + pub const fn new(texture: &'a T, color: u32, x: i32, y: i32, width: u32, height: u32) -> Self { + Self { + texture, + color, + x, + y, + width, + height, + rotation_radians: 0.0, + scale: 1.0, + } + } + + /// Returns an `Iterator` over the vertices of a `Sprite`. See `Sprite::Vertex`. + /// Vertices are aligned to 4 bytes. + pub fn as_vertex_iter(&self) -> impl Iterator> { + Some(Align4(Vertex { + u: 0.0, + v: 0.0, + color: self.color, + x: self.x as f32, + y: self.y as f32, + z: 0.0, + })).into_iter() + .chain( + Some(Align4( + Vertex { + u: self.width as f32, + v: self.height as f32, + color: self.color, + x: self.x as f32 + self.width as f32, + y: self.y as f32 + self.height as f32, + z: 0.0, + }) + ) + ) + } + + /// Returns an array of 2 vertices that make up the `Sprite`. See `Sprite::Vertex`. + /// Vertices are aligned to 4 bytes. + pub fn as_vertices(&self) -> [Align4;2] { + [Align4(Vertex { + u: 0.0, + v: 0.0, + color: self.color, + x: self.x as f32, + y: self.y as f32, + z: 0.0, + }), + Align4( + Vertex { + u: self.width as f32, + v: self.height as f32, + color: self.color, + x: self.x as f32 + self.width as f32, + y: self.y as f32 + self.height as f32, + z: 0.0, + } + )] + } + + /// Makes a `sceGumDrawArray` call for a `Sprite`. + /// + /// Don't use this if you're drawing many sprites, it's really slow + /// Use fn as_vertices() and collect your sprites into a single buffer to + /// draw all at once. + /// + /// # Note + /// + /// This is the only function that preserves `Sprite` scaling and rotation. + /// + /// # Parameters + /// + /// - `displaylist`: A reference to the aligned buffer used as GU's display list in + /// `sceGuStart`. + pub fn draw(&self, displaylist: &mut Align16<[u32; 0x40000]>) { + let vertex_array = self.as_vertices(); + + let vertices: Align16<[Align4;2]> = Align16(vertex_array); + + unsafe { + sys::sceGuStart(GuContextType::Direct, displaylist.0.as_mut_ptr() as *mut _); + + sys::sceGumMatrixMode(sys::MatrixMode::Model); + sys::sceGumLoadIdentity(); + sys::sceGumScale(&ScePspFVector3 { x: self.scale, y: self.scale, z: 1.0 }); + sys::sceGumRotateZ(self.rotation_radians); + // setup texture + sys::sceGuTexImage(MipmapLevel::None, self.width as i32, self.height as i32, self.width as i32, self.texture.as_ref().as_ptr() as *const _); + sys::sceGuTexScale(1.0/self.width as f32, 1.0/self.height as f32); + + sys::sceKernelDcacheWritebackInvalidateAll(); + + // draw sprite + sys::sceGumDrawArray( + GuPrimitive::Sprites, + VertexType::TEXTURE_32BITF | VertexType::COLOR_8888 | VertexType::VERTEX_32BITF | VertexType::TRANSFORM_3D, + 2, + ptr::null_mut(), + &vertices as *const Align16<_> as *const _, + ); + sys::sceGuFinish(); + sys::sceGuSync(GuSyncMode::Finish, GuSyncBehavior::Wait); + + } + } + + /// Sets the position of a `Sprite`. Position is in screen units. + /// + /// # Parameters + /// + /// - `x`: Position on the horizontal axis. + /// - `y`: Position on the vertical axis. + pub fn set_pos(&mut self, x: i32, y: i32) { + self.x = x; + self.y = y; + } + + /// Gets the position of a `Sprite`. Position is in screen units. + /// + /// # Return Value + /// + /// A tuple containing the horizontal position in the first element and the vertical + /// position in the second element, (x, y). + pub fn get_pos(&mut self) -> (i32, i32) { + (self.x, self.y) + } + + /// Sets rotation of the `Sprite`. Only used by the `draw` function. + /// + /// # Parameters + /// + /// - `radians`: Rotation in units of radians. + pub fn set_rotation_radians(&mut self, radians: f32) { + self.rotation_radians = radians; + } + + /// Gets rotation of the `Sprite`. + /// + /// # Return value + /// + /// Rotation in units of radians. + pub fn get_rotation_radians(&mut self) -> f32 { + self.rotation_radians + } + + /// Sets rotation of the `Sprite`. Only used by the `draw` function. + /// + /// # Parameters + /// + /// - `degrees`: Rotation in units of degrees. + pub fn set_rotation_degrees(&mut self, degrees: f32) { + self.rotation_radians = degrees * (PI / 180.0); + } + + /// Gets rotation of the `Sprite`. + /// + /// # Return value + /// + /// Rotation in units of degrees. + pub fn get_rotation_degrees(&mut self) -> f32 { + self.rotation_radians * (180.0 / PI) + } + + /// Sets scale of the `Sprite`. Only used by the `draw` function. + /// + /// # Parameters + /// + /// - `scale`: Scale factor, 1.0 is 100% scale. + pub fn set_scale(&mut self, scale: f32) { + self.scale = scale; + } +} + +impl<'a, T> Clone for Sprite<'a, T> where T: AsRef<[u8]> { + fn clone(&self) -> Self { + Self { + texture: self.texture.clone(), + color: self.color, + x: self.x, + y: self.y, + width: self.width, + height: self.height, + rotation_radians: self.rotation_radians, + scale: self.scale, + } + } +} + +impl <'a, T> Copy for Sprite<'a, T> where T: AsRef<[u8]> {} diff --git a/examples/tetris/src/main.rs b/examples/tetris/src/main.rs new file mode 100644 index 00000000..9c8d5f52 --- /dev/null +++ b/examples/tetris/src/main.rs @@ -0,0 +1,101 @@ +#![no_std] +#![no_main] + +#![allow(dead_code)] +#![feature(const_fn_trait_bound)] + +extern crate alloc; + +mod tetromino; +mod gameboard; +mod game; +mod graphics; +mod audio; + +use core::slice; + +use psp::vram_alloc::get_vram_allocator; +use psp::sys::{self, TexturePixelFormat, SceCtrlData, AudioFormat, CtrlButtons}; + +use crate::graphics::Align4; +use crate::graphics::sprite::Vertex; +use crate::audio::MAX_SAMPLES; + +psp::module!("tetris", 1, 1); + +pub const BLOCK_SIZE: u32 = 16; + +pub const GAMEBOARD_OFFSET: (usize, usize) = (15, 1); +pub const GAMEBOARD_WIDTH: usize = 10; +pub const GAMEBOARD_HEIGHT: usize = 20; + +pub static BLOCK: [u8;BLOCK_SIZE as usize*BLOCK_SIZE as usize*4] = + *include_bytes!("../assets/block.bin"); + +pub static TETRIS_SONG: [u8;3402490] = *include_bytes!("../assets/tetris.pcm.raw"); + +fn psp_main() { + psp::enable_home_button(); + let mut allocator = get_vram_allocator().unwrap(); + graphics::setup(&mut allocator); + + let vertex_buffer = allocator.alloc_sized::(418); + let mut vertex_buffer = unsafe { slice::from_raw_parts_mut(vertex_buffer.as_mut_ptr_direct_to_vram() as *mut Align4, 418) }; + let texture_buffer = allocator.alloc_texture_pixels(16, 16, TexturePixelFormat::Psm8888); + let mut texture_buffer = unsafe { slice::from_raw_parts_mut(texture_buffer.as_mut_ptr_direct_to_vram() as *mut u8, 16*16*4) }; + + let channel = unsafe { sys::sceAudioChReserve(-1, MAX_SAMPLES as i32, AudioFormat::Mono) }; + let mut start_pos: usize = 0; + let mut restlen: i32 = 0; + + let mut game = game::Game::new(); + + graphics::clear_color(0xff554433); + graphics::draw_text_at(130, 136, 0xffff_ffff, "Press Start to Play Tetris!"); + graphics::finish_frame(); + + let ctrl_data = &mut SceCtrlData::default(); + while !ctrl_data.buttons.contains(CtrlButtons::START) { + unsafe { + sys::sceCtrlReadBufferPositive(ctrl_data, 1); + } + } + + let mut loop_end = 0; + let mut loop_start = 0; + let ticks_per_sec = unsafe { sys::sceRtcGetTickResolution() }; + let mut seconds_since_last_loop: f32; + + loop { + seconds_since_last_loop = (loop_end - loop_start) as f32 / ticks_per_sec as f32; + unsafe { + sys::sceRtcGetCurrentTick(&mut loop_start); + } + + let audio_ret = audio::process_audio_loop(channel, start_pos, restlen); + restlen = audio_ret.0; + start_pos = audio_ret.1; + + graphics::clear_color(0xff554433); + let game_over = game.process_game_loop(seconds_since_last_loop); + if game_over { + game.draw(&mut vertex_buffer, &mut texture_buffer); + graphics::draw_text_at(100, 136, 0xffff_ffff, "Game Over. Press Start to Play Again"); + + let ctrl_data = &mut SceCtrlData::default(); + unsafe { + sys::sceCtrlReadBufferPositive(ctrl_data, 1); + } + if ctrl_data.buttons.contains(CtrlButtons::START) { + game = game::Game::new(); + } + } else { + game.draw(vertex_buffer, texture_buffer); + } + graphics::finish_frame(); + unsafe { + sys::sceRtcGetCurrentTick(&mut loop_end); + } + } +} + diff --git a/examples/tetris/src/tetromino.rs b/examples/tetris/src/tetromino.rs new file mode 100644 index 00000000..8a099a53 --- /dev/null +++ b/examples/tetris/src/tetromino.rs @@ -0,0 +1,235 @@ +use crate::graphics::{Align4, sprite::{Sprite, Vertex}}; +use crate::{BLOCK, BLOCK_SIZE}; +use crate::gameboard::Gameboard; +use crate::GAMEBOARD_OFFSET; + +use rand_chacha::ChaChaRng; +use rand::prelude::*; + +#[derive(Debug, Copy, Clone)] +pub struct Tetromino { + x: i32, + y: i32, + color: u32, + block_locs: [(i32, i32); 4] +} + +impl Tetromino { + /// Creates a new O-shaped Tetromino. + pub fn new_o() -> Self { + Self { + x: 0, + y: 0, + color: 0xff00ffff, + block_locs: [ + (0, 1), + (1, 1), + (0, 0), + (1, 0), + ] + } + } + + /// Creates a new I-shaped Tetromino. + pub fn new_i() -> Self { + Self { + x: 0, + y: 0, + color: 0xffffff00, + block_locs: [ + (0, 0), + (0, 1), + (0, 2), + (0, -1), + ] + } + } + + /// Creates a new S-shaped Tetromino. + pub fn new_s() -> Self { + Self { + x: 0, + y: 0, + color: 0xff0000ff, + block_locs: [ + (0, 1), + (-1, 1), + (0, 0), + (1, 0), + ] + } + } + + /// Creates a new Z-shaped Tetromino. + pub fn new_z() -> Self { + Self { + x: 0, + y: 0, + color: 0xff00ff00, + block_locs: [ + (0, 0), + (0, 1), + (-1, 0), + (1, 1), + ] + } + } + + /// Creates a new L-shaped Tetromino. + pub fn new_l() -> Self { + Self { + x: 0, + y: 0, + color: 0xff008cff, + block_locs: [ + (0, 1), + (0, 0), + (0, -1), + (-1, -1), + ] + } + } + + /// Creates a new J-shaped Tetromino. + pub fn new_j() -> Self { + Self { + x: 0, + y: 0, + color: 0xffff00ff, + block_locs: [ + (0, 1), + (0, 0), + (0, -1), + (1, -1), + ] + } + } + + /// Creates a new T-shaped Tetromino. + pub fn new_t() -> Self { + Self { + x: 0, + y: 0, + color: 0xffff0000, + block_locs: [ + (1, 0), + (0, 0), + (-1, 0), + (0, -1), + ] + } + } + + /// Creates a new Tetromino with a random shape + /// + /// # Parameters + /// + /// - `rng`: An initialized `ChaChaRng` random number generator from the + /// `rand_chacha` crate. + pub fn new_random(rng: &mut ChaChaRng) -> Self { + let rand_num = rng.gen_range(0, 7); + match rand_num { + 1 => Tetromino::new_o(), + 2 => Tetromino::new_i(), + 3 => Tetromino::new_s(), + 4 => Tetromino::new_z(), + 5 => Tetromino::new_l(), + 6 => Tetromino::new_j(), + _ => Tetromino::new_t(), + } + } + + /// Returns a representation of a `Tetromino` as 4 `Sprite`s. + pub fn as_sprites<'a>(&self) -> [Sprite<'a, [u8; BLOCK_SIZE as usize * BLOCK_SIZE as usize * 4]>; 4] { + [ + Sprite::new(&BLOCK, self.color, self.block_locs[0].0*BLOCK_SIZE as i32+self.x*BLOCK_SIZE as i32, self.block_locs[0].1*BLOCK_SIZE as i32+self.y*BLOCK_SIZE as i32, BLOCK_SIZE, BLOCK_SIZE), + Sprite::new(&BLOCK, self.color, self.block_locs[1].0*BLOCK_SIZE as i32+self.x*BLOCK_SIZE as i32, self.block_locs[1].1*BLOCK_SIZE as i32+self.y*BLOCK_SIZE as i32, BLOCK_SIZE, BLOCK_SIZE), + Sprite::new(&BLOCK, self.color, self.block_locs[2].0*BLOCK_SIZE as i32+self.x*BLOCK_SIZE as i32, self.block_locs[2].1*BLOCK_SIZE as i32+self.y*BLOCK_SIZE as i32, BLOCK_SIZE, BLOCK_SIZE), + Sprite::new(&BLOCK, self.color, self.block_locs[3].0*BLOCK_SIZE as i32+self.x*BLOCK_SIZE as i32, self.block_locs[3].1*BLOCK_SIZE as i32+self.y*BLOCK_SIZE as i32, BLOCK_SIZE, BLOCK_SIZE), + ] + } + + /// Returns a representation of a `Tetromino` as 8 sprite (rectangular) vertices. See `Sprite::Vertex`. + pub fn as_vertices(&self) -> [Align4; 8] { + let mut ret = [Align4(Vertex::default()); 8]; + self.as_sprites() + .iter() + .flat_map(|s| s.as_vertex_iter()) + .zip(ret.iter_mut()) + .for_each(|(v, dst)| *dst = v); + ret + } + + /// Sets the position of a `Tetromino`. + /// Position is in block units, not screen units, i.e. screen units divided by + /// `BLOCK_SIZE`. + /// + /// # Parameters + /// + /// - `x`: Position on the horizontal axis. + /// - `y`: Position on the vertical axis. + pub fn set_pos(&mut self, x: i32, y: i32) { + self.x = x; + self.y = y; + } + + /// Gets the position of a `Tetromino`. + /// Position is in block units, not screen units, i.e. screen units divided by + /// `BLOCK_SIZE`. + /// + /// # Return Value + /// + /// A tuple containing the horizontal position in the first element and the vertical + /// position in the second element, (x, y). + pub fn get_pos(&mut self) -> (i32, i32) { + (self.x, self.y) + } + + /// Rotates a `Tetromino` counter-clockwise. + pub fn rotate_ccw(&mut self) { + for i in 0..4 { + self.block_locs[i] = (self.block_locs[i].1, 0-self.block_locs[i].0); + } + } + + /// Rotates a `Tetromino` clockwise. + pub fn rotate_cw(&mut self) { + for i in 0..4 { + self.block_locs[i] = (0-self.block_locs[i].1, self.block_locs[i].0); + } + } + + /// Locks a `Tetromino` in place to a `Gameboard` + /// + /// # Parameters + /// + /// - `gameboard`: Mutable reference to a `Gameboard`. + pub fn lock_to_gameboard(&self, gameboard: &mut Gameboard) { + for block_loc in self.block_locs.iter() { + gameboard.set_content((block_loc.0+self.x-GAMEBOARD_OFFSET.0 as i32) as usize, (block_loc.1+self.y - GAMEBOARD_OFFSET.1 as i32) as usize, Some(self.color)).unwrap(); + } + } + + /// Adds an x and y coordinate to the current position. + /// + /// # Parameters + /// + /// - `x`: Horizontal position to add to the current position + /// - `y`: Vertical position to add to the current position + pub fn add_pos(&mut self, x: i32, y: i32) { + self.x += x; + self.y += y; + } + + /// Returns the position of each block subtracted from `GAMEBOARD_OFFSET`. + /// Effectively, the position within a Gameboard. + pub fn get_mapped_locs(&self) -> [(usize, usize) ; 4] { + [ + ((self.block_locs[0].0 + self.x) as usize - GAMEBOARD_OFFSET.0, (self.block_locs[0].1 + self.y) as usize - GAMEBOARD_OFFSET.1), + ((self.block_locs[1].0 + self.x) as usize - GAMEBOARD_OFFSET.0, (self.block_locs[1].1 + self.y) as usize - GAMEBOARD_OFFSET.1), + ((self.block_locs[2].0 + self.x) as usize - GAMEBOARD_OFFSET.0, (self.block_locs[2].1 + self.y) as usize - GAMEBOARD_OFFSET.1), + ((self.block_locs[3].0 + self.x) as usize - GAMEBOARD_OFFSET.0, (self.block_locs[3].1 + self.y) as usize - GAMEBOARD_OFFSET.1), + ] + } +} + diff --git a/psp/src/vram_alloc.rs b/psp/src/vram_alloc.rs index f343bdcb..1b1b09ab 100644 --- a/psp/src/vram_alloc.rs +++ b/psp/src/vram_alloc.rs @@ -91,7 +91,6 @@ impl SimpleVramAllocator { let old_offset = self.offset.load(Ordering::Relaxed); let new_offset = old_offset + size; self.offset.store(new_offset, Ordering::Relaxed); - if new_offset > self.total_mem() { panic!("Total VRAM size exceeded!"); }