const std = @import("std"); const contract = @import("contract"); const video = contract.video; const audio = contract.audio; const input = contract.input; const storage = contract.storage; const state = contract.state; const _cpu = @import("cpu"); pub const common = @import("common.zig"); pub const Region = common.Region; pub const Mirroring = common.Mirroring; pub const Button = common.Button; pub const screen_width = common.screen_width; pub const screen_height = common.screen_height; pub const ppu = @import("ppu/root.zig"); pub const apu = @import("apu/root.zig"); pub const bus_mod = @import("bus.zig"); pub const Bus = bus_mod.Bus; pub const cartridge_mod = @import("cartridge.zig"); pub const Cartridge = cartridge_mod.Cartridge; pub const controller_mod = @import("controller.zig"); pub const Controller = controller_mod.Controller; pub const mapper_mod = @import("mapper/root.zig"); const Cpu = _cpu.m6502.Cpu(Bus, .ricoh2a03, .cycle); pub const Nes = @This(); pub const default_palette_565: [64]u16 = blk: { var table: [64]u16 = undefined; for (ppu.default_palette, 0..) |rgba, i| { const r = @as(u16, @truncate((rgba >> 24) & 0xFF)); const g = @as(u16, @truncate((rgba >> 16) & 0xFF)); const b = @as(u16, @truncate((rgba >> 8) & 0xFF)); const r5 = r >> 3; const g6 = g >> 2; const b5 = b >> 3; table[i] = (r5 << 11) | (g6 << 5) | b5; } break :blk table; }; pub const spec: contract.SystemSpec = .{ .name = "Nintendo Entertainment System", .video_outputs = &.{ .{ .max_width = common.screen_width, .max_height = common.screen_height, .format = .rgb565, .aspect_ratio = .{ .numerator = 4, .denominator = 3, }, .refresh_rate = .{ .numerator = 60, .denominator = 1, }, }, }, .audio_outputs = &.{ .{ .sample_rate = 44_100, .channels = 1, .format = .i16, }, }, .input_devices = &.{ .{ .button_count = 8 }, .{ .button_count = 8 }, }, .storage_devices = &.{ .{ .name = "Battery PRG RAM", .min_size = 0, .max_size = 8192, .persistent = true, .removable = false, .writable = true, }, }, .save_state = .{ .max_size = 64 * 1024, }, }; bus: Bus = undefined, cpu: Cpu = undefined, rgb565_framebuffer: [common.screen_width * common.screen_height]u16 = [_]u16{0} ** (common.screen_width * common.screen_height), pub fn init(self: *Nes, rom_bytes: []const u8) !void { const cart = try Cartridge.init(rom_bytes); self.bus = Bus.init(.ntsc, cart); self.cpu = Cpu.init(&self.bus); self.cpu.reset(); } pub fn reset(self: *Nes) void { self.cpu.bus = &self.bus; self.bus.reset(); self.cpu.reset(); } pub fn runFrame(self: *Nes) void { self.bus.ppu.frame_complete = false; self.bus.apu.clearSamples(); while (!self.bus.ppu.frame_complete) { if (self.bus.dma_active) { self.bus.stepDma(); } else { _ = self.cpu.step(); } } // Convert PPU framebuffer to RGB565 for frontend driver for (self.bus.ppu.framebuffer, 0..) |color_idx, i| { self.rgb565_framebuffer[i] = default_palette_565[color_idx & 0x3f]; } } pub fn videoFrame(self: *const Nes, _: usize) video.VideoFrame { return .{ .data = std.mem.sliceAsBytes(&self.rgb565_framebuffer), .width = common.screen_width, .height = common.screen_height, .pitch = common.screen_width * 2, .format = .rgb565, .frame_number = self.bus.ppu.frame_count, }; } pub fn audioBuffer(self: *const Nes, _: usize) audio.Buffer { const samples = self.bus.apu.getSamples(); return .{ .data = std.mem.sliceAsBytes(samples), .frames = samples.len, .sample_rate = 44_100, .channels = 1, .format = .i16, }; } pub fn setInput(self: *Nes, device_index: usize, device_input: input.DeviceInput) void { if (device_index < 2) { self.bus.controllers[device_index].setButtons(@truncate(device_input.buttons)); } } pub fn storageView(self: *const Nes, index: usize) storage.View { if (index == 0 and self.bus.cartridge != null) { return .{ .data = &self.bus.cartridge.?.prg_ram, .generation = 0, }; } return .{ .data = &.{}, .generation = 0 }; } pub fn loadStorage(self: *Nes, index: usize, data: []const u8) storage.LoadError!void { if (index != 0 or self.bus.cartridge == null) return error.InvalidSlot; if (data.len > self.bus.cartridge.?.prg_ram.len) return error.InvalidSize; @memcpy(self.bus.cartridge.?.prg_ram[0..data.len], data); } pub fn saveState(self: *const Nes, dest: []u8) state.Error!usize { const required_size: usize = 8 + 7 + 0x800 + 0x800 + 256 + 32 + 0x2000; if (dest.len < required_size) return error.BufferTooSmall; var offset: usize = 0; @memcpy(dest[offset .. offset + 8], "NESSTATE"); offset += 8; const regs = self.cpu.registers; dest[offset] = regs.a; dest[offset + 1] = regs.x; dest[offset + 2] = regs.y; dest[offset + 3] = regs.sp; dest[offset + 4] = @bitCast(regs.status); dest[offset + 5] = @truncate(regs.pc); dest[offset + 6] = @truncate(regs.pc >> 8); offset += 7; @memcpy(dest[offset .. offset + 0x800], &self.bus.ram); offset += 0x800; @memcpy(dest[offset .. offset + 0x800], &self.bus.ciram); offset += 0x800; @memcpy(dest[offset .. offset + 256], &self.bus.ppu.registers.oam); offset += 256; @memcpy(dest[offset .. offset + 32], &self.bus.ppu.palette.ram); offset += 32; if (self.bus.cartridge) |*c| { @memcpy(dest[offset .. offset + 0x2000], &c.prg_ram); offset += 0x2000; } return offset; } pub fn loadState(self: *Nes, src: []const u8) state.Error!void { if (src.len < 8 or !std.mem.eql(u8, src[0..8], "NESSTATE")) { return error.InvalidState; } var offset: usize = 8; self.cpu.registers.a = src[offset]; self.cpu.registers.x = src[offset + 1]; self.cpu.registers.y = src[offset + 2]; self.cpu.registers.sp = src[offset + 3]; self.cpu.registers.status = @bitCast(src[offset + 4]); self.cpu.registers.pc = @as(u16, src[offset + 5]) | (@as(u16, src[offset + 6]) << 8); offset += 7; @memcpy(&self.bus.ram, src[offset .. offset + 0x800]); offset += 0x800; @memcpy(&self.bus.ciram, src[offset .. offset + 0x800]); offset += 0x800; @memcpy(&self.bus.ppu.registers.oam, src[offset .. offset + 256]); offset += 256; @memcpy(&self.bus.ppu.palette.ram, src[offset .. offset + 32]); offset += 32; if (self.bus.cartridge) |*c| { if (src.len >= offset + 0x2000) { @memcpy(&c.prg_ram, src[offset .. offset + 0x2000]); offset += 0x2000; } } } test { contract.system.validate(Nes); _ = common; _ = ppu; _ = apu; _ = bus_mod; _ = cartridge_mod; _ = controller_mod; _ = mapper_mod; _ = conformance; } pub const conformance = @import("conformance/root.zig");