//! 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_buffer ppu_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); }