From b50feac08ee38482ed23d7da9f3fc5ffd0e6ecce Mon Sep 17 00:00:00 2001 From: Alessio Cosenza Date: Thu, 21 Sep 2023 01:24:51 +0200 Subject: [PATCH] LCD: Start work on rendering sprites --- emu/src/cpu/hardware/lcd.rs | 261 +++++++++++++++++- emu/src/cpu/hardware/lcd/object_attributes.rs | 84 ++++-- emu/src/cpu/hardware/lcd/point.rs | 99 +++++++ 3 files changed, 419 insertions(+), 25 deletions(-) create mode 100644 emu/src/cpu/hardware/lcd/point.rs diff --git a/emu/src/cpu/hardware/lcd.rs b/emu/src/cpu/hardware/lcd.rs index 1ef1dbb..85945bc 100644 --- a/emu/src/cpu/hardware/lcd.rs +++ b/emu/src/cpu/hardware/lcd.rs @@ -4,7 +4,15 @@ use object_attributes::RotationScaling; use crate::bitwise::Bits; +use self::object_attributes::ColorMode; +use self::object_attributes::ObjMode; +use self::object_attributes::ObjShape; +use self::object_attributes::ObjSize; +use self::object_attributes::TransformationKind; +use self::point::Point; + mod object_attributes; +mod point; /// GBA display width const LCD_WIDTH: usize = 240; @@ -16,6 +24,10 @@ const LCD_HEIGHT: usize = 160; pub struct Color(pub u16); impl Color { + pub const fn from_palette_color(value: u16) -> Self { + Self(value) + } + pub fn from_rgb(red: u8, green: u8, blue: u8) -> Self { let red: u16 = red.into(); let green: u16 = green.into(); @@ -37,6 +49,26 @@ impl Color { } } +enum ObjMappingKind { + TwoDimensional, + OneDimensional, +} + +impl From for ObjMappingKind { + fn from(value: bool) -> Self { + match value { + false => Self::TwoDimensional, + true => Self::OneDimensional, + } + } +} + +#[derive(Copy, Clone, Default)] +struct PixelInfo { + color: Color, + priority: u8, +} + pub struct Lcd { /// LCD Control pub dispcnt: u16, @@ -129,6 +161,7 @@ pub struct Lcd { should_draw: bool, obj_attributes_arr: [ObjAttributes; 128], rotation_scaling_params: [RotationScaling; 32], + sprite_pixels_scanline: [Option; LCD_WIDTH], } impl Default for Lcd { @@ -181,6 +214,7 @@ impl Default for Lcd { should_draw: false, obj_attributes_arr: [ObjAttributes::default(); 128], rotation_scaling_params: [RotationScaling::default(); 32], + sprite_pixels_scanline: [None; LCD_WIDTH], } } } @@ -192,6 +226,224 @@ pub struct LcdStepOutput { } impl Lcd { + fn read_color_from_obj_palette(&self, color_idx: usize) -> Color { + let low_nibble = self.obj_palette_ram[color_idx] as u16; + let high_nibble = self.obj_palette_ram[color_idx + 1] as u16; + + Color::from_palette_color((high_nibble << 8) | low_nibble) + } + + fn get_texture_space_point( + &self, + sprite_size: Point, + pixel_screen_sprite_origin: Point, + transformation_kind: TransformationKind, + obj_mode: ObjMode, + ) -> Point { + if let object_attributes::TransformationKind::RotationScaling { + rotation_scaling_parameter, + } = transformation_kind + { + // We have to use f64 for translating/rot/scale because we might have negative values when using the pixel + // in the carthesian plane having the origin as the center of the sprite. + // We could use i16 as well but then we would still need to use f64 to apply the transformation. + + // RotScale matrix + let rotscale_params = self.rotation_scaling_params[rotation_scaling_parameter as usize]; + let sprite_size = sprite_size.map(|el| el as f64); + + // This is the pixel coordinate in the screen space using the sprite center as origin of the reference system + // This is needed because the rotscale is applied taking the center of the sprite as the origin of the rotation + // If the sprite is in AffineDouble mode then it has double dimensions and the center is at +sprite_width/+sprite_height insted of + // just half ot that. + let pixel_screen_sprite_center = pixel_screen_sprite_origin.map(|el| el as f64) + - match obj_mode { + ObjMode::Affine => sprite_size / 2.0, + ObjMode::AffineDouble => sprite_size, + _ => unreachable!(), + }; + + // Applying transformation. + // The result will be a pixel in the texture space which still has the center of the sprite as the origin of the reference system + let pixel_texture_sprite_center = pixel_screen_sprite_center * rotscale_params; + + // Moving back the reference system to the origin of the sprite (top-left corner). + pixel_texture_sprite_center + sprite_size / 2.0 + } else { + // TODO: Implement flip + pixel_screen_sprite_origin.map(|el| el as f64) + } + } + + fn process_sprites_scanline(&mut self) { + self.sprite_pixels_scanline = [None; LCD_WIDTH]; + + let y = self.vcount; + + for obj in self.obj_attributes_arr.into_iter() { + if matches!( + obj.attribute0.obj_mode, + object_attributes::ObjMode::Disabled + ) || matches!( + obj.attribute0.gfx_mode, + object_attributes::GfxMode::ObjectWindow + ) { + continue; + } + + let (sprite_width, sprite_height) = + match (obj.attribute0.obj_shape, obj.attribute1.obj_size) { + (ObjShape::Square, ObjSize::Size0) => (8_u8, 8_u8), + (ObjShape::Horizontal, ObjSize::Size0) => (16, 8), + (ObjShape::Vertical, ObjSize::Size0) => (8, 16), + (ObjShape::Square, ObjSize::Size1) => (16, 16), + (ObjShape::Horizontal, ObjSize::Size1) => (32, 8), + (ObjShape::Vertical, ObjSize::Size1) => (8, 32), + (ObjShape::Square, ObjSize::Size2) => (32, 32), + (ObjShape::Horizontal, ObjSize::Size2) => (32, 16), + (ObjShape::Vertical, ObjSize::Size2) => (16, 32), + (ObjShape::Square, ObjSize::Size3) => (64, 64), + (ObjShape::Horizontal, ObjSize::Size3) => (64, 32), + (ObjShape::Vertical, ObjSize::Size3) => (32, 64), + }; + + // We can represent the size of the sprite using a point. + let sprite_size = Point::new(sprite_width as u16, sprite_height as u16); + + // Sprite size using tiles as dimensions + let sprite_size_tile = sprite_size / 8; + + let sprite_position = Point::new( + obj.attribute1.x_coordinate, + obj.attribute0.y_coordinate as u16, + ); + + let is_affine_double = matches!( + obj.attribute0.obj_mode, + object_attributes::ObjMode::AffineDouble + ); + + // Sprite size in screen space (takes into account double size sprites) + let sprite_screen_size = sprite_size * if is_affine_double { 2 } else { 1 }; + + for idx in 0..sprite_screen_size.x { + // This is the pixel coordinate in the screen space using the sprite origin (top-left corner) as origin of the reference system + let pixel_screen_sprite_origin = + Point::new(idx, (y + 512 - sprite_position.y) % 512); + + // We check that the coordinates in the screen space are inside the sprite + // Taking care of the fact that if the sprite in AffineDouble it has double the dimensions + if pixel_screen_sprite_origin.x > sprite_screen_size.x + || pixel_screen_sprite_origin.y > sprite_screen_size.y + { + continue; + } + + // We apply the transformation. + // The result is a pixel in the texture space with the origin of the sprite (top-left corner) as the origin of the reference system + let pixel_texture_sprite_origin = self.get_texture_space_point( + sprite_size, + pixel_screen_sprite_origin, + obj.attribute1.transformation_kind, + obj.attribute0.obj_mode, + ); + + // We check that the pixel is inside the sprite + if pixel_texture_sprite_origin.x < 0.0 + || pixel_texture_sprite_origin.y < 0.0 + || pixel_texture_sprite_origin.x >= sprite_size.x as f64 + || pixel_texture_sprite_origin.y >= sprite_size.y as f64 + { + continue; + } + + let pixel_texture_sprite_origin = pixel_texture_sprite_origin.map(|el| el as u16); + + // Pixel in texture space using tiles as dimensions + let pixel_texture_tile = pixel_texture_sprite_origin / 8; + + // Offset of the pixel inside the tile + let y_tile_idx = pixel_texture_sprite_origin.y % 8; + let x_tile_idx = pixel_texture_sprite_origin.x % 8; + + let color_offset = match obj.attribute0.color_mode { + ColorMode::Palette8bpp => { + let tile_number = obj.attribute2.tile_number + + match self.get_obj_character_vram_mapping() { + ObjMappingKind::OneDimensional => { + // In this case memory is seen as a single array. + // tile_number is the offset of the first tile in memory. + // then we access [y][x] by doing y*number_cols + x, as if we were to access an array as a matrix + pixel_texture_tile.y * sprite_size_tile.y * 2 + + pixel_texture_tile.x * 2 + } + ObjMappingKind::TwoDimensional => { + // A charblock is 32x32 tiles + pixel_texture_tile.y * 32 + pixel_texture_tile.x * 2 + } + }; + + // A tile is 8x8 mini-bitmap. + // A tile is 64bytes long in 8bpp. + let palette_offset = + tile_number as u32 * 32 + y_tile_idx as u32 * 8 + x_tile_idx as u32; + + // TODO: Move 0x10000 to a variable. It is the offset where OBJ VRAM starts in vram + self.video_ram[0x10000 + palette_offset as usize] + } + ColorMode::Palette4bpp => { + let tile_number = obj.attribute2.tile_number + + match self.get_obj_character_vram_mapping() { + ObjMappingKind::OneDimensional => { + // In this case memory is seen as a single array. + // tile_number is the offset of the first tile in memory. + // then we access [y][x] by doing y*number_cols + x, as if we were to access an array as a matrix + pixel_texture_tile.y * sprite_size_tile.y + pixel_texture_tile.x + } + ObjMappingKind::TwoDimensional => { + // A charblock is 32x32 tiles + obj.attribute2.tile_number + + pixel_texture_tile.y * 32 + + pixel_texture_tile.x + } + }; + + // A tile is 32bytes long in 4bpp. + let tile_data = tile_number * 32 + y_tile_idx * 4 + x_tile_idx / 2; + + let palette_offset_low = if tile_data % 2 == 0 { + tile_data.get_bits(0..=3) + } else { + tile_data.get_bits(4..=7) + }; + + let palette_offset = + (obj.attribute2.palette_number << 4) | (palette_offset_low as u8); + self.video_ram[0x10000 + palette_offset as usize] + } + }; + + let x_screen = sprite_position.x + idx; + + if self.sprite_pixels_scanline[x_screen as usize].is_none() { + self.sprite_pixels_scanline[x_screen as usize] = Some(PixelInfo { + color: self.read_color_from_obj_palette(color_offset as usize), + priority: obj.attribute2.priority, + }); + } else { + let current_pixel = self.sprite_pixels_scanline[x_screen as usize].unwrap(); + + if current_pixel.priority > obj.attribute2.priority { + self.sprite_pixels_scanline[x_screen as usize] = Some(PixelInfo { + color: self.read_color_from_obj_palette(color_offset as usize), + priority: obj.attribute2.priority, + }); + } + } + } + } + } + pub fn step(&mut self) -> LcdStepOutput { // This will be much more complex obviously let mut output = LcdStepOutput::default(); @@ -208,6 +460,7 @@ impl Lcd { (self.obj_attributes_arr, self.rotation_scaling_params) = object_attributes::get_attributes(self.obj_attributes.as_slice()); + self.process_sprites_scanline(); } else if self.pixel_index == 240 { // We're entering Hblank @@ -235,7 +488,9 @@ impl Lcd { let pixel_y = self.vcount; let pixel_x = self.pixel_index; - self.buffer[pixel_y as usize][pixel_x as usize] = Color::from_rgb(31, 31, 31); + self.buffer[pixel_y as usize][pixel_x as usize] = self.sprite_pixels_scanline + [pixel_x as usize] + .map_or_else(|| Color::from_rgb(31, 31, 31), |info| info.color); } log(format!( @@ -284,6 +539,10 @@ impl Lcd { self.dispcnt.get_bits(0..=2).try_into().unwrap() } + fn get_obj_character_vram_mapping(&self) -> ObjMappingKind { + self.dispcnt.get_bit(6).into() + } + fn get_vcount_setting(&self) -> u8 { self.dispstat.get_byte(1) } diff --git a/emu/src/cpu/hardware/lcd/object_attributes.rs b/emu/src/cpu/hardware/lcd/object_attributes.rs index 219aad3..de6e125 100644 --- a/emu/src/cpu/hardware/lcd/object_attributes.rs +++ b/emu/src/cpu/hardware/lcd/object_attributes.rs @@ -5,7 +5,7 @@ use std::ops::{Index, IndexMut}; use crate::bitwise::Bits; #[derive(Default, Clone, Copy)] -enum ObjMode { +pub enum ObjMode { #[default] Normal, Affine, @@ -26,7 +26,7 @@ impl From for ObjMode { } #[derive(Default, Clone, Copy)] -enum GfxMode { +pub enum GfxMode { #[default] Normal, AlphaBlending, @@ -47,7 +47,7 @@ impl TryFrom for GfxMode { } #[derive(Default, Clone, Copy)] -enum ColorMode { +pub enum ColorMode { /// 16 colors #[default] Palette4bpp, @@ -65,7 +65,7 @@ impl From for ColorMode { } #[derive(Default, Clone, Copy)] -enum ObjShape { +pub enum ObjShape { #[default] Square, Horizontal, @@ -86,7 +86,7 @@ impl TryFrom for ObjShape { } #[derive(Default, Clone, Copy)] -enum ObjSize { +pub enum ObjSize { #[default] Size0, Size1, @@ -108,13 +108,13 @@ impl From for ObjSize { #[allow(dead_code)] #[derive(Default, Clone, Copy)] -struct ObjAttribute0 { - y_coordinate: u8, - obj_mode: ObjMode, - gfx_mode: GfxMode, +pub struct ObjAttribute0 { + pub y_coordinate: u8, + pub obj_mode: ObjMode, + pub gfx_mode: GfxMode, obj_mosaic: bool, - color_mode: ColorMode, - obj_shape: ObjShape, + pub color_mode: ColorMode, + pub obj_shape: ObjShape, } impl TryFrom for ObjAttribute0 { @@ -133,7 +133,7 @@ impl TryFrom for ObjAttribute0 { #[allow(dead_code)] #[derive(Clone, Copy)] -enum TransformationKind { +pub enum TransformationKind { RotationScaling { rotation_scaling_parameter: u8, }, @@ -154,10 +154,10 @@ impl Default for TransformationKind { #[allow(dead_code)] #[derive(Default, Clone, Copy)] -struct ObjAttribute1 { - x_coordinate: u16, - transformation_kind: TransformationKind, - obj_size: ObjSize, +pub struct ObjAttribute1 { + pub x_coordinate: u16, + pub transformation_kind: TransformationKind, + pub obj_size: ObjSize, } impl ObjAttribute1 { @@ -179,11 +179,22 @@ impl ObjAttribute1 { } #[allow(dead_code)] -#[derive(Default, Clone, Copy)] -struct ObjAttribute2 { - tile_number: u16, - priority: u8, - palette_number: u8, +#[derive(Clone, Copy)] +pub struct ObjAttribute2 { + pub tile_number: u16, + pub priority: u8, + pub palette_number: u8, +} + +impl Default for ObjAttribute2 { + fn default() -> Self { + Self { + tile_number: 0, + // Lowest priority + priority: 3, + palette_number: 0, + } + } } impl From for ObjAttribute2 { @@ -199,9 +210,9 @@ impl From for ObjAttribute2 { #[allow(dead_code)] #[derive(Default, Clone, Copy)] pub struct ObjAttributes { - attribute0: ObjAttribute0, - attribute1: ObjAttribute1, - attribute2: ObjAttribute2, + pub attribute0: ObjAttribute0, + pub attribute1: ObjAttribute1, + pub attribute2: ObjAttribute2, } impl TryFrom<[u16; 3]> for ObjAttributes { @@ -225,6 +236,31 @@ pub struct RotationScaling { pd: u16, } +impl RotationScaling { + /// Gives back the result of P*T where + /// P = [ pa pb ] + /// [ pc pd ] + /// and + /// T = [ x ] + /// [ y ] + /// pa, pb, pc, pd are first converted to floating number from the fixed point representation + pub fn apply(&self, x: f64, y: f64) -> (f64, f64) { + // TODO: I don't like the `as` mess. + let a = Self::get_float_from_fixed_point(self.pa); + let b = Self::get_float_from_fixed_point(self.pb); + let c = Self::get_float_from_fixed_point(self.pc); + let d = Self::get_float_from_fixed_point(self.pd); + + (x.mul_add(a, y * b), x.mul_add(c, y * d)) + } + + fn get_float_from_fixed_point(value: u16) -> f64 { + // We interpret the value as signed and we divide by 2^8 since the rotation/scaling parameter + // is represented as an 8.8 fixed point value. + (value as i16) as f64 / 256.0 + } +} + impl Index for RotationScaling { type Output = u16; fn index(&self, index: usize) -> &Self::Output { diff --git a/emu/src/cpu/hardware/lcd/point.rs b/emu/src/cpu/hardware/lcd/point.rs new file mode 100644 index 0000000..d167bc2 --- /dev/null +++ b/emu/src/cpu/hardware/lcd/point.rs @@ -0,0 +1,99 @@ +use std::ops; + +use super::object_attributes::RotationScaling; + +/// A simple struct to represent a point in a carthesian plane. +#[derive(Copy, Clone, Eq, PartialEq, Debug)] +pub(super) struct Point { + pub(super) x: T, + pub(super) y: T, +} + +impl Point { + pub(super) const fn new(x: T, y: T) -> Self { + Self { x, y } + } + + pub(super) fn map(self, f: fn(T) -> U) -> Point { + Point:: { + x: f(self.x), + y: f(self.y), + } + } +} + +impl ops::Add for Point +where + T: ops::Add, +{ + type Output = Self; + fn add(self, rhs: Self) -> Self::Output { + Self { + x: self.x + rhs.x, + y: self.y + rhs.y, + } + } +} + +impl ops::Sub for Point +where + T: ops::Sub, +{ + type Output = Self; + fn sub(self, rhs: Self) -> Self::Output { + Self { + x: self.x - rhs.x, + y: self.y - rhs.y, + } + } +} + +impl ops::Mul for Point { + type Output = Self; + fn mul(self, rhs: RotationScaling) -> Self::Output { + let r = rhs.apply(self.x, self.y); + + Self { x: r.0, y: r.1 } + } +} + +impl ops::Mul for Point +where + T: ops::Mul + Copy, +{ + type Output = Self; + fn mul(self, rhs: T) -> Self::Output { + Self { + x: self.x * rhs, + y: self.y * rhs, + } + } +} + +impl ops::Div for Point +where + T: ops::Div + Copy, +{ + type Output = Self; + fn div(self, rhs: T) -> Self::Output { + Self { + x: self.x / rhs, + y: self.y / rhs, + } + } +} + +#[cfg(test)] +mod tests { + use super::Point; + + #[test] + fn test_point() { + let p = Point { x: 10_u16, y: 10 }; + + assert_eq!(p / 2, Point { x: 5_u16, y: 5 }); + assert_eq!(p * 2, Point { x: 20_u16, y: 20 }); + assert_eq!(p + Point { x: 1_u16, y: 1 }, Point { x: 11_u16, y: 11 }); + assert_eq!(p - Point { x: 1_u16, y: 1 }, Point { x: 9_u16, y: 9 }); + } +}