Initial commit
This commit is contained in:
495
src/ppu.rs
Normal file
495
src/ppu.rs
Normal file
@@ -0,0 +1,495 @@
|
||||
use iced::{Point, Size, widget::canvas::Frame};
|
||||
use iced_graphics::geometry::{Fill, Renderer};
|
||||
|
||||
use crate::{hex_view::Memory, mem::{MemoryMap, Segment}};
|
||||
|
||||
#[derive(Clone, Copy, PartialEq, Eq, Hash)]
|
||||
pub struct Color {
|
||||
pub r: u8,
|
||||
pub g: u8,
|
||||
pub b: u8,
|
||||
}
|
||||
|
||||
impl Into<Fill> for Color {
|
||||
fn into(self) -> Fill {
|
||||
iced::Color::from_rgb8(self.r, self.g, self.b).into()
|
||||
}
|
||||
}
|
||||
|
||||
pub struct RenderBuffer<const W: usize, const H: usize> {
|
||||
buffer: Box<[Color]>,
|
||||
}
|
||||
|
||||
impl<const W: usize, const H: usize> RenderBuffer<W, H> {
|
||||
pub fn empty() -> Self {
|
||||
Self {
|
||||
buffer: vec![Color { r: 0, g: 0, b: 0 }; W * H].into_boxed_slice(),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn write(&mut self, line: usize, pixel: usize, color: Color) {
|
||||
assert!(line < H && pixel < W);
|
||||
self.buffer[line * W + pixel] = color;
|
||||
}
|
||||
pub fn read(&self, line: usize, pixel: usize) -> Color {
|
||||
assert!(line < H && pixel < W);
|
||||
self.buffer[line * W + pixel]
|
||||
}
|
||||
|
||||
pub fn clone_from(&mut self, other: &Self) {
|
||||
self.buffer.copy_from_slice(&other.buffer);
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) enum PPUMMRegisters {
|
||||
Palette,
|
||||
}
|
||||
|
||||
pub struct OAM {
|
||||
mem: Vec<u8>,
|
||||
addr: u8,
|
||||
}
|
||||
|
||||
enum BackgroundState {
|
||||
NameTableBytePre,
|
||||
NameTableByte,
|
||||
AttrTableBytePre,
|
||||
AttrTableByte,
|
||||
PatternTableTileLowPre,
|
||||
PatternTableTileLow,
|
||||
PatternTableTileHighPre,
|
||||
PatternTableTileHigh,
|
||||
}
|
||||
|
||||
pub struct Background {
|
||||
/// Current vram address, 15 bits
|
||||
v: u16,
|
||||
/// Temp vram address, 15 bits
|
||||
t: u16,
|
||||
/// Fine X control, 3 bits
|
||||
x: u8,
|
||||
/// Whether this is the first or second write to PPUSCROLL
|
||||
/// When false, writes to x
|
||||
w: bool,
|
||||
|
||||
/// When true, v is incremented by 32 after each read
|
||||
vram_column: bool,
|
||||
|
||||
state: BackgroundState,
|
||||
|
||||
cur_shift_high: u8,
|
||||
cur_shift_low: u8,
|
||||
}
|
||||
|
||||
struct Mask {
|
||||
grayscale: bool,
|
||||
background_on_left_edge: bool,
|
||||
sprites_on_left_edge: bool,
|
||||
enable_background: bool,
|
||||
enable_sprites: bool,
|
||||
em_red: bool,
|
||||
em_green: bool,
|
||||
em_blue: bool,
|
||||
}
|
||||
|
||||
pub struct PPU {
|
||||
// registers: PPURegisters,
|
||||
frame_count: usize,
|
||||
nmi_enabled: bool,
|
||||
nmi_waiting: bool,
|
||||
even: bool,
|
||||
scanline: usize,
|
||||
pixel: usize,
|
||||
|
||||
mask: Mask,
|
||||
vblank: bool,
|
||||
|
||||
pub(crate) memory: MemoryMap<PPUMMRegisters>,
|
||||
background: Background,
|
||||
oam: OAM,
|
||||
pub render_buffer: RenderBuffer<256, 240>,
|
||||
pub dbg_int: bool,
|
||||
pub cycle: usize,
|
||||
}
|
||||
|
||||
bitfield::bitfield! {
|
||||
pub struct PPUStatus(u8);
|
||||
impl Debug;
|
||||
vblank, set_vblank: 7;
|
||||
sprite0, set_sprite0: 6;
|
||||
sprite_overflow, set_sprite_overflow: 5;
|
||||
}
|
||||
|
||||
impl std::fmt::Debug for PPU {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
write!(
|
||||
f,
|
||||
"PPU: f {}, s {}, p {}",
|
||||
self.frame_count, self.scanline, self.pixel
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
impl PPU {
|
||||
pub fn with_chr_rom(rom: &[u8]) -> Self {
|
||||
Self {
|
||||
cycle: 25,
|
||||
dbg_int: false,
|
||||
|
||||
mask: Mask {
|
||||
grayscale: false,
|
||||
background_on_left_edge: false,
|
||||
sprites_on_left_edge: false,
|
||||
enable_background: false,
|
||||
enable_sprites: false,
|
||||
em_red: false,
|
||||
em_green: false,
|
||||
em_blue: false,
|
||||
},
|
||||
vblank: false,
|
||||
frame_count: 0,
|
||||
nmi_enabled: false,
|
||||
nmi_waiting: false,
|
||||
even: true, // ??
|
||||
scanline: 0,
|
||||
pixel: 25,
|
||||
render_buffer: RenderBuffer::empty(),
|
||||
memory: MemoryMap::new(vec![
|
||||
Segment::rom("CHR ROM", 0x0000, rom),
|
||||
Segment::ram("Internal VRAM", 0x2000, 0x1000),
|
||||
Segment::mirror("Mirror of VRAM", 0x3000, 0x0F00, 1),
|
||||
Segment::reg("Palette Control", 0x3F00, 0x0100, PPUMMRegisters::Palette),
|
||||
]),
|
||||
background: Background {
|
||||
v: 0,
|
||||
t: 0,
|
||||
x: 0,
|
||||
w: false,
|
||||
vram_column: false,
|
||||
state: BackgroundState::NameTableBytePre,
|
||||
cur_shift_high: 0,
|
||||
cur_shift_low: 0,
|
||||
},
|
||||
oam: OAM {
|
||||
mem: vec![0u8; 256],
|
||||
addr: 0,
|
||||
},
|
||||
}
|
||||
}
|
||||
pub fn reset(&mut self) {
|
||||
*self = Self {
|
||||
memory: std::mem::replace(&mut self.memory, MemoryMap::new(vec![])),
|
||||
..Self::with_chr_rom(&[])
|
||||
};
|
||||
}
|
||||
|
||||
pub fn read_reg(&mut self, offset: u16) -> u8 {
|
||||
match offset {
|
||||
0 => panic!("ppuctrl is write-only"),
|
||||
1 => panic!("ppumask is write-only"),
|
||||
2 => {
|
||||
let tmp = if self.vblank { 0b1000_0000 } else { 0 };
|
||||
self.vblank = false;
|
||||
self.background.w = false;
|
||||
tmp
|
||||
}
|
||||
3 => panic!("oamaddr is write-only"),
|
||||
4 => self.oam.mem[self.oam.addr as usize],
|
||||
5 => panic!("ppuscroll is write-only"),
|
||||
6 => panic!("ppuaddr is write-only"),
|
||||
7 => {
|
||||
let val = self.memory.read(self.background.v).reg_map(|a, _| match a {
|
||||
PPUMMRegisters::Palette => todo!(),
|
||||
});
|
||||
// if self.background
|
||||
self.background.v = self
|
||||
.background
|
||||
.v
|
||||
.wrapping_add(if self.background.vram_column { 32 } else { 1 });
|
||||
val
|
||||
}
|
||||
// 7 => self.registers.data,
|
||||
_ => panic!("No register at {:02X}", offset),
|
||||
}
|
||||
}
|
||||
pub fn write_reg(&mut self, offset: u16, val: u8) {
|
||||
match offset {
|
||||
0x00 => {
|
||||
self.nmi_enabled = val & 0b1000_0000 != 0;
|
||||
self.background.t =
|
||||
(self.background.t & 0b0001_1000_0000_0000) | (((val & 0b11) as u16) << 10);
|
||||
self.background.vram_column = val & 0b0000_0010 != 0;
|
||||
// TODO: other control fields
|
||||
}
|
||||
0x01 => {
|
||||
// self.dbg_int = true;
|
||||
self.mask.grayscale = val & 0b0000_0001 != 0;
|
||||
self.mask.background_on_left_edge = val & 0b0000_0010 != 0;
|
||||
self.mask.sprites_on_left_edge = val & 0b0000_0100 != 0;
|
||||
self.mask.enable_background = val & 0b0000_1000 != 0;
|
||||
self.mask.enable_background = val & 0b0001_0000 != 0;
|
||||
self.mask.em_red = val & 0b0010_0000 != 0;
|
||||
self.mask.em_green = val & 0b0100_0000 != 0;
|
||||
self.mask.em_blue = val & 0b1000_0000 != 0;
|
||||
// todo!("Mask: {:02X}", val)
|
||||
}
|
||||
0x02 => {
|
||||
todo!("Unable to write to PPU status")
|
||||
// TODO: ppu status
|
||||
}
|
||||
0x03 => self.oam.addr = val,
|
||||
0x04 => {
|
||||
self.oam.mem[self.oam.addr as usize] = val;
|
||||
self.oam.addr = self.oam.addr.wrapping_add(1);
|
||||
}
|
||||
0x05 => {
|
||||
if self.background.w {
|
||||
// Is y scroll
|
||||
self.background.t = (self.background.t & 0b0000_1100_0001_1111)
|
||||
| (((val as u16) << 2) & 0b0000_0011_1110_0000)
|
||||
| (((val as u16) << 12) & 0b0111_0000_0000_0000);
|
||||
self.background.w = false;
|
||||
} else {
|
||||
// Is x scroll
|
||||
self.background.x = val & 0b111; // Lowest three bits
|
||||
self.background.t =
|
||||
(self.background.t & 0b0111_1111_1110_0000) | (val as u16 >> 3);
|
||||
self.background.w = true;
|
||||
}
|
||||
}
|
||||
0x06 => {
|
||||
// TODO: ppu addr
|
||||
if self.background.w {
|
||||
self.background.v =
|
||||
u16::from_le_bytes([val, self.background.v.to_le_bytes()[1]]);
|
||||
self.background.w = true;
|
||||
} else {
|
||||
self.background.v =
|
||||
u16::from_le_bytes([self.background.v.to_le_bytes()[0], val & 0b0011_1111]);
|
||||
self.background.w = true;
|
||||
}
|
||||
// todo!("PPUADDR write")
|
||||
}
|
||||
0x07 => {
|
||||
// TODO: ppu data
|
||||
}
|
||||
_ => panic!("No register at {:02X}", offset),
|
||||
}
|
||||
// TODO: use data in PPU
|
||||
}
|
||||
// pub fn write_oamdma(&mut self, val: u8) {
|
||||
// // TODO: OAM high addr
|
||||
// }
|
||||
|
||||
pub fn run_one_clock_cycle(&mut self) -> bool {
|
||||
self.cycle += 1;
|
||||
self.pixel += 1;
|
||||
if self.scanline == 261 && (self.pixel == 341 || (self.pixel == 340 && self.even)) {
|
||||
self.scanline = 0;
|
||||
self.pixel = 0;
|
||||
self.even = !self.even;
|
||||
}
|
||||
if self.pixel == 341 {
|
||||
self.pixel = 0;
|
||||
self.scanline += 1;
|
||||
}
|
||||
if self.mask.enable_background || self.mask.enable_sprites {
|
||||
if self.pixel == 257 {
|
||||
self.background.v = (self.background.v & 0b0111_1011_1110_0000)
|
||||
| (self.background.t & 0b0000_0100_0001_1111);
|
||||
}
|
||||
if self.pixel == 280 && self.scanline == 260 {
|
||||
self.background.v = (self.background.v & 0b0000_0100_0001_1111)
|
||||
| (self.background.t & 0b0111_1011_1110_0000);
|
||||
}
|
||||
if self.scanline < 240 || self.scanline == 261 {
|
||||
// TODO
|
||||
if self.pixel == 0 {
|
||||
// self.dbg_int = true;
|
||||
// idle cycle
|
||||
} else if self.pixel < 257 {
|
||||
// self.dbg_int = true;
|
||||
if self.scanline < 240 {
|
||||
self.render_buffer.write(
|
||||
self.scanline,
|
||||
self.pixel - 1,
|
||||
Color { r: 255, g: 0, b: 0 },
|
||||
); // TODO: this should come from shift registers
|
||||
}
|
||||
self.background.state = match self.background.state {
|
||||
BackgroundState::NameTableByte => {
|
||||
// TODO: Fetch name table byte
|
||||
BackgroundState::AttrTableBytePre
|
||||
}
|
||||
BackgroundState::AttrTableByte => {
|
||||
// TODO: Fetch attr table byte
|
||||
BackgroundState::PatternTableTileLowPre
|
||||
}
|
||||
BackgroundState::PatternTableTileLow => {
|
||||
// TODO: Fetch
|
||||
BackgroundState::PatternTableTileHighPre
|
||||
}
|
||||
BackgroundState::PatternTableTileHigh => {
|
||||
// TODO: Fetch
|
||||
BackgroundState::NameTableBytePre
|
||||
}
|
||||
BackgroundState::NameTableBytePre => BackgroundState::NameTableByte,
|
||||
BackgroundState::AttrTableBytePre => BackgroundState::AttrTableByte,
|
||||
BackgroundState::PatternTableTileLowPre => {
|
||||
BackgroundState::PatternTableTileLow
|
||||
}
|
||||
BackgroundState::PatternTableTileHighPre => {
|
||||
BackgroundState::PatternTableTileHigh
|
||||
}
|
||||
};
|
||||
} else {
|
||||
// TODO: Sprite fetches
|
||||
}
|
||||
}
|
||||
}
|
||||
if self.scanline == 241 && self.pixel == 1 {
|
||||
self.vblank = true;
|
||||
self.nmi_waiting = self.nmi_enabled;
|
||||
self.frame_count += 1;
|
||||
self.background.state = BackgroundState::NameTableBytePre;
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
pub fn nmi_waiting(&mut self) -> bool {
|
||||
if self.nmi_waiting {
|
||||
self.nmi_waiting = false;
|
||||
return true;
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
pub fn irq_waiting(&mut self) -> bool {
|
||||
false
|
||||
}
|
||||
|
||||
pub fn render_name_table<R: Renderer>(&self, frame: &mut Frame<R>) {
|
||||
for y in 0..280 / 8 {
|
||||
for x in 0..512 / 8 {
|
||||
let name = self.memory.peek(x + y * 512 / 8 + 0x2000).unwrap() as u16 * 16;
|
||||
for y_off in 0..8 {
|
||||
let low = self.memory.peek(name + y_off).unwrap();
|
||||
let high = self.memory.peek(name + y_off + 8).unwrap();
|
||||
for bit in (0..8).rev() {
|
||||
frame.fill_rectangle(
|
||||
Point::new(x as f32 * 8. + bit as f32, y as f32 * 8. + y_off as f32),
|
||||
Size::new(1., 1.),
|
||||
match (low & (1 << bit) != 0, high & (1 << bit) != 0) {
|
||||
(false, false) => Color { r: 0, g: 0, b: 0 },
|
||||
(true, false) => Color { r: 64, g: 64, b: 64 },
|
||||
(false, true) => Color { r: 128, g: 128, b: 128 },
|
||||
(true, true) => Color { r: 255, g: 255, b: 255 },
|
||||
}
|
||||
);
|
||||
}
|
||||
}
|
||||
// for
|
||||
// let pat = self.memory.peek();
|
||||
}
|
||||
}
|
||||
}
|
||||
pub fn render_pattern_tables<R: Renderer>(&self, frame: &mut Frame<R>) {
|
||||
for y in 0..16 {
|
||||
for x in 0..16 {
|
||||
let name = (y * 16 + x) * 16;
|
||||
for y_off in 0..8 {
|
||||
let low = self.memory.peek(name + y_off).unwrap();
|
||||
let high = self.memory.peek(name + y_off + 8).unwrap();
|
||||
for bit in 0..8 {
|
||||
frame.fill_rectangle(
|
||||
Point::new(x as f32 * 8. + 8. - bit as f32, y as f32 * 8. + y_off as f32),
|
||||
Size::new(1., 1.),
|
||||
match (low & (1 << bit) != 0, high & (1 << bit) != 0) {
|
||||
(false, false) => Color { r: 0, g: 0, b: 0 },
|
||||
(true, false) => Color { r: 64, g: 64, b: 64 },
|
||||
(false, true) => Color { r: 128, g: 128, b: 128 },
|
||||
(true, true) => Color { r: 255, g: 255, b: 255 },
|
||||
}
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
for y in 0..16 {
|
||||
for x in 0..16 {
|
||||
let name = (y * 16 + x) * 16;
|
||||
for y_off in 0..8 {
|
||||
let low = self.memory.peek(name + y_off + 0x1000).unwrap();
|
||||
let high = self.memory.peek(name + y_off + 8 + 0x1000).unwrap();
|
||||
for bit in 0..8 {
|
||||
frame.fill_rectangle(
|
||||
Point::new(x as f32 * 8. + 8. - bit as f32, y as f32 * 8. + y_off as f32 + 130.),
|
||||
Size::new(1., 1.),
|
||||
match (low & (1 << bit) != 0, high & (1 << bit) != 0) {
|
||||
(false, false) => Color { r: 0, g: 0, b: 0 },
|
||||
(true, false) => Color { r: 64, g: 64, b: 64 },
|
||||
(false, true) => Color { r: 128, g: 128, b: 128 },
|
||||
(true, true) => Color { r: 255, g: 255, b: 255 },
|
||||
}
|
||||
);
|
||||
}
|
||||
}
|
||||
// for
|
||||
// let pat = self.memory.peek();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn ppu_registers() {
|
||||
let mut ppu = PPU::with_chr_rom(&[0u8; 8192]);
|
||||
assert_eq!(ppu.background.v, 0);
|
||||
assert_eq!(ppu.background.t, 0);
|
||||
assert_eq!(ppu.background.x, 0);
|
||||
assert_eq!(ppu.background.w, false);
|
||||
ppu.write_reg(0, 0);
|
||||
assert_eq!(ppu.background.v, 0);
|
||||
ppu.write_reg(0, 0b11);
|
||||
assert_eq!(
|
||||
ppu.background.t, 0b0001100_00000000,
|
||||
"Actual: {:016b}",
|
||||
ppu.background.t
|
||||
);
|
||||
assert_eq!(ppu.background.w, false);
|
||||
|
||||
ppu.write_reg(5, 0x7D);
|
||||
assert_eq!(
|
||||
ppu.background.t, 0b0001100_00001111,
|
||||
"Actual: {:016b}",
|
||||
ppu.background.t
|
||||
);
|
||||
assert_eq!(ppu.background.x, 0b101);
|
||||
assert_eq!(ppu.background.w, true);
|
||||
ppu.write_reg(5, 0x5E);
|
||||
assert_eq!(
|
||||
ppu.background.t, 0b1101101_01101111,
|
||||
"Actual: {:016b}",
|
||||
ppu.background.t
|
||||
);
|
||||
assert_eq!(ppu.background.x, 0b101);
|
||||
assert_eq!(ppu.background.w, false);
|
||||
|
||||
ppu.write_reg(5, 0x7D);
|
||||
assert_eq!(
|
||||
ppu.background.t, 0b1101101_01101111,
|
||||
"Actual: {:016b}",
|
||||
ppu.background.t
|
||||
);
|
||||
assert_eq!(ppu.background.x, 0b101);
|
||||
assert_eq!(ppu.background.w, true);
|
||||
ppu.read_reg(2);
|
||||
assert_eq!(ppu.background.w, false);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user