Zesty - a pin-accurate, cycle-accurate NES emulator written in Zig
Something went wrong. Try again.
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177//! The NES's 2C02 Picture Processing Unit (PPU).const Ppu = @This();
const std = @import("std");const zesty = @import("zesty.zig");
oam: [0x100]u8 = @splat(0xaa),palette: [0x20]u8 = @splat(0xaa),
//------------------------------------------// Registers
/// If false, then this is the first write./// Cleared by reading `status`first_write: bool = false,
ctrl: Ctrl = .init,mask: Mask = .init,status: Status = .init,oam_addr: u8 = 0,oam_data: u8 = 0,
scroll_x: u8 = 0,scroll_y: u8 = 0,
/// PPUDATA read buffer./// See https://www.nesdev.org/wiki/PPU_registers#The_PPUDATA_read_bufferppu_data_buf: u8 = 0,
pub const Ctrl = packed struct(u8) { scroll: u2, increment: enum(u1) { horizontal, vertical }, spr_tile_select: bool, bgr_tile_select: bool, sprite_height: bool, master_slave: bool, nmi_enable: bool,
pub const init: Ctrl = .{ .scroll = 0, .increment = .horizontal, .spr_tile_select = false, .bgr_tile_select = false, .sprite_height = false, .master_slave = false, .nmi_enable = false, };};pub const Mask = packed struct(u8) { grayscale: bool, bgr_left_col_enable: bool, spr_left_col_enable: bool, bgr_enable: bool, spr_enable: bool, emphasize_red: bool, emphasize_green: bool, emphasize_blue: bool,
pub const init: Mask = .{ .grayscale = false, .bgr_left_col_enable = false, .spr_left_col_enable = false, .bgr_enable = false, .spr_enable = false, .emphasize_red = false, .emphasize_green = false, .emphasize_blue = false, };};pub const Status = packed struct(u8) { _open_bus: u5, spr_overflow: bool, spr_0_hit: bool, vblank: bool,
pub const init: Status = .{ ._open_bus = 0, .spr_overflow = true, .spr_0_hit = false, .vblank = true, };};
pub const Oam = packed struct { y: u8, tile: u8, attr: u8, x: u8,};
pub fn tick(self: *Ppu, pins: *zesty.Pins) void { if (pins.ppuChipSelect()) self.updateRegisters(pins);}
/// Refresh the CPU-side address and data pins/// from contents of registers.fn updateRegisters(self: *Ppu, pins: *zesty.Pins) void { switch (pins.cpu_addr & 0x7) { // PPUCTRL (write only) 0 => switch (pins.cpu_rw) { .read => {}, .write => self.ctrl = @bitCast(pins.cpu_data), }, // PPUMASK (write only) 1 => switch (pins.cpu_rw) { .read => {}, .write => self.mask = @bitCast(pins.cpu_data), }, // PPUSTATUS (read only) 2 => switch (pins.cpu_rw) { .read => { // TODO: Add 2C05 identifier support self.status._open_bus = @truncate(pins.cpu_data); // Side-effect: clear first write flag self.first_write = false; pins.cpu_data = @bitCast(self.status); }, .write => {}, }, // OAMADDR (write only) 3 => switch (pins.cpu_rw) { .read => {}, .write => self.oam_addr = pins.cpu_data, }, // TODO: OAMDATA (read/write) 4 => {}, // PPUSCROLL (write only) 5 => switch (pins.cpu_rw) { .read => {}, .write => { if (self.first_write) self.scroll_x = pins.cpu_data else self.scroll_y = pins.cpu_data; self.first_write = !self.first_write; }, }, // PPUADDR (write only) 6 => switch (pins.cpu_rw) { .read => {}, .write => { if (self.first_write) pins.ppu_addr_hi = @truncate(pins.cpu_data) else pins.ppu_addr_data = pins.cpu_data; self.first_write = !self.first_write; }, }, // PPUDATA (read/write) 7 => { switch (pins.cpu_rw) { .read => { // Use the cached PPUDATA pins.cpu_data = self.ppu_data_buf; pins.ppu_rd = true; pins.ppu_wr = true; }, .write => { pins.ppu_addr_data = pins.cpu_data; pins.ppu_wr = true; pins.ppu_rd = false; }, } // TODO: increment // self.pins.addr_data +%= switch (self.ppu.ctrl.increment) { // .horizontal => 1, // .vertical => 32, // }; }, else => unreachable, }}
pub fn oamSlice(self: *Ppu) [64]Oam { return std.mem.bytesAsSlice(self.oam);}