Remove nes implementation so that we can implement it in a better way from scratch.

This commit is contained in:
2026-08-27 00:04:59 +02:00
parent 60a6364625
commit 425ebe0938
47 changed files with 0 additions and 7116 deletions
-276
View File
@@ -1,276 +0,0 @@
const std = @import("std");
const Apu = @This();
pub const sample_rate: u32 = 44_100;
sample_buffer: [2048]i16 = [_]i16{0} ** 2048,
sample_count: usize = 0,
cycle_counter: u32 = 0,
// Pulse 1
pulse1_enabled: bool = false,
pulse1_timer_reload: u11 = 0,
pulse1_timer: u11 = 0,
pulse1_duty: u2 = 0,
pulse1_duty_pos: u3 = 0,
pulse1_volume: u4 = 0,
// Pulse 2
pulse2_enabled: bool = false,
pulse2_timer_reload: u11 = 0,
pulse2_timer: u11 = 0,
pulse2_duty: u2 = 0,
pulse2_duty_pos: u3 = 0,
pulse2_volume: u4 = 0,
// Triangle
triangle_enabled: bool = false,
triangle_timer_reload: u11 = 0,
triangle_timer: u11 = 0,
triangle_seq_pos: u5 = 0,
// Noise
noise_enabled: bool = false,
noise_timer_reload: u12 = 0,
noise_timer: u12 = 0,
noise_lfsr: u16 = 1,
noise_volume: u4 = 0,
// DMC
dmc_enabled: bool = false,
dmc_loop: bool = false,
dmc_rate_index: u4 = 0,
dmc_timer: u12 = 0,
dmc_timer_reload: u12 = 0,
dmc_output_level: u7 = 0,
dmc_sample_address: u16 = 0xC000,
dmc_sample_length: u16 = 1,
dmc_current_address: u16 = 0xC000,
dmc_bytes_remaining: u16 = 0,
dmc_shift_register: u8 = 0,
dmc_bits_remaining: u4 = 0,
dmc_silence: bool = true,
const duty_tables: [4][8]u8 = .{
.{ 0, 1, 0, 0, 0, 0, 0, 0 }, // 12.5%
.{ 0, 1, 1, 0, 0, 0, 0, 0 }, // 25%
.{ 0, 1, 1, 1, 1, 0, 0, 0 }, // 50%
.{ 1, 0, 0, 1, 1, 1, 1, 1 }, // 75%
};
const triangle_table: [32]u4 = .{
15, 14, 13, 12, 11, 10, 9, 8, 7, 6, 5, 4, 3, 2, 1, 0,
0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15,
};
const noise_periods: [16]u12 = .{
4, 8, 16, 32, 64, 96, 128, 160, 202, 254, 380, 508, 762, 1016, 2034, 4068,
};
const dmc_periods: [16]u12 = .{
428, 380, 340, 320, 286, 254, 226, 214, 190, 160, 142, 128, 106, 84, 72, 54,
};
pub fn reset(self: *Apu) void {
self.sample_count = 0;
self.cycle_counter = 0;
self.pulse1_enabled = false;
self.pulse2_enabled = false;
self.triangle_enabled = false;
self.noise_enabled = false;
self.noise_lfsr = 1;
self.dmc_enabled = false;
self.dmc_output_level = 0;
}
pub inline fn tick(self: *Apu) void {
// Pulse 1 timer
if (self.pulse1_timer == 0) {
self.pulse1_timer = self.pulse1_timer_reload;
self.pulse1_duty_pos +%= 1;
} else {
self.pulse1_timer -= 1;
}
// Pulse 2 timer
if (self.pulse2_timer == 0) {
self.pulse2_timer = self.pulse2_timer_reload;
self.pulse2_duty_pos +%= 1;
} else {
self.pulse2_timer -= 1;
}
// Triangle timer
if (self.triangle_timer == 0) {
self.triangle_timer = self.triangle_timer_reload;
self.triangle_seq_pos +%= 1;
} else {
self.triangle_timer -= 1;
}
// Noise timer
if (self.noise_timer == 0) {
self.noise_timer = self.noise_timer_reload;
const feedback = (self.noise_lfsr & 1) ^ ((self.noise_lfsr >> 1) & 1);
self.noise_lfsr = (self.noise_lfsr >> 1) | (feedback << 14);
} else {
self.noise_timer -= 1;
}
// DMC timer
if (self.dmc_timer == 0) {
self.dmc_timer = self.dmc_timer_reload;
if (!self.dmc_silence) {
if ((self.dmc_shift_register & 1) != 0) {
if (self.dmc_output_level <= 125) self.dmc_output_level += 2;
} else {
if (self.dmc_output_level >= 2) self.dmc_output_level -= 2;
}
self.dmc_shift_register >>= 1;
}
if (self.dmc_bits_remaining > 0) {
self.dmc_bits_remaining -= 1;
}
if (self.dmc_bits_remaining == 0) {
self.dmc_silence = true;
}
} else {
self.dmc_timer -= 1;
}
// Sample downsampling at ~44.1 kHz (every ~40 CPU cycles)
self.cycle_counter += 1;
if (self.cycle_counter >= 40) {
self.cycle_counter = 0;
if (self.sample_count < self.sample_buffer.len) {
const p1_val: i32 = if (self.pulse1_enabled and duty_tables[self.pulse1_duty][self.pulse1_duty_pos] != 0 and self.pulse1_timer_reload > 8)
@as(i32, self.pulse1_volume)
else
0;
const p2_val: i32 = if (self.pulse2_enabled and duty_tables[self.pulse2_duty][self.pulse2_duty_pos] != 0 and self.pulse2_timer_reload > 8)
@as(i32, self.pulse2_volume)
else
0;
const tri_val: i32 = if (self.triangle_enabled and self.triangle_timer_reload > 2)
@as(i32, triangle_table[self.triangle_seq_pos])
else
0;
const noise_val: i32 = if (self.noise_enabled and (self.noise_lfsr & 1) == 0)
@as(i32, self.noise_volume)
else
0;
const dmc_val: i32 = if (self.dmc_enabled) @as(i32, self.dmc_output_level) else 0;
const sample_i32 = (p1_val + p2_val) * 400 + tri_val * 350 + noise_val * 300 + dmc_val * 150;
self.sample_buffer[self.sample_count] = @intCast(std.math.clamp(sample_i32, -32000, 32000));
self.sample_count += 1;
}
}
}
pub fn write(self: *Apu, address: u16, value: u8) void {
switch (address) {
// Pulse 1
0x4000 => {
self.pulse1_duty = @truncate((value >> 6) & 3);
self.pulse1_volume = @truncate(value & 0x0f);
},
0x4002 => {
self.pulse1_timer_reload = (self.pulse1_timer_reload & 0x0700) | value;
},
0x4003 => {
self.pulse1_timer_reload = (self.pulse1_timer_reload & 0x00ff) | (@as(u11, value & 7) << 8);
self.pulse1_duty_pos = 0;
},
// Pulse 2
0x4004 => {
self.pulse2_duty = @truncate((value >> 6) & 3);
self.pulse2_volume = @truncate(value & 0x0f);
},
0x4006 => {
self.pulse2_timer_reload = (self.pulse2_timer_reload & 0x0700) | value;
},
0x4007 => {
self.pulse2_timer_reload = (self.pulse2_timer_reload & 0x00ff) | (@as(u11, value & 7) << 8);
self.pulse2_duty_pos = 0;
},
// Triangle
0x400a => {
self.triangle_timer_reload = (self.triangle_timer_reload & 0x0700) | value;
},
0x400b => {
self.triangle_timer_reload = (self.triangle_timer_reload & 0x00ff) | (@as(u11, value & 7) << 8);
},
// Noise
0x400c => {
self.noise_volume = @truncate(value & 0x0f);
},
0x400e => {
self.noise_timer_reload = noise_periods[value & 0x0f];
},
// DMC
0x4010 => {
self.dmc_loop = (value & 0x40) != 0;
self.dmc_rate_index = @truncate(value & 0x0f);
self.dmc_timer_reload = dmc_periods[self.dmc_rate_index];
},
0x4011 => {
self.dmc_output_level = @truncate(value & 0x7f);
},
0x4012 => {
self.dmc_sample_address = 0xC000 + (@as(u16, value) << 6);
},
0x4013 => {
self.dmc_sample_length = (@as(u16, value) << 4) + 1;
},
else => {},
}
}
pub fn readStatus(_: *Apu) u8 {
return 0;
}
pub fn writeStatus(self: *Apu, value: u8) void {
self.pulse1_enabled = (value & 0x01) != 0;
self.pulse2_enabled = (value & 0x02) != 0;
self.triangle_enabled = (value & 0x04) != 0;
self.noise_enabled = (value & 0x08) != 0;
self.dmc_enabled = (value & 0x10) != 0;
}
pub fn writeFrameCounter(_: *Apu, _: u8) void {}
pub fn irqAsserted(_: *const Apu) bool {
return false;
}
test "NES APU - Sound Synthesis & Sample Generation" {
var apu = Apu{};
apu.reset();
// Enable Pulse 1 and Pulse 2
apu.writeStatus(0x03);
// Setup Pulse 1 volume and frequency
apu.write(0x4000, 0xbf); // 50% duty, max volume 15
apu.write(0x4002, 100);
apu.write(0x4003, 0);
// Tick APU for 100 cycles to produce PCM samples
for (0..100) |_| {
apu.tick();
}
try std.testing.expect(apu.sample_count > 0);
}
-157
View File
@@ -1,157 +0,0 @@
const Cartridge = @import("cartridge.zig");
const Controller = @import("controller.zig");
const Ppu = @import("ppu.zig");
const Apu = @import("apu.zig");
const Bus = @This();
ram: [0x800]u8 =
[_]u8{0} ** 0x800,
cartridge: Cartridge,
ppu: Ppu = .{},
apu: Apu = .{},
controllers: [2]Controller = .{
.{},
.{},
},
open_bus: u8 = 0,
oam_dma_page: ?u8 = null,
pub fn init(
rom: []const u8,
) Cartridge.Error!Bus {
return .{
.cartridge = try Cartridge.init(rom),
};
}
pub fn reset(
self: *Bus,
) void {
self.ppu.reset();
self.apu.reset();
for (&self.controllers) |*controller|
controller.reset();
self.oam_dma_page = null;
}
pub inline fn read(
self: *Bus,
address: u16,
) u8 {
const value: u8 = switch (address) {
// 2 KiB internal RAM + mirrors.
0x0000...0x1fff => self.ram[address & 0x07ff],
// PPU registers + mirrors.
0x2000...0x3fff => self.ppu.cpuRead(
&self.cartridge,
address & 7,
),
// APU status.
0x4015 => self.apu.readStatus(),
// Controller 1.
0x4016 => self.controllers[0].read(),
// Controller 2.
0x4017 => self.controllers[1].read(),
// Cartridge space.
0x4020...0xffff => self.cartridge.cpuRead(address),
else => self.open_bus,
};
self.open_bus = value;
return value;
}
pub inline fn write(
self: *Bus,
address: u16,
value: u8,
) void {
self.open_bus = value;
switch (address) {
0x0000...0x1fff => self.ram[address & 0x07ff] =
value,
0x2000...0x3fff => self.ppu.cpuWrite(
&self.cartridge,
address & 7,
value,
),
0x4000...0x4013 => self.apu.write(
address,
value,
),
// OAMDMA
0x4014 => self.oam_dma_page = value,
0x4015 => self.apu.writeStatus(value),
0x4016 => {
self.controllers[0].write(value);
self.controllers[1].write(value);
},
0x4017 => self.apu.writeFrameCounter(value),
else => self.cartridge.cpuWrite(
address,
value,
),
}
}
/// Called exactly once for every CPU cycle.
pub inline fn tick(
self: *Bus,
) void {
self.apu.tick();
// NTSC NES: PPU runs at 3× CPU clock.
self.ppu.tick(&self.cartridge);
self.ppu.tick(&self.cartridge);
self.ppu.tick(&self.cartridge);
}
pub fn nmiLine(
self: *Bus,
) bool {
return self.ppu.nmiAsserted();
}
pub fn irqLine(
self: *Bus,
) bool {
return self.apu.irqAsserted() or
self.cartridge.irqAsserted();
}
pub fn takeOamDmaPage(
self: *Bus,
) ?u8 {
const page =
self.oam_dma_page;
self.oam_dma_page = null;
return page;
}
File diff suppressed because it is too large Load Diff
-345
View File
@@ -1,345 +0,0 @@
// minimal system test runner for nes-test-roms & NES system contract conformance
const std = @import("std");
const testing = std.testing;
const build_options = @import("build_options");
const contract = @import("contract");
const Nes = @import("root.zig");
fn loadRomAlloc(allocator: std.mem.Allocator, relative_path: []const u8) ![]u8 {
const base_dir = build_options.nes_test_roms_dir orelse return error.TestRomsNotFetched;
var dir = try std.Io.Dir.openDirAbsolute(testing.io, base_dir, .{});
defer dir.close(testing.io);
return dir.readFileAlloc(testing.io, relative_path, allocator, @enumFromInt(10 * 1024 * 1024));
}
/// Runs a blargg test ROM on the full NES system implementation.
/// Blargg test ROMs signal completion via $6000 (0x80 = running, 0x00 = passed)
/// and output result text starting at $6004.
pub fn runBlarggTest(relative_path: []const u8, max_instructions: u64) !void {
const rom_bytes = try loadRomAlloc(testing.allocator, relative_path);
defer testing.allocator.free(rom_bytes);
var nes: Nes = undefined;
try nes.init(rom_bytes);
var i: u64 = 0;
var result_ready = false;
while (i < max_instructions) : (i += 1) {
_ = nes.step() orelse break;
const status = nes.bus.read(0x6000);
if (status != 0x80) {
const sig0 = nes.bus.read(0x6001);
const sig1 = nes.bus.read(0x6002);
const sig2 = nes.bus.read(0x6003);
if (sig0 == 0xDE and sig1 == 0xB0 and sig2 == 0x61) {
result_ready = true;
break;
}
}
}
const status = nes.bus.read(0x6000);
var msg_buf: [256]u8 = undefined;
var msg_len: usize = 0;
while (msg_len < msg_buf.len) : (msg_len += 1) {
const char = nes.bus.read(@intCast(0x6004 + msg_len));
if (char == 0) break;
msg_buf[msg_len] = char;
}
const msg = msg_buf[0..msg_len];
if (!result_ready and status == 0x80) {
std.debug.print(
"\n[TIMEOUT] Test '{s}' timed out after {d} instructions. Text output: {s}\n",
.{ relative_path, i, msg },
);
return error.TestTimeout;
}
if (status != 0) {
std.debug.print(
"\n[FAIL] Test '{s}' failed with status 0x{X:0>2}: {s}\n",
.{ relative_path, status, msg },
);
return error.TestFailed;
}
}
/// Runs Kevtris' nestest.nes ROM on full NES system (PC = $C000).
pub fn runNesTestSystem(relative_path: []const u8, max_instructions: u64) !void {
const rom_bytes = try loadRomAlloc(testing.allocator, relative_path);
defer testing.allocator.free(rom_bytes);
var nes: Nes = undefined;
try nes.init(rom_bytes);
nes.cpu.registers.pc = 0xC000;
var previous_pc: u16 = 0;
var i: u64 = 0;
while (i < max_instructions) : (i += 1) {
previous_pc = nes.cpu.registers.pc;
_ = nes.step() orelse break;
if (nes.cpu.registers.pc == previous_pc) break;
}
const err1 = nes.bus.read(0x0002);
const err2 = nes.bus.read(0x0003);
if (err1 != 0 or err2 != 0) {
std.debug.print(
"\n[FAIL] Nestest system test failed with error codes $0002: 0x{X:0>2}, $0003: 0x{X:0>2} at PC 0x{X:0>4}\n",
.{ err1, err2, nes.cpu.registers.pc },
);
return error.NestestFailed;
}
}
test "NES System Conformance - Contract SystemSpec" {
comptime contract.validateSystem(Nes);
try testing.expectEqualStrings("Nintendo Entertainment System", Nes.spec.name);
try testing.expectEqual(@as(usize, 1), Nes.spec.video_outputs.len);
try testing.expectEqual(@as(usize, 1), Nes.spec.audio_outputs.len);
try testing.expectEqual(@as(usize, 2), Nes.spec.input_devices.len);
try testing.expectEqual(@as(usize, 1), Nes.spec.storage_devices.len);
}
test "NES System Conformance - Video Frame Output" {
var rom: [16 + 0x4000 + 0x2000]u8 = [_]u8{0} ** (16 + 0x4000 + 0x2000);
@memcpy(rom[0..4], "NES\x1a");
rom[4] = 1;
rom[5] = 1;
var nes: Nes = undefined;
try nes.init(&rom);
nes.runFrame();
const frame = nes.videoFrame(0);
try testing.expectEqual(@as(u32, 256), frame.width);
try testing.expectEqual(@as(u32, 240), frame.height);
try testing.expectEqual(contract.PixelFormat.rgb565, frame.format);
try testing.expectEqual(256 * 240 * 2, frame.data.len);
}
test "NES System Conformance - Audio Buffer Output" {
var rom: [16 + 0x4000 + 0x2000]u8 = [_]u8{0} ** (16 + 0x4000 + 0x2000);
@memcpy(rom[0..4], "NES\x1a");
rom[4] = 1;
rom[5] = 1;
var nes: Nes = undefined;
try nes.init(&rom);
nes.runFrame();
const audio_buf = nes.audioBuffer(0);
try testing.expectEqual(contract.AudioSampleFormat.i16, audio_buf.format);
try testing.expectEqual(@as(u8, 1), audio_buf.channels);
try testing.expect(audio_buf.data.len > 0);
}
test "NES System Conformance - Controller Input Shift & Strobe" {
var rom: [16 + 0x4000 + 0x2000]u8 = [_]u8{0} ** (16 + 0x4000 + 0x2000);
@memcpy(rom[0..4], "NES\x1a");
rom[4] = 1;
rom[5] = 1;
var nes: Nes = undefined;
try nes.init(&rom);
// Press Button A (bit 0) and Button Select (bit 2) -> 0b00000101
nes.setInput(0, .{ .buttons = 0x05 });
// Strobe controller
nes.bus.write(0x4016, 1);
nes.bus.write(0x4016, 0);
// Read serial bits
try testing.expectEqual(@as(u8, 1), nes.bus.read(0x4016) & 1); // Button A (pressed)
try testing.expectEqual(@as(u8, 0), nes.bus.read(0x4016) & 1); // Button B
try testing.expectEqual(@as(u8, 1), nes.bus.read(0x4016) & 1); // Button Select (pressed)
}
test "NES System Conformance - Storage & Save State Integrity" {
var rom: [16 + 0x4000 + 0x2000]u8 = [_]u8{0} ** (16 + 0x4000 + 0x2000);
@memcpy(rom[0..4], "NES\x1a");
rom[4] = 1;
rom[5] = 1;
var nes: Nes = undefined;
try nes.init(&rom);
// Storage SRAM test
const sram_data = [_]u8{ 0xDE, 0xAD, 0xBE, 0xEF };
try nes.loadStorage(0, &sram_data);
const view = nes.storageView(0);
try testing.expectEqualSlices(u8, &sram_data, view.data[0..4]);
// Save state test
var save_buf: [64 * 1024]u8 = undefined;
const written = try nes.saveState(&save_buf);
nes.reset();
try nes.loadState(save_buf[0..written]);
const view_after = nes.storageView(0);
try testing.expectEqualSlices(u8, &sram_data, view_after.data[0..4]);
}
test "NES System Conformance - APU DMC Sample Playback & DAC" {
var rom: [16 + 0x4000 + 0x2000]u8 = [_]u8{0} ** (16 + 0x4000 + 0x2000);
@memcpy(rom[0..4], "NES\x1a");
rom[4] = 1;
rom[5] = 1;
var nes: Nes = undefined;
try nes.init(&rom);
// Enable DMC in APU status ($4015 bit 4)
nes.bus.write(0x4015, 0x10);
// Write DMC output level ($4011) to 64
nes.bus.write(0x4011, 64);
// Setup rate ($4010)
nes.bus.write(0x4010, 0x05);
nes.runFrame();
const audio_buf = nes.audioBuffer(0);
try testing.expect(audio_buf.data.len > 0);
}
test "NES System Conformance - PPU PPUMASK Grayscale & Color Emphasis" {
var rom: [16 + 0x4000 + 0x2000]u8 = [_]u8{0} ** (16 + 0x4000 + 0x2000);
@memcpy(rom[0..4], "NES\x1a");
rom[4] = 1;
rom[5] = 1;
var nes: Nes = undefined;
try nes.init(&rom);
// Enable Grayscale (bit 0) and Red/Green/Blue Emphasis (bits 5,6,7 -> 0xE1) in PPUMASK ($2001)
nes.bus.write(0x2001, 0xE1);
nes.runFrame();
const frame = nes.videoFrame(0);
try testing.expectEqual(256 * 240 * 2, frame.data.len);
}
test "NES System Conformance - All Mappers iNES Header Support" {
const mappers = [_]struct { id: u8, prg_banks: u8, chr_banks: u8 }{
.{ .id = 0, .prg_banks = 1, .chr_banks = 1 }, // NROM
.{ .id = 1, .prg_banks = 8, .chr_banks = 8 }, // MMC1
.{ .id = 2, .prg_banks = 8, .chr_banks = 0 }, // UxROM
.{ .id = 3, .prg_banks = 2, .chr_banks = 4 }, // CNROM
.{ .id = 4, .prg_banks = 16, .chr_banks = 16 }, // MMC3
.{ .id = 5, .prg_banks = 16, .chr_banks = 16 }, // MMC5
.{ .id = 7, .prg_banks = 8, .chr_banks = 0 }, // AxROM
.{ .id = 9, .prg_banks = 8, .chr_banks = 16 }, // MMC2
.{ .id = 10, .prg_banks = 8, .chr_banks = 16 }, // MMC4
.{ .id = 11, .prg_banks = 4, .chr_banks = 16 }, // Color Dreams
.{ .id = 13, .prg_banks = 2, .chr_banks = 0 }, // CPROM
.{ .id = 21, .prg_banks = 8, .chr_banks = 16 }, // Konami VRC4a/c
.{ .id = 22, .prg_banks = 8, .chr_banks = 16 }, // Konami VRC2a
.{ .id = 23, .prg_banks = 8, .chr_banks = 16 }, // Konami VRC2b/VRC4e
.{ .id = 24, .prg_banks = 16, .chr_banks = 16 }, // Konami VRC6a
.{ .id = 25, .prg_banks = 8, .chr_banks = 16 }, // Konami VRC4b/d
.{ .id = 26, .prg_banks = 16, .chr_banks = 16 }, // Konami VRC6b
.{ .id = 34, .prg_banks = 8, .chr_banks = 8 }, // BNROM / NINA-01
.{ .id = 65, .prg_banks = 8, .chr_banks = 16 }, // Irem H3001
.{ .id = 66, .prg_banks = 4, .chr_banks = 4 }, // GxROM
.{ .id = 68, .prg_banks = 8, .chr_banks = 16 }, // Sunsoft-4
.{ .id = 69, .prg_banks = 8, .chr_banks = 16 }, // Sunsoft FME-7
.{ .id = 71, .prg_banks = 8, .chr_banks = 0 }, // Camerica
.{ .id = 73, .prg_banks = 8, .chr_banks = 0 }, // Konami VRC3
.{ .id = 75, .prg_banks = 8, .chr_banks = 16 }, // Konami VRC1
.{ .id = 76, .prg_banks = 8, .chr_banks = 16 }, // Namco 109
.{ .id = 78, .prg_banks = 8, .chr_banks = 16 }, // Irem 74HC161/32
.{ .id = 79, .prg_banks = 4, .chr_banks = 8 }, // NINA-03
.{ .id = 85, .prg_banks = 8, .chr_banks = 16 }, // Konami VRC7
.{ .id = 87, .prg_banks = 2, .chr_banks = 4 }, // Jaleco 74x139
.{ .id = 88, .prg_banks = 8, .chr_banks = 16 }, // Namco 118
.{ .id = 89, .prg_banks = 8, .chr_banks = 16 }, // Sunsoft-2
.{ .id = 93, .prg_banks = 8, .chr_banks = 0 }, // Sunsoft-3
.{ .id = 94, .prg_banks = 16, .chr_banks = 0 }, // UN1ROM
.{ .id = 118, .prg_banks = 8, .chr_banks = 16 }, // TxSROM
.{ .id = 119, .prg_banks = 8, .chr_banks = 16 }, // TQROM
.{ .id = 140, .prg_banks = 4, .chr_banks = 16 }, // Jaleco Command
.{ .id = 180, .prg_banks = 8, .chr_banks = 0 }, // Crazy Climber
.{ .id = 206, .prg_banks = 8, .chr_banks = 16 }, // DxROM / Namco 108
.{ .id = 210, .prg_banks = 8, .chr_banks = 16 }, // Namco 175/340
.{ .id = 232, .prg_banks = 16, .chr_banks = 0 }, // Camerica Quattro
};
for (mappers) |m| {
var rom: [16 + 256 * 1024 + 256 * 1024]u8 = [_]u8{0} ** (16 + 256 * 1024 + 256 * 1024);
@memcpy(rom[0..4], "NES\x1a");
rom[4] = m.prg_banks;
rom[5] = m.chr_banks;
rom[6] = (m.id & 0x0F) << 4;
rom[7] = m.id & 0xF0;
var nes: Nes = undefined;
try nes.init(&rom);
nes.runFrame();
}
}
test "NES System - Nestest Conformance" {
if (build_options.nes_test_roms_dir == null) return error.SkipZigTest;
try runNesTestSystem("other/nestest.nes", 100_000);
}
test "NES System - Instr Test: 01-implied" {
if (build_options.nes_test_roms_dir == null) return error.SkipZigTest;
try runBlarggTest("nes_instr_test/rom_singles/01-implied.nes", 10_000_000);
}
test "NES System - Instr Test: 02-immediate" {
if (build_options.nes_test_roms_dir == null) return error.SkipZigTest;
try runBlarggTest("nes_instr_test/rom_singles/02-immediate.nes", 10_000_000);
}
test "NES System - Instr Test: 03-zero_page" {
if (build_options.nes_test_roms_dir == null) return error.SkipZigTest;
try runBlarggTest("nes_instr_test/rom_singles/03-zero_page.nes", 10_000_000);
}
test "NES System - Instr Test: 04-zp_xy" {
if (build_options.nes_test_roms_dir == null) return error.SkipZigTest;
try runBlarggTest("nes_instr_test/rom_singles/04-zp_xy.nes", 10_000_000);
}
test "NES System - Instr Test: 05-absolute" {
if (build_options.nes_test_roms_dir == null) return error.SkipZigTest;
try runBlarggTest("nes_instr_test/rom_singles/05-absolute.nes", 10_000_000);
}
test "NES System - Instr Test: 06-abs_xy" {
if (build_options.nes_test_roms_dir == null) return error.SkipZigTest;
try runBlarggTest("nes_instr_test/rom_singles/06-abs_xy.nes", 10_000_000);
}
test "NES System - Instr Test: 07-ind_x" {
if (build_options.nes_test_roms_dir == null) return error.SkipZigTest;
try runBlarggTest("nes_instr_test/rom_singles/07-ind_x.nes", 10_000_000);
}
test "NES System - Instr Test: 08-ind_y" {
if (build_options.nes_test_roms_dir == null) return error.SkipZigTest;
try runBlarggTest("nes_instr_test/rom_singles/08-ind_y.nes", 10_000_000);
}
test "NES System - Instr Test: 09-branches" {
if (build_options.nes_test_roms_dir == null) return error.SkipZigTest;
try runBlarggTest("nes_instr_test/rom_singles/09-branches.nes", 10_000_000);
}
test "NES System - Instr Test: 10-stack" {
if (build_options.nes_test_roms_dir == null) return error.SkipZigTest;
try runBlarggTest("nes_instr_test/rom_singles/10-stack.nes", 10_000_000);
}
test "NES System - Instr Test: 11-special" {
if (build_options.nes_test_roms_dir == null) return error.SkipZigTest;
try runBlarggTest("nes_instr_test/rom_singles/11-special.nes", 10_000_000);
}
-70
View File
@@ -1,70 +0,0 @@
pub const Button = enum(u3) {
a = 0,
b = 1,
select = 2,
start = 3,
up = 4,
down = 5,
left = 6,
right = 7,
};
const Controller = @This();
buttons: u8 = 0,
shift_register: u8 = 0,
strobe: bool = false,
pub fn reset(
self: *Controller,
) void {
self.shift_register = 0;
self.strobe = false;
}
pub fn setButtons(
self: *Controller,
buttons: u8,
) void {
self.buttons = buttons;
if (self.strobe)
self.shift_register = buttons;
}
pub fn write(
self: *Controller,
value: u8,
) void {
const new_strobe =
(value & 1) != 0;
if (new_strobe) {
self.shift_register =
self.buttons;
} else if (self.strobe) {
// Falling edge latches controller.
self.shift_register =
self.buttons;
}
self.strobe = new_strobe;
}
pub fn read(
self: *Controller,
) u8 {
if (self.strobe)
return self.buttons & 1;
const value =
self.shift_register & 1;
// Real controllers return 1 after all
// eight buttons have shifted out.
self.shift_register =
(self.shift_register >> 1) | 0x80;
return value;
}
-64
View File
@@ -1,64 +0,0 @@
const types = @import("types.zig");
const Mirroring = types.Mirroring;
const Axrom = @This();
prg_bank_select: u8 = 0,
mirroring_select: u1 = 0,
prg_bank_count: usize,
pub fn init(
prg_size: usize,
) Axrom {
return .{
.prg_bank_count = prg_size / 0x8000,
};
}
pub fn cpuMapRead(
self: *const Axrom,
address: u16,
) ?usize {
if (address < 0x8000) return null;
const offset = @as(usize, address - 0x8000);
const bank = @as(usize, self.prg_bank_select & 0x07) % self.prg_bank_count;
return bank * 0x8000 + offset;
}
pub fn cpuWrite(
self: *Axrom,
address: u16,
value: u8,
) void {
if (address >= 0x8000) {
self.prg_bank_select = value & 0x07;
self.mirroring_select = @truncate((value >> 4) & 1);
}
}
pub fn ppuMapRead(
_: *const Axrom,
address: u16,
) ?usize {
if (address >= 0x2000) return null;
return address;
}
pub fn ppuMapWrite(
_: *const Axrom,
address: u16,
) ?usize {
if (address >= 0x2000) return null;
return address;
}
pub fn mirroring(
self: *const Axrom,
) Mirroring {
return if (self.mirroring_select == 0) .single_screen_lower else .single_screen_upper;
}
pub fn irqAsserted(_: *const Axrom) bool {
return false;
}
-82
View File
@@ -1,82 +0,0 @@
const types = @import("types.zig");
const Mirroring = types.Mirroring;
const Camerica = @This();
prg_bank: u8 = 0,
mirroring_select: u1 = 0,
mirroring_mode: Mirroring,
has_single_screen_mirroring: bool = false,
prg_bank_count: usize,
pub fn init(
prg_size: usize,
_mirroring: Mirroring,
) Camerica {
return .{
.prg_bank_count = prg_size / 0x4000,
.mirroring_mode = _mirroring,
};
}
pub fn cpuMapRead(
self: *const Camerica,
address: u16,
) ?usize {
if (address < 0x8000) return null;
if (address < 0xC000) {
const offset = @as(usize, address - 0x8000);
const bank = @as(usize, self.prg_bank) % self.prg_bank_count;
return bank * 0x4000 + offset;
} else {
const offset = @as(usize, address - 0xC000);
const fixed_bank = if (self.prg_bank_count > 0) self.prg_bank_count - 1 else 0;
return fixed_bank * 0x4000 + offset;
}
}
pub fn cpuWrite(
self: *Camerica,
address: u16,
value: u8,
) void {
if (address < 0x8000) return;
if (address >= 0x8000 and address <= 0x9FFF) {
self.has_single_screen_mirroring = true;
self.mirroring_select = @truncate((value >> 4) & 1);
} else if (address >= 0xC000) {
self.prg_bank = value & 0x0F;
}
}
pub fn ppuMapRead(
_: *const Camerica,
address: u16,
) ?usize {
if (address >= 0x2000) return null;
return address;
}
pub fn ppuMapWrite(
_: *const Camerica,
address: u16,
) ?usize {
if (address >= 0x2000) return null;
return address;
}
pub fn mirroring(
self: *const Camerica,
) Mirroring {
if (self.has_single_screen_mirroring) {
return if (self.mirroring_select == 0) .single_screen_lower else .single_screen_upper;
}
return self.mirroring_mode;
}
pub fn irqAsserted(_: *const Camerica) bool {
return false;
}
-69
View File
@@ -1,69 +0,0 @@
const types = @import("types.zig");
const Mirroring = types.Mirroring;
const Cnrom = @This();
prg_16k: bool,
chr_bank_select: u8 = 0,
chr_bank_count: usize,
mirroring_mode: Mirroring,
pub fn init(
prg_size: usize,
chr_size: usize,
_mirroring: Mirroring,
) Cnrom {
return .{
.prg_16k = prg_size == 0x4000,
.chr_bank_count = if (chr_size > 0) chr_size / 0x2000 else 1,
.mirroring_mode = _mirroring,
};
}
pub fn cpuMapRead(
self: *const Cnrom,
address: u16,
) ?usize {
if (address < 0x8000) return null;
const offset = @as(usize, address - 0x8000);
return if (self.prg_16k) offset & 0x3fff else offset;
}
pub fn cpuWrite(
self: *Cnrom,
address: u16,
value: u8,
) void {
if (address >= 0x8000) {
self.chr_bank_select = value;
}
}
pub fn ppuMapRead(
self: *const Cnrom,
address: u16,
) ?usize {
if (address >= 0x2000) return null;
const bank = @as(usize, self.chr_bank_select) % self.chr_bank_count;
return bank * 0x2000 + @as(usize, address);
}
pub fn ppuMapWrite(
_: *const Cnrom,
address: u16,
) ?usize {
if (address >= 0x2000) return null;
return address;
}
pub fn mirroring(
self: *const Cnrom,
) Mirroring {
return self.mirroring_mode;
}
pub fn irqAsserted(_: *const Cnrom) bool {
return false;
}
-73
View File
@@ -1,73 +0,0 @@
const types = @import("types.zig");
const Mirroring = types.Mirroring;
const ColorDreams = @This();
prg_bank: u8 = 0,
chr_bank: u8 = 0,
prg_bank_count: usize,
chr_bank_count: usize,
mirroring_mode: Mirroring,
pub fn init(
prg_size: usize,
chr_size: usize,
_mirroring: Mirroring,
) ColorDreams {
return .{
.prg_bank_count = prg_size / 0x8000,
.chr_bank_count = if (chr_size > 0) chr_size / 0x2000 else 1,
.mirroring_mode = _mirroring,
};
}
pub fn cpuMapRead(
self: *const ColorDreams,
address: u16,
) ?usize {
if (address < 0x8000) return null;
const offset = @as(usize, address - 0x8000);
const bank = @as(usize, self.prg_bank) % self.prg_bank_count;
return bank * 0x8000 + offset;
}
pub fn cpuWrite(
self: *ColorDreams,
address: u16,
value: u8,
) void {
if (address >= 0x8000) {
self.prg_bank = value & 0x03;
self.chr_bank = (value >> 4) & 0x0F;
}
}
pub fn ppuMapRead(
self: *const ColorDreams,
address: u16,
) ?usize {
if (address >= 0x2000) return null;
const bank = @as(usize, self.chr_bank) % self.chr_bank_count;
return bank * 0x2000 + @as(usize, address);
}
pub fn ppuMapWrite(
_: *const ColorDreams,
address: u16,
) ?usize {
if (address >= 0x2000) return null;
return address;
}
pub fn mirroring(
self: *const ColorDreams,
) Mirroring {
return self.mirroring_mode;
}
pub fn irqAsserted(_: *const ColorDreams) bool {
return false;
}
-73
View File
@@ -1,73 +0,0 @@
const types = @import("types.zig");
const Mirroring = types.Mirroring;
const Gxrom = @This();
prg_bank: u8 = 0,
chr_bank: u8 = 0,
prg_bank_count: usize,
chr_bank_count: usize,
mirroring_mode: Mirroring,
pub fn init(
prg_size: usize,
chr_size: usize,
_mirroring: Mirroring,
) Gxrom {
return .{
.prg_bank_count = prg_size / 0x8000,
.chr_bank_count = if (chr_size > 0) chr_size / 0x2000 else 1,
.mirroring_mode = _mirroring,
};
}
pub fn cpuMapRead(
self: *const Gxrom,
address: u16,
) ?usize {
if (address < 0x8000) return null;
const offset = @as(usize, address - 0x8000);
const bank = @as(usize, self.prg_bank) % self.prg_bank_count;
return bank * 0x8000 + offset;
}
pub fn cpuWrite(
self: *Gxrom,
address: u16,
value: u8,
) void {
if (address >= 0x8000) {
self.chr_bank = value & 0x03;
self.prg_bank = (value >> 4) & 0x03;
}
}
pub fn ppuMapRead(
self: *const Gxrom,
address: u16,
) ?usize {
if (address >= 0x2000) return null;
const bank = @as(usize, self.chr_bank) % self.chr_bank_count;
return bank * 0x2000 + @as(usize, address);
}
pub fn ppuMapWrite(
_: *const Gxrom,
address: u16,
) ?usize {
if (address >= 0x2000) return null;
return address;
}
pub fn mirroring(
self: *const Gxrom,
) Mirroring {
return self.mirroring_mode;
}
pub fn irqAsserted(_: *const Gxrom) bool {
return false;
}
-78
View File
@@ -1,78 +0,0 @@
const types = @import("types.zig");
const Mirroring = types.Mirroring;
const Irem78 = @This();
prg_bank: u8 = 0,
chr_bank: u8 = 0,
mirroring_select: u1 = 0,
prg_bank_count: usize,
chr_bank_count: usize,
pub fn init(
prg_size: usize,
chr_size: usize,
) Irem78 {
return .{
.prg_bank_count = prg_size / 0x4000,
.chr_bank_count = if (chr_size > 0) chr_size / 0x2000 else 1,
};
}
pub fn cpuMapRead(
self: *const Irem78,
address: u16,
) ?usize {
if (address < 0x8000) return null;
if (address < 0xC000) {
const offset = @as(usize, address - 0x8000);
const bank = @as(usize, self.prg_bank) % self.prg_bank_count;
return bank * 0x4000 + offset;
} else {
const offset = @as(usize, address - 0xC000);
const fixed_bank = if (self.prg_bank_count > 0) self.prg_bank_count - 1 else 0;
return fixed_bank * 0x4000 + offset;
}
}
pub fn cpuWrite(
self: *Irem78,
address: u16,
value: u8,
) void {
if (address < 0x8000) return;
self.prg_bank = value & 0x07;
self.mirroring_select = @truncate((value >> 3) & 1);
self.chr_bank = (value >> 4) & 0x0F;
}
pub fn ppuMapRead(
self: *const Irem78,
address: u16,
) ?usize {
if (address >= 0x2000) return null;
const bank = @as(usize, self.chr_bank) % self.chr_bank_count;
return bank * 0x2000 + @as(usize, address);
}
pub fn ppuMapWrite(
_: *const Irem78,
address: u16,
) ?usize {
if (address >= 0x2000) return null;
return address;
}
pub fn mirroring(
self: *const Irem78,
) Mirroring {
return if (self.mirroring_select == 0) .single_screen_lower else .single_screen_upper;
}
pub fn irqAsserted(_: *const Irem78) bool {
return false;
}
-69
View File
@@ -1,69 +0,0 @@
const types = @import("types.zig");
const Mirroring = types.Mirroring;
const Mmc3 = @import("mmc3.zig");
const Mapper118 = @This();
mmc3: Mmc3,
mirror_bit: u1 = 0,
pub fn init(
prg_size: usize,
chr_size: usize,
) Mapper118 {
return .{
.mmc3 = Mmc3.init(prg_size, chr_size),
};
}
pub fn cpuMapRead(
self: *const Mapper118,
address: u16,
) ?usize {
return self.mmc3.cpuMapRead(address);
}
pub fn cpuWrite(
self: *Mapper118,
address: u16,
value: u8,
) void {
if (address == 0x8001) {
if (self.mmc3.bank_select <= 5) {
self.mirror_bit = @truncate((value >> 7) & 1);
}
}
self.mmc3.cpuWrite(address, value);
}
pub fn ppuMapRead(
self: *Mapper118,
address: u16,
) ?usize {
return self.mmc3.ppuMapRead(address);
}
pub fn ppuMapWrite(
self: *const Mapper118,
address: u16,
) ?usize {
return self.mmc3.ppuMapWrite(address);
}
pub fn mirroring(
self: *const Mapper118,
) Mirroring {
return if (self.mirror_bit == 1) .single_screen_upper else .single_screen_lower;
}
pub fn irqAsserted(
self: *const Mapper118,
) bool {
return self.mmc3.irqAsserted();
}
pub fn handleScanline(
self: *Mapper118,
) void {
self.mmc3.handleScanline();
}
-113
View File
@@ -1,113 +0,0 @@
const types = @import("types.zig");
const Mirroring = types.Mirroring;
const Mmc3 = @import("mmc3.zig");
const Mapper119 = @This();
mmc3: Mmc3,
pub fn init(
prg_size: usize,
chr_size: usize,
) Mapper119 {
return .{
.mmc3 = Mmc3.init(prg_size, chr_size),
};
}
pub fn cpuMapRead(
self: *const Mapper119,
address: u16,
) ?usize {
return self.mmc3.cpuMapRead(address);
}
pub fn cpuWrite(
self: *Mapper119,
address: u16,
value: u8,
) void {
self.mmc3.cpuWrite(address, value);
}
pub fn ppuMapRead(
self: *Mapper119,
address: u16,
) ?usize {
if (address >= 0x2000) return null;
const bank_val = self.getChrBankValue(address);
const offset = (address % 0x0400);
if ((bank_val & 0x40) != 0) {
// Bit 6 is set -> Route to CHR RAM
const ram_bank = @as(usize, bank_val & 7);
return 0x80000000 | (ram_bank * 0x0400 + offset);
} else {
// Route to CHR ROM
return self.mmc3.ppuMapRead(address);
}
}
pub fn ppuMapWrite(
self: *const Mapper119,
address: u16,
) ?usize {
if (address >= 0x2000) return null;
const bank_val = self.getChrBankValue(address);
const offset = (address % 0x0400);
if ((bank_val & 0x40) != 0) {
// Bit 6 is set -> Route write to CHR RAM
const ram_bank = @as(usize, bank_val & 7);
return 0x80000000 | (ram_bank * 0x0400 + offset);
} else {
return self.mmc3.ppuMapWrite(address);
}
}
fn getChrBankValue(self: *const Mapper119, address: u16) u8 {
const chr_mode = (self.mmc3.bank_select & 0x80) != 0;
const addr_k = address / 0x0400;
if (!chr_mode) {
return switch (addr_k) {
0, 1 => (self.mmc3.bank_registers[0] & 0xfe) + @as(u8, @intCast(addr_k & 1)),
2, 3 => (self.mmc3.bank_registers[1] & 0xfe) + @as(u8, @intCast(addr_k & 1)),
4 => self.mmc3.bank_registers[2],
5 => self.mmc3.bank_registers[3],
6 => self.mmc3.bank_registers[4],
7 => self.mmc3.bank_registers[5],
else => unreachable,
};
} else {
return switch (addr_k) {
0 => self.mmc3.bank_registers[2],
1 => self.mmc3.bank_registers[3],
2 => self.mmc3.bank_registers[4],
3 => self.mmc3.bank_registers[5],
4, 5 => (self.mmc3.bank_registers[0] & 0xfe) + @as(u8, @intCast(addr_k & 1)),
6, 7 => (self.mmc3.bank_registers[1] & 0xfe) + @as(u8, @intCast(addr_k & 1)),
else => unreachable,
};
}
}
pub fn mirroring(
self: *const Mapper119,
) Mirroring {
return self.mmc3.mirroring();
}
pub fn irqAsserted(
self: *const Mapper119,
) bool {
return self.mmc3.irqAsserted();
}
pub fn handleScanline(
self: *Mapper119,
) void {
self.mmc3.handleScanline();
}
-69
View File
@@ -1,69 +0,0 @@
const types = @import("types.zig");
const Mirroring = types.Mirroring;
const Mapper13 = @This();
chr_bank: u8 = 0,
fixed_mirroring: Mirroring,
prg_mask: usize,
pub fn init(
prg_size: usize,
mirroring_mode: Mirroring,
) Mapper13 {
return .{
.fixed_mirroring = mirroring_mode,
.prg_mask = if (prg_size > 0) prg_size - 1 else 0x7fff,
};
}
pub fn cpuMapRead(
self: *const Mapper13,
address: u16,
) ?usize {
if (address < 0x8000) return null;
return (@as(usize, address) - 0x8000) & self.prg_mask;
}
pub fn cpuWrite(
self: *Mapper13,
address: u16,
value: u8,
) void {
if (address < 0x8000) return;
self.chr_bank = value & 0x03;
}
pub fn ppuMapRead(
self: *const Mapper13,
address: u16,
) ?usize {
if (address >= 0x2000) return null;
if (address < 0x1000) {
return 0x80000000 | @as(usize, address);
} else {
const bank = @as(usize, self.chr_bank & 3);
const offset = @as(usize, address & 0x0fff);
return 0x80000000 | (bank * 0x1000 + offset);
}
}
pub fn ppuMapWrite(
self: *const Mapper13,
address: u16,
) ?usize {
return self.ppuMapRead(address);
}
pub fn mirroring(
self: *const Mapper13,
) Mirroring {
return self.fixed_mirroring;
}
pub fn irqAsserted(
_: *const Mapper13,
) bool {
return false;
}
-73
View File
@@ -1,73 +0,0 @@
const types = @import("types.zig");
const Mirroring = types.Mirroring;
const Mapper140 = @This();
prg_bank: u8 = 0,
chr_bank: u8 = 0,
prg_bank_count: usize,
chr_bank_count: usize,
mirroring_mode: Mirroring,
pub fn init(
prg_size: usize,
chr_size: usize,
_mirroring: Mirroring,
) Mapper140 {
return .{
.prg_bank_count = prg_size / 0x8000,
.chr_bank_count = if (chr_size > 0) chr_size / 0x2000 else 1,
.mirroring_mode = _mirroring,
};
}
pub fn cpuMapRead(
self: *const Mapper140,
address: u16,
) ?usize {
if (address < 0x8000) return null;
const offset = @as(usize, address - 0x8000);
const bank = @as(usize, self.prg_bank) % self.prg_bank_count;
return bank * 0x8000 + offset;
}
pub fn cpuWrite(
self: *Mapper140,
address: u16,
value: u8,
) void {
if (address >= 0x6000 and address <= 0x7FFF) {
self.prg_bank = (value >> 4) & 0x03;
self.chr_bank = value & 0x0F;
}
}
pub fn ppuMapRead(
self: *const Mapper140,
address: u16,
) ?usize {
if (address >= 0x2000) return null;
const bank = @as(usize, self.chr_bank) % self.chr_bank_count;
return bank * 0x2000 + @as(usize, address);
}
pub fn ppuMapWrite(
_: *const Mapper140,
address: u16,
) ?usize {
if (address >= 0x2000) return null;
return address;
}
pub fn mirroring(
self: *const Mapper140,
) Mirroring {
return self.mirroring_mode;
}
pub fn irqAsserted(_: *const Mapper140) bool {
return false;
}
-70
View File
@@ -1,70 +0,0 @@
const types = @import("types.zig");
const Mirroring = types.Mirroring;
const Mapper180 = @This();
prg_bank: u8 = 0,
prg_bank_count: usize,
mirroring_mode: Mirroring,
pub fn init(
prg_size: usize,
_mirroring: Mirroring,
) Mapper180 {
return .{
.prg_bank_count = prg_size / 0x4000,
.mirroring_mode = _mirroring,
};
}
pub fn cpuMapRead(
self: *const Mapper180,
address: u16,
) ?usize {
if (address < 0x8000) return null;
if (address < 0xC000) {
const offset = @as(usize, address - 0x8000);
return offset; // Fixed bank 0
} else {
const offset = @as(usize, address - 0xC000);
const bank = @as(usize, self.prg_bank) % self.prg_bank_count;
return bank * 0x4000 + offset;
}
}
pub fn cpuWrite(
self: *Mapper180,
address: u16,
value: u8,
) void {
if (address >= 0x8000) {
self.prg_bank = value & 0x07;
}
}
pub fn ppuMapRead(
_: *const Mapper180,
address: u16,
) ?usize {
if (address >= 0x2000) return null;
return address;
}
pub fn ppuMapWrite(
_: *const Mapper180,
address: u16,
) ?usize {
if (address >= 0x2000) return null;
return address;
}
pub fn mirroring(
self: *const Mapper180,
) Mirroring {
return self.mirroring_mode;
}
pub fn irqAsserted(_: *const Mapper180) bool {
return false;
}
-108
View File
@@ -1,108 +0,0 @@
const types = @import("types.zig");
const Mirroring = types.Mirroring;
const Mapper206 = @This();
bank_select: u3 = 0,
prg_bank0: u8 = 0,
prg_bank1: u8 = 1,
chr_banks: [6]u8 = [_]u8{ 0, 2, 4, 5, 6, 7 },
prg_bank_count: usize,
chr_bank_count: usize,
mirroring_mode: Mirroring,
pub fn init(
prg_size: usize,
chr_size: usize,
_mirroring: Mirroring,
) Mapper206 {
return .{
.prg_bank_count = prg_size / 0x2000,
.chr_bank_count = if (chr_size > 0) chr_size / 0x0400 else 1,
.mirroring_mode = _mirroring,
};
}
pub fn cpuMapRead(
self: *const Mapper206,
address: u16,
) ?usize {
if (address < 0x8000) return null;
if (address < 0xA000) {
const offset = @as(usize, address - 0x8000);
const bank = @as(usize, self.prg_bank0) % self.prg_bank_count;
return bank * 0x2000 + offset;
} else if (address < 0xC000) {
const offset = @as(usize, address - 0xA000);
const bank = @as(usize, self.prg_bank1) % self.prg_bank_count;
return bank * 0x2000 + offset;
} else if (address < 0xE000) {
const offset = @as(usize, address - 0xC000);
const bank = if (self.prg_bank_count >= 2) self.prg_bank_count - 2 else 0;
return bank * 0x2000 + offset;
} else {
const offset = @as(usize, address - 0xE000);
const bank = if (self.prg_bank_count >= 1) self.prg_bank_count - 1 else 0;
return bank * 0x2000 + offset;
}
}
pub fn cpuWrite(
self: *Mapper206,
address: u16,
value: u8,
) void {
if (address >= 0x8000 and address <= 0x9FFF) {
if ((address & 1) == 0) {
self.bank_select = @truncate(value & 0x07);
} else {
switch (self.bank_select) {
0...5 => |i| self.chr_banks[i] = value & 0x3F,
6 => self.prg_bank0 = value & 0x0F,
7 => self.prg_bank1 = value & 0x0F,
}
}
}
}
pub fn ppuMapRead(
self: *const Mapper206,
address: u16,
) ?usize {
if (address >= 0x2000) return null;
if (address < 0x0800) {
const bank = (@as(usize, self.chr_banks[0] & 0xFE)) % self.chr_bank_count;
return bank * 0x0400 + @as(usize, address);
} else if (address < 0x1000) {
const bank = (@as(usize, self.chr_banks[1] & 0xFE)) % self.chr_bank_count;
return bank * 0x0400 + @as(usize, address - 0x0800);
} else {
const idx = 2 + (address - 0x1000) / 0x0400;
const offset = (address - 0x1000) % 0x0400;
const bank = @as(usize, self.chr_banks[idx]) % self.chr_bank_count;
return bank * 0x0400 + @as(usize, offset);
}
}
pub fn ppuMapWrite(
_: *const Mapper206,
address: u16,
) ?usize {
if (address >= 0x2000) return null;
return address;
}
pub fn mirroring(
self: *const Mapper206,
) Mirroring {
return self.mirroring_mode;
}
pub fn irqAsserted(_: *const Mapper206) bool {
return false;
}
-105
View File
@@ -1,105 +0,0 @@
const types = @import("types.zig");
const Mirroring = types.Mirroring;
const Mapper210 = @This();
prg_bank0: u8 = 0,
prg_bank1: u8 = 0,
prg_bank2: u8 = 0,
chr_banks: [8]u8 = [_]u8{ 0, 1, 2, 3, 4, 5, 6, 7 },
mirroring_mode: Mirroring = .vertical,
prg_bank_count: usize,
chr_bank_count: usize,
pub fn init(
prg_size: usize,
chr_size: usize,
) Mapper210 {
return .{
.prg_bank_count = prg_size / 0x2000,
.chr_bank_count = if (chr_size > 0) chr_size / 0x0400 else 1,
};
}
pub fn cpuMapRead(
self: *const Mapper210,
address: u16,
) ?usize {
if (address < 0x8000) return null;
if (address < 0xA000) {
const offset = @as(usize, address - 0x8000);
const bank = @as(usize, self.prg_bank0) % self.prg_bank_count;
return bank * 0x2000 + offset;
} else if (address < 0xC000) {
const offset = @as(usize, address - 0xA000);
const bank = @as(usize, self.prg_bank1) % self.prg_bank_count;
return bank * 0x2000 + offset;
} else if (address < 0xE000) {
const offset = @as(usize, address - 0xC000);
const bank = @as(usize, self.prg_bank2) % self.prg_bank_count;
return bank * 0x2000 + offset;
} else {
const offset = @as(usize, address - 0xE000);
const fixed_bank = if (self.prg_bank_count > 0) self.prg_bank_count - 1 else 0;
return fixed_bank * 0x2000 + offset;
}
}
pub fn cpuWrite(
self: *Mapper210,
address: u16,
value: u8,
) void {
if (address < 0x8000) return;
if (address >= 0x8000 and address <= 0xBFFF) {
const index = (address - 0x8000) / 0x0800;
self.chr_banks[index] = value;
} else if (address >= 0xC000 and address <= 0xC7FF) {
self.prg_bank0 = value & 0x3F;
} else if (address >= 0xC800 and address <= 0xCFFF) {
self.prg_bank1 = value & 0x3F;
} else if (address >= 0xD000 and address <= 0xD7FF) {
self.prg_bank2 = value & 0x3F;
} else if (address >= 0xE000 and address <= 0xE7FF) {
self.mirroring_mode = switch (@as(u2, @truncate(value >> 6))) {
0 => .vertical,
1 => .horizontal,
2 => .single_screen_lower,
3 => .single_screen_upper,
};
}
}
pub fn ppuMapRead(
self: *const Mapper210,
address: u16,
) ?usize {
if (address >= 0x2000) return null;
const bank_idx = address / 0x0400;
const offset = address % 0x0400;
const bank = @as(usize, self.chr_banks[bank_idx]) % self.chr_bank_count;
return bank * 0x0400 + offset;
}
pub fn ppuMapWrite(
_: *const Mapper210,
address: u16,
) ?usize {
if (address >= 0x2000) return null;
return address;
}
pub fn mirroring(
self: *const Mapper210,
) Mirroring {
return self.mirroring_mode;
}
pub fn irqAsserted(_: *const Mapper210) bool {
return false;
}
-77
View File
@@ -1,77 +0,0 @@
const types = @import("types.zig");
const Mirroring = types.Mirroring;
const Mapper232 = @This();
outer_block: u8 = 0,
inner_bank: u8 = 0,
prg_bank_count: usize,
mirroring_mode: Mirroring,
pub fn init(
prg_size: usize,
_mirroring: Mirroring,
) Mapper232 {
return .{
.prg_bank_count = prg_size / 0x4000,
.mirroring_mode = _mirroring,
};
}
pub fn cpuMapRead(
self: *const Mapper232,
address: u16,
) ?usize {
if (address < 0x8000) return null;
const block_start = (@as(usize, self.outer_block) * 4) % self.prg_bank_count;
if (address < 0xC000) {
const offset = @as(usize, address - 0x8000);
const bank = (block_start + (@as(usize, self.inner_bank) & 3)) % self.prg_bank_count;
return bank * 0x4000 + offset;
} else {
const offset = @as(usize, address - 0xC000);
const bank = (block_start + 3) % self.prg_bank_count;
return bank * 0x4000 + offset;
}
}
pub fn cpuWrite(
self: *Mapper232,
address: u16,
value: u8,
) void {
if (address >= 0x8000 and address <= 0x9FFF) {
self.outer_block = (value >> 3) & 0x03;
} else if (address >= 0xA000) {
self.inner_bank = value & 0x03;
}
}
pub fn ppuMapRead(
_: *const Mapper232,
address: u16,
) ?usize {
if (address >= 0x2000) return null;
return address;
}
pub fn ppuMapWrite(
_: *const Mapper232,
address: u16,
) ?usize {
if (address >= 0x2000) return null;
return address;
}
pub fn mirroring(
self: *const Mapper232,
) Mirroring {
return self.mirroring_mode;
}
pub fn irqAsserted(_: *const Mapper232) bool {
return false;
}
-92
View File
@@ -1,92 +0,0 @@
const types = @import("types.zig");
const Mirroring = types.Mirroring;
const Mapper34 = @This();
prg_bank: u8 = 0,
chr_bank0: u8 = 0,
chr_bank1: u8 = 1,
prg_bank_count: usize,
chr_bank_count: usize,
fixed_mirroring: Mirroring,
is_nina01: bool,
pub fn init(
prg_size: usize,
chr_size: usize,
mirroring_mode: Mirroring,
) Mapper34 {
return .{
.prg_bank_count = if (prg_size >= 0x8000) prg_size / 0x8000 else 1,
.chr_bank_count = if (chr_size > 0) chr_size / 0x1000 else 1,
.fixed_mirroring = mirroring_mode,
.is_nina01 = (chr_size > 0),
};
}
pub fn cpuMapRead(
self: *const Mapper34,
address: u16,
) ?usize {
if (address < 0x8000) return null;
const offset = @as(usize, address & 0x7fff);
const bank = @as(usize, self.prg_bank) % self.prg_bank_count;
return bank * 0x8000 + offset;
}
pub fn cpuWrite(
self: *Mapper34,
address: u16,
value: u8,
) void {
if (self.is_nina01) {
switch (address) {
0x7ffd => self.prg_bank = value,
0x7ffe => self.chr_bank0 = value,
0x7fff => self.chr_bank1 = value,
else => {},
}
} else {
if (address >= 0x8000) {
self.prg_bank = value;
}
}
}
pub fn ppuMapRead(
self: *const Mapper34,
address: u16,
) ?usize {
if (address >= 0x2000) return null;
if (self.is_nina01) {
const offset = @as(usize, address & 0x0fff);
const bank_raw = if (address < 0x1000) self.chr_bank0 else self.chr_bank1;
const bank = @as(usize, bank_raw) % self.chr_bank_count;
return bank * 0x1000 + offset;
} else {
return @as(usize, address);
}
}
pub fn ppuMapWrite(
self: *const Mapper34,
address: u16,
) ?usize {
return self.ppuMapRead(address);
}
pub fn mirroring(
self: *const Mapper34,
) Mirroring {
return self.fixed_mirroring;
}
pub fn irqAsserted(
_: *const Mapper34,
) bool {
return false;
}
-118
View File
@@ -1,118 +0,0 @@
const types = @import("types.zig");
const Mirroring = types.Mirroring;
const Mapper65 = @This();
prg_banks: [3]u8 = [_]u8{ 0, 1, 2 },
chr_banks: [8]u8 = [_]u8{ 0, 1, 2, 3, 4, 5, 6, 7 },
mirroring_mode: Mirroring = .vertical,
irq_enabled: bool = false,
irq_counter: u16 = 0,
irq_reload: u16 = 0,
irq_assert: bool = false,
prg_bank_count: usize,
chr_bank_count: usize,
pub fn init(
prg_size: usize,
chr_size: usize,
mirroring_mode: Mirroring,
) Mapper65 {
return .{
.prg_bank_count = prg_size / 0x2000,
.chr_bank_count = if (chr_size > 0) chr_size / 0x0400 else 1,
.mirroring_mode = mirroring_mode,
};
}
pub fn cpuMapRead(
self: *const Mapper65,
address: u16,
) ?usize {
if (address < 0x8000) return null;
const offset = @as(usize, address & 0x1fff);
const last_bank = self.prg_bank_count - 1;
switch ((address - 0x8000) / 0x2000) {
0 => return (@as(usize, self.prg_banks[0]) % self.prg_bank_count) * 0x2000 + offset,
1 => return (@as(usize, self.prg_banks[1]) % self.prg_bank_count) * 0x2000 + offset,
2 => return (@as(usize, self.prg_banks[2]) % self.prg_bank_count) * 0x2000 + offset,
3 => return last_bank * 0x2000 + offset,
else => unreachable,
}
}
pub fn cpuWrite(
self: *Mapper65,
address: u16,
value: u8,
) void {
if (address < 0x8000) return;
switch (address) {
0x8000 => self.prg_banks[0] = value,
0xa000 => self.prg_banks[1] = value,
0xc000 => self.prg_banks[2] = value,
0x9001 => self.mirroring_mode = if ((value & 0x80) != 0) .horizontal else .vertical,
0x9003 => {
self.irq_enabled = (value & 0x80) != 0;
self.irq_assert = false;
},
0x9004 => {
self.irq_reload = (self.irq_reload & 0xff00) | value;
},
0x9005 => {
self.irq_reload = (self.irq_reload & 0x00ff) | (@as(u16, value) << 8);
self.irq_counter = self.irq_reload;
self.irq_assert = false;
},
0xb000 => self.chr_banks[0] = value,
0xb001 => self.chr_banks[1] = value,
0xb002 => self.chr_banks[2] = value,
0xb003 => self.chr_banks[3] = value,
0xb004 => self.chr_banks[4] = value,
0xb005 => self.chr_banks[5] = value,
0xb006 => self.chr_banks[6] = value,
0xb007 => self.chr_banks[7] = value,
else => {},
}
}
pub fn ppuMapRead(
self: *const Mapper65,
address: u16,
) ?usize {
if (address >= 0x2000) return null;
const slot = address / 0x0400;
const offset = @as(usize, address & 0x03ff);
const bank = @as(usize, self.chr_banks[slot]) % self.chr_bank_count;
return bank * 0x0400 + offset;
}
pub fn ppuMapWrite(
self: *const Mapper65,
address: u16,
) ?usize {
return self.ppuMapRead(address);
}
pub fn mirroring(
self: *const Mapper65,
) Mirroring {
return self.mirroring_mode;
}
pub fn irqAsserted(
self: *const Mapper65,
) bool {
return self.irq_assert;
}
-103
View File
@@ -1,103 +0,0 @@
const types = @import("types.zig");
const Mirroring = types.Mirroring;
const Mapper68 = @This();
chr_banks: [4]u8 = [_]u8{ 0, 0, 0, 0 },
nt_banks: [2]u8 = [_]u8{ 0, 0 },
prg_bank: u8 = 0,
nametable_control: u8 = 0,
prg_bank_count: usize,
chr_bank_count: usize,
fixed_mirroring: Mirroring,
pub fn init(
prg_size: usize,
chr_size: usize,
mirroring_mode: Mirroring,
) Mapper68 {
return .{
.prg_bank_count = prg_size / 0x4000,
.chr_bank_count = if (chr_size > 0) chr_size / 0x0800 else 1,
.fixed_mirroring = mirroring_mode,
};
}
pub fn cpuMapRead(
self: *const Mapper68,
address: u16,
) ?usize {
if (address < 0x8000) return null;
const offset = @as(usize, address & 0x3fff);
const last_bank = self.prg_bank_count - 1;
if (address < 0xc000) {
const bank = @as(usize, self.prg_bank) % self.prg_bank_count;
return bank * 0x4000 + offset;
} else {
return last_bank * 0x4000 + offset;
}
}
pub fn cpuWrite(
self: *Mapper68,
address: u16,
value: u8,
) void {
if (address < 0x8000) return;
switch (address & 0xf000) {
0x8000 => self.chr_banks[0] = value,
0x9000 => self.chr_banks[1] = value,
0xa000 => self.chr_banks[2] = value,
0xb000 => self.chr_banks[3] = value,
0xc000 => self.nt_banks[0] = value,
0xd000 => self.nt_banks[1] = value,
0xe000 => self.nametable_control = value,
0xf000 => self.prg_bank = value & 0x0f,
else => {},
}
}
pub fn ppuMapRead(
self: *const Mapper68,
address: u16,
) ?usize {
if (address >= 0x2000) return null;
const slot = address / 0x0800;
const offset = @as(usize, address & 0x07ff);
const bank = @as(usize, self.chr_banks[slot]) % self.chr_bank_count;
return bank * 0x0800 + offset;
}
pub fn ppuMapWrite(
self: *const Mapper68,
address: u16,
) ?usize {
return self.ppuMapRead(address);
}
pub fn mirroring(
self: *const Mapper68,
) Mirroring {
if ((self.nametable_control & 0x10) == 0) {
switch (self.nametable_control & 0x03) {
0 => return .vertical,
1 => return .horizontal,
2 => return .single_screen_lower,
3 => return .single_screen_upper,
else => unreachable,
}
}
return self.fixed_mirroring;
}
pub fn irqAsserted(
_: *const Mapper68,
) bool {
return false;
}
-123
View File
@@ -1,123 +0,0 @@
const types = @import("types.zig");
const Mirroring = types.Mirroring;
const Mapper69 = @This();
command: u8 = 0,
chr_banks: [8]u8 = [_]u8{ 0, 1, 2, 3, 4, 5, 6, 7 },
prg_banks: [4]u8 = [_]u8{ 0, 1, 2, 3 }, // $6000, $8000, $A000, $C000
mirroring_mode: Mirroring = .vertical,
irq_enabled: bool = false,
irq_counter: u16 = 0,
irq_assert: bool = false,
prg_bank_count: usize,
chr_bank_count: usize,
pub fn init(
prg_size: usize,
chr_size: usize,
mirroring_mode: Mirroring,
) Mapper69 {
return .{
.prg_bank_count = prg_size / 0x2000,
.chr_bank_count = if (chr_size > 0) chr_size / 0x0400 else 1,
.mirroring_mode = mirroring_mode,
};
}
pub fn cpuMapRead(
self: *const Mapper69,
address: u16,
) ?usize {
if (address < 0x6000) return null;
const offset = @as(usize, address & 0x1fff);
const last_bank = self.prg_bank_count - 1;
if (address < 0x8000) {
const bank = @as(usize, self.prg_banks[0] & 0x3f) % self.prg_bank_count;
return bank * 0x2000 + offset;
}
switch ((address - 0x8000) / 0x2000) {
0 => return (@as(usize, self.prg_banks[1] & 0x3f) % self.prg_bank_count) * 0x2000 + offset,
1 => return (@as(usize, self.prg_banks[2] & 0x3f) % self.prg_bank_count) * 0x2000 + offset,
2 => return (@as(usize, self.prg_banks[3] & 0x3f) % self.prg_bank_count) * 0x2000 + offset,
3 => return last_bank * 0x2000 + offset,
else => unreachable,
}
}
pub fn cpuWrite(
self: *Mapper69,
address: u16,
value: u8,
) void {
if (address < 0x8000) return;
if (address < 0xa000) {
self.command = value & 0x0f;
} else if (address < 0xc000) {
switch (self.command) {
0...7 => |i| self.chr_banks[i] = value,
8 => self.prg_banks[0] = value,
9 => self.prg_banks[1] = value,
0xa => self.prg_banks[2] = value,
0xb => self.prg_banks[3] = value,
0xc => {
switch (value & 0x03) {
0 => self.mirroring_mode = .vertical,
1 => self.mirroring_mode = .horizontal,
2 => self.mirroring_mode = .single_screen_lower,
3 => self.mirroring_mode = .single_screen_upper,
else => unreachable,
}
},
0xd => {
self.irq_enabled = (value & 0x01) != 0;
self.irq_assert = false;
},
0xe => {
self.irq_counter = (self.irq_counter & 0xff00) | value;
},
0xf => {
self.irq_counter = (self.irq_counter & 0x00ff) | (@as(u16, value) << 8);
},
else => {},
}
}
}
pub fn ppuMapRead(
self: *const Mapper69,
address: u16,
) ?usize {
if (address >= 0x2000) return null;
const slot = address / 0x0400;
const offset = @as(usize, address & 0x03ff);
const bank = @as(usize, self.chr_banks[slot]) % self.chr_bank_count;
return bank * 0x0400 + offset;
}
pub fn ppuMapWrite(
self: *const Mapper69,
address: u16,
) ?usize {
return self.ppuMapRead(address);
}
pub fn mirroring(
self: *const Mapper69,
) Mirroring {
return self.mirroring_mode;
}
pub fn irqAsserted(
self: *const Mapper69,
) bool {
return self.irq_assert;
}
-91
View File
@@ -1,91 +0,0 @@
const types = @import("types.zig");
const Mirroring = types.Mirroring;
const Mapper76 = @This();
bank_select: u8 = 0,
bank_registers: [8]u8 = [_]u8{0} ** 8,
prg_bank_count: usize,
chr_bank_count: usize,
fixed_mirroring: Mirroring,
pub fn init(
prg_size: usize,
chr_size: usize,
mirroring_mode: Mirroring,
) Mapper76 {
return .{
.prg_bank_count = prg_size / 0x2000,
.chr_bank_count = if (chr_size > 0) chr_size / 0x0800 else 1,
.fixed_mirroring = mirroring_mode,
};
}
pub fn cpuMapRead(
self: *const Mapper76,
address: u16,
) ?usize {
if (address < 0x8000) return null;
const offset = @as(usize, address & 0x1fff);
const last_bank = self.prg_bank_count - 1;
const second_last_bank = self.prg_bank_count - 2;
const r6 = @as(usize, self.bank_registers[6]) % self.prg_bank_count;
const r7 = @as(usize, self.bank_registers[7]) % self.prg_bank_count;
switch ((address - 0x8000) / 0x2000) {
0 => return r6 * 0x2000 + offset,
1 => return r7 * 0x2000 + offset,
2 => return second_last_bank * 0x2000 + offset,
3 => return last_bank * 0x2000 + offset,
else => unreachable,
}
}
pub fn cpuWrite(
self: *Mapper76,
address: u16,
value: u8,
) void {
if (address < 0x8000) return;
if ((address & 1) == 0) {
self.bank_select = value & 0x07;
} else {
self.bank_registers[self.bank_select] = value;
}
}
pub fn ppuMapRead(
self: *const Mapper76,
address: u16,
) ?usize {
if (address >= 0x2000) return null;
const offset = @as(usize, address & 0x07ff);
const slot = address / 0x0800;
const reg_idx = @as(usize, 2 + slot);
const bank = (@as(usize, self.bank_registers[reg_idx])) % self.chr_bank_count;
return bank * 0x0800 + offset;
}
pub fn ppuMapWrite(
self: *const Mapper76,
address: u16,
) ?usize {
return self.ppuMapRead(address);
}
pub fn mirroring(
self: *const Mapper76,
) Mirroring {
return self.fixed_mirroring;
}
pub fn irqAsserted(
_: *const Mapper76,
) bool {
return false;
}
-70
View File
@@ -1,70 +0,0 @@
const types = @import("types.zig");
const Mirroring = types.Mirroring;
const Mapper87 = @This();
chr_bank: u8 = 0,
prg_bank_count: usize,
chr_bank_count: usize,
mirroring_mode: Mirroring,
pub fn init(
prg_size: usize,
chr_size: usize,
_mirroring: Mirroring,
) Mapper87 {
return .{
.prg_bank_count = prg_size / 0x8000,
.chr_bank_count = if (chr_size > 0) chr_size / 0x2000 else 1,
.mirroring_mode = _mirroring,
};
}
pub fn cpuMapRead(
self: *const Mapper87,
address: u16,
) ?usize {
if (address < 0x8000) return null;
const offset = @as(usize, address - 0x8000);
const bank = if (self.prg_bank_count > 0) (self.prg_bank_count - 1) else 0;
return bank * 0x8000 + offset;
}
pub fn cpuWrite(
self: *Mapper87,
address: u16,
value: u8,
) void {
if (address >= 0x6000 and address <= 0x7FFF) {
self.chr_bank = ((value & 1) << 1) | ((value >> 1) & 1);
}
}
pub fn ppuMapRead(
self: *const Mapper87,
address: u16,
) ?usize {
if (address >= 0x2000) return null;
const bank = @as(usize, self.chr_bank) % self.chr_bank_count;
return bank * 0x2000 + @as(usize, address);
}
pub fn ppuMapWrite(
_: *const Mapper87,
address: u16,
) ?usize {
if (address >= 0x2000) return null;
return address;
}
pub fn mirroring(
self: *const Mapper87,
) Mirroring {
return self.mirroring_mode;
}
pub fn irqAsserted(_: *const Mapper87) bool {
return false;
}
-103
View File
@@ -1,103 +0,0 @@
const types = @import("types.zig");
const Mirroring = types.Mirroring;
const Mapper88 = @This();
bank_select: u8 = 0,
bank_registers: [8]u8 = [_]u8{0} ** 8,
prg_bank_count: usize,
chr_bank_count: usize,
fixed_mirroring: Mirroring,
pub fn init(
prg_size: usize,
chr_size: usize,
mirroring_mode: Mirroring,
) Mapper88 {
return .{
.prg_bank_count = prg_size / 0x2000,
.chr_bank_count = if (chr_size > 0) chr_size / 0x0400 else 1,
.fixed_mirroring = mirroring_mode,
};
}
pub fn cpuMapRead(
self: *const Mapper88,
address: u16,
) ?usize {
if (address < 0x8000) return null;
const offset = @as(usize, address & 0x1fff);
const last_bank = self.prg_bank_count - 1;
const second_last_bank = self.prg_bank_count - 2;
const r6 = @as(usize, self.bank_registers[6] & 0x3f) % self.prg_bank_count;
const r7 = @as(usize, self.bank_registers[7] & 0x3f) % self.prg_bank_count;
switch ((address - 0x8000) / 0x2000) {
0 => return r6 * 0x2000 + offset,
1 => return r7 * 0x2000 + offset,
2 => return second_last_bank * 0x2000 + offset,
3 => return last_bank * 0x2000 + offset,
else => unreachable,
}
}
pub fn cpuWrite(
self: *Mapper88,
address: u16,
value: u8,
) void {
if (address < 0x8000) return;
if ((address & 1) == 0) {
self.bank_select = value & 0x07;
} else {
self.bank_registers[self.bank_select] = value;
}
}
pub fn ppuMapRead(
self: *const Mapper88,
address: u16,
) ?usize {
if (address >= 0x2000) return null;
const offset = @as(usize, address & 0x03ff);
const page = address / 0x0400;
var bank: usize = 0;
switch (page) {
0, 1 => bank = (@as(usize, self.bank_registers[0] & 0xfe) + (page & 1)),
2, 3 => bank = (@as(usize, self.bank_registers[1] & 0xfe) + (page & 1)),
4, 5, 6, 7 => {
const reg_idx = page - 2; // registers 2, 3, 4, 5
const val = self.bank_registers[reg_idx];
// Bit 6 maps to 64KB CHR block selection (bit 6 -> 0x40 added to bank number)
bank = (@as(usize, val & 0x3f) | (@as(usize, val & 0x40)));
},
else => unreachable,
}
bank = bank % self.chr_bank_count;
return bank * 0x0400 + offset;
}
pub fn ppuMapWrite(
self: *const Mapper88,
address: u16,
) ?usize {
return self.ppuMapRead(address);
}
pub fn mirroring(
self: *const Mapper88,
) Mirroring {
return self.fixed_mirroring;
}
pub fn irqAsserted(
_: *const Mapper88,
) bool {
return false;
}
-78
View File
@@ -1,78 +0,0 @@
const types = @import("types.zig");
const Mirroring = types.Mirroring;
const Mapper89 = @This();
prg_bank: u8 = 0,
chr_bank: u8 = 0,
mirroring_mode: Mirroring = .single_screen_lower,
prg_bank_count: usize,
chr_bank_count: usize,
pub fn init(
prg_size: usize,
chr_size: usize,
) Mapper89 {
return .{
.prg_bank_count = prg_size / 0x4000,
.chr_bank_count = if (chr_size > 0) chr_size / 0x2000 else 1,
};
}
pub fn cpuMapRead(
self: *const Mapper89,
address: u16,
) ?usize {
if (address < 0x8000) return null;
if (address < 0xC000) {
const offset = @as(usize, address - 0x8000);
const bank = @as(usize, self.prg_bank) % self.prg_bank_count;
return bank * 0x4000 + offset;
} else {
const offset = @as(usize, address - 0xC000);
const fixed_bank = if (self.prg_bank_count > 0) self.prg_bank_count - 1 else 0;
return fixed_bank * 0x4000 + offset;
}
}
pub fn cpuWrite(
self: *Mapper89,
address: u16,
value: u8,
) void {
if (address >= 0x8000) {
self.prg_bank = (value >> 4) & 0x07;
self.chr_bank = (value & 0x07) | ((value & 0x80) >> 4);
self.mirroring_mode = if ((value & 0x08) != 0) .single_screen_upper else .single_screen_lower;
}
}
pub fn ppuMapRead(
self: *const Mapper89,
address: u16,
) ?usize {
if (address >= 0x2000) return null;
const bank = @as(usize, self.chr_bank) % self.chr_bank_count;
return bank * 0x2000 + @as(usize, address);
}
pub fn ppuMapWrite(
_: *const Mapper89,
address: u16,
) ?usize {
if (address >= 0x2000) return null;
return address;
}
pub fn mirroring(
self: *const Mapper89,
) Mirroring {
return self.mirroring_mode;
}
pub fn irqAsserted(_: *const Mapper89) bool {
return false;
}
-73
View File
@@ -1,73 +0,0 @@
const types = @import("types.zig");
const Mirroring = types.Mirroring;
const Mapper93 = @This();
prg_bank: u8 = 0,
mirroring_mode: Mirroring,
prg_bank_count: usize,
pub fn init(
prg_size: usize,
_mirroring: Mirroring,
) Mapper93 {
return .{
.prg_bank_count = prg_size / 0x4000,
.mirroring_mode = _mirroring,
};
}
pub fn cpuMapRead(
self: *const Mapper93,
address: u16,
) ?usize {
if (address < 0x8000) return null;
if (address < 0xC000) {
const offset = @as(usize, address - 0x8000);
const bank = @as(usize, self.prg_bank) % self.prg_bank_count;
return bank * 0x4000 + offset;
} else {
const offset = @as(usize, address - 0xC000);
const fixed_bank = if (self.prg_bank_count > 0) self.prg_bank_count - 1 else 0;
return fixed_bank * 0x4000 + offset;
}
}
pub fn cpuWrite(
self: *Mapper93,
address: u16,
value: u8,
) void {
if (address >= 0x8000) {
self.prg_bank = (value >> 4) & 0x07;
self.mirroring_mode = if ((value & 1) != 0) .horizontal else .vertical;
}
}
pub fn ppuMapRead(
_: *const Mapper93,
address: u16,
) ?usize {
if (address >= 0x2000) return null;
return address;
}
pub fn ppuMapWrite(
_: *const Mapper93,
address: u16,
) ?usize {
if (address >= 0x2000) return null;
return address;
}
pub fn mirroring(
self: *const Mapper93,
) Mirroring {
return self.mirroring_mode;
}
pub fn irqAsserted(_: *const Mapper93) bool {
return false;
}
-71
View File
@@ -1,71 +0,0 @@
const types = @import("types.zig");
const Mirroring = types.Mirroring;
const Mapper94 = @This();
prg_bank: u8 = 0,
prg_bank_count: usize,
mirroring_mode: Mirroring,
pub fn init(
prg_size: usize,
_mirroring: Mirroring,
) Mapper94 {
return .{
.prg_bank_count = prg_size / 0x4000,
.mirroring_mode = _mirroring,
};
}
pub fn cpuMapRead(
self: *const Mapper94,
address: u16,
) ?usize {
if (address < 0x8000) return null;
if (address < 0xC000) {
const offset = @as(usize, address - 0x8000);
const bank = @as(usize, self.prg_bank) % self.prg_bank_count;
return bank * 0x4000 + offset;
} else {
const offset = @as(usize, address - 0xC000);
const fixed_bank = if (self.prg_bank_count > 0) self.prg_bank_count - 1 else 0;
return fixed_bank * 0x4000 + offset;
}
}
pub fn cpuWrite(
self: *Mapper94,
address: u16,
value: u8,
) void {
if (address >= 0x8000) {
self.prg_bank = (value >> 2) & 0x07;
}
}
pub fn ppuMapRead(
_: *const Mapper94,
address: u16,
) ?usize {
if (address >= 0x2000) return null;
return address;
}
pub fn ppuMapWrite(
_: *const Mapper94,
address: u16,
) ?usize {
if (address >= 0x2000) return null;
return address;
}
pub fn mirroring(
self: *const Mapper94,
) Mirroring {
return self.mirroring_mode;
}
pub fn irqAsserted(_: *const Mapper94) bool {
return false;
}
-139
View File
@@ -1,139 +0,0 @@
const types = @import("types.zig");
const Mirroring = types.Mirroring;
const Mmc1 = @This();
shift_register: u8 = 0x10,
control: u8 = 0x0c, // Default: PRG mode 3 (fix $C000)
chr_bank_0: u8 = 0,
chr_bank_1: u8 = 0,
prg_bank: u8 = 0,
prg_bank_count: usize,
chr_bank_count: usize,
pub fn init(
prg_size: usize,
chr_size: usize,
) Mmc1 {
return .{
.prg_bank_count = prg_size / 0x4000,
.chr_bank_count = if (chr_size > 0) chr_size / 0x1000 else 2,
};
}
pub fn cpuMapRead(
self: *const Mmc1,
address: u16,
) ?usize {
if (address < 0x8000) return null;
const prg_mode = (self.control >> 2) & 0x03;
const offset = @as(usize, address & 0x3fff);
switch (prg_mode) {
0, 1 => {
// 32 KB mode
const bank = @as(usize, self.prg_bank & 0x0e) % self.prg_bank_count;
return bank * 0x4000 + @as(usize, address - 0x8000);
},
2 => {
// Fix first bank at $8000, switch 16 KB bank at $C000
if (address < 0xc000) {
return offset;
} else {
const bank = @as(usize, self.prg_bank & 0x0f) % self.prg_bank_count;
return bank * 0x4000 + offset;
}
},
3 => {
// Fix last bank at $C000, switch 16 KB bank at $8000
if (address < 0xc000) {
const bank = @as(usize, self.prg_bank & 0x0f) % self.prg_bank_count;
return bank * 0x4000 + offset;
} else {
const last_bank = self.prg_bank_count - 1;
return last_bank * 0x4000 + offset;
}
},
else => unreachable,
}
}
pub fn cpuWrite(
self: *Mmc1,
address: u16,
value: u8,
) void {
if (address < 0x8000) return;
// Bit 7 reset flag
if ((value & 0x80) != 0) {
self.shift_register = 0x10;
self.control |= 0x0c;
return;
}
const complete = (self.shift_register & 1) != 0;
self.shift_register = (self.shift_register >> 1) | ((value & 1) << 4);
if (complete) {
const reg_data = self.shift_register;
self.shift_register = 0x10;
switch ((address >> 13) & 0x03) {
0 => self.control = reg_data,
1 => self.chr_bank_0 = reg_data,
2 => self.chr_bank_1 = reg_data,
3 => self.prg_bank = reg_data,
else => unreachable,
}
}
}
pub fn ppuMapRead(
self: *const Mmc1,
address: u16,
) ?usize {
if (address >= 0x2000) return null;
const chr_mode = (self.control & 0x10) != 0;
if (!chr_mode) {
// 8 KB CHR mode
const bank = @as(usize, self.chr_bank_0 & 0x1e) % self.chr_bank_count;
return bank * 0x1000 + @as(usize, address);
} else {
// 4 KB CHR mode
if (address < 0x1000) {
const bank = @as(usize, self.chr_bank_0) % self.chr_bank_count;
return bank * 0x1000 + @as(usize, address & 0x0fff);
} else {
const bank = @as(usize, self.chr_bank_1) % self.chr_bank_count;
return bank * 0x1000 + @as(usize, address & 0x0fff);
}
}
}
pub fn ppuMapWrite(
_: *const Mmc1,
address: u16,
) ?usize {
if (address >= 0x2000) return null;
return address;
}
pub fn mirroring(
self: *const Mmc1,
) Mirroring {
return switch (self.control & 0x03) {
0 => .single_screen_lower,
1 => .single_screen_upper,
2 => .vertical,
3 => .horizontal,
else => unreachable,
};
}
pub fn irqAsserted(_: *const Mmc1) bool {
return false;
}
-104
View File
@@ -1,104 +0,0 @@
const types = @import("types.zig");
const Mirroring = types.Mirroring;
const Mmc2 = @This();
prg_bank: u8 = 0,
chr_0fd: u8 = 0,
chr_0fe: u8 = 0,
chr_1fd: u8 = 0,
chr_1fe: u8 = 0,
latch0: u1 = 0, // 0 = 0FD, 1 = 0FE
latch1: u1 = 0, // 0 = 1FD, 1 = 1FE
mirroring_mode: Mirroring = .vertical,
prg_bank_count: usize,
chr_bank_count: usize,
pub fn init(
prg_size: usize,
chr_size: usize,
) Mmc2 {
return .{
.prg_bank_count = prg_size / 0x2000,
.chr_bank_count = if (chr_size > 0) chr_size / 0x1000 else 1,
};
}
pub fn cpuMapRead(
self: *const Mmc2,
address: u16,
) ?usize {
if (address < 0x8000) return null;
if (address < 0xA000) {
const bank = @as(usize, self.prg_bank) % self.prg_bank_count;
return bank * 0x2000 + @as(usize, address - 0x8000);
} else {
const offset = @as(usize, address - 0xA000);
const fixed_start = if (self.prg_bank_count >= 3) (self.prg_bank_count - 3) * 0x2000 else 0;
return fixed_start + offset;
}
}
pub fn cpuWrite(
self: *Mmc2,
address: u16,
value: u8,
) void {
if (address < 0x8000) return;
switch (address & 0xF000) {
0xA000 => self.prg_bank = value & 0x0F,
0xB000 => self.chr_0fd = value & 0x1F,
0xC000 => self.chr_0fe = value & 0x1F,
0xD000 => self.chr_1fd = value & 0x1F,
0xE000 => self.chr_1fe = value & 0x1F,
0xF000 => self.mirroring_mode = if ((value & 1) == 0) .vertical else .horizontal,
else => {},
}
}
pub fn ppuMapRead(
self: *Mmc2,
address: u16,
) ?usize {
if (address >= 0x2000) return null;
var bank: usize = 0;
if (address < 0x1000) {
const selected_bank = if (self.latch0 == 0) self.chr_0fd else self.chr_0fe;
bank = @as(usize, selected_bank) % self.chr_bank_count;
} else {
const selected_bank = if (self.latch1 == 0) self.chr_1fd else self.chr_1fe;
bank = @as(usize, selected_bank) % self.chr_bank_count;
}
// Check PPU tile latches
if (address == 0x0FD0) self.latch0 = 0;
if (address == 0x0FE0) self.latch0 = 1;
if (address == 0x1FD0) self.latch1 = 0;
if (address == 0x1FE0) self.latch1 = 1;
return bank * 0x1000 + @as(usize, address & 0x0FFF);
}
pub fn ppuMapWrite(
_: *const Mmc2,
address: u16,
) ?usize {
if (address >= 0x2000) return null;
return address;
}
pub fn mirroring(
self: *const Mmc2,
) Mirroring {
return self.mirroring_mode;
}
pub fn irqAsserted(_: *const Mmc2) bool {
return false;
}
-161
View File
@@ -1,161 +0,0 @@
const types = @import("types.zig");
const Mirroring = types.Mirroring;
const Mmc3 = @This();
bank_select: u8 = 0,
bank_registers: [8]u8 = [_]u8{0} ** 8,
mirroring_mode: Mirroring = .horizontal,
irq_latch: u8 = 0,
irq_counter: u8 = 0,
irq_reload: bool = false,
irq_enabled: bool = false,
irq_pending: bool = false,
prg_bank_count: usize,
chr_bank_count: usize,
pub fn init(
prg_size: usize,
chr_size: usize,
) Mmc3 {
return .{
.prg_bank_count = prg_size / 0x2000,
.chr_bank_count = if (chr_size > 0) chr_size / 0x0400 else 8,
};
}
pub fn cpuMapRead(
self: *const Mmc3,
address: u16,
) ?usize {
if (address < 0x8000) return null;
const offset = @as(usize, address & 0x1fff);
const prg_mode = (self.bank_select & 0x40) != 0;
const last_bank = self.prg_bank_count - 1;
const second_last_bank = self.prg_bank_count - 2;
const r6 = @as(usize, self.bank_registers[6] & 0x3f) % self.prg_bank_count;
const r7 = @as(usize, self.bank_registers[7] & 0x3f) % self.prg_bank_count;
switch ((address - 0x8000) / 0x2000) {
0 => { // $8000-$9FFF
const bank = if (!prg_mode) r6 else second_last_bank;
return bank * 0x2000 + offset;
},
1 => { // $A000-$BFFF
return r7 * 0x2000 + offset;
},
2 => { // $C000-$DFFF
const bank = if (!prg_mode) second_last_bank else r6;
return bank * 0x2000 + offset;
},
3 => { // $E000-$FFFF
return last_bank * 0x2000 + offset;
},
else => unreachable,
}
}
pub fn cpuWrite(
self: *Mmc3,
address: u16,
value: u8,
) void {
if (address < 0x8000) return;
switch (address & 0xe001) {
0x8000 => self.bank_select = value,
0x8001 => {
const reg = self.bank_select & 0x07;
self.bank_registers[reg] = value;
},
0xa000 => {
self.mirroring_mode = if ((value & 1) != 0) .horizontal else .vertical;
},
0xa001 => {}, // PRG RAM protect
0xc000 => self.irq_latch = value,
0xc001 => self.irq_reload = true,
0xe000 => {
self.irq_enabled = false;
self.irq_pending = false;
},
0xe001 => self.irq_enabled = true,
else => {},
}
}
pub fn ppuMapRead(
self: *const Mmc3,
address: u16,
) ?usize {
if (address >= 0x2000) return null;
const chr_mode = (self.bank_select & 0x80) != 0;
const offset = @as(usize, address & 0x03ff);
var bank: usize = 0;
const addr_k = address / 0x0400;
if (!chr_mode) {
switch (addr_k) {
0, 1 => bank = (@as(usize, self.bank_registers[0] & 0xfe) + (addr_k & 1)),
2, 3 => bank = (@as(usize, self.bank_registers[1] & 0xfe) + (addr_k & 1)),
4 => bank = self.bank_registers[2],
5 => bank = self.bank_registers[3],
6 => bank = self.bank_registers[4],
7 => bank = self.bank_registers[5],
else => unreachable,
}
} else {
switch (addr_k) {
0 => bank = self.bank_registers[2],
1 => bank = self.bank_registers[3],
2 => bank = self.bank_registers[4],
3 => bank = self.bank_registers[5],
4, 5 => bank = (@as(usize, self.bank_registers[0] & 0xfe) + (addr_k & 1)),
6, 7 => bank = (@as(usize, self.bank_registers[1] & 0xfe) + (addr_k & 1)),
else => unreachable,
}
}
bank = bank % self.chr_bank_count;
return bank * 0x0400 + offset;
}
pub fn ppuMapWrite(
_: *const Mmc3,
address: u16,
) ?usize {
if (address >= 0x2000) return null;
return address;
}
pub fn handleScanline(self: *Mmc3) void {
if (self.irq_counter == 0 or self.irq_reload) {
self.irq_counter = self.irq_latch;
self.irq_reload = false;
} else {
self.irq_counter -= 1;
}
if (self.irq_counter == 0 and self.irq_enabled) {
self.irq_pending = true;
}
}
pub fn mirroring(
self: *const Mmc3,
) Mirroring {
return self.mirroring_mode;
}
pub fn irqAsserted(
self: *const Mmc3,
) bool {
return self.irq_pending;
}
-117
View File
@@ -1,117 +0,0 @@
const types = @import("types.zig");
const Mirroring = types.Mirroring;
const Mmc4 = @This();
prg_bank: u8 = 0,
chr_banks_fd: [2]u8 = [_]u8{ 0, 0 },
chr_banks_fe: [2]u8 = [_]u8{ 0, 0 },
latch: [2]u8 = [_]u8{ 0xfe, 0xfe },
mirroring_mode: Mirroring = .vertical,
prg_bank_count: usize,
chr_bank_count: usize,
pub fn init(
prg_size: usize,
chr_size: usize,
) Mmc4 {
return .{
.prg_bank_count = prg_size / 0x4000,
.chr_bank_count = if (chr_size > 0) chr_size / 0x1000 else 2,
};
}
pub fn cpuMapRead(
self: *const Mmc4,
address: u16,
) ?usize {
if (address < 0x8000) return null;
const offset = @as(usize, address & 0x3fff);
const last_bank = self.prg_bank_count - 1;
if (address < 0xc000) {
const bank = @as(usize, self.prg_bank) % self.prg_bank_count;
return bank * 0x4000 + offset;
} else {
return last_bank * 0x4000 + offset;
}
}
pub fn cpuWrite(
self: *Mmc4,
address: u16,
value: u8,
) void {
if (address < 0xa000) return;
switch (address & 0xf000) {
0xa000 => self.prg_bank = value & 0x0f,
0xb000 => self.chr_banks_fd[0] = value & 0x1f,
0xc000 => self.chr_banks_fe[0] = value & 0x1f,
0xd000 => self.chr_banks_fd[1] = value & 0x1f,
0xe000 => self.chr_banks_fe[1] = value & 0x1f,
0xf000 => self.mirroring_mode = if ((value & 1) != 0) .horizontal else .vertical,
else => {},
}
}
pub fn ppuMapRead(
self: *Mmc4,
address: u16,
) ?usize {
if (address >= 0x2000) return null;
const slot = address / 0x1000;
const offset = @as(usize, address & 0x0fff);
const bank_raw = if (self.latch[slot] == 0xfd)
self.chr_banks_fd[slot]
else
self.chr_banks_fe[slot];
const bank = @as(usize, bank_raw) % self.chr_bank_count;
const result = bank * 0x1000 + offset;
// Check PPU tile latch triggers ($0FD0-$0FDF, $0FE0-$0FEF, $1FD0-$1FDF, $1FE0-$1FEF)
if (address == 0x0fd0 or address == 0x0fe0) {
self.latch[0] = @truncate(address >> 4);
} else if (address == 0x1fd0 or address == 0x1fe0) {
self.latch[1] = @truncate(address >> 4);
}
return result;
}
pub fn ppuMapWrite(
self: *const Mmc4,
address: u16,
) ?usize {
if (address >= 0x2000) return null;
const slot = address / 0x1000;
const offset = @as(usize, address & 0x0fff);
const bank_raw = if (self.latch[slot] == 0xfd)
self.chr_banks_fd[slot]
else
self.chr_banks_fe[slot];
const bank = @as(usize, bank_raw) % self.chr_bank_count;
return bank * 0x1000 + offset;
}
pub fn mirroring(
self: *const Mmc4,
) Mirroring {
return self.mirroring_mode;
}
pub fn irqAsserted(
_: *const Mmc4,
) bool {
return false;
}
-171
View File
@@ -1,171 +0,0 @@
const types = @import("types.zig");
const Mirroring = types.Mirroring;
const Mmc5 = @This();
prg_mode: u2 = 3,
chr_mode: u2 = 0,
prg_banks: [4]u8 = [_]u8{ 0, 0, 1, 2 }, // $6000, $8000, $A000, $C000
chr_banks: [12]u16 = [_]u16{0} ** 12,
mult_a: u8 = 0,
mult_b: u8 = 0,
irq_scanline: u8 = 0,
irq_enabled: bool = false,
irq_assert: bool = false,
irq_in_frame: bool = false,
current_scanline: u8 = 0,
mirroring_mode: Mirroring = .vertical,
ex_ram: [1024]u8 = [_]u8{0} ** 1024,
prg_bank_count: usize,
chr_bank_count: usize,
pub fn init(
prg_size: usize,
chr_size: usize,
) Mmc5 {
return .{
.prg_bank_count = prg_size / 0x2000,
.chr_bank_count = if (chr_size > 0) chr_size / 0x0400 else 1,
};
}
pub fn cpuMapRead(
self: *const Mmc5,
address: u16,
) ?usize {
if (address < 0x5c00) return null;
if (address >= 0x5c00 and address <= 0x5fff) {
return 0x80000000 | @as(usize, address - 0x5c00);
}
if (address < 0x8000) {
const bank = @as(usize, self.prg_banks[0] & 0x7f) % self.prg_bank_count;
return bank * 0x2000 + (address & 0x1fff);
}
const last_bank = self.prg_bank_count - 1;
const offset = @as(usize, address & 0x1fff);
switch ((address - 0x8000) / 0x2000) {
0 => return (@as(usize, self.prg_banks[1] & 0x7f) % self.prg_bank_count) * 0x2000 + offset,
1 => return (@as(usize, self.prg_banks[2] & 0x7f) % self.prg_bank_count) * 0x2000 + offset,
2 => return (@as(usize, self.prg_banks[3] & 0x7f) % self.prg_bank_count) * 0x2000 + offset,
3 => return last_bank * 0x2000 + offset,
else => unreachable,
}
}
pub fn cpuWrite(
self: *Mmc5,
address: u16,
value: u8,
) void {
if (address >= 0x5c00 and address <= 0x5fff) {
self.ex_ram[address - 0x5c00] = value;
return;
}
switch (address) {
0x5100 => self.prg_mode = @truncate(value & 3),
0x5101 => self.chr_mode = @truncate(value & 3),
0x5105 => {
switch (@as(u2, @truncate(value & 3))) {
0 => self.mirroring_mode = .vertical,
1 => self.mirroring_mode = .horizontal,
2 => self.mirroring_mode = .single_screen_lower,
3 => self.mirroring_mode = .single_screen_upper,
}
},
0x5114 => self.prg_banks[0] = value,
0x5115 => self.prg_banks[1] = value,
0x5116 => self.prg_banks[2] = value,
0x5117 => self.prg_banks[3] = value,
0x5120...0x512b => |addr| {
const idx = addr - 0x5120;
self.chr_banks[idx] = value;
},
0x5203 => self.irq_scanline = value,
0x5204 => {
self.irq_enabled = (value & 0x80) != 0;
self.irq_assert = false;
},
0x5205 => self.mult_a = value,
0x5206 => self.mult_b = value,
else => {},
}
}
pub fn readRegister(
self: *Mmc5,
address: u16,
) ?u8 {
switch (address) {
0x5204 => {
const res: u8 = (if (self.irq_assert) @as(u8, 0x80) else 0) | (if (self.irq_in_frame) @as(u8, 0x40) else 0);
self.irq_assert = false;
return res;
},
0x5205 => {
const prod = @as(u16, self.mult_a) * @as(u16, self.mult_b);
return @truncate(prod);
},
0x5206 => {
const prod = @as(u16, self.mult_a) * @as(u16, self.mult_b);
return @truncate(prod >> 8);
},
else => return null,
}
}
pub fn ppuMapRead(
self: *const Mmc5,
address: u16,
) ?usize {
if (address >= 0x2000) return null;
const slot = address / 0x0400;
const offset = @as(usize, address & 0x03ff);
const bank = @as(usize, self.chr_banks[slot]) % self.chr_bank_count;
return bank * 0x0400 + offset;
}
pub fn ppuMapWrite(
self: *const Mmc5,
address: u16,
) ?usize {
return self.ppuMapRead(address);
}
pub fn handleScanline(
self: *Mmc5,
) void {
self.current_scanline += 1;
if (self.current_scanline == self.irq_scanline) {
if (self.irq_enabled) {
self.irq_assert = true;
}
}
if (self.current_scanline >= 240) {
self.current_scanline = 0;
self.irq_in_frame = false;
}
}
pub fn mirroring(
self: *const Mmc5,
) Mirroring {
return self.mirroring_mode;
}
pub fn irqAsserted(
self: *const Mmc5,
) bool {
return self.irq_assert;
}
-73
View File
@@ -1,73 +0,0 @@
const types = @import("types.zig");
const Mirroring = types.Mirroring;
const Nina03 = @This();
prg_bank: u8 = 0,
chr_bank: u8 = 0,
prg_bank_count: usize,
chr_bank_count: usize,
mirroring_mode: Mirroring,
pub fn init(
prg_size: usize,
chr_size: usize,
_mirroring: Mirroring,
) Nina03 {
return .{
.prg_bank_count = prg_size / 0x8000,
.chr_bank_count = if (chr_size > 0) chr_size / 0x2000 else 1,
.mirroring_mode = _mirroring,
};
}
pub fn cpuMapRead(
self: *const Nina03,
address: u16,
) ?usize {
if (address < 0x8000) return null;
const offset = @as(usize, address - 0x8000);
const bank = @as(usize, self.prg_bank) % self.prg_bank_count;
return bank * 0x8000 + offset;
}
pub fn cpuWrite(
self: *Nina03,
address: u16,
value: u8,
) void {
if (address >= 0x4100 and address <= 0x5FFF) {
self.prg_bank = @truncate((value >> 3) & 1);
self.chr_bank = value & 0x07;
}
}
pub fn ppuMapRead(
self: *const Nina03,
address: u16,
) ?usize {
if (address >= 0x2000) return null;
const bank = @as(usize, self.chr_bank) % self.chr_bank_count;
return bank * 0x2000 + @as(usize, address);
}
pub fn ppuMapWrite(
_: *const Nina03,
address: u16,
) ?usize {
if (address >= 0x2000) return null;
return address;
}
pub fn mirroring(
self: *const Nina03,
) Mirroring {
return self.mirroring_mode;
}
pub fn irqAsserted(_: *const Nina03) bool {
return false;
}
-71
View File
@@ -1,71 +0,0 @@
const types = @import("types.zig");
const Mirroring = types.Mirroring;
const Nrom = @This();
prg_16k: bool,
mirroring_mode: Mirroring,
pub fn init(
prg_size: usize,
_mirroring: Mirroring,
) Nrom {
return .{
.prg_16k = prg_size == 0x4000,
.mirroring_mode = _mirroring,
};
}
pub fn cpuMapRead(
self: *const Nrom,
address: u16,
) ?usize {
if (address < 0x8000)
return null;
const offset =
@as(usize, address) - 0x8000;
return if (self.prg_16k)
offset & 0x3fff
else
offset;
}
pub fn cpuWrite(
_: *Nrom,
_: u16,
_: u8,
) void {
// NROM has no mapper registers.
}
pub fn ppuMapRead(
_: *const Nrom,
address: u16,
) ?usize {
if (address >= 0x2000)
return null;
return address;
}
pub fn ppuMapWrite(
_: *const Nrom,
address: u16,
) ?usize {
if (address >= 0x2000)
return null;
return address;
}
pub fn mirroring(
self: *const Nrom,
) Mirroring {
return self.mirroring_mode;
}
pub fn irqAsserted(_: *const Nrom) bool {
return false;
}
-147
View File
@@ -1,147 +0,0 @@
pub const Nrom = @import("nrom.zig");
pub const Uxrom = @import("uxrom.zig");
pub const Mmc1 = @import("mmc1.zig");
pub const Cnrom = @import("cnrom.zig");
pub const Mmc3 = @import("mmc3.zig");
pub const Mmc4 = @import("mmc4.zig");
pub const Mmc5 = @import("mmc5.zig");
pub const Axrom = @import("axrom.zig");
pub const Gxrom = @import("gxrom.zig");
pub const Mmc2 = @import("mmc2.zig");
pub const ColorDreams = @import("colordreams.zig");
pub const Camerica = @import("camerica.zig");
pub const Mapper13 = @import("mapper13.zig");
pub const Mapper34 = @import("mapper34.zig");
pub const Mapper65 = @import("mapper65.zig");
pub const Mapper68 = @import("mapper68.zig");
pub const Mapper69 = @import("mapper69.zig");
pub const Nina03 = @import("nina03.zig");
pub const Mapper76 = @import("mapper76.zig");
pub const Irem78 = @import("irem78.zig");
pub const Mapper87 = @import("mapper87.zig");
pub const Mapper88 = @import("mapper88.zig");
pub const Mapper89 = @import("mapper89.zig");
pub const Mapper93 = @import("mapper93.zig");
pub const Mapper94 = @import("mapper94.zig");
pub const Mapper118 = @import("mapper118.zig");
pub const Mapper119 = @import("mapper119.zig");
pub const Mapper140 = @import("mapper140.zig");
pub const Mapper180 = @import("mapper180.zig");
pub const Vrc1 = @import("vrc1.zig");
pub const Vrc2 = @import("vrc2.zig");
pub const Mapper232 = @import("mapper232.zig");
pub const Vrc3 = @import("vrc3.zig");
pub const Vrc4 = @import("vrc4.zig");
pub const Vrc6 = @import("vrc6.zig");
pub const Vrc7 = @import("vrc7.zig");
pub const Mapper210 = @import("mapper210.zig");
pub const Mapper206 = @import("mapper206.zig");
const types = @import("types.zig");
pub const Mirroring = types.Mirroring;
pub const Mapper = union(enum) {
nrom: Nrom,
uxrom: Uxrom,
mmc1: Mmc1,
cnrom: Cnrom,
mmc3: Mmc3,
mmc4: Mmc4,
mmc5: Mmc5,
axrom: Axrom,
gxrom: Gxrom,
mmc2: Mmc2,
color_dreams: ColorDreams,
camerica: Camerica,
mapper13: Mapper13,
mapper34: Mapper34,
mapper65: Mapper65,
mapper68: Mapper68,
mapper69: Mapper69,
nina03: Nina03,
mapper76: Mapper76,
irem78: Irem78,
mapper87: Mapper87,
mapper88: Mapper88,
mapper89: Mapper89,
mapper93: Mapper93,
mapper94: Mapper94,
mapper118: Mapper118,
mapper119: Mapper119,
mapper140: Mapper140,
mapper180: Mapper180,
vrc1: Vrc1,
vrc2: Vrc2,
mapper232: Mapper232,
vrc3: Vrc3,
vrc4: Vrc4,
vrc6: Vrc6,
vrc7: Vrc7,
mapper210: Mapper210,
mapper206: Mapper206,
pub fn cpuMapRead(
self: *const Mapper,
address: u16,
) ?usize {
return switch (self.*) {
inline else => |*mapper| mapper.cpuMapRead(address),
};
}
pub fn cpuWrite(
self: *Mapper,
address: u16,
value: u8,
) void {
switch (self.*) {
inline else => |*mapper| mapper.cpuWrite(address, value),
}
}
pub fn ppuMapRead(
self: *Mapper,
address: u16,
) ?usize {
return switch (self.*) {
inline else => |*mapper| mapper.ppuMapRead(address),
};
}
pub fn ppuMapWrite(
self: *const Mapper,
address: u16,
) ?usize {
return switch (self.*) {
inline else => |*mapper| mapper.ppuMapWrite(address),
};
}
pub fn mirroring(
self: *const Mapper,
) Mirroring {
return switch (self.*) {
inline else => |*mapper| mapper.mirroring(),
};
}
pub fn irqAsserted(
self: *const Mapper,
) bool {
return switch (self.*) {
inline else => |*mapper| mapper.irqAsserted(),
};
}
pub fn handleScanline(
self: *Mapper,
) void {
switch (self.*) {
.mmc3 => |*mmc3| mmc3.handleScanline(),
.mmc5 => |*mmc5| mmc5.handleScanline(),
.mapper118 => |*m118| m118.handleScanline(),
.mapper119 => |*m119| m119.handleScanline(),
else => {},
}
}
};
-9
View File
@@ -1,9 +0,0 @@
pub const Mirroring = enum {
horizontal,
vertical,
four_screen,
// Needed by later mappers.
single_screen_lower,
single_screen_upper,
};
-73
View File
@@ -1,73 +0,0 @@
const types = @import("types.zig");
const Mirroring = types.Mirroring;
const Uxrom = @This();
prg_bank_select: u8 = 0,
prg_bank_count: usize,
mirroring_mode: Mirroring,
pub fn init(
prg_size: usize,
_mirroring: Mirroring,
) Uxrom {
return .{
.prg_bank_count = prg_size / 0x4000,
.mirroring_mode = _mirroring,
};
}
pub fn cpuMapRead(
self: *const Uxrom,
address: u16,
) ?usize {
if (address < 0x8000) return null;
const offset = @as(usize, address & 0x3fff);
if (address < 0xc000) {
// Switchable 16 KB bank
const bank = @as(usize, self.prg_bank_select) % self.prg_bank_count;
return bank * 0x4000 + offset;
} else {
// Fixed to last 16 KB bank
const last_bank = self.prg_bank_count - 1;
return last_bank * 0x4000 + offset;
}
}
pub fn cpuWrite(
self: *Uxrom,
address: u16,
value: u8,
) void {
if (address >= 0x8000) {
self.prg_bank_select = value;
}
}
pub fn ppuMapRead(
_: *const Uxrom,
address: u16,
) ?usize {
if (address >= 0x2000) return null;
return address;
}
pub fn ppuMapWrite(
_: *const Uxrom,
address: u16,
) ?usize {
if (address >= 0x2000) return null;
return address;
}
pub fn mirroring(
self: *const Uxrom,
) Mirroring {
return self.mirroring_mode;
}
pub fn irqAsserted(_: *const Uxrom) bool {
return false;
}
-106
View File
@@ -1,106 +0,0 @@
const types = @import("types.zig");
const Mirroring = types.Mirroring;
const Vrc1 = @This();
prg_bank0: u8 = 0,
prg_bank1: u8 = 0,
prg_bank2: u8 = 0,
chr_bank0: u8 = 0,
chr_bank1: u8 = 0,
mirroring_mode: Mirroring = .vertical,
prg_bank_count: usize,
chr_bank_count: usize,
pub fn init(
prg_size: usize,
chr_size: usize,
) Vrc1 {
return .{
.prg_bank_count = prg_size / 0x2000,
.chr_bank_count = if (chr_size > 0) chr_size / 0x1000 else 1,
};
}
pub fn cpuMapRead(
self: *const Vrc1,
address: u16,
) ?usize {
if (address < 0x8000) return null;
if (address < 0xA000) {
const offset = @as(usize, address - 0x8000);
const bank = @as(usize, self.prg_bank0) % self.prg_bank_count;
return bank * 0x2000 + offset;
} else if (address < 0xC000) {
const offset = @as(usize, address - 0xA000);
const bank = @as(usize, self.prg_bank1) % self.prg_bank_count;
return bank * 0x2000 + offset;
} else if (address < 0xE000) {
const offset = @as(usize, address - 0xC000);
const bank = @as(usize, self.prg_bank2) % self.prg_bank_count;
return bank * 0x2000 + offset;
} else {
const offset = @as(usize, address - 0xE000);
const fixed_bank = if (self.prg_bank_count > 0) self.prg_bank_count - 1 else 0;
return fixed_bank * 0x2000 + offset;
}
}
pub fn cpuWrite(
self: *Vrc1,
address: u16,
value: u8,
) void {
if (address < 0x8000) return;
switch (address & 0xF000) {
0x8000 => self.prg_bank0 = value & 0x0F,
0x9000 => {
self.mirroring_mode = if ((value & 1) != 0) .horizontal else .vertical;
self.chr_bank0 = (self.chr_bank0 & 0x0F) | ((value & 0x02) << 3);
self.chr_bank1 = (self.chr_bank1 & 0x0F) | ((value & 0x04) << 2);
},
0xA000 => self.prg_bank1 = value & 0x0F,
0xC000 => self.prg_bank2 = value & 0x0F,
0xE000 => self.chr_bank0 = (self.chr_bank0 & 0x10) | (value & 0x0F),
0xF000 => self.chr_bank1 = (self.chr_bank1 & 0x10) | (value & 0x0F),
else => {},
}
}
pub fn ppuMapRead(
self: *const Vrc1,
address: u16,
) ?usize {
if (address >= 0x2000) return null;
if (address < 0x1000) {
const bank = @as(usize, self.chr_bank0) % self.chr_bank_count;
return bank * 0x1000 + @as(usize, address);
} else {
const bank = @as(usize, self.chr_bank1) % self.chr_bank_count;
return bank * 0x1000 + @as(usize, address - 0x1000);
}
}
pub fn ppuMapWrite(
_: *const Vrc1,
address: u16,
) ?usize {
if (address >= 0x2000) return null;
return address;
}
pub fn mirroring(
self: *const Vrc1,
) Mirroring {
return self.mirroring_mode;
}
pub fn irqAsserted(_: *const Vrc1) bool {
return false;
}
-127
View File
@@ -1,127 +0,0 @@
const types = @import("types.zig");
const Mirroring = types.Mirroring;
const Vrc2 = @This();
prg_banks: [2]u8 = [_]u8{ 0, 1 },
chr_banks_lo: [8]u4 = [_]u4{0} ** 8,
chr_banks_hi: [8]u5 = [_]u5{0} ** 8,
mirroring_mode: Mirroring = .vertical,
mapper_id: u8,
shift_a: u3,
shift_b: u3,
prg_bank_count: usize,
chr_bank_count: usize,
pub fn init(
prg_size: usize,
chr_size: usize,
mapper_id: u8,
) Vrc2 {
const shift_a: u3 = if (mapper_id == 22) 1 else 0;
const shift_b: u3 = if (mapper_id == 22) 0 else 1;
return .{
.mapper_id = mapper_id,
.prg_bank_count = prg_size / 0x2000,
.chr_bank_count = if (chr_size > 0) chr_size / 0x0400 else 1,
.shift_a = shift_a,
.shift_b = shift_b,
};
}
pub fn cpuMapRead(
self: *const Vrc2,
address: u16,
) ?usize {
if (address < 0x8000) return null;
const offset = @as(usize, address & 0x1fff);
const last_bank = self.prg_bank_count - 1;
const second_last_bank = self.prg_bank_count - 2;
const r0 = @as(usize, self.prg_banks[0] & 0x1f) % self.prg_bank_count;
const r1 = @as(usize, self.prg_banks[1] & 0x1f) % self.prg_bank_count;
switch ((address - 0x8000) / 0x2000) {
0 => return r0 * 0x2000 + offset,
1 => return r1 * 0x2000 + offset,
2 => return second_last_bank * 0x2000 + offset,
3 => return last_bank * 0x2000 + offset,
else => unreachable,
}
}
pub fn cpuWrite(
self: *Vrc2,
address: u16,
value: u8,
) void {
if (address < 0x8000) return;
const reg_addr = address & 0xf000;
const a = @as(u1, @truncate((address >> self.shift_a) & 1));
const b = @as(u1, @truncate((address >> self.shift_b) & 1));
const line = (@as(u2, b) << 1) | a;
switch (reg_addr) {
0x8000 => self.prg_banks[0] = value & 0x1f,
0x9000 => {
self.mirroring_mode = if ((value & 0x01) != 0) .horizontal else .vertical;
},
0xa000 => self.prg_banks[1] = value & 0x1f,
0xb000, 0xc000, 0xd000, 0xe000 => {
const slot_base = ((reg_addr - 0xb000) / 0x1000) * 2;
const is_hi = (line & 2) != 0;
const is_odd = (line & 1) != 0;
const slot = slot_base + (if (is_odd) @as(usize, 1) else 0);
if (!is_hi) {
self.chr_banks_lo[slot] = @truncate(value & 0x0f);
} else {
self.chr_banks_hi[slot] = @truncate(value & 0x1f);
}
},
else => {},
}
}
pub fn ppuMapRead(
self: *const Vrc2,
address: u16,
) ?usize {
if (address >= 0x2000) return null;
const slot = address / 0x0400;
const offset = @as(usize, address & 0x03ff);
var bank_val = (@as(usize, self.chr_banks_hi[slot]) << 4) | @as(usize, self.chr_banks_lo[slot]);
if (self.mapper_id == 22) {
bank_val >>= 1;
}
const bank = bank_val % self.chr_bank_count;
return bank * 0x0400 + offset;
}
pub fn ppuMapWrite(
self: *const Vrc2,
address: u16,
) ?usize {
return self.ppuMapRead(address);
}
pub fn mirroring(
self: *const Vrc2,
) Mirroring {
return self.mirroring_mode;
}
pub fn irqAsserted(
_: *const Vrc2,
) bool {
return false;
}
-118
View File
@@ -1,118 +0,0 @@
const types = @import("types.zig");
const Mirroring = types.Mirroring;
const Vrc3 = @This();
prg_bank: u8 = 0,
prg_bank_count: usize,
mirroring_mode: Mirroring,
irq_latch: u16 = 0,
irq_counter: u16 = 0,
irq_enabled: bool = false,
irq_mode_8bit: bool = false,
irq_asserted_flag: bool = false,
pub fn init(
prg_size: usize,
_mirroring: Mirroring,
) Vrc3 {
return .{
.prg_bank_count = prg_size / 0x4000,
.mirroring_mode = _mirroring,
};
}
pub fn cpuMapRead(
self: *const Vrc3,
address: u16,
) ?usize {
if (address < 0x8000) return null;
if (address < 0xC000) {
const offset = @as(usize, address - 0x8000);
const bank = @as(usize, self.prg_bank) % self.prg_bank_count;
return bank * 0x4000 + offset;
} else {
const offset = @as(usize, address - 0xC000);
const fixed_bank = if (self.prg_bank_count > 0) self.prg_bank_count - 1 else 0;
return fixed_bank * 0x4000 + offset;
}
}
pub fn cpuWrite(
self: *Vrc3,
address: u16,
value: u8,
) void {
if (address < 0x8000) return;
switch (address & 0xF000) {
0x8000 => self.irq_latch = (self.irq_latch & 0xFFF0) | (value & 0x0F),
0x9000 => self.irq_latch = (self.irq_latch & 0xFF0F) | ((@as(u16, value) & 0x0F) << 4),
0xA000 => self.irq_latch = (self.irq_latch & 0xF0FF) | ((@as(u16, value) & 0x0F) << 8),
0xB000 => self.irq_latch = (self.irq_latch & 0x0FFF) | ((@as(u16, value) & 0x0F) << 12),
0xC000 => {
self.irq_enabled = (value & 0x02) != 0;
self.irq_mode_8bit = (value & 0x04) != 0;
self.irq_asserted_flag = false;
if (self.irq_enabled) {
self.irq_counter = self.irq_latch;
}
},
0xD000 => {
self.irq_asserted_flag = false;
},
0xF000 => {
self.prg_bank = value & 0x07;
},
else => {},
}
}
pub fn tickCycle(self: *Vrc3) void {
if (self.irq_enabled) {
if (self.irq_mode_8bit) {
const low = @as(u8, @truncate(self.irq_counter));
if (low == 0xFF) {
self.irq_counter = (self.irq_counter & 0xFF00) | @as(u16, @as(u8, @truncate(self.irq_latch)));
self.irq_asserted_flag = true;
} else {
self.irq_counter += 1;
}
} else {
if (self.irq_counter == 0xFFFF) {
self.irq_counter = self.irq_latch;
self.irq_asserted_flag = true;
} else {
self.irq_counter += 1;
}
}
}
}
pub fn ppuMapRead(
_: *const Vrc3,
address: u16,
) ?usize {
if (address >= 0x2000) return null;
return address;
}
pub fn ppuMapWrite(
_: *const Vrc3,
address: u16,
) ?usize {
if (address >= 0x2000) return null;
return address;
}
pub fn mirroring(
self: *const Vrc3,
) Mirroring {
return self.mirroring_mode;
}
pub fn irqAsserted(self: *const Vrc3) bool {
return self.irq_asserted_flag;
}
-179
View File
@@ -1,179 +0,0 @@
const types = @import("types.zig");
const Mirroring = types.Mirroring;
const Vrc4 = @This();
prg_banks: [2]u8 = [_]u8{ 0, 1 },
chr_banks_lo: [8]u4 = [_]u4{0} ** 8,
chr_banks_hi: [8]u5 = [_]u5{0} ** 8,
prg_mode: u1 = 0,
mirroring_mode: Mirroring = .vertical,
irq_reload: u8 = 0,
irq_counter: u8 = 0,
irq_enabled: bool = false,
irq_enable_on_ack: bool = false,
irq_mode: u1 = 0, // 0 = cycle, 1 = scanline
irq_prescaler: i16 = 341,
irq_assert: bool = false,
shift_a: u3,
shift_b: u3,
prg_bank_count: usize,
chr_bank_count: usize,
pub fn init(
prg_size: usize,
chr_size: usize,
mapper_id: u8,
) Vrc4 {
const shift_a: u3 = if (mapper_id == 21) 1 else 0;
const shift_b: u3 = if (mapper_id == 21) 2 else 2;
return .{
.prg_bank_count = prg_size / 0x2000,
.chr_bank_count = if (chr_size > 0) chr_size / 0x0400 else 1,
.shift_a = shift_a,
.shift_b = shift_b,
};
}
pub fn cpuMapRead(
self: *const Vrc4,
address: u16,
) ?usize {
if (address < 0x8000) return null;
const offset = @as(usize, address & 0x1fff);
const last_bank = self.prg_bank_count - 1;
const second_last_bank = self.prg_bank_count - 2;
const r0 = @as(usize, self.prg_banks[0] & 0x1f) % self.prg_bank_count;
const r1 = @as(usize, self.prg_banks[1] & 0x1f) % self.prg_bank_count;
const bank_idx = (address - 0x8000) / 0x2000;
var bank: usize = 0;
if (self.prg_mode == 0) {
switch (bank_idx) {
0 => bank = r0,
1 => bank = r1,
2 => bank = second_last_bank,
3 => bank = last_bank,
else => unreachable,
}
} else {
switch (bank_idx) {
0 => bank = second_last_bank,
1 => bank = r1,
2 => bank = r0,
3 => bank = last_bank,
else => unreachable,
}
}
return bank * 0x2000 + offset;
}
pub fn cpuWrite(
self: *Vrc4,
address: u16,
value: u8,
) void {
if (address < 0x8000) return;
const reg_addr = address & 0xf000;
const a = @as(u1, @truncate((address >> self.shift_a) & 1));
const b = @as(u1, @truncate((address >> self.shift_b) & 1));
const line = (@as(u2, b) << 1) | a;
switch (reg_addr) {
0x8000 => {
self.prg_banks[0] = value & 0x1f;
},
0x9000 => {
if (line == 0) {
switch (@as(u2, @truncate(value & 3))) {
0 => self.mirroring_mode = .vertical,
1 => self.mirroring_mode = .horizontal,
2 => self.mirroring_mode = .single_screen_lower,
3 => self.mirroring_mode = .single_screen_upper,
}
} else if (line == 1 or line == 2) {
self.prg_mode = @truncate((value >> 1) & 1);
}
},
0xa000 => {
self.prg_banks[1] = value & 0x1f;
},
0xb000, 0xc000, 0xd000, 0xe000 => {
const slot_base = ((reg_addr - 0xb000) / 0x1000) * 2;
const is_hi = (line & 2) != 0;
const is_odd = (line & 1) != 0;
const slot = slot_base + (if (is_odd) @as(usize, 1) else 0);
if (!is_hi) {
self.chr_banks_lo[slot] = @truncate(value & 0x0f);
} else {
self.chr_banks_hi[slot] = @truncate(value & 0x1f);
}
},
0xf000 => {
switch (line) {
0 => self.irq_reload = (self.irq_reload & 0xf0) | (value & 0x0f),
1 => self.irq_reload = (self.irq_reload & 0x0f) | ((value & 0x0f) << 4),
2 => {
self.irq_mode = @truncate((value >> 2) & 1);
self.irq_enabled = (value & 0x02) != 0;
self.irq_enable_on_ack = (value & 0x01) != 0;
if (self.irq_enabled) {
self.irq_counter = self.irq_reload;
self.irq_prescaler = 341;
}
self.irq_assert = false;
},
3 => {
self.irq_enabled = self.irq_enable_on_ack;
self.irq_assert = false;
},
}
},
else => {},
}
}
pub fn ppuMapRead(
self: *const Vrc4,
address: u16,
) ?usize {
if (address >= 0x2000) return null;
const slot = address / 0x0400;
const offset = @as(usize, address & 0x03ff);
const bank_val = (@as(usize, self.chr_banks_hi[slot]) << 4) | @as(usize, self.chr_banks_lo[slot]);
const bank = bank_val % self.chr_bank_count;
return bank * 0x0400 + offset;
}
pub fn ppuMapWrite(
self: *const Vrc4,
address: u16,
) ?usize {
return self.ppuMapRead(address);
}
pub fn mirroring(
self: *const Vrc4,
) Mirroring {
return self.mirroring_mode;
}
pub fn irqAsserted(
self: *const Vrc4,
) bool {
return self.irq_assert;
}
-152
View File
@@ -1,152 +0,0 @@
const types = @import("types.zig");
const Mirroring = types.Mirroring;
const Vrc6 = @This();
prg_banks: [2]u8 = [_]u8{ 0, 1 },
chr_banks: [8]u8 = [_]u8{ 0, 1, 2, 3, 4, 5, 6, 7 },
mirroring_mode: Mirroring = .vertical,
irq_reload: u8 = 0,
irq_counter: u8 = 0,
irq_enabled: bool = false,
irq_enable_on_ack: bool = false,
irq_mode: u1 = 0,
irq_assert: bool = false,
shift_a: u3,
shift_b: u3,
prg_bank_count: usize,
chr_bank_count: usize,
pub fn init(
prg_size: usize,
chr_size: usize,
mapper_id: u8,
) Vrc6 {
const shift_a: u3 = if (mapper_id == 26) 1 else 0;
const shift_b: u3 = if (mapper_id == 26) 0 else 1;
return .{
.prg_bank_count = prg_size / 0x2000,
.chr_bank_count = if (chr_size > 0) chr_size / 0x0400 else 1,
.shift_a = shift_a,
.shift_b = shift_b,
};
}
pub fn cpuMapRead(
self: *const Vrc6,
address: u16,
) ?usize {
if (address < 0x8000) return null;
const last_bank = self.prg_bank_count - 1;
if (address < 0xc000) {
const offset = @as(usize, address & 0x3fff);
const bank16 = @as(usize, self.prg_banks[0] & 0x0f) % (self.prg_bank_count / 2);
return bank16 * 0x4000 + offset;
} else if (address < 0xe000) {
const offset = @as(usize, address & 0x1fff);
const bank8 = @as(usize, self.prg_banks[1] & 0x1f) % self.prg_bank_count;
return bank8 * 0x2000 + offset;
} else {
const offset = @as(usize, address & 0x1fff);
return last_bank * 0x2000 + offset;
}
}
pub fn cpuWrite(
self: *Vrc6,
address: u16,
value: u8,
) void {
if (address < 0x8000) return;
const reg_addr = address & 0xf000;
const a = @as(u1, @truncate((address >> self.shift_a) & 1));
const b = @as(u1, @truncate((address >> self.shift_b) & 1));
const line = (@as(u2, b) << 1) | a;
switch (reg_addr) {
0x8000 => {
if (line == 0) self.prg_banks[0] = value & 0x0f;
},
0x9000...0xb000 => {
if (reg_addr == 0xb000 and line == 3) {
switch (@as(u2, @truncate((value >> 2) & 3))) {
0 => self.mirroring_mode = .vertical,
1 => self.mirroring_mode = .horizontal,
2 => self.mirroring_mode = .single_screen_lower,
3 => self.mirroring_mode = .single_screen_upper,
}
}
},
0xc000 => {
if (line == 0) self.prg_banks[1] = value & 0x1f;
},
0xd000 => {
const slot = @as(usize, line);
self.chr_banks[slot] = value;
},
0xe000 => {
const slot = 4 + @as(usize, line);
self.chr_banks[slot] = value;
},
0xf000 => {
switch (line) {
0 => self.irq_reload = value,
1 => {
self.irq_mode = @truncate((value >> 2) & 1);
self.irq_enabled = (value & 0x02) != 0;
self.irq_enable_on_ack = (value & 0x01) != 0;
if (self.irq_enabled) {
self.irq_counter = self.irq_reload;
}
self.irq_assert = false;
},
2 => {
self.irq_enabled = self.irq_enable_on_ack;
self.irq_assert = false;
},
else => {},
}
},
else => {},
}
}
pub fn ppuMapRead(
self: *const Vrc6,
address: u16,
) ?usize {
if (address >= 0x2000) return null;
const slot = address / 0x0400;
const offset = @as(usize, address & 0x03ff);
const bank = @as(usize, self.chr_banks[slot]) % self.chr_bank_count;
return bank * 0x0400 + offset;
}
pub fn ppuMapWrite(
self: *const Vrc6,
address: u16,
) ?usize {
return self.ppuMapRead(address);
}
pub fn mirroring(
self: *const Vrc6,
) Mirroring {
return self.mirroring_mode;
}
pub fn irqAsserted(
self: *const Vrc6,
) bool {
return self.irq_assert;
}
-145
View File
@@ -1,145 +0,0 @@
const types = @import("types.zig");
const Mirroring = types.Mirroring;
const Vrc7 = @This();
prg_banks: [3]u8 = [_]u8{ 0, 1, 2 },
chr_banks: [8]u8 = [_]u8{ 0, 1, 2, 3, 4, 5, 6, 7 },
mirroring_mode: Mirroring = .vertical,
irq_reload: u8 = 0,
irq_counter: u8 = 0,
irq_enabled: bool = false,
irq_enable_on_ack: bool = false,
irq_mode: u1 = 0,
irq_assert: bool = false,
prg_bank_count: usize,
chr_bank_count: usize,
pub fn init(
prg_size: usize,
chr_size: usize,
) Vrc7 {
return .{
.prg_bank_count = prg_size / 0x2000,
.chr_bank_count = if (chr_size > 0) chr_size / 0x0400 else 1,
};
}
pub fn cpuMapRead(
self: *const Vrc7,
address: u16,
) ?usize {
if (address < 0x8000) return null;
const offset = @as(usize, address & 0x1fff);
const last_bank = self.prg_bank_count - 1;
switch ((address - 0x8000) / 0x2000) {
0 => return (@as(usize, self.prg_banks[0] & 0x3f) % self.prg_bank_count) * 0x2000 + offset,
1 => return (@as(usize, self.prg_banks[1] & 0x3f) % self.prg_bank_count) * 0x2000 + offset,
2 => return (@as(usize, self.prg_banks[2] & 0x3f) % self.prg_bank_count) * 0x2000 + offset,
3 => return last_bank * 0x2000 + offset,
else => unreachable,
}
}
pub fn cpuWrite(
self: *Vrc7,
address: u16,
value: u8,
) void {
if (address < 0x8000) return;
const reg_addr = address & 0xf000;
const a0 = if ((address & 0x10) != 0 or (address & 0x08) != 0) @as(u2, 1) else 0;
const a1 = if ((address & 0x20) != 0) @as(u2, 2) else 0;
const line = a0 | a1;
switch (reg_addr) {
0x8000 => {
if (line == 0) self.prg_banks[0] = value & 0x3f;
if (line == 1 or line == 2) self.prg_banks[1] = value & 0x3f;
},
0x9000 => {
if (line == 0) self.prg_banks[2] = value & 0x3f;
if (line == 2) {
switch (@as(u2, @truncate(value & 3))) {
0 => self.mirroring_mode = .vertical,
1 => self.mirroring_mode = .horizontal,
2 => self.mirroring_mode = .single_screen_lower,
3 => self.mirroring_mode = .single_screen_upper,
}
}
},
0xa000 => {
if (line == 0) self.chr_banks[0] = value;
if (line == 1 or line == 2) self.chr_banks[1] = value;
},
0xb000 => {
if (line == 0) self.chr_banks[2] = value;
if (line == 1 or line == 2) self.chr_banks[3] = value;
},
0xc000 => {
if (line == 0) self.chr_banks[4] = value;
if (line == 1 or line == 2) self.chr_banks[5] = value;
},
0xd000 => {
if (line == 0) self.chr_banks[6] = value;
if (line == 1 or line == 2) self.chr_banks[7] = value;
},
0xe000 => {
if (line == 0) self.irq_reload = value;
if (line == 1 or line == 2) {
self.irq_mode = @truncate((value >> 2) & 1);
self.irq_enabled = (value & 0x02) != 0;
self.irq_enable_on_ack = (value & 0x01) != 0;
if (self.irq_enabled) {
self.irq_counter = self.irq_reload;
}
self.irq_assert = false;
}
},
0xf000 => {
if (line == 0) {
self.irq_enabled = self.irq_enable_on_ack;
self.irq_assert = false;
}
},
else => {},
}
}
pub fn ppuMapRead(
self: *const Vrc7,
address: u16,
) ?usize {
if (address >= 0x2000) return null;
const slot = address / 0x0400;
const offset = @as(usize, address & 0x03ff);
const bank = @as(usize, self.chr_banks[slot]) % self.chr_bank_count;
return bank * 0x0400 + offset;
}
pub fn ppuMapWrite(
self: *const Vrc7,
address: u16,
) ?usize {
return self.ppuMapRead(address);
}
pub fn mirroring(
self: *const Vrc7,
) Mirroring {
return self.mirroring_mode;
}
pub fn irqAsserted(
self: *const Vrc7,
) bool {
return self.irq_assert;
}
-779
View File
@@ -1,779 +0,0 @@
const std = @import("std");
const Cartridge = @import("cartridge.zig");
const mapper = @import("mapper/root.zig");
const Mirroring = mapper.Mirroring;
const Ppu = @This();
pub const width = 256;
pub const height = 240;
// NTSC 2C02 64-color system palette mapped directly to RGB565 format
pub const nes_ntsc_palette_rgb565: [64]u16 = .{
0x52AA, 0x00EE, 0x0892, 0x3011, 0x400C, 0x5806, 0x5020, 0x38C0,
0x2140, 0x09C0, 0x0200, 0x01E0, 0x0187, 0x0000, 0x0000, 0x0000,
0x9CB3, 0x0A78, 0x319D, 0x58FC, 0x88B6, 0xA08C, 0x9904, 0x79E0,
0x52C0, 0x2B80, 0x0BE0, 0x03A5, 0x032F, 0x0000, 0x0000, 0x0000,
0xEF7D, 0x4CDD, 0x7BFD, 0xB31D, 0xE2BD, 0xEACE, 0xEB4C, 0xD444,
0xA540, 0x7620, 0x4E84, 0x3E6D, 0x3DA9, 0x3DF7, 0x0000, 0x0000,
0xEF7D, 0xAE6D, 0xBD7D, 0xD59D, 0xED7D, 0xED7A, 0xEDB6, 0xE632,
0xCE8F, 0xB6EF, 0xAF12, 0x9F16, 0xA6BD, 0xA514, 0x0000, 0x0000,
};
framebuffer: [width * height]u16 =
[_]u16{0} ** (width * height),
/// Four-screen sized so the same structure
/// can handle that mode without allocation.
nametable: [0x1000]u8 =
[_]u8{0} ** 0x1000,
palette: [32]u8 =
[_]u8{0} ** 32,
oam: [256]u8 =
[_]u8{0} ** 256,
ctrl: u8 = 0,
mask: u8 = 0,
status: u8 = 0,
oam_addr: u8 = 0,
vram_addr: u16 = 0,
temp_addr: u16 = 0,
fine_x: u3 = 0,
write_latch: bool = false,
read_buffer: u8 = 0,
io_latch: u8 = 0,
dot: u16 = 0,
scanline: u16 = 261,
frame_number: u64 = 0,
frame_ready: bool = false,
// internal shift registers & tile latches for background rendering
bg_shift_pattern_lo: u16 = 0,
bg_shift_pattern_hi: u16 = 0,
bg_shift_attrib_lo: u16 = 0,
bg_shift_attrib_hi: u16 = 0,
next_tile_id: u8 = 0,
next_tile_attrib: u8 = 0,
next_tile_lsb: u8 = 0,
next_tile_msb: u8 = 0,
// secondary OAM scanline sprite evaluation buffer
sec_oam_pattern_lo: [8]u8 = [_]u8{0} ** 8,
sec_oam_pattern_hi: [8]u8 = [_]u8{0} ** 8,
sec_oam_attribute: [8]u8 = [_]u8{0} ** 8,
sec_oam_x: [8]u8 = [_]u8{0} ** 8,
sec_oam_is_zero: [8]bool = [_]bool{false} ** 8,
sec_oam_count: u8 = 0,
pub fn reset(
self: *Ppu,
) void {
self.ctrl = 0;
self.mask = 0;
self.status = 0;
self.oam_addr = 0;
self.vram_addr = 0;
self.temp_addr = 0;
self.fine_x = 0;
self.write_latch = false;
self.read_buffer = 0;
self.io_latch = 0;
self.dot = 0;
self.scanline = 261;
self.frame_ready = false;
self.bg_shift_pattern_lo = 0;
self.bg_shift_pattern_hi = 0;
self.bg_shift_attrib_lo = 0;
self.bg_shift_attrib_hi = 0;
self.next_tile_id = 0;
self.next_tile_attrib = 0;
self.next_tile_lsb = 0;
self.next_tile_msb = 0;
self.sec_oam_count = 0;
}
pub fn cpuRead(
self: *Ppu,
cartridge: *Cartridge,
register: u16,
) u8 {
const reg = register & 7;
const value: u8 = switch (reg) {
// PPUSTATUS
2 => blk: {
const result =
(self.status & 0xe0) |
(self.io_latch & 0x1f);
self.status &= ~@as(u8, 0x80);
self.write_latch = false;
break :blk result;
},
// OAMDATA
4 => self.oam[self.oam_addr],
// PPUDATA
7 => self.readData(cartridge),
else => self.io_latch,
};
self.io_latch = value;
return value;
}
pub fn cpuWrite(
self: *Ppu,
cartridge: *Cartridge,
register: u16,
value: u8,
) void {
const reg = register & 7;
self.io_latch = value;
switch (reg) {
// PPUCTRL
0 => {
self.ctrl = value;
self.temp_addr =
(self.temp_addr & 0xf3ff) |
(@as(u16, value & 0x03) << 10);
},
// PPUMASK
1 => {
self.mask = value;
},
// OAMADDR
3 => {
self.oam_addr = value;
},
// OAMDATA
4 => {
self.writeOamDma(value);
},
// PPUSCROLL
5 => {
if (!self.write_latch) {
self.fine_x =
@truncate(value & 7);
self.temp_addr =
(self.temp_addr & 0xffe0) |
@as(u16, value >> 3);
self.write_latch = true;
} else {
self.temp_addr =
(self.temp_addr & 0x8c1f) |
(@as(u16, value & 0x07) << 12) |
(@as(u16, value & 0xf8) << 2);
self.write_latch = false;
}
},
// PPUADDR
6 => {
if (!self.write_latch) {
self.temp_addr =
(self.temp_addr & 0x00ff) |
(@as(u16, value & 0x3f) << 8);
self.write_latch = true;
} else {
self.temp_addr =
(self.temp_addr & 0x7f00) |
@as(u16, value);
self.vram_addr =
self.temp_addr;
self.write_latch = false;
}
},
// PPUDATA
7 => {
self.writeMemory(
cartridge,
self.vram_addr,
value,
);
self.incrementVramAddress();
},
else => {},
}
}
pub fn writeOamDma(
self: *Ppu,
value: u8,
) void {
self.oam[self.oam_addr] = value;
self.oam_addr +%= 1;
}
pub fn tick(
self: *Ppu,
cartridge: *Cartridge,
) void {
const rendering_enabled = (self.mask & 0x18) != 0;
const is_visible_scanline = (self.scanline < 240);
const is_prerender_scanline = (self.scanline == 261);
// Start of VBlank.
if (self.scanline == 241 and
self.dot == 1)
{
self.status |= 0x80;
}
// Pre-render line.
if (is_prerender_scanline and
self.dot == 1)
{
// Clear: VBlank, sprite zero hit, sprite overflow
self.status &= ~@as(u8, 0xe0);
}
// --- Rendering Pipeline ---
if (rendering_enabled and (is_visible_scanline or is_prerender_scanline)) {
if ((self.dot >= 1 and self.dot <= 256) or (self.dot >= 321 and self.dot <= 336)) {
self.shiftBackgroundRegisters();
const cycle_in_tile = (self.dot - 1) % 8;
switch (cycle_in_tile) {
0 => {
self.loadShiftRegisters();
const nt_addr = 0x2000 | (self.vram_addr & 0x0fff);
self.next_tile_id = self.readMemory(cartridge, nt_addr);
},
2 => {
const attr_addr = 0x23c0 |
(self.vram_addr & 0x0c00) |
((self.vram_addr >> 4) & 0x38) |
((self.vram_addr >> 2) & 0x07);
const attr_byte = self.readMemory(cartridge, attr_addr);
const shift: u3 = @intCast(((self.vram_addr >> 4) & 4) | (self.vram_addr & 2));
self.next_tile_attrib = (attr_byte >> shift) & 0x03;
},
4 => {
const bg_table: u16 = if ((self.ctrl & 0x10) != 0) 0x1000 else 0x0000;
const fine_y = (self.vram_addr >> 12) & 7;
const pattern_addr = bg_table + (@as(u16, self.next_tile_id) << 4) + fine_y;
self.next_tile_lsb = self.readMemory(cartridge, pattern_addr);
},
6 => {
const bg_table: u16 = if ((self.ctrl & 0x10) != 0) 0x1000 else 0x0000;
const fine_y = (self.vram_addr >> 12) & 7;
const pattern_addr = bg_table + (@as(u16, self.next_tile_id) << 4) + fine_y + 8;
self.next_tile_msb = self.readMemory(cartridge, pattern_addr);
},
7 => {
self.incrementCoarseX();
},
else => {},
}
}
if (self.dot == 256) {
self.incrementY();
}
if (self.dot == 257) {
self.loadShiftRegisters();
self.copyHorizontal();
}
if (is_prerender_scanline and self.dot >= 280 and self.dot <= 304) {
self.copyVertical();
}
if (self.dot == 257 and is_visible_scanline) {
self.evaluateSprites(cartridge);
}
if (self.dot == 260 and is_visible_scanline) {
cartridge.handleScanline();
}
}
// --- Pixel Output ---
if (is_visible_scanline and self.dot >= 1 and self.dot <= 256) {
self.renderPixel(cartridge);
}
// --- Dot & Scanline Counters ---
self.dot += 1;
if (self.dot == 341) {
self.dot = 0;
self.scanline += 1;
if (self.scanline == 262) {
self.scanline = 0;
self.frame_number +%= 1;
self.frame_ready = true;
}
}
}
pub fn nmiAsserted(
self: *const Ppu,
) bool {
const nmi_enabled =
(self.ctrl & 0x80) != 0;
const in_vblank =
(self.status & 0x80) != 0;
return nmi_enabled and in_vblank;
}
fn shiftBackgroundRegisters(self: *Ppu) void {
if ((self.mask & 0x08) != 0) {
self.bg_shift_pattern_lo <<= 1;
self.bg_shift_pattern_hi <<= 1;
self.bg_shift_attrib_lo <<= 1;
self.bg_shift_attrib_hi <<= 1;
}
}
fn loadShiftRegisters(self: *Ppu) void {
self.bg_shift_pattern_lo = (self.bg_shift_pattern_lo & 0xff00) | self.next_tile_lsb;
self.bg_shift_pattern_hi = (self.bg_shift_pattern_hi & 0xff00) | self.next_tile_msb;
const attr_lo: u16 = if ((self.next_tile_attrib & 1) != 0) 0x00ff else 0x0000;
const attr_hi: u16 = if ((self.next_tile_attrib & 2) != 0) 0x00ff else 0x0000;
self.bg_shift_attrib_lo = (self.bg_shift_attrib_lo & 0xff00) | attr_lo;
self.bg_shift_attrib_hi = (self.bg_shift_attrib_hi & 0xff00) | attr_hi;
}
inline fn incrementCoarseX(self: *Ppu) void {
if ((self.vram_addr & 0x001f) == 31) {
self.vram_addr &= ~@as(u16, 0x001f);
self.vram_addr ^= 0x0400;
} else {
self.vram_addr += 1;
}
}
inline fn incrementY(self: *Ppu) void {
if ((self.vram_addr & 0x7000) != 0x7000) {
self.vram_addr += 0x1000;
} else {
self.vram_addr &= ~@as(u16, 0x7000);
var y = (self.vram_addr & 0x03e0) >> 5;
if (y == 29) {
y = 0;
self.vram_addr ^= 0x0800;
} else if (y == 31) {
y = 0;
} else {
y += 1;
}
self.vram_addr = (self.vram_addr & ~@as(u16, 0x03e0)) | (y << 5);
}
}
inline fn copyHorizontal(self: *Ppu) void {
self.vram_addr = (self.vram_addr & ~@as(u16, 0x041f)) | (self.temp_addr & 0x041f);
}
inline fn copyVertical(self: *Ppu) void {
self.vram_addr = (self.vram_addr & ~@as(u16, 0x7be0)) | (self.temp_addr & 0x7be0);
}
inline fn evaluateSprites(
self: *Ppu,
cartridge: *Cartridge,
) void {
self.sec_oam_count = 0;
const sprite_height: u16 = if ((self.ctrl & 0x20) != 0) 16 else 8;
var oam_idx: usize = 0;
var i: usize = 0;
while (i < 64) : ({
i += 1;
oam_idx += 4;
}) {
const sprite_y = @as(u16, self.oam[oam_idx]);
const diff = @as(i16, @intCast(self.scanline)) - @as(i16, @intCast(sprite_y));
if (diff >= 0 and diff < sprite_height) {
if (self.sec_oam_count < 8) {
const idx = self.sec_oam_count;
const tile = self.oam[oam_idx + 1];
const attr = self.oam[oam_idx + 2];
const x = self.oam[oam_idx + 3];
var row: u16 = @intCast(diff);
// Vertical flip
if ((attr & 0x80) != 0) {
row = (sprite_height - 1) - row;
}
var pattern_addr: u16 = 0;
if (sprite_height == 8) {
const pattern_table: u16 = if ((self.ctrl & 0x08) != 0) 0x1000 else 0x0000;
pattern_addr = pattern_table + (@as(u16, tile) << 4) + row;
} else {
// 8x16 mode
const pattern_table: u16 = if ((tile & 1) != 0) 0x1000 else 0x0000;
const tile_index: u16 = tile & 0xfe;
if (row < 8) {
pattern_addr = pattern_table + (tile_index << 4) + row;
} else {
pattern_addr = pattern_table + ((tile_index + 1) << 4) + (row - 8);
}
}
var pat_lo = self.readMemory(cartridge, pattern_addr);
var pat_hi = self.readMemory(cartridge, pattern_addr + 8);
// Horizontal flip
if ((attr & 0x40) != 0) {
pat_lo = @bitReverse(pat_lo);
pat_hi = @bitReverse(pat_hi);
}
self.sec_oam_pattern_lo[idx] = pat_lo;
self.sec_oam_pattern_hi[idx] = pat_hi;
self.sec_oam_attribute[idx] = attr;
self.sec_oam_x[idx] = x;
self.sec_oam_is_zero[idx] = (i == 0);
self.sec_oam_count += 1;
} else {
// Sprite overflow
self.status |= 0x20;
break;
}
}
}
}
fn renderPixel(
self: *Ppu,
_: *Cartridge,
) void {
const x = self.dot - 1;
var bg_pixel: u8 = 0;
var bg_palette: u8 = 0;
if ((self.mask & 0x08) != 0 and (x >= 8 or (self.mask & 0x02) != 0)) {
const bit_shift: u4 = 15 - @as(u4, self.fine_x);
const p1 = @as(u8, @truncate((self.bg_shift_pattern_lo >> bit_shift) & 1));
const p2 = @as(u8, @truncate((self.bg_shift_pattern_hi >> bit_shift) & 1));
bg_pixel = (p2 << 1) | p1;
const a1 = @as(u8, @truncate((self.bg_shift_attrib_lo >> bit_shift) & 1));
const a2 = @as(u8, @truncate((self.bg_shift_attrib_hi >> bit_shift) & 1));
bg_palette = (a2 << 1) | a1;
}
var fg_pixel: u8 = 0;
var fg_palette: u8 = 0;
var fg_priority: bool = false;
var is_sprite_zero: bool = false;
if (self.sec_oam_count > 0 and (self.mask & 0x10) != 0 and (x >= 8 or (self.mask & 0x04) != 0)) {
for (0..self.sec_oam_count) |i| {
const spr_x = self.sec_oam_x[i];
if (x >= spr_x and x < spr_x + 8) {
const offset: u3 = @intCast(x - spr_x);
const shift: u3 = 7 - offset;
const p1 = (self.sec_oam_pattern_lo[i] >> shift) & 1;
const p2 = (self.sec_oam_pattern_hi[i] >> shift) & 1;
const pixel = (p2 << 1) | p1;
if (pixel != 0) {
fg_pixel = pixel;
fg_palette = 4 + (self.sec_oam_attribute[i] & 0x03);
fg_priority = (self.sec_oam_attribute[i] & 0x20) != 0;
is_sprite_zero = self.sec_oam_is_zero[i];
break;
}
}
}
}
var final_palette_entry: u16 = 0;
if (bg_pixel == 0 and fg_pixel == 0) {
final_palette_entry = 0x00;
} else if (bg_pixel == 0 and fg_pixel != 0) {
final_palette_entry = @as(u16, fg_palette) * 4 + fg_pixel;
} else if (bg_pixel != 0 and fg_pixel == 0) {
final_palette_entry = @as(u16, bg_palette) * 4 + bg_pixel;
} else {
// Both bg and fg pixels present
if (is_sprite_zero and x < 255 and (self.mask & 0x18) == 0x18) {
self.status |= 0x40; // Sprite 0 hit
}
if (fg_priority) {
final_palette_entry = @as(u16, bg_palette) * 4 + bg_pixel;
} else {
final_palette_entry = @as(u16, fg_palette) * 4 + fg_pixel;
}
}
var color_idx = self.palette[mapPalette(0x3f00 + final_palette_entry)] & 0x3f;
if ((self.mask & 0x01) != 0) {
color_idx &= 0x30;
}
var pixel_rgb = nes_ntsc_palette_rgb565[color_idx];
const emphasis = (self.mask >> 5) & 7;
if (emphasis != 0) {
var r = (pixel_rgb >> 11) & 0x1F;
var g = (pixel_rgb >> 5) & 0x3F;
var b = pixel_rgb & 0x1F;
// Red emphasis (bit 5)
if ((emphasis & 1) != 0) {
g = (g * 3) / 4;
b = (b * 3) / 4;
}
// Green emphasis (bit 6)
if ((emphasis & 2) != 0) {
r = (r * 3) / 4;
b = (b * 3) / 4;
}
// Blue emphasis (bit 7)
if ((emphasis & 4) != 0) {
r = (r * 3) / 4;
g = (g * 3) / 4;
}
pixel_rgb = (@as(u16, r) << 11) | (@as(u16, g) << 5) | @as(u16, b);
}
self.framebuffer[self.scanline * 256 + x] = pixel_rgb;
}
fn incrementVramAddress(
self: *Ppu,
) void {
const increment: u16 =
if ((self.ctrl & 0x04) != 0)
32
else
1;
self.vram_addr =
(self.vram_addr +% increment) &
0x3fff;
}
fn readData(
self: *Ppu,
cartridge: *Cartridge,
) u8 {
const address =
self.vram_addr & 0x3fff;
const value: u8 =
if (address < 0x3f00) blk: {
const previous =
self.read_buffer;
self.read_buffer =
self.readMemory(
cartridge,
address,
);
break :blk previous;
} else blk: {
const result =
self.readMemory(
cartridge,
address,
);
// Palette reads aren't delayed, but
// still update the internal buffer.
self.read_buffer =
self.readMemory(
cartridge,
address - 0x1000,
);
break :blk result;
};
self.incrementVramAddress();
return value;
}
inline fn readMemory(
self: *Ppu,
cartridge: *Cartridge,
address_: u16,
) u8 {
const address =
address_ & 0x3fff;
return switch (address) {
0x0000...0x1fff => cartridge.ppuRead(address),
0x2000...0x3eff => self.nametable[
self.mapNametable(
cartridge.mirroring(),
address,
)
],
0x3f00...0x3fff => self.palette[
mapPalette(address)
],
else => unreachable,
};
}
inline fn writeMemory(
self: *Ppu,
cartridge: *Cartridge,
address_: u16,
value: u8,
) void {
const address =
address_ & 0x3fff;
switch (address) {
0x0000...0x1fff => cartridge.ppuWrite(
address,
value,
),
0x2000...0x3eff => self.nametable[
self.mapNametable(
cartridge.mirroring(),
address,
)
] = value,
0x3f00...0x3fff => self.palette[
mapPalette(address)
] = value,
else => unreachable,
}
}
inline fn mapNametable(
_: *const Ppu,
mirroring: Mirroring,
address: u16,
) usize {
const relative =
(@as(usize, address) - 0x2000) &
0x0fff;
const table =
relative >> 10;
const offset =
relative & 0x3ff;
const mapped_table: usize =
switch (mirroring) {
.vertical => switch (table) {
0, 2 => 0,
1, 3 => 1,
else => unreachable,
},
.horizontal => switch (table) {
0, 1 => 0,
2, 3 => 1,
else => unreachable,
},
.single_screen_lower => 0,
.single_screen_upper => 1,
.four_screen => table,
};
return mapped_table * 0x400 +
offset;
}
inline fn mapPalette(
address: u16,
) usize {
var index: usize =
(@as(usize, address) - 0x3f00) &
0x1f;
// Universal background color mirrors.
switch (index) {
0x10 => index = 0x00,
0x14 => index = 0x04,
0x18 => index = 0x08,
0x1c => index = 0x0c,
else => {},
}
return index;
}
test "NES PPU - Palette Mirroring & NTSC Palette LUT" {
try std.testing.expectEqual(@as(usize, 0), mapPalette(0x3F00));
try std.testing.expectEqual(@as(usize, 0), mapPalette(0x3F10));
try std.testing.expectEqual(@as(usize, 4), mapPalette(0x3F04));
try std.testing.expectEqual(@as(usize, 4), mapPalette(0x3F14));
// Verify system palette RGB565 LUT contains non-zero color constants
try std.testing.expectEqual(@as(u16, 0x52AA), nes_ntsc_palette_rgb565[0]);
try std.testing.expectEqual(@as(u16, 0xEF7D), nes_ntsc_palette_rgb565[0x20]);
}
test "NES PPU - Scroll Address Increments" {
var ppu = Ppu{};
ppu.reset();
ppu.vram_addr = 0x001F; // Coarse X = 31
ppu.incrementCoarseX();
try std.testing.expectEqual(@as(u16, 0x0400), ppu.vram_addr); // Coarse X = 0, switched nametable X
ppu.vram_addr = 0x73A0; // Coarse Y = 29, Fine Y = 7
ppu.incrementY();
try std.testing.expectEqual(@as(u16, 0x0800), ppu.vram_addr); // Coarse Y = 0, Fine Y = 0, switched nametable Y
}
-421
View File
@@ -1,421 +0,0 @@
const std = @import("std");
const contract = @import("contract");
const _cpu = @import("cpu");
const Bus = @import("bus.zig");
const Cartridge = @import("cartridge.zig");
const controller = @import("controller.zig");
pub const Button = controller.Button;
// Adjust this enum spelling to whatever your CPU package
// currently exports for the Ricoh 2A03.
const Cpu = _cpu.m6502.Cpu(
Bus,
.ricoh2a03,
.cycle,
);
const Nes = @This();
pub const spec = contract.SystemSpec{
.name = "Nintendo Entertainment System",
.video_outputs = &.{
.{
.max_width = 256,
.max_height = 240,
.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 = 32 * 1024,
},
};
bus: Bus = undefined,
cpu: Cpu = undefined,
/// Initialize in place.
///
/// `rom` must remain valid for the lifetime of this NES
/// because Cartridge stores slices into it.
pub fn init(
self: *Nes,
rom: []const u8,
) Cartridge.Error!void {
self.bus =
try Bus.init(rom);
self.cpu =
Cpu.init(&self.bus);
self.reset();
}
pub fn reset(
self: *Nes,
) void {
self.bus.reset();
self.cpu.reset();
}
pub fn step(
self: *Nes,
) ?u8 {
const cycles = self.cpu.step() orelse return null;
self.serviceOamDma();
return cycles;
}
pub fn runFrame(
self: *Nes,
) void {
self.bus.ppu.frame_ready = false;
self.bus.apu.sample_count = 0;
while (!self.bus.ppu.frame_ready) {
_ = self.step() orelse
return;
}
}
pub const conformance = @import("conformance.zig");
test {
_ = conformance;
}
comptime {
contract.validateSystem(Nes);
}
pub fn videoFrame(
self: *const Nes,
output: usize,
) contract.VideoFrame {
std.debug.assert(output == 0);
return .{
.data = std.mem.sliceAsBytes(
self.bus.ppu.framebuffer[0..],
),
.width = 256,
.height = 240,
.pitch = 256 * @sizeOf(u16),
.format = .rgb565,
.frame_number = self.bus.ppu.frame_number,
};
}
pub fn audioBuffer(
self: *const Nes,
output: usize,
) contract.AudioBuffer {
std.debug.assert(output == 0);
const samples = self.bus.apu.sample_buffer[0..self.bus.apu.sample_count];
return .{
.data = std.mem.sliceAsBytes(samples),
.frames = self.bus.apu.sample_count,
.sample_rate = 44_100,
.channels = 1,
.format = .i16,
};
}
pub fn setInput(
self: *Nes,
device: usize,
input: contract.DeviceInput,
) void {
std.debug.assert(device < 2);
self.bus.controllers[device]
.setButtons(
@truncate(input.buttons),
);
}
pub fn storageView(
self: *const Nes,
slot: usize,
) contract.StorageView {
std.debug.assert(slot == 0);
return .{
.data = &self.bus.cartridge.prg_ram,
.generation = 1,
};
}
pub fn loadStorage(
self: *Nes,
slot: usize,
data: []const u8,
) contract.StorageLoadError!void {
if (slot != 0) 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);
}
fn serviceOamDma(
self: *Nes,
) void {
const page =
self.bus.takeOamDmaPage() orelse return;
const odd_cycle =
(self.cpu.cycles & 1) != 0;
// Mandatory DMA halt cycle.
self.cpu.tick();
// Alignment cycle when necessary.
if (odd_cycle)
self.cpu.tick();
const base =
@as(u16, page) << 8;
for (0..256) |i| {
// DMA read cycle.
const value =
self.cpu.readCycle(
base |
@as(u16, @intCast(i)),
);
// DMA write cycle.
self.bus.ppu.writeOamDma(value);
self.cpu.tick();
}
}
pub fn saveState(
self: *const Nes,
buffer: []u8,
) contract.state.Error!usize {
var offset: usize = 0;
if (buffer.len < 8) return error.BufferTooSmall;
@memcpy(buffer[offset..][0..8], "6SOZNES1");
offset += 8;
// CPU
if (offset + 15 > buffer.len) return error.BufferTooSmall;
std.mem.writeInt(u16, buffer[offset..][0..2], self.cpu.registers.pc, .little);
offset += 2;
buffer[offset] = self.cpu.registers.a;
offset += 1;
buffer[offset] = self.cpu.registers.x;
offset += 1;
buffer[offset] = self.cpu.registers.y;
offset += 1;
buffer[offset] = self.cpu.registers.sp;
offset += 1;
buffer[offset] = @bitCast(self.cpu.registers.status);
offset += 1;
std.mem.writeInt(u64, buffer[offset..][0..8], self.cpu.cycles, .little);
offset += 8;
// Bus RAM & PRG RAM
if (offset + self.bus.ram.len + self.bus.cartridge.prg_ram.len > buffer.len) return error.BufferTooSmall;
@memcpy(buffer[offset..][0..self.bus.ram.len], &self.bus.ram);
offset += self.bus.ram.len;
@memcpy(buffer[offset..][0..self.bus.cartridge.prg_ram.len], &self.bus.cartridge.prg_ram);
offset += self.bus.cartridge.prg_ram.len;
if (self.bus.cartridge.chr_is_ram) {
if (offset + self.bus.cartridge.chr_ram.len > buffer.len) return error.BufferTooSmall;
@memcpy(buffer[offset..][0..self.bus.cartridge.chr_ram.len], &self.bus.cartridge.chr_ram);
offset += self.bus.cartridge.chr_ram.len;
}
// PPU State
const ppu_size = 1 + 1 + 1 + 1 + 2 + 2 + 1 + 1 + 2 + 2 + 4096 + 32 + 256;
if (offset + ppu_size > buffer.len) return error.BufferTooSmall;
buffer[offset] = self.bus.ppu.ctrl;
offset += 1;
buffer[offset] = self.bus.ppu.mask;
offset += 1;
buffer[offset] = self.bus.ppu.status;
offset += 1;
buffer[offset] = self.bus.ppu.oam_addr;
offset += 1;
std.mem.writeInt(u16, buffer[offset..][0..2], self.bus.ppu.vram_addr, .little);
offset += 2;
std.mem.writeInt(u16, buffer[offset..][0..2], self.bus.ppu.temp_addr, .little);
offset += 2;
buffer[offset] = self.bus.ppu.fine_x;
offset += 1;
buffer[offset] = if (self.bus.ppu.write_latch) 1 else 0;
offset += 1;
std.mem.writeInt(u16, buffer[offset..][0..2], self.bus.ppu.dot, .little);
offset += 2;
std.mem.writeInt(u16, buffer[offset..][0..2], self.bus.ppu.scanline, .little);
offset += 2;
@memcpy(buffer[offset..][0..4096], &self.bus.ppu.nametable);
offset += 4096;
@memcpy(buffer[offset..][0..32], &self.bus.ppu.palette);
offset += 32;
@memcpy(buffer[offset..][0..256], &self.bus.ppu.oam);
offset += 256;
return offset;
}
pub fn loadState(
self: *Nes,
buffer: []const u8,
) contract.state.Error!void {
var offset: usize = 0;
if (buffer.len < 8) return error.InvalidState;
if (!std.mem.eql(u8, buffer[0..8], "6SOZNES1")) return error.InvalidState;
offset += 8;
// CPU
if (offset + 15 > buffer.len) return error.InvalidState;
self.cpu.registers.pc = std.mem.readInt(u16, buffer[offset..][0..2], .little);
offset += 2;
self.cpu.registers.a = buffer[offset];
offset += 1;
self.cpu.registers.x = buffer[offset];
offset += 1;
self.cpu.registers.y = buffer[offset];
offset += 1;
self.cpu.registers.sp = buffer[offset];
offset += 1;
self.cpu.registers.status = @bitCast(buffer[offset]);
offset += 1;
self.cpu.cycles = std.mem.readInt(u64, buffer[offset..][0..8], .little);
offset += 8;
// Bus RAM & PRG RAM
if (offset + self.bus.ram.len + self.bus.cartridge.prg_ram.len > buffer.len) return error.InvalidState;
@memcpy(&self.bus.ram, buffer[offset..][0..self.bus.ram.len]);
offset += self.bus.ram.len;
@memcpy(&self.bus.cartridge.prg_ram, buffer[offset..][0..self.bus.cartridge.prg_ram.len]);
offset += self.bus.cartridge.prg_ram.len;
if (self.bus.cartridge.chr_is_ram) {
if (offset + self.bus.cartridge.chr_ram.len > buffer.len) return error.InvalidState;
@memcpy(&self.bus.cartridge.chr_ram, buffer[offset..][0..self.bus.cartridge.chr_ram.len]);
offset += self.bus.cartridge.chr_ram.len;
}
// PPU State
const ppu_size = 1 + 1 + 1 + 1 + 2 + 2 + 1 + 1 + 2 + 2 + 4096 + 32 + 256;
if (offset + ppu_size > buffer.len) return error.InvalidState;
self.bus.ppu.ctrl = buffer[offset];
offset += 1;
self.bus.ppu.mask = buffer[offset];
offset += 1;
self.bus.ppu.status = buffer[offset];
offset += 1;
self.bus.ppu.oam_addr = buffer[offset];
offset += 1;
self.bus.ppu.vram_addr = std.mem.readInt(u16, buffer[offset..][0..2], .little);
offset += 2;
self.bus.ppu.temp_addr = std.mem.readInt(u16, buffer[offset..][0..2], .little);
offset += 2;
self.bus.ppu.fine_x = @truncate(buffer[offset]);
offset += 1;
self.bus.ppu.write_latch = buffer[offset] != 0;
offset += 1;
self.bus.ppu.dot = std.mem.readInt(u16, buffer[offset..][0..2], .little);
offset += 2;
self.bus.ppu.scanline = std.mem.readInt(u16, buffer[offset..][0..2], .little);
offset += 2;
@memcpy(&self.bus.ppu.nametable, buffer[offset..][0..4096]);
offset += 4096;
@memcpy(&self.bus.ppu.palette, buffer[offset..][0..32]);
offset += 32;
@memcpy(&self.bus.ppu.oam, buffer[offset..][0..256]);
offset += 256;
}
test "NES System - Save State Serialization Roundtrip" {
// Construct dummy iNES NROM header + 16K PRG + 8K CHR
var rom: [16 + 0x4000 + 0x2000]u8 = [_]u8{0} ** (16 + 0x4000 + 0x2000);
@memcpy(rom[0..4], "NES\x1a");
rom[4] = 1; // 16K PRG
rom[5] = 1; // 8K CHR
var nes: Nes = undefined;
try nes.init(&rom);
// Mutate state
nes.cpu.registers.pc = 0x1234;
nes.cpu.registers.a = 0x42;
nes.bus.ram[0x05] = 0xAA;
nes.bus.ppu.vram_addr = 0x2050;
var save_buf: [32768]u8 = undefined;
const bytes_written = try nes.saveState(&save_buf);
// Reset nes
nes.reset();
try std.testing.expect(nes.cpu.registers.pc != 0x1234);
// Load state
try nes.loadState(save_buf[0..bytes_written]);
try std.testing.expectEqual(@as(u16, 0x1234), nes.cpu.registers.pc);
try std.testing.expectEqual(@as(u8, 0x42), nes.cpu.registers.a);
try std.testing.expectEqual(@as(u8, 0xAA), nes.bus.ram[0x05]);
try std.testing.expectEqual(@as(u16, 0x2050), nes.bus.ppu.vram_addr);
}
comptime {
contract.validateSystem(Nes);
}