Newer
Older
pub const VRAM_SIZE: usize = 8192;
pub const RGB_SIZE: usize = 3;
/// The number of tiles that can be store in Game Boy's
/// VRAM memory according to specifications.
pub const TILE_COUNT: usize = 384;
/// The width of the Game Boy screen in pixels.
pub const DISPLAY_WIDTH: usize = 160;
/// The height of the Game Boy screen in pixels.
pub const DISPLAY_HEIGHT: usize = 154;
/// Represents the Game Boy PPU (Pixel Processing Unit) and controls
/// all of the logic behind the graphics processing and presentation.
/// Should store both the VRAM and HRAM together with the internal
/// graphic related registers.
/// Outputs the screen as a RGB 8 bit frame buffer.
///
/// # Basic usage
/// ```rust
/// let ppu = Ppu::new();
/// ppu.tick();
/// ```
/// The 8 bit based RGB frame buffer with the
/// processed set of pixels ready to be displayed on screen.
pub frame_buffer: Box<[u8; DISPLAY_WIDTH * DISPLAY_HEIGHT * RGB_SIZE]>,
/// Video dedicated memory (VRAM) where both the tiles and
/// the sprites are going to be stored.
/// High RAM memory that should provide extra speed for regular
/// operations.
/// The current set of processed tiles that are store in the
/// PPU related structures.
tiles: [[[u8; 8]; 8]; TILE_COUNT],
/// The palette of colors that is currently loaded in Game Boy.
palette: [[u8; RGB_SIZE]; PALETTE_SIZE],
/// The scroll Y register that controls the Y offset
/// of the background.
scy: u8,
/// The scroll X register that controls the X offset
/// of the background.
scx: u8,
/// The current scan line in processing, should
/// range between 0 (0x00) and 153 (0x99), representing
/// the 154 lines plus 10 extra v-blank lines.
line: u8,
switch_bg: bool,
bg_map: bool,
bg_tile: bool,
switch_lcd: bool,
/// The current execution mode of the PPU, should change
/// between states over the drawing of a frame.
mode: PpuMode,
/// Internal clock counter used to control the time in ticks
/// spent in each of the PPU modes.
mode_clock: u16,
}
pub enum PpuMode {
OamRead,
VramRead,
Hblank,
VBlank,
}
impl Ppu {
pub fn new() -> Ppu {
Ppu {
frame_buffer: Box::new([0u8; DISPLAY_WIDTH * DISPLAY_HEIGHT * RGB_SIZE]),
palette: [[0u8; RGB_SIZE]; PALETTE_SIZE],
scy: 0x0,
scx: 0x0,
line: 0x0,
switch_bg: false,
bg_map: false,
bg_tile: false,
switch_lcd: false,
mode: PpuMode::OamRead,
mode_clock: 0,
pub fn clock(&mut self, cycles: u8) {
self.mode_clock += cycles as u16;
match self.mode {
PpuMode::OamRead => {
if self.mode_clock >= 204 {
self.mode_clock = 0;
self.mode = PpuMode::VramRead;
}
}
PpuMode::VramRead => {
if self.mode_clock >= 172 {
self.render_line();
self.mode_clock = 0;
self.mode = PpuMode::Hblank;
}
}
PpuMode::Hblank => {
if self.mode_clock >= 204 {
self.line += 1;
// in case we've reached the end of the
// screen we're now entering the v-blank
if self.line == 143 {
self.mode = PpuMode::VBlank;
} else {
self.mode = PpuMode::OamRead;
}
self.mode_clock = 0;
}
}
PpuMode::VBlank => {
if self.mode_clock >= 456 {
self.line += 1;
// in case the end of v-blank has been reached then
// we must jump again to the OAM read mode and reset
// the scan line counter to the zero value
if self.line == 153 {
self.mode = PpuMode::OamRead;
self.line = 0;
}
self.mode_clock = 0;
}
}
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
pub fn read(&mut self, addr: u16) -> u8 {
match addr & 0x00ff {
0x0040 => {
let value = if self.switch_bg { 0x01 } else { 0x00 }
| if self.bg_map { 0x08 } else { 0x00 }
| if self.bg_tile { 0x10 } else { 0x00 }
| if self.switch_lcd { 0x80 } else { 0x00 };
value
}
0x0042 => self.scy,
0x0043 => self.scx,
0x0044 => self.line,
addr => panic!("Reading from unknown PPU location 0x{:04x}", addr),
}
}
pub fn write(&mut self, addr: u16, value: u8) {
match addr & 0x00ff {
0x0040 => {
self.switch_bg = value & 0x01 == 0x01;
self.bg_map = value & 0x08 == 0x08;
self.bg_tile = value & 0x10 == 0x10;
self.switch_lcd = value & 0x80 == 0x80;
}
0x0042 => self.scy = value,
0x0043 => self.scx = value,
0x0047 => {
for index in 0..PALETTE_SIZE {
match (value >> (index * 2)) & 3 {
0 => self.palette[index] = [255, 255, 255],
1 => self.palette[index] = [192, 192, 192],
2 => self.palette[index] = [96, 96, 96],
3 => self.palette[index] = [0, 0, 0],
color_index => panic!("Invalid palette color index {:04x}", color_index),
}
}
}
addr => panic!("Writing in unknown PPU location 0x{:04x}", addr),
/// Updates the tile structure with the value that has
/// just been written to a location on the VRAM associated
/// with tiles.
pub fn update_tile(&mut self, addr: u16, _value: u8) {
let addr = (addr & 0x1ffe) as usize;
let tile_index = ((addr >> 4) & 0x01ff) as usize;
let y = ((addr >> 1) & 0x0007) as usize;
let mut mask;
for x in 0..8 {
mask = 1 << (7 - x);
self.tiles[tile_index][y][x] =
if self.vram[addr] & mask > 0 {
} | if self.vram[addr + 1] & mask > 0 {
0x2
} else {
0x0
}
}
}
fn render_line(&mut self) {
let mut map_offset: usize = if self.bg_map { 0x1c00 } else { 0x1800 };
map_offset += (((self.line + self.scy) & 0xff) >> 3) as usize;
// calculates the sprite line offset by using the SCX register
// shifted by 3 meaning as the tiles are 8x8
let mut line_offset: usize = (self.scx >> 3) as usize;
let y = ((self.scy + self.line) & 0x07) as usize;
let mut x = (self.scx & 0x07) as usize;
// calculates the index of the initial tile in drawing,
// if the tile data set in use is #1, the indices are
// signed, then calculates a real tile offset
let mut tile_index = self.vram[map_offset + line_offset] as usize;
if self.bg_tile && tile_index < 128 {
tile_index += 256;
}
// calculates the frame buffer offset position assuming the proper
// Game Boy screen width and RGB pixel (3 bytes) size
let mut frame_offset = self.line as usize * DISPLAY_WIDTH * RGB_SIZE;
for _index in 0..DISPLAY_WIDTH {
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
// in case the end of tile width has been reached then
// a new tile must be retrieved for plotting
if x == 8 {
// resets the tile X position to the base value
// as a new tile is going to be drawn
x = 0;
// calculates the new line tile offset making sure that
// the maximum of 32 is not overflown
line_offset = (line_offset + 1) & 31;
// calculates the tile index nad makes sure the value
// takes into consideration the bg tile value
tile_index = self.vram[map_offset + line_offset] as usize;
if self.bg_tile && tile_index < 128 {
tile_index += 256;
}
}
// obtains the current pixel data from the tile and
// re-maps it according to the current palette
let pixel = self.tiles[tile_index][y][x];
let color = self.palette[pixel as usize];
// set the color pixel in the frame buffer
self.frame_buffer[frame_offset] = color[0];
self.frame_buffer[frame_offset + 1] = color[1];
self.frame_buffer[frame_offset + 2] = color[2];
frame_offset += RGB_SIZE;
// increments the current tile X position in drawing
x += 1;
}