Remove nes implementation so that we can implement it in a better way from scratch.
This commit is contained in:
@@ -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);
|
|
||||||
}
|
|
||||||
@@ -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
@@ -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);
|
|
||||||
}
|
|
||||||
@@ -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;
|
|
||||||
}
|
|
||||||
@@ -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;
|
|
||||||
}
|
|
||||||
@@ -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;
|
|
||||||
}
|
|
||||||
@@ -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;
|
|
||||||
}
|
|
||||||
@@ -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;
|
|
||||||
}
|
|
||||||
@@ -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;
|
|
||||||
}
|
|
||||||
@@ -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;
|
|
||||||
}
|
|
||||||
@@ -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();
|
|
||||||
}
|
|
||||||
@@ -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();
|
|
||||||
}
|
|
||||||
@@ -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;
|
|
||||||
}
|
|
||||||
@@ -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;
|
|
||||||
}
|
|
||||||
@@ -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;
|
|
||||||
}
|
|
||||||
@@ -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;
|
|
||||||
}
|
|
||||||
@@ -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;
|
|
||||||
}
|
|
||||||
@@ -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;
|
|
||||||
}
|
|
||||||
@@ -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;
|
|
||||||
}
|
|
||||||
@@ -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;
|
|
||||||
}
|
|
||||||
@@ -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;
|
|
||||||
}
|
|
||||||
@@ -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;
|
|
||||||
}
|
|
||||||
@@ -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;
|
|
||||||
}
|
|
||||||
@@ -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;
|
|
||||||
}
|
|
||||||
@@ -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;
|
|
||||||
}
|
|
||||||
@@ -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;
|
|
||||||
}
|
|
||||||
@@ -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;
|
|
||||||
}
|
|
||||||
@@ -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;
|
|
||||||
}
|
|
||||||
@@ -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;
|
|
||||||
}
|
|
||||||
@@ -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;
|
|
||||||
}
|
|
||||||
@@ -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;
|
|
||||||
}
|
|
||||||
@@ -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;
|
|
||||||
}
|
|
||||||
@@ -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;
|
|
||||||
}
|
|
||||||
@@ -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;
|
|
||||||
}
|
|
||||||
@@ -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;
|
|
||||||
}
|
|
||||||
@@ -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 => {},
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
|
||||||
@@ -1,9 +0,0 @@
|
|||||||
pub const Mirroring = enum {
|
|
||||||
horizontal,
|
|
||||||
vertical,
|
|
||||||
four_screen,
|
|
||||||
|
|
||||||
// Needed by later mappers.
|
|
||||||
single_screen_lower,
|
|
||||||
single_screen_upper,
|
|
||||||
};
|
|
||||||
@@ -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;
|
|
||||||
}
|
|
||||||
@@ -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;
|
|
||||||
}
|
|
||||||
@@ -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;
|
|
||||||
}
|
|
||||||
@@ -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;
|
|
||||||
}
|
|
||||||
@@ -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;
|
|
||||||
}
|
|
||||||
@@ -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;
|
|
||||||
}
|
|
||||||
@@ -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;
|
|
||||||
}
|
|
||||||
@@ -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
|
|
||||||
}
|
|
||||||
@@ -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);
|
|
||||||
}
|
|
||||||
|
|||||||
Reference in New Issue
Block a user