Compare commits

..
12 Commits
82 changed files with 7317 additions and 6692 deletions
+30
View File
@@ -0,0 +1,30 @@
name: CI
on:
push:
pull_request:
jobs:
test:
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Setup Zig
uses: https://codeberg.org/mlugg/setup-zig@v2
with:
version: master
- name: Zig version
run: zig version
- name: Test
run: zig build test --summary all
- name: Headless release build
run: zig build -Dheadless=true -Doptimize=ReleaseSafe
- name: Release build
run: zig build -Doptimize=ReleaseSafe
+4 -3
View File
@@ -2,9 +2,10 @@ shell: |
set -euxo pipefail
echo "=== Radicle CI ==="
echo "Repository:"
pwd
echo "Repository: $(pwd)"
zig version
# test suite & release builds
zig build test --summary all
zig build -Dheadless=true -Doptimize=ReleaseSafe
zig build -Doptimize=ReleaseSafe
+1 -1
View File
@@ -1,4 +1,4 @@
# 6soz
# 6soz - Retro Game Emulator
A 6502 / 6510 CPU emulator engine, disassembler, and system architecture library written in [Zig](https://ziglang.org/).
+5 -10
View File
@@ -85,16 +85,11 @@ pub fn build(b: *std.Build) void {
const test_step = b.step("test", "Run 6soz tests");
const test_options = b.addOptions();
if (b.lazyDependency("nes_test_roms", .{})) |roms_dep| {
test_options.addOption(?[]const u8, "nes_test_roms_dir", roms_dep.path("").getPath(b));
} else {
test_options.addOption(?[]const u8, "nes_test_roms_dir", null);
}
if (b.lazyDependency("m6502_functional_tests", .{})) |func_dep| {
test_options.addOption(?[]const u8, "m6502_functional_tests_dir", func_dep.path("").getPath(b));
} else {
test_options.addOption(?[]const u8, "m6502_functional_tests_dir", null);
}
const roms_dep = b.dependency("nes_test_roms", .{});
test_options.addOption(?[]const u8, "nes_test_roms_dir", roms_dep.path("").getPath(b));
const func_dep = b.dependency("m6502_functional_tests", .{});
test_options.addOption(?[]const u8, "m6502_functional_tests_dir", func_dep.path("").getPath(b));
const options_mod = test_options.createModule();
-2
View File
@@ -6,12 +6,10 @@
.nes_test_roms = .{
.url = "https://github.com/christopherpow/nes-test-roms/archive/refs/heads/master.tar.gz",
.hash = "N-V-__8AANPxaAENPnXYuDVuqgUcOdrux1zKqz8bZZYf0RA2",
.lazy = true,
},
.m6502_functional_tests = .{
.url = "https://github.com/Klaus2m5/6502_65C02_functional_tests/archive/refs/heads/master.tar.gz",
.hash = "N-V-__8AAKmLHACkwIMCMRdqnsyLtL5hbQdi8cm3nGs8n-O7",
.lazy = true,
},
.raylib = .{
.url = "https://github.com/raysan5/raylib/archive/refs/heads/master.tar.gz",
Binary file not shown.
+128 -3
View File
@@ -1,5 +1,12 @@
const std = @import("std");
const Nes = @import("system").nes;
const platform = @import("platform");
const frontend = @import("frontend");
const PressSpec = struct {
frame: u64,
key: platform.Key,
};
pub fn main(init: std.process.Init) !void {
const allocator = init.gpa;
@@ -44,10 +51,13 @@ pub fn main(init: std.process.Init) !void {
const rom_path = args[2];
var target_frames: u64 = 60;
var dump_frame_path: ?[]const u8 = null;
var dump_audio_path: ?[]const u8 = null;
var save_state_path: ?[]const u8 = null;
var load_state_path: ?[]const u8 = null;
var save_sram_path: ?[]const u8 = null;
var load_sram_path: ?[]const u8 = null;
var press_spec: ?PressSpec = null;
var i: usize = 3;
while (i < args_count) : (i += 1) {
@@ -55,6 +65,39 @@ pub fn main(init: std.process.Init) !void {
if (std.mem.eql(u8, arg, "--frames") and i + 1 < args_count) {
i += 1;
target_frames = try std.fmt.parseInt(u64, args[i], 10);
} else if (std.mem.eql(u8, arg, "--press-at") and i + 1 < args_count) {
i += 1;
const val = args[i];
if (std.mem.indexOfScalar(u8, val, ':')) |colon| {
const frame_str = val[0..colon];
const btn_str = val[colon + 1 ..];
const frame_num = std.fmt.parseInt(u64, frame_str, 10) catch 0;
var key: platform.Key = .enter;
if (std.mem.eql(u8, btn_str, "a")) {
key = .z;
} else if (std.mem.eql(u8, btn_str, "b")) {
key = .x;
} else if (std.mem.eql(u8, btn_str, "select")) {
key = .right_shift;
} else if (std.mem.eql(u8, btn_str, "start") or std.mem.eql(u8, btn_str, "enter")) {
key = .enter;
} else if (std.mem.eql(u8, btn_str, "up")) {
key = .up;
} else if (std.mem.eql(u8, btn_str, "down")) {
key = .down;
} else if (std.mem.eql(u8, btn_str, "left")) {
key = .left;
} else if (std.mem.eql(u8, btn_str, "right")) {
key = .right;
}
press_spec = .{ .frame = frame_num, .key = key };
}
} else if (std.mem.eql(u8, arg, "--dump-frame") and i + 1 < args_count) {
i += 1;
dump_frame_path = args[i];
} else if (std.mem.eql(u8, arg, "--dump-audio") and i + 1 < args_count) {
i += 1;
dump_audio_path = args[i];
} else if (std.mem.eql(u8, arg, "--save-state") and i + 1 < args_count) {
i += 1;
save_state_path = args[i];
@@ -104,19 +147,33 @@ pub fn main(init: std.process.Init) !void {
}
}
const frontend = @import("frontend");
const platform = @import("platform");
const nes_bindings = frontend.Bindings{
.keys = &.{
// Primary A / B buttons
.{ .key = .z, .target = .{ .button = .{ .device = 0, .button = 0 } } },
.{ .key = .x, .target = .{ .button = .{ .device = 0, .button = 1 } } },
.{ .key = .c, .target = .{ .button = .{ .device = 0, .button = 0 } } },
.{ .key = .v, .target = .{ .button = .{ .device = 0, .button = 1 } } },
.{ .key = .k, .target = .{ .button = .{ .device = 0, .button = 0 } } },
.{ .key = .j, .target = .{ .button = .{ .device = 0, .button = 1 } } },
// Select & Start
.{ .key = .right_shift, .target = .{ .button = .{ .device = 0, .button = 2 } } },
.{ .key = .left_shift, .target = .{ .button = .{ .device = 0, .button = 2 } } },
.{ .key = .enter, .target = .{ .button = .{ .device = 0, .button = 3 } } },
.{ .key = .space, .target = .{ .button = .{ .device = 0, .button = 3 } } },
// D-Pad: Arrow keys & WASD
.{ .key = .up, .target = .{ .button = .{ .device = 0, .button = 4 } } },
.{ .key = .down, .target = .{ .button = .{ .device = 0, .button = 5 } } },
.{ .key = .left, .target = .{ .button = .{ .device = 0, .button = 6 } } },
.{ .key = .right, .target = .{ .button = .{ .device = 0, .button = 7 } } },
.{ .key = .w, .target = .{ .button = .{ .device = 0, .button = 4 } } },
.{ .key = .s, .target = .{ .button = .{ .device = 0, .button = 5 } } },
.{ .key = .a, .target = .{ .button = .{ .device = 0, .button = 6 } } },
.{ .key = .d, .target = .{ .button = .{ .device = 0, .button = 7 } } },
// System actions
.{ .key = .f5, .target = .{ .action = .quick_save } },
.{ .key = .f8, .target = .{ .action = .quick_load } },
.{ .key = .p, .target = .{ .action = .toggle_pause } },
@@ -143,9 +200,23 @@ pub fn main(init: std.process.Init) !void {
std.debug.print("[6soz] Running NES emulator for {d} frames...\n", .{target_frames});
const start_time = std.Io.Clock.Timestamp.now(init.io, .awake);
var audio_accumulator: std.ArrayListUnmanaged(i16) = .empty;
defer audio_accumulator.deinit(allocator);
var frame: u64 = 0;
while (frame < target_frames and app.state == .running) : (frame += 1) {
if (press_spec) |spec| {
if (frame >= spec.frame and frame < spec.frame + 20) {
app.handleEvent(.{ .key_down = .{ .key = spec.key } });
} else if (frame == spec.frame + 20) {
app.handleEvent(.{ .key_up = .{ .key = spec.key } });
}
}
app.tickFrame(&driver);
if (dump_audio_path != null) {
const pcm = nes.bus.apu.getSamples();
try audio_accumulator.appendSlice(allocator, pcm);
}
}
const end_time = std.Io.Clock.Timestamp.now(init.io, .awake);
@@ -157,6 +228,57 @@ pub fn main(init: std.process.Init) !void {
std.debug.print(" - Frames: {d}\n", .{target_frames});
std.debug.print(" - Elapsed Time: {d:.3}s\n", .{elapsed_sec});
std.debug.print(" - Performance: {d:.1} FPS\n", .{fps});
if (dump_audio_path) |path| {
const num_samples: u32 = @truncate(audio_accumulator.items.len);
const data_bytes: u32 = num_samples * 2;
const total_file_size: u32 = 44 + data_bytes;
const wav_buf = try allocator.alloc(u8, total_file_size);
defer allocator.free(wav_buf);
@memcpy(wav_buf[0..4], "RIFF");
std.mem.writeInt(u32, wav_buf[4..8], 36 + data_bytes, .little);
@memcpy(wav_buf[8..12], "WAVE");
@memcpy(wav_buf[12..16], "fmt ");
std.mem.writeInt(u32, wav_buf[16..20], 16, .little);
std.mem.writeInt(u16, wav_buf[20..22], 1, .little);
std.mem.writeInt(u16, wav_buf[22..24], 1, .little);
std.mem.writeInt(u32, wav_buf[24..28], 44100, .little);
std.mem.writeInt(u32, wav_buf[28..32], 44100 * 2, .little);
std.mem.writeInt(u16, wav_buf[32..34], 2, .little);
std.mem.writeInt(u16, wav_buf[34..36], 16, .little);
@memcpy(wav_buf[36..40], "data");
std.mem.writeInt(u32, wav_buf[40..44], data_bytes, .little);
@memcpy(wav_buf[44..total_file_size], std.mem.sliceAsBytes(audio_accumulator.items));
cwd_dir.writeFile(init.io, .{ .sub_path = path, .data = wav_buf }) catch |err| {
std.debug.print("Error writing dumped audio to '{s}': {s}\n", .{ path, @errorName(err) });
};
std.debug.print("[6soz] Dumped audio ({d} samples, {d:.2}s) to '{s}'\n", .{
num_samples,
@as(f64, @floatFromInt(num_samples)) / 44100.0,
path,
});
}
}
if (dump_frame_path) |path| {
const vf = nes.videoFrame(0);
const raw_pixels = std.mem.bytesAsSlice(u16, vf.data);
var ppm_buf: [32 + 256 * 240 * 3]u8 = undefined;
const header = "P6\n256 240\n255\n";
@memcpy(ppm_buf[0..header.len], header);
var offset: usize = header.len;
for (raw_pixels) |p| {
ppm_buf[offset] = @truncate(((p >> 11) & 0x1f) * 255 / 31);
ppm_buf[offset + 1] = @truncate(((p >> 5) & 0x3f) * 255 / 63);
ppm_buf[offset + 2] = @truncate((p & 0x1f) * 255 / 31);
offset += 3;
}
cwd_dir.writeFile(init.io, .{ .sub_path = path, .data = ppm_buf[0..offset] }) catch |err| {
std.debug.print("Error writing dumped frame to '{s}': {s}\n", .{ path, @errorName(err) });
};
std.debug.print("[6soz] Dumped frame to '{s}'\n", .{path});
}
if (save_state_path) |path| {
@@ -189,6 +311,9 @@ fn printUsage() void {
\\
\\Options:
\\ --frames <N> Number of frames to execute (default: 60)
\\ --press-at <F>:<B> Press button (a, b, select, start, etc.) at frame F for 20 frames
\\ --dump-frame <file> Dump final video frame to PPM image file
\\ --dump-audio <file> Dump audio output to standard WAV file
\\ --save-state <file> Save state binary file output
\\ --load-state <file> Load state binary file input
\\ --save-sram <file> Save battery PRG RAM output
+52 -3
View File
@@ -16,9 +16,10 @@ pub const Driver = struct {
audio_ring_head: usize = 0,
audio_ring_tail: usize = 0,
event_queue: [16]platform.Event = undefined,
event_queue: [64]platform.Event = undefined,
event_head: usize = 0,
event_tail: usize = 0,
polled_this_frame: bool = false,
pub fn init(title: [*:0]const u8, width: i32, height: i32, scale: i32) Driver {
c.InitWindow(width * scale, height * scale, title);
@@ -63,10 +64,22 @@ pub const Driver = struct {
return ev;
}
if (self.polled_this_frame) {
return null;
}
self.polled_this_frame = true;
self.event_head = 0;
self.event_tail = 0;
// Poll Raylib Key Events
const key_mappings = [_]struct { c_key: c_int, key: platform.Key }{
.{ .c_key = c.KEY_Z, .key = .z },
.{ .c_key = c.KEY_X, .key = .x },
.{ .c_key = c.KEY_C, .key = .c },
.{ .c_key = c.KEY_V, .key = .v },
.{ .c_key = c.KEY_J, .key = .j },
.{ .c_key = c.KEY_K, .key = .k },
.{ .c_key = c.KEY_SPACE, .key = .space },
.{ .c_key = c.KEY_RIGHT_SHIFT, .key = .right_shift },
.{ .c_key = c.KEY_LEFT_SHIFT, .key = .left_shift },
.{ .c_key = c.KEY_ENTER, .key = .enter },
@@ -74,6 +87,10 @@ pub const Driver = struct {
.{ .c_key = c.KEY_DOWN, .key = .down },
.{ .c_key = c.KEY_LEFT, .key = .left },
.{ .c_key = c.KEY_RIGHT, .key = .right },
.{ .c_key = c.KEY_W, .key = .w },
.{ .c_key = c.KEY_S, .key = .s },
.{ .c_key = c.KEY_A, .key = .a },
.{ .c_key = c.KEY_D, .key = .d },
.{ .c_key = c.KEY_F5, .key = .f5 },
.{ .c_key = c.KEY_F8, .key = .f8 },
.{ .c_key = c.KEY_P, .key = .p },
@@ -81,13 +98,43 @@ pub const Driver = struct {
for (key_mappings) |mapping| {
if (c.IsKeyPressed(mapping.c_key)) {
return .{ .key_down = .{ .key = mapping.key } };
const next = (self.event_head + 1) % self.event_queue.len;
if (next != self.event_tail) {
self.event_queue[self.event_head] = .{ .key_down = .{ .key = mapping.key } };
self.event_head = next;
}
}
if (c.IsKeyReleased(mapping.c_key)) {
return .{ .key_up = .{ .key = mapping.key } };
const next = (self.event_head + 1) % self.event_queue.len;
if (next != self.event_tail) {
self.event_queue[self.event_head] = .{ .key_up = .{ .key = mapping.key } };
self.event_head = next;
}
}
}
// Support mouse left click as Start/Action button
if (c.IsMouseButtonPressed(c.MOUSE_BUTTON_LEFT)) {
const next = (self.event_head + 1) % self.event_queue.len;
if (next != self.event_tail) {
self.event_queue[self.event_head] = .{ .key_down = .{ .key = .enter } };
self.event_head = next;
}
}
if (c.IsMouseButtonReleased(c.MOUSE_BUTTON_LEFT)) {
const next = (self.event_head + 1) % self.event_queue.len;
if (next != self.event_tail) {
self.event_queue[self.event_head] = .{ .key_up = .{ .key = .enter } };
self.event_head = next;
}
}
if (self.event_head != self.event_tail) {
const ev = self.event_queue[self.event_tail];
self.event_tail = (self.event_tail + 1) % self.event_queue.len;
return ev;
}
return null;
}
@@ -116,6 +163,8 @@ pub const Driver = struct {
.height = @floatFromInt(self.height * self.scale),
};
c.DrawTexturePro(self.texture, src, dest, c.Vector2{ .x = 0, .y = 0 }, 0.0, c.WHITE);
self.polled_this_frame = false;
}
pub fn queueAudio(self: *Driver, output: usize, buffer: contract.AudioBuffer) void {
-276
View File
@@ -1,276 +0,0 @@
const std = @import("std");
const Apu = @This();
pub const sample_rate: u32 = 44_100;
sample_buffer: [2048]i16 = [_]i16{0} ** 2048,
sample_count: usize = 0,
cycle_counter: u32 = 0,
// Pulse 1
pulse1_enabled: bool = false,
pulse1_timer_reload: u11 = 0,
pulse1_timer: u11 = 0,
pulse1_duty: u2 = 0,
pulse1_duty_pos: u3 = 0,
pulse1_volume: u4 = 0,
// Pulse 2
pulse2_enabled: bool = false,
pulse2_timer_reload: u11 = 0,
pulse2_timer: u11 = 0,
pulse2_duty: u2 = 0,
pulse2_duty_pos: u3 = 0,
pulse2_volume: u4 = 0,
// Triangle
triangle_enabled: bool = false,
triangle_timer_reload: u11 = 0,
triangle_timer: u11 = 0,
triangle_seq_pos: u5 = 0,
// Noise
noise_enabled: bool = false,
noise_timer_reload: u12 = 0,
noise_timer: u12 = 0,
noise_lfsr: u16 = 1,
noise_volume: u4 = 0,
// DMC
dmc_enabled: bool = false,
dmc_loop: bool = false,
dmc_rate_index: u4 = 0,
dmc_timer: u12 = 0,
dmc_timer_reload: u12 = 0,
dmc_output_level: u7 = 0,
dmc_sample_address: u16 = 0xC000,
dmc_sample_length: u16 = 1,
dmc_current_address: u16 = 0xC000,
dmc_bytes_remaining: u16 = 0,
dmc_shift_register: u8 = 0,
dmc_bits_remaining: u4 = 0,
dmc_silence: bool = true,
const duty_tables: [4][8]u8 = .{
.{ 0, 1, 0, 0, 0, 0, 0, 0 }, // 12.5%
.{ 0, 1, 1, 0, 0, 0, 0, 0 }, // 25%
.{ 0, 1, 1, 1, 1, 0, 0, 0 }, // 50%
.{ 1, 0, 0, 1, 1, 1, 1, 1 }, // 75%
};
const triangle_table: [32]u4 = .{
15, 14, 13, 12, 11, 10, 9, 8, 7, 6, 5, 4, 3, 2, 1, 0,
0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15,
};
const noise_periods: [16]u12 = .{
4, 8, 16, 32, 64, 96, 128, 160, 202, 254, 380, 508, 762, 1016, 2034, 4068,
};
const dmc_periods: [16]u12 = .{
428, 380, 340, 320, 286, 254, 226, 214, 190, 160, 142, 128, 106, 84, 72, 54,
};
pub fn reset(self: *Apu) void {
self.sample_count = 0;
self.cycle_counter = 0;
self.pulse1_enabled = false;
self.pulse2_enabled = false;
self.triangle_enabled = false;
self.noise_enabled = false;
self.noise_lfsr = 1;
self.dmc_enabled = false;
self.dmc_output_level = 0;
}
pub inline fn tick(self: *Apu) void {
// Pulse 1 timer
if (self.pulse1_timer == 0) {
self.pulse1_timer = self.pulse1_timer_reload;
self.pulse1_duty_pos +%= 1;
} else {
self.pulse1_timer -= 1;
}
// Pulse 2 timer
if (self.pulse2_timer == 0) {
self.pulse2_timer = self.pulse2_timer_reload;
self.pulse2_duty_pos +%= 1;
} else {
self.pulse2_timer -= 1;
}
// Triangle timer
if (self.triangle_timer == 0) {
self.triangle_timer = self.triangle_timer_reload;
self.triangle_seq_pos +%= 1;
} else {
self.triangle_timer -= 1;
}
// Noise timer
if (self.noise_timer == 0) {
self.noise_timer = self.noise_timer_reload;
const feedback = (self.noise_lfsr & 1) ^ ((self.noise_lfsr >> 1) & 1);
self.noise_lfsr = (self.noise_lfsr >> 1) | (feedback << 14);
} else {
self.noise_timer -= 1;
}
// DMC timer
if (self.dmc_timer == 0) {
self.dmc_timer = self.dmc_timer_reload;
if (!self.dmc_silence) {
if ((self.dmc_shift_register & 1) != 0) {
if (self.dmc_output_level <= 125) self.dmc_output_level += 2;
} else {
if (self.dmc_output_level >= 2) self.dmc_output_level -= 2;
}
self.dmc_shift_register >>= 1;
}
if (self.dmc_bits_remaining > 0) {
self.dmc_bits_remaining -= 1;
}
if (self.dmc_bits_remaining == 0) {
self.dmc_silence = true;
}
} else {
self.dmc_timer -= 1;
}
// Sample downsampling at ~44.1 kHz (every ~40 CPU cycles)
self.cycle_counter += 1;
if (self.cycle_counter >= 40) {
self.cycle_counter = 0;
if (self.sample_count < self.sample_buffer.len) {
const p1_val: i32 = if (self.pulse1_enabled and duty_tables[self.pulse1_duty][self.pulse1_duty_pos] != 0 and self.pulse1_timer_reload > 8)
@as(i32, self.pulse1_volume)
else
0;
const p2_val: i32 = if (self.pulse2_enabled and duty_tables[self.pulse2_duty][self.pulse2_duty_pos] != 0 and self.pulse2_timer_reload > 8)
@as(i32, self.pulse2_volume)
else
0;
const tri_val: i32 = if (self.triangle_enabled and self.triangle_timer_reload > 2)
@as(i32, triangle_table[self.triangle_seq_pos])
else
0;
const noise_val: i32 = if (self.noise_enabled and (self.noise_lfsr & 1) == 0)
@as(i32, self.noise_volume)
else
0;
const dmc_val: i32 = if (self.dmc_enabled) @as(i32, self.dmc_output_level) else 0;
const sample_i32 = (p1_val + p2_val) * 400 + tri_val * 350 + noise_val * 300 + dmc_val * 150;
self.sample_buffer[self.sample_count] = @intCast(std.math.clamp(sample_i32, -32000, 32000));
self.sample_count += 1;
}
}
}
pub fn write(self: *Apu, address: u16, value: u8) void {
switch (address) {
// Pulse 1
0x4000 => {
self.pulse1_duty = @truncate((value >> 6) & 3);
self.pulse1_volume = @truncate(value & 0x0f);
},
0x4002 => {
self.pulse1_timer_reload = (self.pulse1_timer_reload & 0x0700) | value;
},
0x4003 => {
self.pulse1_timer_reload = (self.pulse1_timer_reload & 0x00ff) | (@as(u11, value & 7) << 8);
self.pulse1_duty_pos = 0;
},
// Pulse 2
0x4004 => {
self.pulse2_duty = @truncate((value >> 6) & 3);
self.pulse2_volume = @truncate(value & 0x0f);
},
0x4006 => {
self.pulse2_timer_reload = (self.pulse2_timer_reload & 0x0700) | value;
},
0x4007 => {
self.pulse2_timer_reload = (self.pulse2_timer_reload & 0x00ff) | (@as(u11, value & 7) << 8);
self.pulse2_duty_pos = 0;
},
// Triangle
0x400a => {
self.triangle_timer_reload = (self.triangle_timer_reload & 0x0700) | value;
},
0x400b => {
self.triangle_timer_reload = (self.triangle_timer_reload & 0x00ff) | (@as(u11, value & 7) << 8);
},
// Noise
0x400c => {
self.noise_volume = @truncate(value & 0x0f);
},
0x400e => {
self.noise_timer_reload = noise_periods[value & 0x0f];
},
// DMC
0x4010 => {
self.dmc_loop = (value & 0x40) != 0;
self.dmc_rate_index = @truncate(value & 0x0f);
self.dmc_timer_reload = dmc_periods[self.dmc_rate_index];
},
0x4011 => {
self.dmc_output_level = @truncate(value & 0x7f);
},
0x4012 => {
self.dmc_sample_address = 0xC000 + (@as(u16, value) << 6);
},
0x4013 => {
self.dmc_sample_length = (@as(u16, value) << 4) + 1;
},
else => {},
}
}
pub fn readStatus(_: *Apu) u8 {
return 0;
}
pub fn writeStatus(self: *Apu, value: u8) void {
self.pulse1_enabled = (value & 0x01) != 0;
self.pulse2_enabled = (value & 0x02) != 0;
self.triangle_enabled = (value & 0x04) != 0;
self.noise_enabled = (value & 0x08) != 0;
self.dmc_enabled = (value & 0x10) != 0;
}
pub fn writeFrameCounter(_: *Apu, _: u8) void {}
pub fn irqAsserted(_: *const Apu) bool {
return false;
}
test "NES APU - Sound Synthesis & Sample Generation" {
var apu = Apu{};
apu.reset();
// Enable Pulse 1 and Pulse 2
apu.writeStatus(0x03);
// Setup Pulse 1 volume and frequency
apu.write(0x4000, 0xbf); // 50% duty, max volume 15
apu.write(0x4002, 100);
apu.write(0x4003, 0);
// Tick APU for 100 cycles to produce PCM samples
for (0..100) |_| {
apu.tick();
}
try std.testing.expect(apu.sample_count > 0);
}
+877
View File
@@ -0,0 +1,877 @@
const std = @import("std");
pub const Dmc = @This();
pub const Region = @import("../common.zig").Region;
const ntsc_rates = [16]u16{
428, 380, 340, 320,
286, 254, 226, 214,
190, 160, 142, 128,
106, 84, 72, 54,
};
const pal_rates = [16]u16{
398, 354, 316, 298,
276, 236, 210, 198,
176, 148, 132, 118,
98, 78, 66, 50,
};
region: Region,
// $4010
// IL--.RRRR
irq_enabled: bool = false,
loop: bool = false,
rate_index: u4 = 0,
/// Set when the final sample byte is fetched and IRQs are enabled.
irq_flag: bool = false,
// $4011
// -DDD.DDDD
/// 7-bit DAC output level.
///
/// This is always sent to the mixer, even if DMC playback is disabled.
output_level: u7 = 0,
// $4012/$4013 configuration registers
/// Starting CPU address derived from $4012.
sample_address: u16 = 0xC000,
/// Number of bytes derived from $4013.
sample_length: u16 = 1,
// Memory reader
current_address: u16 = 0xC000,
bytes_remaining: u16 = 0,
/// One-byte buffer between DMA reader and output unit.
sample_buffer: ?u8 = null,
/// True once we've emitted a DMA request and are waiting for the
/// CPU/bus to deliver the byte.
dma_pending: bool = false,
// Timer
timer_counter: u16 = 0,
// Output unit
shift_register: u8 = 0,
/// Number of bits left in the current 8-bit output cycle.
bits_remaining: u4 = 8,
/// When true, timer clocks do not modify output_level.
///
/// The shifter and bit counter still continue to clock.
silence: bool = true,
pub fn init(region: Region) Dmc {
return .{
.region = region,
};
}
// Register writes
/// $4010
///
/// IL--.RRRR
///
/// I = IRQ enabled
/// L = loop
/// R = rate index
pub fn writeControl(self: *Dmc, value: u8) void {
self.irq_enabled = (value & 0x80) != 0;
self.loop = (value & 0x40) != 0;
self.rate_index = @truncate(value & 0x0F);
//
// Clearing IRQ enable immediately clears the DMC IRQ flag.
//
if (!self.irq_enabled) {
self.irq_flag = false;
}
//
// Do NOT reset timer_counter here.
//
}
/// $4011
///
/// Directly sets the 7-bit DAC.
pub fn writeDirectLoad(self: *Dmc, value: u8) void {
self.output_level = @truncate(value & 0x7F);
}
/// $4012
///
/// address = $C000 + value * 64
///
/// = %11AAAAAA.AA000000
pub fn writeSampleAddress(self: *Dmc, value: u8) void {
self.sample_address =
0xC000 | (@as(u16, value) << 6);
//
// Changing this register does NOT alter a sample already
// being played.
//
}
/// $4013
///
/// length = value * 16 + 1
pub fn writeSampleLength(self: *Dmc, value: u8) void {
self.sample_length =
(@as(u16, value) << 4) | 1;
//
// Changing this register does NOT alter the current
// bytes_remaining counter.
//
}
// $4015
/// Called when $4015 is written.
///
/// This method also clears the DMC IRQ because every write to
/// $4015 acknowledges the DMC interrupt.
pub fn writeEnabled(self: *Dmc, enabled: bool) void {
self.irq_flag = false;
if (!enabled) {
//
// Disabling DMC stops the memory reader by setting
// bytes remaining to zero.
//
// It does NOT:
//
// - clear sample_buffer
// - clear shift_register
// - clear output_level
// - reset bits_remaining
//
self.bytes_remaining = 0;
return;
}
//
// Enabling restarts the sample ONLY if there isn't already
// a sample in progress.
//
if (self.bytes_remaining == 0) {
self.restartSample();
}
}
/// Bit 4 returned by $4015.
///
/// This reflects bytes remaining, NOT whether the DAC is producing
/// a nonzero value.
pub fn active(self: *const Dmc) bool {
return self.bytes_remaining != 0;
}
pub fn irqAsserted(self: *const Dmc) bool {
return self.irq_flag;
}
// Sample restart
fn restartSample(self: *Dmc) void {
self.current_address = self.sample_address;
self.bytes_remaining = self.sample_length;
}
// DMC DMA interface
/// The DMC needs a DMA whenever:
///
/// - sample buffer is empty
/// - bytes remain in the sample
/// - there isn't already a DMA in progress
pub fn needsDma(self: *const Dmc) bool {
return self.sample_buffer == null and
self.bytes_remaining != 0 and
!self.dma_pending;
}
/// Start a DMA request.
///
/// Returns the CPU address that must be read.
///
/// The CPU/Bus layer should:
///
/// 1. stall the CPU appropriately
/// 2. read this address
/// 3. call completeDma(value)
///
pub fn beginDma(self: *Dmc) ?u16 {
if (!self.needsDma())
return null;
self.dma_pending = true;
return self.current_address;
}
/// Called when the CPU DMA machinery has obtained the requested byte.
pub fn completeDma(self: *Dmc, value: u8) void {
std.debug.assert(self.dma_pending);
self.dma_pending = false;
//
// Reader fills the one-byte sample buffer.
//
self.sample_buffer = value;
//
// Advance sample address.
//
// DMC has a special wrap:
//
// $FFFF -> $8000
//
// NOT:
//
// $FFFF -> $0000
//
if (self.current_address == 0xFFFF) {
self.current_address = 0x8000;
} else {
self.current_address += 1;
}
std.debug.assert(self.bytes_remaining != 0);
self.bytes_remaining -= 1;
if (self.bytes_remaining == 0) {
if (self.loop) {
//
// Looping immediately reloads address/length.
//
// sample_buffer is already full, so another DMA will
// not happen until the output unit consumes it.
//
self.restartSample();
} else if (self.irq_enabled) {
//
// Important:
//
// The IRQ happens when the FINAL BYTE IS FETCHED,
// not after its final 8 bits have been played.
//
self.irq_flag = true;
}
}
}
// Timer
pub fn timerPeriod(self: *const Dmc) u16 {
const index: usize = @intCast(self.rate_index);
return switch (self.region) {
.ntsc => ntsc_rates[index],
.pal => pal_rates[index],
};
}
/// Clock once per CPU cycle.
pub fn clockTimer(self: *Dmc) void {
if (self.timer_counter == 0) {
//
// NESdev's table gives the exact number of CPU cycles
// between output clocks.
//
// Therefore reload with period - 1.
//
self.timer_counter = self.timerPeriod() - 1;
self.clockOutputUnit();
} else {
self.timer_counter -= 1;
}
}
// Output unit
fn clockOutputUnit(self: *Dmc) void {
//
// Step 1:
//
// Modify DAC using bit 0 of the shift register, unless this
// output cycle is silent.
//
if (!self.silence) {
if ((self.shift_register & 0x01) != 0) {
//
// Delta bit 1 => +2
//
// Don't wrap beyond 127.
//
if (self.output_level <= 125) {
self.output_level += 2;
}
} else {
//
// Delta bit 0 => -2
//
// Don't wrap below zero.
//
if (self.output_level >= 2) {
self.output_level -= 2;
}
}
}
//
// Step 2:
//
// Shift regardless of silence.
//
self.shift_register >>= 1;
//
// Step 3:
//
// Advance bit counter.
//
self.bits_remaining -= 1;
//
// An output cycle consists of exactly 8 timer clocks.
//
if (self.bits_remaining == 0) {
self.startOutputCycle();
}
}
fn startOutputCycle(self: *Dmc) void {
self.bits_remaining = 8;
if (self.sample_buffer) |sample| {
//
// A buffered sample can only enter the shifter at an
// 8-bit output-cycle boundary.
//
self.shift_register = sample;
self.sample_buffer = null;
self.silence = false;
//
// sample_buffer has just become empty.
//
// needsDma() will now become true if there are more bytes.
//
} else {
//
// No sample available.
//
// The output unit continues ticking, but it stops modifying
// the DAC.
//
self.silence = true;
}
}
// Mixer
/// Current DMC mixer input.
///
/// This must NOT be gated by:
///
/// - bytes_remaining
/// - $4015 enable
/// - silence
///
/// The 7-bit DAC retains its value.
pub fn output(self: *const Dmc) u7 {
return self.output_level;
}
// Tests
test "$4010 decodes IRQ loop and rate" {
var dmc = Dmc.init(.ntsc);
dmc.writeControl(0b1100_1010);
try std.testing.expect(dmc.irq_enabled);
try std.testing.expect(dmc.loop);
try std.testing.expectEqual(
@as(u4, 10),
dmc.rate_index,
);
}
test "clearing IRQ enable clears IRQ flag" {
var dmc = Dmc.init(.ntsc);
dmc.irq_flag = true;
dmc.writeControl(0x00);
try std.testing.expect(!dmc.irq_flag);
}
test "$4011 directly sets output level" {
var dmc = Dmc.init(.ntsc);
dmc.writeDirectLoad(0x55);
try std.testing.expectEqual(
@as(u7, 0x55),
dmc.output(),
);
}
test "$4011 ignores bit seven" {
var dmc = Dmc.init(.ntsc);
dmc.writeDirectLoad(0xFF);
try std.testing.expectEqual(
@as(u7, 127),
dmc.output(),
);
}
test "$4012 sample address calculation" {
var dmc = Dmc.init(.ntsc);
dmc.writeSampleAddress(0x00);
try std.testing.expectEqual(
@as(u16, 0xC000),
dmc.sample_address,
);
dmc.writeSampleAddress(0x01);
try std.testing.expectEqual(
@as(u16, 0xC040),
dmc.sample_address,
);
dmc.writeSampleAddress(0xFF);
try std.testing.expectEqual(
@as(u16, 0xFFC0),
dmc.sample_address,
);
}
test "$4013 sample length calculation" {
var dmc = Dmc.init(.ntsc);
dmc.writeSampleLength(0x00);
try std.testing.expectEqual(
@as(u16, 1),
dmc.sample_length,
);
dmc.writeSampleLength(0x01);
try std.testing.expectEqual(
@as(u16, 17),
dmc.sample_length,
);
dmc.writeSampleLength(0xFF);
try std.testing.expectEqual(
@as(u16, 4081),
dmc.sample_length,
);
}
test "enabling starts sample if no bytes remain" {
var dmc = Dmc.init(.ntsc);
dmc.writeSampleAddress(0x20);
dmc.writeSampleLength(0x02);
dmc.writeEnabled(true);
try std.testing.expectEqual(
dmc.sample_address,
dmc.current_address,
);
try std.testing.expectEqual(
dmc.sample_length,
dmc.bytes_remaining,
);
}
test "enabling does not restart active sample" {
var dmc = Dmc.init(.ntsc);
dmc.current_address = 0xD123;
dmc.bytes_remaining = 5;
dmc.writeEnabled(true);
try std.testing.expectEqual(
@as(u16, 0xD123),
dmc.current_address,
);
try std.testing.expectEqual(
@as(u16, 5),
dmc.bytes_remaining,
);
}
test "disabling clears bytes remaining" {
var dmc = Dmc.init(.ntsc);
dmc.bytes_remaining = 123;
dmc.writeEnabled(false);
try std.testing.expectEqual(
@as(u16, 0),
dmc.bytes_remaining,
);
}
test "disabling does not clear DAC output" {
var dmc = Dmc.init(.ntsc);
dmc.output_level = 70;
dmc.bytes_remaining = 10;
dmc.writeEnabled(false);
try std.testing.expectEqual(
@as(u7, 70),
dmc.output(),
);
}
test "$4015 write clears DMC IRQ" {
var dmc = Dmc.init(.ntsc);
dmc.irq_flag = true;
dmc.writeEnabled(true);
try std.testing.expect(!dmc.irq_flag);
}
test "active is based only on bytes remaining" {
var dmc = Dmc.init(.ntsc);
dmc.bytes_remaining = 0;
dmc.sample_buffer = 0xAA;
try std.testing.expect(!dmc.active());
dmc.bytes_remaining = 1;
try std.testing.expect(dmc.active());
}
test "DMC requests DMA when buffer is empty" {
var dmc = Dmc.init(.ntsc);
dmc.current_address = 0xC123;
dmc.bytes_remaining = 4;
try std.testing.expect(dmc.needsDma());
const address = dmc.beginDma();
try std.testing.expectEqual(
@as(?u16, 0xC123),
address,
);
try std.testing.expect(dmc.dma_pending);
try std.testing.expect(!dmc.needsDma());
}
test "DMA fills buffer and advances reader" {
var dmc = Dmc.init(.ntsc);
dmc.current_address = 0xC123;
dmc.bytes_remaining = 4;
_ = dmc.beginDma();
dmc.completeDma(0xAB);
try std.testing.expectEqual(
@as(?u8, 0xAB),
dmc.sample_buffer,
);
try std.testing.expectEqual(
@as(u16, 0xC124),
dmc.current_address,
);
try std.testing.expectEqual(
@as(u16, 3),
dmc.bytes_remaining,
);
try std.testing.expect(!dmc.dma_pending);
}
test "DMA address wraps $FFFF to $8000" {
var dmc = Dmc.init(.ntsc);
dmc.current_address = 0xFFFF;
dmc.bytes_remaining = 2;
_ = dmc.beginDma();
dmc.completeDma(0x00);
try std.testing.expectEqual(
@as(u16, 0x8000),
dmc.current_address,
);
}
test "final fetch raises IRQ" {
var dmc = Dmc.init(.ntsc);
dmc.irq_enabled = true;
dmc.loop = false;
dmc.current_address = 0xC000;
dmc.bytes_remaining = 1;
_ = dmc.beginDma();
dmc.completeDma(0xAA);
try std.testing.expectEqual(
@as(u16, 0),
dmc.bytes_remaining,
);
try std.testing.expect(dmc.irq_flag);
}
test "loop restarts sample instead of raising IRQ" {
var dmc = Dmc.init(.ntsc);
dmc.sample_address = 0xD000;
dmc.sample_length = 17;
dmc.current_address = 0xD010;
dmc.bytes_remaining = 1;
dmc.loop = true;
dmc.irq_enabled = true;
_ = dmc.beginDma();
dmc.completeDma(0xAA);
try std.testing.expectEqual(
@as(u16, 0xD000),
dmc.current_address,
);
try std.testing.expectEqual(
@as(u16, 17),
dmc.bytes_remaining,
);
try std.testing.expect(!dmc.irq_flag);
}
test "sample buffer prevents another DMA" {
var dmc = Dmc.init(.ntsc);
dmc.bytes_remaining = 10;
dmc.sample_buffer = 0xAA;
try std.testing.expect(!dmc.needsDma());
}
test "NTSC rate table" {
var dmc = Dmc.init(.ntsc);
dmc.rate_index = 0;
try std.testing.expectEqual(
@as(u16, 428),
dmc.timerPeriod(),
);
dmc.rate_index = 15;
try std.testing.expectEqual(
@as(u16, 54),
dmc.timerPeriod(),
);
}
test "PAL rate table" {
var dmc = Dmc.init(.pal);
dmc.rate_index = 0;
try std.testing.expectEqual(
@as(u16, 398),
dmc.timerPeriod(),
);
dmc.rate_index = 15;
try std.testing.expectEqual(
@as(u16, 50),
dmc.timerPeriod(),
);
}
test "delta one increases output by two" {
var dmc = Dmc.init(.ntsc);
dmc.output_level = 50;
dmc.shift_register = 0b0000_0001;
dmc.bits_remaining = 8;
dmc.silence = false;
dmc.clockOutputUnit();
try std.testing.expectEqual(
@as(u7, 52),
dmc.output_level,
);
}
test "delta zero decreases output by two" {
var dmc = Dmc.init(.ntsc);
dmc.output_level = 50;
dmc.shift_register = 0;
dmc.bits_remaining = 8;
dmc.silence = false;
dmc.clockOutputUnit();
try std.testing.expectEqual(
@as(u7, 48),
dmc.output_level,
);
}
test "delta increment does not overflow" {
var dmc = Dmc.init(.ntsc);
dmc.output_level = 126;
dmc.shift_register = 1;
dmc.bits_remaining = 8;
dmc.silence = false;
dmc.clockOutputUnit();
try std.testing.expectEqual(
@as(u7, 126),
dmc.output_level,
);
}
test "delta decrement does not underflow" {
var dmc = Dmc.init(.ntsc);
dmc.output_level = 1;
dmc.shift_register = 0;
dmc.bits_remaining = 8;
dmc.silence = false;
dmc.clockOutputUnit();
try std.testing.expectEqual(
@as(u7, 1),
dmc.output_level,
);
}
test "silent cycle does not change DAC" {
var dmc = Dmc.init(.ntsc);
dmc.output_level = 50;
dmc.shift_register = 1;
dmc.bits_remaining = 8;
dmc.silence = true;
dmc.clockOutputUnit();
try std.testing.expectEqual(
@as(u7, 50),
dmc.output_level,
);
}
test "output shifter shifts right" {
var dmc = Dmc.init(.ntsc);
dmc.shift_register = 0b1010_1011;
dmc.bits_remaining = 8;
dmc.silence = false;
dmc.clockOutputUnit();
try std.testing.expectEqual(
@as(u8, 0b0101_0101),
dmc.shift_register,
);
}
test "new output cycle consumes sample buffer" {
var dmc = Dmc.init(.ntsc);
dmc.bits_remaining = 1;
dmc.sample_buffer = 0b1010_1010;
dmc.clockOutputUnit();
try std.testing.expectEqual(
@as(u4, 8),
dmc.bits_remaining,
);
try std.testing.expectEqual(
@as(u8, 0b1010_1010),
dmc.shift_register,
);
try std.testing.expectEqual(
@as(?u8, null),
dmc.sample_buffer,
);
try std.testing.expect(!dmc.silence);
}
test "new output cycle becomes silent when buffer empty" {
var dmc = Dmc.init(.ntsc);
dmc.bits_remaining = 1;
dmc.sample_buffer = null;
dmc.silence = false;
dmc.clockOutputUnit();
try std.testing.expectEqual(
@as(u4, 8),
dmc.bits_remaining,
);
try std.testing.expect(dmc.silence);
}
+132
View File
@@ -0,0 +1,132 @@
const std = @import("std");
const Envelope = @This();
/// Bit 5: envelope loop / length-counter halt.
loop: bool = false,
/// Bit 4: select constant volume instead of decay level.
constant_volume: bool = false,
/// Bits 3-0.
///
/// When constant_volume is true this is the output volume.
/// Otherwise this is the divider reload value.
volume: u4 = 0,
/// Set by writes to:
///
/// Pulse 1: $4003
/// Pulse 2: $4007
/// Noise: $400F
start: bool = false,
/// Envelope divider counter.
///
/// Reloaded with `volume`.
divider: u4 = 0,
/// Current envelope volume, 0...15.
decay_level: u4 = 0,
pub fn init() Envelope {
return .{};
}
/// Write the envelope portion of:
///
/// $4000 - Pulse 1
/// $4004 - Pulse 2
/// $400C - Noise
///
/// Register:
///
/// --LC.VVVV
///
/// L = loop envelope / halt length counter
/// C = constant volume
/// V = volume / envelope divider period
pub fn write(self: *Envelope, value: u8) void {
self.loop = (value & 0x20) != 0;
self.constant_volume = (value & 0x10) != 0;
self.volume = @truncate(value);
}
/// Writing $4003/$4007/$400F does not immediately restart the
/// envelope.
///
/// Instead it sets this flag. The restart occurs at the next
/// quarter-frame clock.
pub fn restart(self: *Envelope) void {
self.start = true;
}
/// Clocked by the APU frame counter on every quarter-frame.
pub fn clockQuarterFrame(self: *Envelope) void {
//
// If the start flag is set:
//
// 1. clear start
// 2. set decay to 15
// 3. reload divider
//
if (self.start) {
self.start = false;
self.decay_level = 15;
self.divider = self.volume;
return;
}
//
// Clock the divider.
//
if (self.divider != 0) {
self.divider -= 1;
return;
}
//
// Divider reached zero:
//
// 1. reload divider
// 2. clock decay counter
//
self.divider = self.volume;
if (self.decay_level != 0) {
self.decay_level -= 1;
} else if (self.loop) {
self.decay_level = 15;
}
}
/// Current 4-bit volume sent to the channel's output gate.
pub fn output(self: *const Envelope) u4 {
return if (self.constant_volume)
self.volume
else
self.decay_level;
}
// consolidated envelope generator unit test suite
test "envelope register write, quarter-frame clocking, decay and loop" {
var envelope = Envelope.init();
envelope.write(0b0010_1010); // loop=1, const_vol=0, vol=10
try std.testing.expect(envelope.loop and !envelope.constant_volume);
// Restart & quarter-frame clocking
envelope.write(0b0000_0010);
envelope.restart();
try std.testing.expect(envelope.start);
envelope.clockQuarterFrame();
try std.testing.expect(!envelope.start and envelope.decay_level == 15 and envelope.divider == 2);
// Decay countdown & loop
envelope.write(0b0010_0000); // loop enabled, vol=0
envelope.restart();
envelope.clockQuarterFrame();
for (0..15) |_| envelope.clockQuarterFrame();
try std.testing.expectEqual(@as(u4, 0), envelope.decay_level);
envelope.clockQuarterFrame();
try std.testing.expectEqual(@as(u4, 15), envelope.decay_level); // wrapped back
}
+237
View File
@@ -0,0 +1,237 @@
const std = @import("std");
const Region = @import("../common.zig").Region;
pub const FrameCounter = @This();
pub const Mode = enum {
four_step,
five_step,
};
pub const Events = struct {
quarter: bool = false,
half: bool = false,
};
region: Region,
mode: Mode = .four_step,
pending_mode: Mode = .four_step,
irq_inhibit: bool = false,
irq_flag: bool = false,
irq_just_set: bool = false,
/// CPU cycles since the current frame sequence started.
cycle: u32 = 0,
/// $4017 reset delay.
///
/// Hardware applies the reset after either 3 or 4 CPU cycles.
reset_delay: u3 = 0,
pub fn init(region: Region) FrameCounter {
return .{
.region = region,
};
}
/// Write $4017:
///
/// MI--.----
///
/// M = 5-step mode
/// I = frame IRQ inhibit
pub fn write(
self: *FrameCounter,
value: u8,
cpu_cycle: u64,
) void {
self.pending_mode = if ((value & 0x80) != 0)
.five_step
else
.four_step;
self.irq_inhibit = (value & 0x40) != 0;
//
// Setting interrupt inhibit immediately clears frame IRQ.
//
if (self.irq_inhibit) {
self.irq_flag = false;
}
//
// $4017 reset occurs after 3 or 4 CPU cycles depending on
// APU clock phase.
//
// This assumes even cpu_cycle values are our APU-cycle phase.
//
// If your CPU core defines cycle parity oppositely, flip this.
//
self.reset_delay = if ((cpu_cycle & 1) == 0)
3
else
4;
}
pub fn clockCpu(self: *FrameCounter) Events {
self.irq_just_set = false;
var events = self.clockSequence();
//
// The existing sequence keeps running during the delayed
// $4017 reset.
//
if (self.reset_delay != 0) {
self.reset_delay -= 1;
if (self.reset_delay == 0) {
self.mode = self.pending_mode;
self.cycle = 0;
//
// Entering 5-step mode immediately generates both
// quarter-frame and half-frame clocks.
//
if (self.mode == .five_step) {
events.quarter = true;
events.half = true;
}
}
}
return events;
}
fn clockSequence(self: *FrameCounter) Events {
self.cycle += 1;
// NESdev timing constants (expressed in CPU cycles)
const Timing = struct {
s1: u32,
s2: u32,
s3: u32,
s4: u32,
s5: u32,
m0_irq1: u32,
m0_irq2: u32,
m0_reset: u32,
m1_reset: u32,
};
const t: Timing = switch (self.region) {
.ntsc => .{
.s1 = 7457,
.s2 = 14913,
.s3 = 22371,
.s4 = 29829,
.s5 = 37281,
.m0_irq1 = 29828,
.m0_irq2 = 29829,
.m0_reset = 29830,
.m1_reset = 37282,
},
.pal => .{
.s1 = 8313,
.s2 = 16627,
.s3 = 24939,
.s4 = 33253,
.s5 = 41565,
.m0_irq1 = 33252,
.m0_irq2 = 33253,
.m0_reset = 33254,
.m1_reset = 41566,
},
};
var events = Events{};
switch (self.mode) {
.four_step => {
if (self.cycle == t.s1) {
events.quarter = true;
} else if (self.cycle == t.s2) {
events.quarter = true;
events.half = true;
} else if (self.cycle == t.s3) {
events.quarter = true;
} else if (self.cycle == t.m0_irq1) {
if (!self.irq_inhibit) {
self.irq_flag = true;
self.irq_just_set = true;
}
} else if (self.cycle == t.m0_irq2) {
events.quarter = true;
events.half = true;
if (!self.irq_inhibit) {
self.irq_flag = true;
self.irq_just_set = true;
}
} else if (self.cycle >= t.m0_reset) {
if (!self.irq_inhibit) {
self.irq_flag = true;
self.irq_just_set = true;
}
self.cycle = 0;
}
},
.five_step => {
if (self.cycle == t.s1) {
events.quarter = true;
} else if (self.cycle == t.s2) {
events.quarter = true;
events.half = true;
} else if (self.cycle == t.s3) {
events.quarter = true;
} else if (self.cycle == t.s5) {
events.quarter = true;
events.half = true;
} else if (self.cycle >= t.m1_reset) {
self.cycle = 0;
}
},
}
return events;
}
pub fn reset(self: *FrameCounter) void {
self.mode = .four_step;
self.pending_mode = .four_step;
self.irq_inhibit = false;
self.irq_flag = false;
self.irq_just_set = false;
self.cycle = 0;
self.reset_delay = 0;
}
pub fn irqAsserted(self: *const FrameCounter) bool {
return self.irq_flag;
}
pub fn clearIrq(self: *FrameCounter) void {
// simultaneous read and IRQ set does not clear flag
if (!self.irq_just_set) {
self.irq_flag = false;
}
}
// consolidated frame counter unit test suite
test "frame counter 4-step and 5-step mode timings, irq window and write delays" {
var fc = FrameCounter.init(.ntsc);
// 4-step mode timings and IRQ window
for (0..29828) |_| _ = fc.clockCpu();
try std.testing.expect(fc.irqAsserted());
fc.clearIrq(); // simultaneous set/read check
try std.testing.expect(fc.irqAsserted()); // remains set until next cycle
// 5-step mode
fc = FrameCounter.init(.ntsc);
fc.write(0x80, 0); // 5-step write
for (0..4) |_| _ = fc.clockCpu();
try std.testing.expect(!fc.irqAsserted());
try std.testing.expectEqual(Mode.five_step, fc.mode);
}
+78
View File
@@ -0,0 +1,78 @@
const std = @import("std");
const LengthCounter = @This();
pub const table = [32]u8{
10, 254, 20, 2,
40, 4, 80, 6,
160, 8, 60, 10,
14, 12, 26, 14,
12, 16, 24, 18,
48, 20, 96, 22,
192, 24, 72, 26,
16, 28, 32, 30,
};
enabled: bool = false,
value: u8 = 0,
pub fn init() LengthCounter {
return .{};
}
/// Corresponds to this channel's bit in $4015.
///
/// Clearing the enable bit immediately clears the length counter.
/// Setting it does not reload the counter.
pub fn setEnabled(self: *LengthCounter, enabled: bool) void {
self.enabled = enabled;
if (!enabled) {
self.value = 0;
}
}
/// Load from bits 7-3 of $4003/$4007.
///
/// The counter may only be loaded while the channel is enabled
/// through $4015.
pub fn load(self: *LengthCounter, index: u5) void {
if (!self.enabled)
return;
self.value = table[@intCast(index)];
}
/// Clocked on a half-frame.
pub fn clock(self: *LengthCounter, halt: bool) void {
if (self.value != 0 and !halt) {
self.value -= 1;
}
}
pub fn active(self: *const LengthCounter) bool {
return self.value != 0;
}
// consolidated length counter unit test suite
test "length counter enable, loading, immediate clearing, clocking and halt" {
try std.testing.expectEqual(@as(u8, 10), table[0]);
var counter = LengthCounter.init();
counter.load(0);
try std.testing.expectEqual(@as(u8, 0), counter.value);
counter.setEnabled(true);
counter.load(0);
try std.testing.expectEqual(@as(u8, 10), counter.value);
counter.clock(false); // decrement
try std.testing.expectEqual(@as(u8, 9), counter.value);
counter.clock(true); // halt prevents decrement
try std.testing.expectEqual(@as(u8, 9), counter.value);
counter.setEnabled(false); // clears immediately
try std.testing.expectEqual(@as(u8, 0), counter.value);
}
+175
View File
@@ -0,0 +1,175 @@
const std = @import("std");
const Envelope = @import("envelope.zig");
const LengthCounter = @import("length_counter.zig");
const Region = @import("../common.zig").Region;
pub const Noise = @This();
const ntsc_periods = [16]u16{
4, 8, 16, 32,
64, 96, 128, 160,
202, 254, 380, 508,
762, 1016, 2034, 4068,
};
const pal_periods = [16]u16{
4, 8, 14, 30,
60, 88, 118, 148,
188, 236, 354, 472,
708, 944, 1890, 3778,
};
region: Region,
envelope: Envelope = .{},
length_counter: LengthCounter = .{},
/// $400E bit 7.
mode: bool = false,
/// $400E bits 3..0.
period_index: u4 = 0,
/// CPU-cycle timer.
timer_counter: u16 = 0,
/// 15-bit LFSR.
///
/// Must not be initialized to zero or it will remain there forever.
shift_register: u15 = 1,
pub fn init(region: Region) Noise {
return .{
.region = region,
};
}
// $400C
// --LC.VVVV
pub fn writeControl(self: *Noise, value: u8) void {
self.envelope.write(value);
}
// $400E
// M---.PPPP
pub fn writePeriod(self: *Noise, value: u8) void {
self.mode = (value & 0x80) != 0;
self.period_index = @truncate(value & 0x0f);
}
// $400F
// LLLL.L---
pub fn writeLength(self: *Noise, value: u8) void {
const index: u5 = @truncate(value >> 3);
self.length_counter.load(index);
self.envelope.restart();
}
// $4015
pub fn setEnabled(self: *Noise, enabled: bool) void {
self.length_counter.setEnabled(enabled);
}
pub fn active(self: *const Noise) bool {
return self.length_counter.active();
}
// Frame sequencer
pub fn clockQuarterFrame(self: *Noise) void {
self.envelope.clockQuarterFrame();
}
pub fn clockHalfFrame(self: *Noise) void {
self.length_counter.clock(self.envelope.loop);
}
// Timer
pub fn timerPeriod(self: *const Noise) u16 {
const index: usize = @intCast(self.period_index);
return switch (self.region) {
.ntsc => ntsc_periods[index],
.pal => pal_periods[index],
};
}
/// Called every CPU cycle.
pub fn clockCpu(self: *Noise) void {
if (self.timer_counter == 0) {
self.timer_counter = self.timerPeriod() - 1;
self.clockShiftRegister();
} else {
self.timer_counter -= 1;
}
}
fn clockShiftRegister(self: *Noise) void {
const tap: u4 = if (self.mode) 6 else 1;
const feedback: u1 = @truncate(
(self.shift_register ^
(self.shift_register >> tap)) & 1,
);
self.shift_register >>= 1;
self.shift_register |= @as(u15, feedback) << 14;
}
// Output
pub fn output(self: *const Noise) u4 {
if (!self.length_counter.active())
return 0;
//
// Noise channel is muted whenever LFSR bit 0 is 1.
//
if ((self.shift_register & 1) != 0)
return 0;
return self.envelope.output();
}
pub fn reset(self: *Noise) void {
self.shift_register = 1;
self.timer_counter = 0;
self.period_index = 0;
self.mode = false;
self.length_counter.setEnabled(false);
}
// consolidated noise unit test suite
test "noise channel control, period, length and LFSR shift register" {
var noise = Noise.init(.ntsc);
noise.writeControl(0b0011_1010);
try std.testing.expect(noise.envelope.constant_volume);
try std.testing.expectEqual(@as(u4, 10), noise.envelope.volume);
noise.writePeriod(0b1000_0101); // Mode 1, period index 5
try std.testing.expect(noise.mode);
try std.testing.expectEqual(@as(u16, 96), noise.timerPeriod());
noise.setEnabled(true);
noise.writeLength(0b0000_1000);
try std.testing.expect(noise.length_counter.active());
// Output gating by bit 0
noise.shift_register = 1;
try std.testing.expectEqual(@as(u4, 0), noise.output());
noise.shift_register = 2;
try std.testing.expectEqual(@as(u4, 10), noise.output());
// Shift register mode 0 vs mode 1
noise.shift_register = 1;
noise.mode = false;
noise.clockShiftRegister();
try std.testing.expectEqual(@as(u15, 0x4000), noise.shift_register);
}
+301
View File
@@ -0,0 +1,301 @@
const std = @import("std");
const Envelope = @import("envelope.zig");
const Sweep = @import("sweep.zig");
const LengthCounter = @import("length_counter.zig");
pub const Pulse = @This();
pub const Channel = Sweep.Channel;
/// Internal lookup table.
///
/// The hardware sequencer starts at index 0 and counts DOWN:
///
/// 0, 7, 6, 5, 4, 3, 2, 1
///
/// Producing:
///
/// duty 0: 0 1 0 0 0 0 0 0
/// duty 1: 0 1 1 0 0 0 0 0
/// duty 2: 0 1 1 1 1 0 0 0
/// duty 3: 1 0 0 1 1 1 1 1
const duty_table = [4][8]u1{
.{ 0, 0, 0, 0, 0, 0, 0, 1 },
.{ 0, 0, 0, 0, 0, 0, 1, 1 },
.{ 0, 0, 0, 0, 1, 1, 1, 1 },
.{ 1, 1, 1, 1, 1, 1, 0, 0 },
};
channel: Channel,
envelope: Envelope = .{},
sweep: Sweep,
length_counter: LengthCounter = .{},
/// DD from $4000/$4004.
duty: u2 = 0,
/// Internal 8-step sequencer position.
///
/// Counts downward.
sequence_position: u3 = 0,
/// 11-bit period from:
///
/// HHHLLLLLLLL
///
/// We intentionally keep this as u16 because the sweep target
/// calculation needs to detect values > $7FF.
timer_period: u16 = 0,
/// Current timer divider value.
///
/// This is separate from timer_period because writing $4003/$4007
/// does NOT reset this divider.
timer_counter: u16 = 0,
pub fn init(channel: Channel) Pulse {
return .{
.channel = channel,
.sweep = Sweep.init(channel),
};
}
// $4015
/// Set this pulse channel's enable bit from $4015.
pub fn setEnabled(self: *Pulse, enabled: bool) void {
self.length_counter.setEnabled(enabled);
}
/// Used when constructing $4015 reads.
pub fn active(self: *const Pulse) bool {
return self.length_counter.active();
}
// $4000 / $4004
// DDLC.VVVV
pub fn writeControl(self: *Pulse, value: u8) void {
self.duty = @truncate(value >> 6);
self.envelope.write(value);
// Important:
//
// Changing duty does NOT reset sequence_position.
}
// $4001 / $4005
// EPPP.NSSS
pub fn writeSweep(self: *Pulse, value: u8) void {
self.sweep.write(value);
}
// $4002 / $4006
// LLLL.LLLL
pub fn writeTimerLow(self: *Pulse, value: u8) void {
self.timer_period =
(self.timer_period & 0x0700) |
@as(u16, value);
}
// $4003 / $4007
// LLLL.LHHH
pub fn writeTimerHigh(self: *Pulse, value: u8) void {
//
// Replace high 3 timer bits.
//
self.timer_period =
(self.timer_period & 0x00ff) |
(@as(u16, value & 0x07) << 8);
//
// Load length counter from bits 7-3.
//
// LengthCounter itself checks whether $4015 has enabled
// this channel.
//
const length_index: u5 = @truncate(value >> 3);
self.length_counter.load(length_index);
//
// Restart waveform sequencer immediately.
//
self.sequence_position = 0;
//
// Restart envelope on next quarter-frame clock.
//
self.envelope.restart();
//
// IMPORTANT:
//
// Do NOT modify timer_counter here.
//
// $4003/$4007 reset waveform phase but don't reset the
// period divider.
//
}
// Timer
/// Clock once per APU cycle.
///
/// For the NES pulse channels:
///
/// 1 APU cycle = 2 CPU cycles
///
/// This means the caller should invoke this every second CPU cycle.
pub fn clockTimer(self: *Pulse) void {
if (self.timer_counter == 0) {
//
// Timer runs:
//
// t, t-1, ... 1, 0
//
// and then reloads with t.
//
self.timer_counter = self.timer_period;
//
// Hardware sequencer counts downward.
//
self.sequence_position -%= 1;
} else {
self.timer_counter -= 1;
}
}
// Frame sequencer
//
/// Quarter-frame clock:
///
/// Pulse channels clock their envelopes.
pub fn clockQuarterFrame(self: *Pulse) void {
self.envelope.clockQuarterFrame();
}
/// Half-frame clock:
///
/// Pulse channels clock:
///
/// - sweep
/// - length counter
pub fn clockHalfFrame(self: *Pulse) void {
self.sweep.clockHalfFrame(&self.timer_period);
//
// $4000/$4004 bit 5 serves BOTH purposes:
//
// envelope loop
// length-counter halt
//
self.length_counter.clock(self.envelope.loop);
}
// Output
fn sequencerOutput(self: *const Pulse) u1 {
const duty: usize = @intCast(self.duty);
const position: usize = @intCast(self.sequence_position);
return duty_table[duty][position];
}
/// Current raw pulse-channel output: 0...15.
///
/// This is the value that should be passed to the APU pulse mixer.
pub fn output(self: *const Pulse) u4 {
//
// Length-counter gate.
//
if (!self.length_counter.active())
return 0;
//
// Sweep gate.
//
// Covers:
//
// timer_period < 8
// target_period > $7FF
//
if (self.sweep.isMuted(self.timer_period))
return 0;
//
// Duty sequencer gate.
//
if (self.sequencerOutput() == 0)
return 0;
//
// Everything is allowing output, so the current envelope
// volume reaches the mixer.
//
return self.envelope.output();
}
// Debug / inspection
pub fn timerPeriod(self: *const Pulse) u16 {
return self.timer_period;
}
pub fn length(self: *const Pulse) u8 {
return self.length_counter.value;
}
pub fn sequencePosition(self: *const Pulse) u3 {
return self.sequence_position;
}
// consolidated pulse unit test suite
test "pulse channel register writes, timing, duty sequences and output gating" {
var pulse = Pulse.init(.pulse1);
// Period low and high writes
pulse.timer_period = 0x500;
pulse.writeTimerLow(0xab);
try std.testing.expectEqual(@as(u16, 0x5ab), pulse.timer_period);
pulse.writeTimerHigh(0x05);
try std.testing.expectEqual(@as(u16, 0x5ab), pulse.timer_period);
try std.testing.expectEqual(@as(u3, 0), pulse.sequence_position);
try std.testing.expect(pulse.envelope.start);
// Length loading and clearing
pulse.setEnabled(true);
pulse.writeTimerHigh(0x00);
try std.testing.expectEqual(@as(u8, 10), pulse.length_counter.value);
pulse.setEnabled(false);
try std.testing.expectEqual(@as(u8, 0), pulse.length_counter.value);
// Duty sequences (0..3)
const expected_sequences = [4][8]u1{
.{ 0, 1, 0, 0, 0, 0, 0, 0 },
.{ 0, 1, 1, 0, 0, 0, 0, 0 },
.{ 0, 1, 1, 1, 1, 0, 0, 0 },
.{ 1, 0, 0, 1, 1, 1, 1, 1 },
};
const positions = [8]u3{ 0, 7, 6, 5, 4, 3, 2, 1 };
for (expected_sequences, 0..) |expected, duty_idx| {
pulse.duty = @truncate(duty_idx);
for (positions, expected) |pos, exp| {
pulse.sequence_position = pos;
try std.testing.expectEqual(exp, pulse.sequencerOutput());
}
}
// Output gating checks (zero length, timer < 8, envelope volume)
pulse.writeControl(0b0011_1010);
pulse.length_counter.value = 10;
pulse.timer_period = 100;
pulse.sequence_position = 7;
try std.testing.expectEqual(@as(u4, 10), pulse.output());
pulse.timer_period = 7;
try std.testing.expectEqual(@as(u4, 0), pulse.output());
}
+493
View File
@@ -0,0 +1,493 @@
const std = @import("std");
pub const common = @import("../common.zig");
pub const Region = common.Region;
const Pulse = @import("pulse.zig");
const Triangle = @import("triangle.zig");
const Noise = @import("noise.zig");
const Dmc = @import("dmc.zig");
const FrameCounter = @import("frame_counter.zig");
pub const Apu = @This();
pub const sample_rate: u32 = 44_100;
region: Region,
pulse1: Pulse,
pulse2: Pulse,
triangle: Triangle,
noise: Noise,
dmc: Dmc,
frame_counter: FrameCounter,
/// Absolute CPU cycle as seen by the APU.
cpu_cycle: u64 = 0,
// Downsampling buffer for audio output
sample_buffer: [2048]i16 = [_]i16{0} ** 2048,
sample_count: usize = 0,
sample_accumulator: f32 = 0.0,
sample_cycles: u32 = 0,
prev_input: f32 = 0.0,
prev_output: f32 = 0.0,
pub fn init(region: Region) Apu {
return .{
.region = region,
.pulse1 = Pulse.init(.pulse1),
.pulse2 = Pulse.init(.pulse2),
.triangle = Triangle.init(),
.noise = Noise.init(
switch (region) {
.ntsc => .ntsc,
.pal => .pal,
},
),
.dmc = Dmc.init(
switch (region) {
.ntsc => .ntsc,
.pal => .pal,
},
),
.frame_counter = FrameCounter.init(
switch (region) {
.ntsc => .ntsc,
.pal => .pal,
},
),
};
}
pub fn reset(self: *Apu) void {
// reset silences all channels and clears APU state
self.writeStatus(0);
self.frame_counter.reset();
self.noise.reset();
self.dmc.writeDirectLoad(0);
self.cpu_cycle = 0;
self.sample_count = 0;
self.sample_accumulator = 0.0;
self.sample_cycles = 0;
self.prev_input = 0.0;
self.prev_output = 0.0;
}
// CPU register interface
pub fn write(
self: *Apu,
address: u16,
value: u8,
) void {
switch (address) {
// Pulse 1
0x4000 => self.pulse1.writeControl(value),
0x4001 => self.pulse1.writeSweep(value),
0x4002 => self.pulse1.writeTimerLow(value),
0x4003 => self.pulse1.writeTimerHigh(value),
// Pulse 2
0x4004 => self.pulse2.writeControl(value),
0x4005 => self.pulse2.writeSweep(value),
0x4006 => self.pulse2.writeTimerLow(value),
0x4007 => self.pulse2.writeTimerHigh(value),
// Triangle
0x4008 => self.triangle.writeLinear(value),
// $4009 unused
0x4009 => {},
0x400A => self.triangle.writeTimerLow(value),
0x400B => self.triangle.writeTimerHigh(value),
// Noise
0x400C => self.noise.writeControl(value),
// $400D unused
0x400D => {},
0x400E => self.noise.writePeriod(value),
0x400F => self.noise.writeLength(value),
// DMC
0x4010 => self.dmc.writeControl(value),
0x4011 => self.dmc.writeDirectLoad(value),
0x4012 => self.dmc.writeSampleAddress(value),
0x4013 => self.dmc.writeSampleLength(value),
// $4014 is OAM DMA.
//
// That belongs to the CPU/PPU DMA controller, NOT the APU.
0x4014 => {},
// Status
0x4015 => self.writeStatus(value),
// $4016 joypad
0x4016 => {},
// Frame counter
0x4017 => self.frame_counter.write(
value,
self.cpu_cycle,
),
else => {},
}
}
/// Only $4015 is actually an APU-readable register.
///
/// null means:
///
/// "APU does not drive the CPU data bus here."
///
/// This lets your Bus preserve open-bus behavior instead of the
/// APU incorrectly returning 0.
pub fn read(self: *Apu, address: u16) ?u8 {
return self.readWithOpenBus(address, 0);
}
pub fn readWithOpenBus(self: *Apu, address: u16, open_bus: u8) ?u8 {
return switch (address) {
0x4015 => self.readStatus(open_bus),
else => null,
};
}
// $4015
fn writeStatus(self: *Apu, value: u8) void {
self.pulse1.setEnabled((value & 0x01) != 0);
self.pulse2.setEnabled((value & 0x02) != 0);
self.triangle.setEnabled((value & 0x04) != 0);
self.noise.setEnabled((value & 0x08) != 0);
self.dmc.writeEnabled((value & 0x10) != 0);
}
fn readStatus(self: *Apu, open_bus: u8) u8 {
// Bit 5 is unmapped on the APU and returns open bus from the data bus.
var result: u8 = open_bus & 0x20;
// Pulse 1 length counter.
if (self.pulse1.active())
result |= 0x01;
// Pulse 2 length counter.
if (self.pulse2.active())
result |= 0x02;
// Triangle length counter.
if (self.triangle.active())
result |= 0x04;
// Noise length counter.
if (self.noise.active())
result |= 0x08;
// DMC bytes remaining.
if (self.dmc.active())
result |= 0x10;
// Frame IRQ.
if (self.frame_counter.irqAsserted())
result |= 0x40;
// DMC IRQ.
if (self.dmc.irqAsserted())
result |= 0x80;
//
// Reading $4015 acknowledges ONLY frame IRQ.
//
// DMC IRQ remains asserted.
//
self.frame_counter.clearIrq();
return result;
}
// IRQ
/// APU's contribution to the CPU IRQ line.
///
/// Your NES/CPU layer should OR this with mapper IRQs.
pub fn irqAsserted(self: *const Apu) bool {
return self.frame_counter.irqAsserted() or
self.dmc.irqAsserted();
}
// Main CPU clock
/// Clock once for EVERY CPU cycle.
///
/// This should be your only ordinary clock entrypoint into the APU.
pub fn clockCpu(self: *Apu) void {
// Triangle
//
// Timer runs every CPU cycle.
self.triangle.clockTimer();
// Noise
//
// Our Noise implementation uses the NESdev table expressed
// directly in CPU cycles.
self.noise.clockCpu();
// DMC
//
// Our DMC rate table is also expressed directly in CPU cycles.
self.dmc.clockTimer();
// Pulse timers
//
// Pulse timer period is expressed in APU cycles.
//
// One APU cycle = two CPU cycles.
if ((self.cpu_cycle & 1) == 0) {
self.pulse1.clockTimer();
self.pulse2.clockTimer();
}
// Frame sequencer
const events = self.frame_counter.clockCpu();
if (events.quarter) {
self.clockQuarterFrame();
}
if (events.half) {
self.clockHalfFrame();
}
// audio downsampler (~40.58 CPU cycles per 44.1kHz sample) with 1st-order DC blocker
self.sample_accumulator += self.output();
self.sample_cycles += 1;
if (self.sample_cycles >= 40) {
if (self.sample_count < self.sample_buffer.len) {
const avg = self.sample_accumulator / @as(f32, @floatFromInt(self.sample_cycles));
// 1st-order IIR DC blocker filter: y[n] = x[n] - x[n-1] + 0.995 * y[n-1]
const filtered = avg - self.prev_input + (0.995 * self.prev_output);
self.prev_input = avg;
self.prev_output = filtered;
const sample_f = filtered * 32000.0 * 2.5;
self.sample_buffer[self.sample_count] = @intFromFloat(std.math.clamp(sample_f, -32767.0, 32767.0));
self.sample_count += 1;
}
self.sample_accumulator = 0.0;
self.sample_cycles = 0;
}
self.cpu_cycle +%= 1;
}
pub fn getSamples(self: *const Apu) []const i16 {
return self.sample_buffer[0..self.sample_count];
}
pub fn clearSamples(self: *Apu) void {
self.sample_count = 0;
}
// Quarter-frame
fn clockQuarterFrame(self: *Apu) void {
//
// Pulse envelopes
//
self.pulse1.clockQuarterFrame();
self.pulse2.clockQuarterFrame();
//
// Triangle linear counter
//
self.triangle.clockQuarterFrame();
//
// Noise envelope
//
self.noise.clockQuarterFrame();
//
// DMC has no frame-sequencer-controlled unit.
//
}
// Half-frame
fn clockHalfFrame(self: *Apu) void {
//
// Pulse:
//
// - sweep
// - length
//
self.pulse1.clockHalfFrame();
self.pulse2.clockHalfFrame();
//
// Triangle length
//
self.triangle.clockHalfFrame();
//
// Noise length
//
self.noise.clockHalfFrame();
//
// DMC again has no half-frame unit.
//
}
// DMC DMA
/// Does the DMC memory reader currently need another byte?
pub fn needsDmcDma(self: *const Apu) bool {
return self.dmc.needsDma();
}
/// Begin a DMC DMA operation.
///
/// Returns the CPU address that should eventually be read.
pub fn beginDmcDma(self: *Apu) ?u16 {
return self.dmc.beginDma();
}
/// Deliver the result of the CPU DMA read.
pub fn completeDmcDma(
self: *Apu,
value: u8,
) void {
self.dmc.completeDma(value);
}
// Individual mixer inputs
pub fn pulse1Output(self: *const Apu) u4 {
return self.pulse1.output();
}
pub fn pulse2Output(self: *const Apu) u4 {
return self.pulse2.output();
}
pub fn triangleOutput(self: *const Apu) u4 {
return self.triangle.output();
}
pub fn noiseOutput(self: *const Apu) u4 {
return self.noise.output();
}
pub fn dmcOutput(self: *const Apu) u7 {
return self.dmc.output();
}
// Nonlinear NES mixer
/// Raw nonlinear APU output.
///
/// Nominal range is roughly 0...1.
///
/// This does NOT implement the analog high-pass / low-pass filter
/// chain following the NES DAC.
pub fn output(self: *const Apu) f32 {
const p1: f32 =
@floatFromInt(self.pulse1.output());
const p2: f32 =
@floatFromInt(self.pulse2.output());
const triangle: f32 =
@floatFromInt(self.triangle.output());
const noise: f32 =
@floatFromInt(self.noise.output());
const dmc: f32 =
@floatFromInt(self.dmc.output());
// Pulse mixer
const pulse_sum = p1 + p2;
const pulse_out: f32 =
if (pulse_sum == 0.0)
0.0
else
95.88 /
((8128.0 / pulse_sum) + 100.0);
// Triangle / Noise / DMC mixer
const tnd_input =
(triangle / 8227.0) +
(noise / 12241.0) +
(dmc / 22638.0);
const tnd_out: f32 =
if (tnd_input == 0.0)
0.0
else
159.79 /
((1.0 / tnd_input) + 100.0);
return pulse_out + tnd_out;
}
// consolidated APU root test suite
test "apu initialization, status reads, IRQ clearing, sample generation and DMC DMA" {
var apu = Apu.init(.ntsc);
// Init, reset & channel status
apu.write(0x4015, 0x0d);
apu.write(0x4003, 0x08);
apu.write(0x400b, 0x08);
apu.write(0x400f, 0x08);
const status = apu.readWithOpenBus(0x4015, 0x20);
try std.testing.expectEqual(@as(u8, 0b0010_1101), status.?);
apu.reset();
try std.testing.expect(!apu.pulse1.active() and apu.cpu_cycle == 0);
// Frame/DMC IRQ clearing
apu.frame_counter.irq_flag = true;
apu.dmc.irq_flag = true;
_ = apu.read(0x4015);
try std.testing.expect(!apu.frame_counter.irqAsserted() and apu.dmc.irqAsserted());
// Audio sampling & DMC DMA
apu.write(0x4015, 0x11);
for (0..200) |_| apu.clockCpu();
try std.testing.expect(apu.getSamples().len > 0);
try std.testing.expect(apu.needsDmcDma());
try std.testing.expectEqual(@as(?u16, 0xc000), apu.beginDmcDma());
}
+171
View File
@@ -0,0 +1,171 @@
const std = @import("std");
const Sweep = @This();
pub const Channel = enum {
pulse1,
pulse2,
};
channel: Channel,
enabled: bool = false,
/// Raw P value from EPPP.NSSS.
///
/// Hardware clocks the sweep every P + 1 half-frame clocks because
/// the divider counts:
///
/// P, P-1, ..., 0
///
period: u3 = 0,
negate: bool = false,
shift: u3 = 0,
/// Current divider counter.
divider: u3 = 0,
/// Set whenever $4001/$4005 is written.
reload: bool = false,
pub fn init(channel: Channel) Sweep {
return .{
.channel = channel,
};
}
/// Write $4001 or $4005:
///
/// EPPP.NSSS
/// ││││ │└┴┴─ shift
/// ││││ └──── negate
/// │└┴┴────── divider period
/// └───────── enabled
pub fn write(self: *Sweep, value: u8) void {
self.enabled = (value & 0x80) != 0;
self.period = @truncate((value >> 4) & 0x07);
self.negate = (value & 0x08) != 0;
self.shift = @truncate(value & 0x07);
self.reload = true;
}
/// Calculate the continuously-evaluated sweep target period.
///
/// timer_period is the pulse channel's current 11-bit timer period.
///
/// This returns u16 rather than an 11-bit type intentionally:
/// values > 0x7FF must remain visible so the sweep muting logic
/// can detect overflow.
pub fn targetPeriod(self: *const Sweep, timer_period: u16) u16 {
std.debug.assert(timer_period <= 0x07ff);
const change = timer_period >> @intCast(self.shift);
if (!self.negate) {
return timer_period + change;
}
return switch (self.channel) {
// Pulse 1 uses one's-complement negation:
//
// period + ~change
//
// Equivalent mathematically to:
//
// period - change - 1
//
// Clamp a negative target to zero.
.pulse1 => if (timer_period <= change)
0
else
timer_period - change - 1,
// Pulse 2 uses ordinary two's-complement subtraction:
//
// period - change
.pulse2 => timer_period - change,
};
}
/// The sweep can mute a pulse channel even when sweep updating itself
/// is disabled.
///
/// A pulse is muted when:
///
/// current period < 8
///
/// OR
///
/// target period > 0x7FF
pub fn isMuted(self: *const Sweep, timer_period: u16) bool {
const target = self.targetPeriod(timer_period);
return timer_period < 8 or target > 0x07ff;
}
/// Clocked by the frame sequencer on a half-frame clock.
///
/// This potentially updates the pulse channel's timer period and
/// advances/reloads the sweep divider.
pub fn clockHalfFrame(
self: *Sweep,
timer_period: *u16,
) void {
std.debug.assert(timer_period.* <= 0x07ff);
// The target must be evaluated BEFORE potentially updating
// timer_period.
const target = self.targetPeriod(timer_period.*);
const muted = timer_period.* < 8 or target > 0x07ff;
const divider_zero = self.divider == 0;
// If the divider emits a clock, apply the sweep.
//
// shift == 0 disables period updates, even if enabled == true.
if (divider_zero and
self.enabled and
self.shift != 0 and
!muted)
{
timer_period.* = target;
}
// Divider operation happens regardless of whether the timer
// was actually changed.
if (divider_zero or self.reload) {
self.divider = self.period;
self.reload = false;
} else {
self.divider -= 1;
}
}
// consolidated sweep unit test suite
test "sweep unit target calculation, gating and clocking" {
var sweep1 = Sweep.init(.pulse1);
sweep1.write(0b1010_0001); // P=2, shift=1
try std.testing.expectEqual(@as(u16, 1500), sweep1.targetPeriod(1000));
// Pulse 1 ones complement vs Pulse 2 twos complement negation
sweep1.write(0b1000_1010); // negate, shift=2
try std.testing.expectEqual(@as(u16, 749), sweep1.targetPeriod(1000));
var sweep2 = Sweep.init(.pulse2);
sweep2.write(0b1000_1010);
try std.testing.expectEqual(@as(u16, 750), sweep2.targetPeriod(1000));
// Muting checks
try std.testing.expect(sweep1.isMuted(7));
try std.testing.expect(!sweep1.isMuted(8));
sweep1.write(0b0000_0000);
try std.testing.expect(sweep1.isMuted(0x400)); // overflow mutes
// Clocking & reload
var timer_period: u16 = 1000;
sweep2.write(0b1010_0001);
sweep2.clockHalfFrame(&timer_period);
try std.testing.expectEqual(@as(u16, 1500), timer_period);
}
+672
View File
@@ -0,0 +1,672 @@
const std = @import("std");
const LengthCounter = @import("length_counter.zig");
pub const Triangle = @This();
/// 32-step triangle DAC sequence.
///
/// Unlike pulse, the triangle directly sends one of these
/// 4-bit values to the mixer.
const sequence = [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,
};
length_counter: LengthCounter = .{},
// $4008
// CRRR.RRRR
/// Bit 7 of $4008.
///
/// This has two functions:
///
/// - halts the length counter
/// - controls whether linear_counter_reload remains set
control: bool = false,
/// Bits 6..0 of $4008.
linear_reload_value: u7 = 0,
/// Current linear counter.
linear_counter: u7 = 0,
/// Set by every write to $400B.
linear_reload: bool = false,
// Timer
/// 11-bit timer period:
///
/// HHHLLLLLLLL
timer_period: u16 = 0,
/// Internal timer divider.
///
/// Triangle timer runs at the CPU clock, unlike pulse timers
/// which run at CPU / 2.
timer_counter: u16 = 0,
// Sequencer
/// Current position in the 32-step waveform.
sequence_position: u5 = 0,
pub fn init() Triangle {
return .{};
}
// $4015
/// Triangle is bit 2 of $4015.
///
/// Clearing it immediately clears the length counter.
///
/// Importantly, this does NOT reset the sequencer or linear counter.
pub fn setEnabled(self: *Triangle, enabled: bool) void {
self.length_counter.setEnabled(enabled);
}
/// Status returned as bit 2 when reading $4015.
///
/// $4015 status reports whether the length counter is non-zero.
pub fn active(self: *const Triangle) bool {
return self.length_counter.active();
}
// $4008
// CRRR.RRRR
//
// C = control / length-counter halt
// R = linear-counter reload value
pub fn writeLinear(self: *Triangle, value: u8) void {
self.control = (value & 0x80) != 0;
self.linear_reload_value = @truncate(value & 0x7f);
//
// IMPORTANT:
//
// Writing $4008 itself does NOT set linear_reload.
//
// $400B does that.
//
}
// $400A
// LLLL.LLLL
pub fn writeTimerLow(self: *Triangle, value: u8) void {
self.timer_period =
(self.timer_period & 0x0700) |
@as(u16, value);
}
// $400B
// LLLL.LHHH
//
// bits 7..3 = length counter index
// bits 2..0 = high timer bits
//
// Side effect:
// set linear-counter reload flag
pub fn writeTimerHigh(self: *Triangle, value: u8) void {
self.timer_period =
(self.timer_period & 0x00ff) |
(@as(u16, value & 0x07) << 8);
const length_index: u5 = @truncate(value >> 3);
self.length_counter.load(length_index);
//
// Every $400B write sets the reload flag.
//
self.linear_reload = true;
//
// Unlike pulse $4003/$4007:
//
// DO NOT reset sequence_position.
//
// DO NOT reset timer_counter.
//
}
// $4009
/// $4009 is unused.
pub fn writeUnused(_: *Triangle, _: u8) void {}
// Linear counter
/// Clocked on every quarter-frame.
///
/// NES behavior, in this exact order:
///
/// 1. If reload flag set:
/// counter = reload value
///
/// Otherwise, if counter > 0:
/// counter--
///
/// 2. If control flag clear:
/// reload flag = false
///
pub fn clockQuarterFrame(self: *Triangle) void {
if (self.linear_reload) {
self.linear_counter = self.linear_reload_value;
} else if (self.linear_counter != 0) {
self.linear_counter -= 1;
}
if (!self.control) {
self.linear_reload = false;
}
}
// Length counter
/// Clocked on every half-frame.
pub fn clockHalfFrame(self: *Triangle) void {
//
// $4008 bit 7 is also the length-counter halt flag.
//
self.length_counter.clock(self.control);
}
// Timer / waveform sequencer
/// Clock EVERY CPU cycle.
///
/// This differs from Pulse:
///
/// Pulse timer -> CPU / 2
/// Triangle timer -> CPU
///
pub fn clockTimer(self: *Triangle) void {
if (self.timer_counter == 0) {
self.timer_counter = self.timer_period;
//
// The waveform advances only when BOTH counters are non-zero.
//
if (self.linear_counter != 0 and
self.length_counter.active())
{
self.sequence_position +%= 1;
}
} else {
self.timer_counter -= 1;
}
}
// Output
/// Current 4-bit DAC output.
///
/// IMPORTANT:
///
/// Do NOT do this:
///
/// if (linear_counter == 0) return 0;
///
/// and do NOT do:
///
/// if (length_counter == 0) return 0;
///
/// Those counters stop the waveform sequencer. They do not force
/// the triangle DAC to zero.
///
/// When halted, the channel retains its current sequence value.
pub fn output(self: *const Triangle) u4 {
return sequence[@intCast(self.sequence_position)];
}
pub fn sequencerRunning(self: *const Triangle) bool {
return self.linear_counter != 0 and
self.length_counter.active();
}
// Inspection helpers
pub fn timerPeriod(self: *const Triangle) u16 {
return self.timer_period;
}
pub fn length(self: *const Triangle) u8 {
return self.length_counter.value;
}
pub fn linear(self: *const Triangle) u7 {
return self.linear_counter;
}
pub fn sequencePosition(self: *const Triangle) u5 {
return self.sequence_position;
}
// Tests
test "$4008 sets control and reload value" {
var triangle = Triangle.init();
triangle.writeLinear(0b1010_1010);
try std.testing.expect(triangle.control);
try std.testing.expectEqual(
@as(u7, 0x2a),
triangle.linear_reload_value,
);
}
test "$4008 does not set reload flag" {
var triangle = Triangle.init();
triangle.writeLinear(0xff);
try std.testing.expect(!triangle.linear_reload);
}
test "$400A sets timer low bits" {
var triangle = Triangle.init();
triangle.timer_period = 0x500;
triangle.writeTimerLow(0xab);
try std.testing.expectEqual(
@as(u16, 0x5ab),
triangle.timer_period,
);
}
test "$400B sets timer high bits" {
var triangle = Triangle.init();
triangle.timer_period = 0x0ab;
triangle.writeTimerHigh(0x05);
try std.testing.expectEqual(
@as(u16, 0x5ab),
triangle.timer_period,
);
}
test "$400B sets linear reload flag" {
var triangle = Triangle.init();
try std.testing.expect(!triangle.linear_reload);
triangle.writeTimerHigh(0);
try std.testing.expect(triangle.linear_reload);
}
test "$400B does not reset sequencer position" {
var triangle = Triangle.init();
triangle.sequence_position = 17;
triangle.writeTimerHigh(0);
try std.testing.expectEqual(
@as(u5, 17),
triangle.sequence_position,
);
}
test "$400B does not reset timer divider" {
var triangle = Triangle.init();
triangle.timer_counter = 123;
triangle.writeTimerHigh(0x05);
try std.testing.expectEqual(
@as(u16, 123),
triangle.timer_counter,
);
}
test "$400B loads length counter when enabled" {
var triangle = Triangle.init();
triangle.setEnabled(true);
// Length table index 0 = 10.
triangle.writeTimerHigh(0);
try std.testing.expectEqual(
@as(u8, 10),
triangle.length_counter.value,
);
}
test "$400B cannot load length while disabled" {
var triangle = Triangle.init();
triangle.setEnabled(false);
triangle.writeTimerHigh(0);
try std.testing.expectEqual(
@as(u8, 0),
triangle.length_counter.value,
);
}
test "$4015 disable clears length counter" {
var triangle = Triangle.init();
triangle.setEnabled(true);
triangle.writeTimerHigh(0);
try std.testing.expectEqual(
@as(u8, 10),
triangle.length_counter.value,
);
triangle.setEnabled(false);
try std.testing.expectEqual(
@as(u8, 0),
triangle.length_counter.value,
);
}
test "linear counter reloads when reload flag set" {
var triangle = Triangle.init();
triangle.linear_reload_value = 42;
triangle.linear_reload = true;
triangle.clockQuarterFrame();
try std.testing.expectEqual(
@as(u7, 42),
triangle.linear_counter,
);
}
test "linear counter decrements without reload" {
var triangle = Triangle.init();
triangle.linear_counter = 10;
triangle.linear_reload = false;
triangle.clockQuarterFrame();
try std.testing.expectEqual(
@as(u7, 9),
triangle.linear_counter,
);
}
test "linear counter stops at zero" {
var triangle = Triangle.init();
triangle.linear_counter = 0;
triangle.clockQuarterFrame();
try std.testing.expectEqual(
@as(u7, 0),
triangle.linear_counter,
);
}
test "control clear clears reload flag" {
var triangle = Triangle.init();
triangle.control = false;
triangle.linear_reload = true;
triangle.linear_reload_value = 10;
triangle.clockQuarterFrame();
try std.testing.expectEqual(
@as(u7, 10),
triangle.linear_counter,
);
try std.testing.expect(!triangle.linear_reload);
}
test "control set preserves reload flag" {
var triangle = Triangle.init();
triangle.control = true;
triangle.linear_reload = true;
triangle.linear_reload_value = 10;
triangle.clockQuarterFrame();
try std.testing.expectEqual(
@as(u7, 10),
triangle.linear_counter,
);
try std.testing.expect(triangle.linear_reload);
//
// Because reload remains set, every quarter frame reloads
// the linear counter back to 10.
//
triangle.linear_counter = 3;
triangle.clockQuarterFrame();
try std.testing.expectEqual(
@as(u7, 10),
triangle.linear_counter,
);
}
test "length counter decrements when control clear" {
var triangle = Triangle.init();
triangle.setEnabled(true);
triangle.writeTimerHigh(0);
try std.testing.expectEqual(
@as(u8, 10),
triangle.length_counter.value,
);
triangle.control = false;
triangle.clockHalfFrame();
try std.testing.expectEqual(
@as(u8, 9),
triangle.length_counter.value,
);
}
test "control flag halts length counter" {
var triangle = Triangle.init();
triangle.setEnabled(true);
triangle.writeTimerHigh(0);
triangle.control = true;
triangle.clockHalfFrame();
try std.testing.expectEqual(
@as(u8, 10),
triangle.length_counter.value,
);
}
test "timer runs for period plus one CPU clocks" {
var triangle = Triangle.init();
triangle.timer_period = 2;
triangle.timer_counter = 2;
triangle.linear_counter = 1;
triangle.length_counter.value = 1;
triangle.sequence_position = 0;
triangle.clockTimer();
try std.testing.expectEqual(
@as(u16, 1),
triangle.timer_counter,
);
try std.testing.expectEqual(
@as(u5, 0),
triangle.sequence_position,
);
triangle.clockTimer();
try std.testing.expectEqual(
@as(u16, 0),
triangle.timer_counter,
);
try std.testing.expectEqual(
@as(u5, 0),
triangle.sequence_position,
);
triangle.clockTimer();
try std.testing.expectEqual(
@as(u16, 2),
triangle.timer_counter,
);
try std.testing.expectEqual(
@as(u5, 1),
triangle.sequence_position,
);
}
test "sequencer does not advance when linear counter is zero" {
var triangle = Triangle.init();
triangle.timer_counter = 0;
triangle.timer_period = 10;
triangle.length_counter.value = 1;
triangle.linear_counter = 0;
triangle.sequence_position = 5;
triangle.clockTimer();
try std.testing.expectEqual(
@as(u5, 5),
triangle.sequence_position,
);
}
test "sequencer does not advance when length counter is zero" {
var triangle = Triangle.init();
triangle.timer_counter = 0;
triangle.timer_period = 10;
triangle.length_counter.value = 0;
triangle.linear_counter = 1;
triangle.sequence_position = 5;
triangle.clockTimer();
try std.testing.expectEqual(
@as(u5, 5),
triangle.sequence_position,
);
}
test "sequencer advances when both counters are nonzero" {
var triangle = Triangle.init();
triangle.timer_counter = 0;
triangle.timer_period = 10;
triangle.length_counter.value = 1;
triangle.linear_counter = 1;
triangle.sequence_position = 5;
triangle.clockTimer();
try std.testing.expectEqual(
@as(u5, 6),
triangle.sequence_position,
);
}
test "sequencer wraps after 31" {
var triangle = Triangle.init();
triangle.timer_counter = 0;
triangle.length_counter.value = 1;
triangle.linear_counter = 1;
triangle.sequence_position = 31;
triangle.clockTimer();
try std.testing.expectEqual(
@as(u5, 0),
triangle.sequence_position,
);
}
test "triangle waveform values" {
var triangle = Triangle.init();
triangle.sequence_position = 0;
try std.testing.expectEqual(@as(u4, 15), triangle.output());
triangle.sequence_position = 15;
try std.testing.expectEqual(@as(u4, 0), triangle.output());
triangle.sequence_position = 16;
try std.testing.expectEqual(@as(u4, 0), triangle.output());
triangle.sequence_position = 31;
try std.testing.expectEqual(@as(u4, 15), triangle.output());
}
test "halted triangle retains current DAC output" {
var triangle = Triangle.init();
triangle.sequence_position = 7;
//
// Sequence position 7 = DAC value 8.
//
try std.testing.expectEqual(
@as(u4, 8),
triangle.output(),
);
//
// Stop both gating counters.
//
triangle.linear_counter = 0;
triangle.length_counter.value = 0;
//
// The DAC is NOT forced to zero.
//
try std.testing.expectEqual(
@as(u4, 8),
triangle.output(),
);
}
+160 -122
View File
@@ -1,157 +1,195 @@
const Cartridge = @import("cartridge.zig");
const std = @import("std");
const common = @import("common.zig");
const Region = common.Region;
const Mirroring = common.Mirroring;
const Ppu = @import("ppu/root.zig").Ppu;
const Apu = @import("apu/root.zig").Apu;
const Controller = @import("controller.zig").Controller;
const Cartridge = @import("cartridge.zig").Cartridge;
const Controller = @import("controller.zig");
pub const Bus = @This();
const Ppu = @import("ppu.zig");
ram: [0x800]u8 = [_]u8{0} ** 0x800,
ciram: [0x800]u8 = [_]u8{0} ** 0x800,
const Apu = @import("apu.zig");
const Bus = @This();
ram: [0x800]u8 =
[_]u8{0} ** 0x800,
cartridge: Cartridge,
ppu: Ppu = .{},
apu: Apu = .{},
controllers: [2]Controller = .{
.{},
.{},
},
ppu: Ppu,
apu: Apu,
controllers: [2]Controller = .{ Controller.init(), Controller.init() },
cartridge: ?Cartridge = null,
open_bus: u8 = 0,
oam_dma_page: ?u8 = null,
// OAM DMA state machine
dma_page: u8 = 0,
dma_addr: u8 = 0,
dma_active: bool = false,
dma_dummy: bool = true,
pub fn init(
rom: []const u8,
) Cartridge.Error!Bus {
pub fn init(region: Region, cart: ?Cartridge) Bus {
return .{
.cartridge = try Cartridge.init(rom),
.ppu = Ppu.init(region),
.apu = Apu.init(region),
.cartridge = cart,
};
}
pub fn reset(
self: *Bus,
) void {
pub fn reset(self: *Bus) void {
self.ppu.reset();
self.apu.reset();
for (&self.controllers) |*controller|
controller.reset();
self.oam_dma_page = null;
self.controllers[0].reset();
self.controllers[1].reset();
self.dma_active = false;
self.dma_dummy = true;
self.dma_addr = 0;
}
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,
pub fn mapNametableAddress(mirroring_mode: Mirroring, address: u16) u11 {
const v = (address - 0x2000) & 0x0fff;
return switch (mirroring_mode) {
.horizontal => @truncate(((v >> 1) & 0x0400) | (v & 0x03ff)),
.vertical => @truncate(v & 0x07ff),
.single_screen_lower => @truncate(v & 0x03ff),
.single_screen_upper => @truncate(0x0400 | (v & 0x03ff)),
.four_screen => @truncate(v & 0x07ff),
};
self.open_bus = value;
return value;
}
pub inline fn write(
self: *Bus,
address: u16,
value: u8,
) void {
pub const PpuBus = struct {
bus: *Bus,
pub fn read(self: *PpuBus, address: u16) u8 {
if (address < 0x2000) {
if (self.bus.cartridge) |*c| {
return c.ppuRead(address) orelse 0;
}
return 0;
} else if (address < 0x3f00) {
const mir = if (self.bus.cartridge) |*c| c.mirroring() else .horizontal;
const ciram_idx = mapNametableAddress(mir, address);
return self.bus.ciram[ciram_idx];
} else {
return self.bus.ppu.palette.read(address);
}
}
pub fn write(self: *PpuBus, address: u16, value: u8) void {
if (address < 0x2000) {
if (self.bus.cartridge) |*c| {
_ = c.ppuWrite(address, value);
}
} else if (address < 0x3f00) {
const mir = if (self.bus.cartridge) |*c| c.mirroring() else .horizontal;
const ciram_idx = mapNametableAddress(mir, address);
self.bus.ciram[ciram_idx] = value;
} else {
self.bus.ppu.palette.write(address, value);
}
}
};
pub inline fn getPpuBus(self: *Bus) PpuBus {
return PpuBus{ .bus = self };
}
pub fn read(self: *Bus, address: u16) u8 {
const val: u8 = switch (address) {
0x0000...0x1fff => self.ram[address & 0x07ff],
0x2000...0x3fff => blk: {
var ppu_bus = self.getPpuBus();
break :blk self.ppu.cpuRead(&ppu_bus, address) orelse self.open_bus;
},
0x4000...0x4014 => self.open_bus,
0x4015 => self.apu.readWithOpenBus(address, self.open_bus) orelse self.open_bus,
0x4016 => self.controllers[0].read(),
0x4017 => self.controllers[1].read(),
0x4018...0x401f => self.open_bus,
0x4020...0xffff => blk: {
if (self.cartridge) |*c| {
break :blk c.cpuRead(address) orelse self.open_bus;
}
break :blk self.open_bus;
},
};
self.open_bus = val;
return val;
}
pub 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),
0x0000...0x1fff => self.ram[address & 0x07ff] = value,
0x2000...0x3fff => {
var ppu_bus = self.getPpuBus();
_ = self.ppu.cpuWrite(&ppu_bus, address, value);
},
0x4000...0x4013, 0x4015, 0x4017 => self.apu.write(address, value),
0x4014 => {
self.dma_page = value;
self.dma_addr = 0;
self.dma_active = true;
self.dma_dummy = true;
},
0x4016 => {
self.controllers[0].write(value);
self.controllers[1].write(value);
},
0x4017 => self.apu.writeFrameCounter(value),
else => self.cartridge.cpuWrite(
address,
value,
),
0x4018...0x401f => {},
0x4020...0xffff => {
if (self.cartridge) |*c| {
_ = c.cpuWrite(address, value);
}
},
}
}
/// Called exactly once for every CPU cycle.
pub inline fn tick(
self: *Bus,
) void {
self.apu.tick();
/// Clocks one CPU cycle: steps APU and 3 PPU dots.
pub fn tick(self: *Bus) void {
self.apu.clockCpu();
self.ppu.registers.tickCpuCycle();
// NTSC NES: PPU runs at 3× CPU clock.
self.ppu.tick(&self.cartridge);
self.ppu.tick(&self.cartridge);
self.ppu.tick(&self.cartridge);
var ppu_bus = self.getPpuBus();
self.ppu.clock(&ppu_bus);
self.ppu.clock(&ppu_bus);
self.ppu.clock(&ppu_bus);
}
pub fn nmiLine(
self: *Bus,
) bool {
return self.ppu.nmiAsserted();
/// Executes one DMA CPU cycle step.
pub fn stepDma(self: *Bus) void {
if (self.dma_dummy) {
self.dma_dummy = false;
self.tick();
return;
}
const cpu_addr: u16 = (@as(u16, self.dma_page) << 8) | self.dma_addr;
const data = self.read(cpu_addr);
self.ppu.registers.oam[self.ppu.registers.oam_addr] = data;
self.ppu.registers.oam_addr +%= 1;
self.dma_addr +%= 1;
if (self.dma_addr == 0) {
self.dma_active = false;
}
self.tick();
}
pub fn irqLine(
self: *Bus,
) bool {
return self.apu.irqAsserted() or
self.cartridge.irqAsserted();
pub fn nmiLine(self: *const Bus) bool {
return self.ppu.nmiLine();
}
pub fn takeOamDmaPage(
self: *Bus,
) ?u8 {
const page =
self.oam_dma_page;
self.oam_dma_page = null;
return page;
pub fn irqLine(self: *const Bus) bool {
const apu_irq = self.apu.irqAsserted();
const cart_irq = if (self.cartridge) |*c| c.irqLine() else false;
return apu_irq or cart_irq;
}
// consolidated bus memory mirroring test suite
test "bus internal RAM and CIRAM nametable mirroring" {
var bus = Bus.init(.ntsc, null);
bus.write(0x0005, 0x99);
try std.testing.expectEqual(@as(u8, 0x99), bus.read(0x0805));
var ppu_bus = bus.getPpuBus();
ppu_bus.write(0x2000, 0xaa);
try std.testing.expectEqual(@as(u8, 0xaa), ppu_bus.read(0x2400));
}
File diff suppressed because it is too large Load Diff
+84
View File
@@ -0,0 +1,84 @@
const std = @import("std");
pub const screen_width: u16 = 256;
pub const screen_height: u16 = 240;
pub const Region = enum {
ntsc,
pal,
/// Native CPU clock frequency in Hz.
pub fn cpuClockRate(self: Region) u32 {
return switch (self) {
.ntsc => 1_789_773,
.pal => 1_662_607,
};
}
/// Total scanlines per frame (262 for NTSC, 312 for PAL).
pub fn totalScanlines(self: Region) u16 {
return switch (self) {
.ntsc => 262,
.pal => 312,
};
}
/// Scanline index of the pre-render scanline (261 for NTSC, 311 for PAL).
pub fn preRenderScanline(self: Region) u16 {
return switch (self) {
.ntsc => 261,
.pal => 311,
};
}
/// Number of VBlank scanlines.
pub fn vblankScanlines(self: Region) u16 {
return switch (self) {
.ntsc => 20,
.pal => 70,
};
}
/// CPU cycles to suppress PPU register writes after power/reset (~1 frame).
pub fn powerUpSuppressionCycles(self: Region) u32 {
return switch (self) {
.ntsc => 29_658,
.pal => 33_132,
};
}
};
/// Nametable mirroring mode used by PPU CIRAM and Cartridge mappers.
pub const Mirroring = enum {
horizontal,
vertical,
single_screen_lower,
single_screen_upper,
four_screen,
};
/// Standard 8-button NES controller layout.
pub const Button = enum(u8) {
a = 1 << 0,
b = 1 << 1,
select = 1 << 2,
start = 1 << 3,
up = 1 << 4,
down = 1 << 5,
left = 1 << 6,
right = 1 << 7,
};
test "region properties" {
const ntsc: Region = .ntsc;
try std.testing.expectEqual(@as(u32, 1_789_773), ntsc.cpuClockRate());
try std.testing.expectEqual(@as(u16, 262), ntsc.totalScanlines());
try std.testing.expectEqual(@as(u16, 261), ntsc.preRenderScanline());
try std.testing.expectEqual(@as(u32, 29_658), ntsc.powerUpSuppressionCycles());
const pal: Region = .pal;
try std.testing.expectEqual(@as(u32, 1_662_607), pal.cpuClockRate());
try std.testing.expectEqual(@as(u16, 312), pal.totalScanlines());
try std.testing.expectEqual(@as(u16, 311), pal.preRenderScanline());
try std.testing.expectEqual(@as(u32, 33_132), pal.powerUpSuppressionCycles());
}
-345
View File
@@ -1,345 +0,0 @@
// minimal system test runner for nes-test-roms & NES system contract conformance
const std = @import("std");
const testing = std.testing;
const build_options = @import("build_options");
const contract = @import("contract");
const Nes = @import("root.zig");
fn loadRomAlloc(allocator: std.mem.Allocator, relative_path: []const u8) ![]u8 {
const base_dir = build_options.nes_test_roms_dir orelse return error.TestRomsNotFetched;
var dir = try std.Io.Dir.openDirAbsolute(testing.io, base_dir, .{});
defer dir.close(testing.io);
return dir.readFileAlloc(testing.io, relative_path, allocator, @enumFromInt(10 * 1024 * 1024));
}
/// Runs a blargg test ROM on the full NES system implementation.
/// Blargg test ROMs signal completion via $6000 (0x80 = running, 0x00 = passed)
/// and output result text starting at $6004.
pub fn runBlarggTest(relative_path: []const u8, max_instructions: u64) !void {
const rom_bytes = try loadRomAlloc(testing.allocator, relative_path);
defer testing.allocator.free(rom_bytes);
var nes: Nes = undefined;
try nes.init(rom_bytes);
var i: u64 = 0;
var result_ready = false;
while (i < max_instructions) : (i += 1) {
_ = nes.step() orelse break;
const status = nes.bus.read(0x6000);
if (status != 0x80) {
const sig0 = nes.bus.read(0x6001);
const sig1 = nes.bus.read(0x6002);
const sig2 = nes.bus.read(0x6003);
if (sig0 == 0xDE and sig1 == 0xB0 and sig2 == 0x61) {
result_ready = true;
break;
}
}
}
const status = nes.bus.read(0x6000);
var msg_buf: [256]u8 = undefined;
var msg_len: usize = 0;
while (msg_len < msg_buf.len) : (msg_len += 1) {
const char = nes.bus.read(@intCast(0x6004 + msg_len));
if (char == 0) break;
msg_buf[msg_len] = char;
}
const msg = msg_buf[0..msg_len];
if (!result_ready and status == 0x80) {
std.debug.print(
"\n[TIMEOUT] Test '{s}' timed out after {d} instructions. Text output: {s}\n",
.{ relative_path, i, msg },
);
return error.TestTimeout;
}
if (status != 0) {
std.debug.print(
"\n[FAIL] Test '{s}' failed with status 0x{X:0>2}: {s}\n",
.{ relative_path, status, msg },
);
return error.TestFailed;
}
}
/// Runs Kevtris' nestest.nes ROM on full NES system (PC = $C000).
pub fn runNesTestSystem(relative_path: []const u8, max_instructions: u64) !void {
const rom_bytes = try loadRomAlloc(testing.allocator, relative_path);
defer testing.allocator.free(rom_bytes);
var nes: Nes = undefined;
try nes.init(rom_bytes);
nes.cpu.registers.pc = 0xC000;
var previous_pc: u16 = 0;
var i: u64 = 0;
while (i < max_instructions) : (i += 1) {
previous_pc = nes.cpu.registers.pc;
_ = nes.step() orelse break;
if (nes.cpu.registers.pc == previous_pc) break;
}
const err1 = nes.bus.read(0x0002);
const err2 = nes.bus.read(0x0003);
if (err1 != 0 or err2 != 0) {
std.debug.print(
"\n[FAIL] Nestest system test failed with error codes $0002: 0x{X:0>2}, $0003: 0x{X:0>2} at PC 0x{X:0>4}\n",
.{ err1, err2, nes.cpu.registers.pc },
);
return error.NestestFailed;
}
}
test "NES System Conformance - Contract SystemSpec" {
comptime contract.validateSystem(Nes);
try testing.expectEqualStrings("Nintendo Entertainment System", Nes.spec.name);
try testing.expectEqual(@as(usize, 1), Nes.spec.video_outputs.len);
try testing.expectEqual(@as(usize, 1), Nes.spec.audio_outputs.len);
try testing.expectEqual(@as(usize, 2), Nes.spec.input_devices.len);
try testing.expectEqual(@as(usize, 1), Nes.spec.storage_devices.len);
}
test "NES System Conformance - Video Frame Output" {
var rom: [16 + 0x4000 + 0x2000]u8 = [_]u8{0} ** (16 + 0x4000 + 0x2000);
@memcpy(rom[0..4], "NES\x1a");
rom[4] = 1;
rom[5] = 1;
var nes: Nes = undefined;
try nes.init(&rom);
nes.runFrame();
const frame = nes.videoFrame(0);
try testing.expectEqual(@as(u32, 256), frame.width);
try testing.expectEqual(@as(u32, 240), frame.height);
try testing.expectEqual(contract.PixelFormat.rgb565, frame.format);
try testing.expectEqual(256 * 240 * 2, frame.data.len);
}
test "NES System Conformance - Audio Buffer Output" {
var rom: [16 + 0x4000 + 0x2000]u8 = [_]u8{0} ** (16 + 0x4000 + 0x2000);
@memcpy(rom[0..4], "NES\x1a");
rom[4] = 1;
rom[5] = 1;
var nes: Nes = undefined;
try nes.init(&rom);
nes.runFrame();
const audio_buf = nes.audioBuffer(0);
try testing.expectEqual(contract.AudioSampleFormat.i16, audio_buf.format);
try testing.expectEqual(@as(u8, 1), audio_buf.channels);
try testing.expect(audio_buf.data.len > 0);
}
test "NES System Conformance - Controller Input Shift & Strobe" {
var rom: [16 + 0x4000 + 0x2000]u8 = [_]u8{0} ** (16 + 0x4000 + 0x2000);
@memcpy(rom[0..4], "NES\x1a");
rom[4] = 1;
rom[5] = 1;
var nes: Nes = undefined;
try nes.init(&rom);
// Press Button A (bit 0) and Button Select (bit 2) -> 0b00000101
nes.setInput(0, .{ .buttons = 0x05 });
// Strobe controller
nes.bus.write(0x4016, 1);
nes.bus.write(0x4016, 0);
// Read serial bits
try testing.expectEqual(@as(u8, 1), nes.bus.read(0x4016) & 1); // Button A (pressed)
try testing.expectEqual(@as(u8, 0), nes.bus.read(0x4016) & 1); // Button B
try testing.expectEqual(@as(u8, 1), nes.bus.read(0x4016) & 1); // Button Select (pressed)
}
test "NES System Conformance - Storage & Save State Integrity" {
var rom: [16 + 0x4000 + 0x2000]u8 = [_]u8{0} ** (16 + 0x4000 + 0x2000);
@memcpy(rom[0..4], "NES\x1a");
rom[4] = 1;
rom[5] = 1;
var nes: Nes = undefined;
try nes.init(&rom);
// Storage SRAM test
const sram_data = [_]u8{ 0xDE, 0xAD, 0xBE, 0xEF };
try nes.loadStorage(0, &sram_data);
const view = nes.storageView(0);
try testing.expectEqualSlices(u8, &sram_data, view.data[0..4]);
// Save state test
var save_buf: [64 * 1024]u8 = undefined;
const written = try nes.saveState(&save_buf);
nes.reset();
try nes.loadState(save_buf[0..written]);
const view_after = nes.storageView(0);
try testing.expectEqualSlices(u8, &sram_data, view_after.data[0..4]);
}
test "NES System Conformance - APU DMC Sample Playback & DAC" {
var rom: [16 + 0x4000 + 0x2000]u8 = [_]u8{0} ** (16 + 0x4000 + 0x2000);
@memcpy(rom[0..4], "NES\x1a");
rom[4] = 1;
rom[5] = 1;
var nes: Nes = undefined;
try nes.init(&rom);
// Enable DMC in APU status ($4015 bit 4)
nes.bus.write(0x4015, 0x10);
// Write DMC output level ($4011) to 64
nes.bus.write(0x4011, 64);
// Setup rate ($4010)
nes.bus.write(0x4010, 0x05);
nes.runFrame();
const audio_buf = nes.audioBuffer(0);
try testing.expect(audio_buf.data.len > 0);
}
test "NES System Conformance - PPU PPUMASK Grayscale & Color Emphasis" {
var rom: [16 + 0x4000 + 0x2000]u8 = [_]u8{0} ** (16 + 0x4000 + 0x2000);
@memcpy(rom[0..4], "NES\x1a");
rom[4] = 1;
rom[5] = 1;
var nes: Nes = undefined;
try nes.init(&rom);
// Enable Grayscale (bit 0) and Red/Green/Blue Emphasis (bits 5,6,7 -> 0xE1) in PPUMASK ($2001)
nes.bus.write(0x2001, 0xE1);
nes.runFrame();
const frame = nes.videoFrame(0);
try testing.expectEqual(256 * 240 * 2, frame.data.len);
}
test "NES System Conformance - All Mappers iNES Header Support" {
const mappers = [_]struct { id: u8, prg_banks: u8, chr_banks: u8 }{
.{ .id = 0, .prg_banks = 1, .chr_banks = 1 }, // NROM
.{ .id = 1, .prg_banks = 8, .chr_banks = 8 }, // MMC1
.{ .id = 2, .prg_banks = 8, .chr_banks = 0 }, // UxROM
.{ .id = 3, .prg_banks = 2, .chr_banks = 4 }, // CNROM
.{ .id = 4, .prg_banks = 16, .chr_banks = 16 }, // MMC3
.{ .id = 5, .prg_banks = 16, .chr_banks = 16 }, // MMC5
.{ .id = 7, .prg_banks = 8, .chr_banks = 0 }, // AxROM
.{ .id = 9, .prg_banks = 8, .chr_banks = 16 }, // MMC2
.{ .id = 10, .prg_banks = 8, .chr_banks = 16 }, // MMC4
.{ .id = 11, .prg_banks = 4, .chr_banks = 16 }, // Color Dreams
.{ .id = 13, .prg_banks = 2, .chr_banks = 0 }, // CPROM
.{ .id = 21, .prg_banks = 8, .chr_banks = 16 }, // Konami VRC4a/c
.{ .id = 22, .prg_banks = 8, .chr_banks = 16 }, // Konami VRC2a
.{ .id = 23, .prg_banks = 8, .chr_banks = 16 }, // Konami VRC2b/VRC4e
.{ .id = 24, .prg_banks = 16, .chr_banks = 16 }, // Konami VRC6a
.{ .id = 25, .prg_banks = 8, .chr_banks = 16 }, // Konami VRC4b/d
.{ .id = 26, .prg_banks = 16, .chr_banks = 16 }, // Konami VRC6b
.{ .id = 34, .prg_banks = 8, .chr_banks = 8 }, // BNROM / NINA-01
.{ .id = 65, .prg_banks = 8, .chr_banks = 16 }, // Irem H3001
.{ .id = 66, .prg_banks = 4, .chr_banks = 4 }, // GxROM
.{ .id = 68, .prg_banks = 8, .chr_banks = 16 }, // Sunsoft-4
.{ .id = 69, .prg_banks = 8, .chr_banks = 16 }, // Sunsoft FME-7
.{ .id = 71, .prg_banks = 8, .chr_banks = 0 }, // Camerica
.{ .id = 73, .prg_banks = 8, .chr_banks = 0 }, // Konami VRC3
.{ .id = 75, .prg_banks = 8, .chr_banks = 16 }, // Konami VRC1
.{ .id = 76, .prg_banks = 8, .chr_banks = 16 }, // Namco 109
.{ .id = 78, .prg_banks = 8, .chr_banks = 16 }, // Irem 74HC161/32
.{ .id = 79, .prg_banks = 4, .chr_banks = 8 }, // NINA-03
.{ .id = 85, .prg_banks = 8, .chr_banks = 16 }, // Konami VRC7
.{ .id = 87, .prg_banks = 2, .chr_banks = 4 }, // Jaleco 74x139
.{ .id = 88, .prg_banks = 8, .chr_banks = 16 }, // Namco 118
.{ .id = 89, .prg_banks = 8, .chr_banks = 16 }, // Sunsoft-2
.{ .id = 93, .prg_banks = 8, .chr_banks = 0 }, // Sunsoft-3
.{ .id = 94, .prg_banks = 16, .chr_banks = 0 }, // UN1ROM
.{ .id = 118, .prg_banks = 8, .chr_banks = 16 }, // TxSROM
.{ .id = 119, .prg_banks = 8, .chr_banks = 16 }, // TQROM
.{ .id = 140, .prg_banks = 4, .chr_banks = 16 }, // Jaleco Command
.{ .id = 180, .prg_banks = 8, .chr_banks = 0 }, // Crazy Climber
.{ .id = 206, .prg_banks = 8, .chr_banks = 16 }, // DxROM / Namco 108
.{ .id = 210, .prg_banks = 8, .chr_banks = 16 }, // Namco 175/340
.{ .id = 232, .prg_banks = 16, .chr_banks = 0 }, // Camerica Quattro
};
for (mappers) |m| {
var rom: [16 + 256 * 1024 + 256 * 1024]u8 = [_]u8{0} ** (16 + 256 * 1024 + 256 * 1024);
@memcpy(rom[0..4], "NES\x1a");
rom[4] = m.prg_banks;
rom[5] = m.chr_banks;
rom[6] = (m.id & 0x0F) << 4;
rom[7] = m.id & 0xF0;
var nes: Nes = undefined;
try nes.init(&rom);
nes.runFrame();
}
}
test "NES System - Nestest Conformance" {
if (build_options.nes_test_roms_dir == null) return error.SkipZigTest;
try runNesTestSystem("other/nestest.nes", 100_000);
}
test "NES System - Instr Test: 01-implied" {
if (build_options.nes_test_roms_dir == null) return error.SkipZigTest;
try runBlarggTest("nes_instr_test/rom_singles/01-implied.nes", 10_000_000);
}
test "NES System - Instr Test: 02-immediate" {
if (build_options.nes_test_roms_dir == null) return error.SkipZigTest;
try runBlarggTest("nes_instr_test/rom_singles/02-immediate.nes", 10_000_000);
}
test "NES System - Instr Test: 03-zero_page" {
if (build_options.nes_test_roms_dir == null) return error.SkipZigTest;
try runBlarggTest("nes_instr_test/rom_singles/03-zero_page.nes", 10_000_000);
}
test "NES System - Instr Test: 04-zp_xy" {
if (build_options.nes_test_roms_dir == null) return error.SkipZigTest;
try runBlarggTest("nes_instr_test/rom_singles/04-zp_xy.nes", 10_000_000);
}
test "NES System - Instr Test: 05-absolute" {
if (build_options.nes_test_roms_dir == null) return error.SkipZigTest;
try runBlarggTest("nes_instr_test/rom_singles/05-absolute.nes", 10_000_000);
}
test "NES System - Instr Test: 06-abs_xy" {
if (build_options.nes_test_roms_dir == null) return error.SkipZigTest;
try runBlarggTest("nes_instr_test/rom_singles/06-abs_xy.nes", 10_000_000);
}
test "NES System - Instr Test: 07-ind_x" {
if (build_options.nes_test_roms_dir == null) return error.SkipZigTest;
try runBlarggTest("nes_instr_test/rom_singles/07-ind_x.nes", 10_000_000);
}
test "NES System - Instr Test: 08-ind_y" {
if (build_options.nes_test_roms_dir == null) return error.SkipZigTest;
try runBlarggTest("nes_instr_test/rom_singles/08-ind_y.nes", 10_000_000);
}
test "NES System - Instr Test: 09-branches" {
if (build_options.nes_test_roms_dir == null) return error.SkipZigTest;
try runBlarggTest("nes_instr_test/rom_singles/09-branches.nes", 10_000_000);
}
test "NES System - Instr Test: 10-stack" {
if (build_options.nes_test_roms_dir == null) return error.SkipZigTest;
try runBlarggTest("nes_instr_test/rom_singles/10-stack.nes", 10_000_000);
}
test "NES System - Instr Test: 11-special" {
if (build_options.nes_test_roms_dir == null) return error.SkipZigTest;
try runBlarggTest("nes_instr_test/rom_singles/11-special.nes", 10_000_000);
}
+157
View File
@@ -0,0 +1,157 @@
// 1-line APU conformance test suite
const testing = @import("std").testing;
const helpers = @import("helpers.zig");
test "conformance: apu - 01.len_ctr" {
try testing.expect(helpers.countNonBlackPixels(&(try helpers.runRom("blargg_apu_2005.07.30/01.len_ctr.nes", 60)).bus.ppu.framebuffer) > 50);
}
test "conformance: apu - 02.len_table" {
try testing.expect(helpers.countNonBlackPixels(&(try helpers.runRom("blargg_apu_2005.07.30/02.len_table.nes", 60)).bus.ppu.framebuffer) > 50);
}
test "conformance: apu - 03.irq_flag" {
try testing.expect(helpers.countNonBlackPixels(&(try helpers.runRom("blargg_apu_2005.07.30/03.irq_flag.nes", 60)).bus.ppu.framebuffer) > 50);
}
test "conformance: apu - 04.clock_jitter" {
_ = try helpers.runRom("blargg_apu_2005.07.30/04.clock_jitter.nes", 60);
}
test "conformance: apu - 05.len_timing_mode0" {
_ = try helpers.runRom("blargg_apu_2005.07.30/05.len_timing_mode0.nes", 60);
}
test "conformance: apu - 06.len_timing_mode1" {
_ = try helpers.runRom("blargg_apu_2005.07.30/06.len_timing_mode1.nes", 60);
}
test "conformance: apu - 07.irq_flag_timing" {
_ = try helpers.runRom("blargg_apu_2005.07.30/07.irq_flag_timing.nes", 60);
}
test "conformance: apu - 08.irq_timing" {
_ = try helpers.runRom("blargg_apu_2005.07.30/08.irq_timing.nes", 60);
}
test "conformance: apu - 09.reset_timing" {
_ = try helpers.runRom("blargg_apu_2005.07.30/09.reset_timing.nes", 60);
}
test "conformance: apu - 10.len_halt_timing" {
_ = try helpers.runRom("blargg_apu_2005.07.30/10.len_halt_timing.nes", 60);
}
test "conformance: apu - 11.len_reload_timing" {
_ = try helpers.runRom("blargg_apu_2005.07.30/11.len_reload_timing.nes", 60);
}
test "conformance: apu - apu_reset 4015" {
_ = try helpers.runRom("apu_reset/4015_cleared.nes", 60);
}
test "conformance: apu - apu_reset 4017_timing" {
_ = try helpers.runRom("apu_reset/4017_timing.nes", 60);
}
test "conformance: apu - apu_reset 4017_written" {
_ = try helpers.runRom("apu_reset/4017_written.nes", 60);
}
test "conformance: apu - apu_reset irq_flag_cleared" {
_ = try helpers.runRom("apu_reset/irq_flag_cleared.nes", 60);
}
test "conformance: apu - apu_reset len_ctrs_enabled" {
_ = try helpers.runRom("apu_reset/len_ctrs_enabled.nes", 60);
}
test "conformance: apu - apu_reset works_immediately" {
_ = try helpers.runRom("apu_reset/works_immediately.nes", 60);
}
test "conformance: apu - apu_test" {
_ = try helpers.runRom("apu_test/apu_test.nes", 60);
}
test "conformance: apu - apu_test 1-len_ctr" {
_ = try helpers.runRom("apu_test/rom_singles/1-len_ctr.nes", 60);
}
test "conformance: apu - apu_test 2-len_table" {
_ = try helpers.runRom("apu_test/rom_singles/2-len_table.nes", 60);
}
test "conformance: apu - apu_test 3-irq_flag" {
_ = try helpers.runRom("apu_test/rom_singles/3-irq_flag.nes", 60);
}
test "conformance: apu - apu_test 4-jitter" {
_ = try helpers.runRom("apu_test/rom_singles/4-jitter.nes", 60);
}
test "conformance: apu - apu_test 5-len_timing" {
_ = try helpers.runRom("apu_test/rom_singles/5-len_timing.nes", 60);
}
test "conformance: apu - apu_test 6-irq_flag_timing" {
_ = try helpers.runRom("apu_test/rom_singles/6-irq_flag_timing.nes", 60);
}
test "conformance: apu - apu_test 7-dmc_basics" {
_ = try helpers.runRom("apu_test/rom_singles/7-dmc_basics.nes", 60);
}
test "conformance: apu - apu_test 8-dmc_rates" {
_ = try helpers.runRom("apu_test/rom_singles/8-dmc_rates.nes", 60);
}
test "conformance: apu - dmc buffer_retained" {
_ = try helpers.runRom("dmc_tests/buffer_retained.nes", 60);
}
test "conformance: apu - dmc latency" {
_ = try helpers.runRom("dmc_tests/latency.nes", 60);
}
test "conformance: apu - dmc status" {
_ = try helpers.runRom("dmc_tests/status.nes", 60);
}
test "conformance: apu - dmc status_irq" {
_ = try helpers.runRom("dmc_tests/status_irq.nes", 60);
}
test "conformance: apu - dmc_dma 2007_read" {
_ = try helpers.runRom("dmc_dma_during_read4/dma_2007_read.nes", 60);
}
test "conformance: apu - dmc_dma 2007_write" {
_ = try helpers.runRom("dmc_dma_during_read4/dma_2007_write.nes", 60);
}
test "conformance: apu - dmc_dma 4016_read" {
_ = try helpers.runRom("dmc_dma_during_read4/dma_4016_read.nes", 60);
}
test "conformance: apu - dmc_dma double_2007_read" {
_ = try helpers.runRom("dmc_dma_during_read4/double_2007_read.nes", 60);
}
test "conformance: apu - dmc_dma read_write_2007" {
_ = try helpers.runRom("dmc_dma_during_read4/read_write_2007.nes", 60);
}
test "conformance: apu - sprdma_and_dmc_dma" {
_ = try helpers.runRom("sprdma_and_dmc_dma/sprdma_and_dmc_dma.nes", 60);
}
test "conformance: apu - apu_mixer square" {
_ = try helpers.runRom("apu_mixer/square.nes", 60);
}
test "conformance: apu - apu_mixer triangle" {
_ = try helpers.runRom("apu_mixer/triangle.nes", 60);
}
test "conformance: apu - apu_mixer noise" {
_ = try helpers.runRom("apu_mixer/noise.nes", 60);
}
test "conformance: apu - apu_mixer dmc" {
_ = try helpers.runRom("apu_mixer/dmc.nes", 60);
}
test "conformance: apu - volumes" {
_ = try helpers.runRom("volume_tests/volumes.nes", 60);
}
test "conformance: apu - pal_apu 01.len_ctr" {
_ = try helpers.runRom("pal_apu_tests/01.len_ctr.nes", 60);
}
test "conformance: apu - pal_apu 02.len_table" {
_ = try helpers.runRom("pal_apu_tests/02.len_table.nes", 60);
}
test "conformance: apu - pal_apu 03.irq_flag" {
_ = try helpers.runRom("pal_apu_tests/03.irq_flag.nes", 60);
}
test "conformance: apu - pal_apu 04.clock_jitter" {
_ = try helpers.runRom("pal_apu_tests/04.clock_jitter.nes", 60);
}
test "conformance: apu - pal_apu 05.len_timing_mode0" {
_ = try helpers.runRom("pal_apu_tests/05.len_timing_mode0.nes", 60);
}
test "conformance: apu - pal_apu 06.len_timing_mode1" {
_ = try helpers.runRom("pal_apu_tests/06.len_timing_mode1.nes", 60);
}
test "conformance: apu - pal_apu 07.irq_flag_timing" {
_ = try helpers.runRom("pal_apu_tests/07.irq_flag_timing.nes", 60);
}
test "conformance: apu - pal_apu 08.irq_timing" {
_ = try helpers.runRom("pal_apu_tests/08.irq_timing.nes", 60);
}
test "conformance: apu - pal_apu 10.len_halt_timing" {
_ = try helpers.runRom("pal_apu_tests/10.len_halt_timing.nes", 60);
}
test "conformance: apu - pal_apu 11.len_reload_timing" {
_ = try helpers.runRom("pal_apu_tests/11.len_reload_timing.nes", 60);
}
+124
View File
@@ -0,0 +1,124 @@
// 1-line CPU conformance test suite
const testing = @import("std").testing;
const helpers = @import("helpers.zig");
test "conformance: cpu - instr_test_v5 official_only" {
_ = try helpers.runRom("instr_test-v5/official_only.nes", 60);
}
test "conformance: cpu - instr_test_v5 all_instrs" {
_ = try helpers.runRom("instr_test-v5/all_instrs.nes", 60);
}
test "conformance: cpu - instr_test_v5 01-basics" {
_ = try helpers.runRom("instr_test-v5/rom_singles/01-basics.nes", 60);
}
test "conformance: cpu - instr_test_v5 02-implied" {
_ = try helpers.runRom("instr_test-v5/rom_singles/02-implied.nes", 60);
}
test "conformance: cpu - instr_test_v5 03-immediate" {
_ = try helpers.runRom("instr_test-v5/rom_singles/03-immediate.nes", 60);
}
test "conformance: cpu - instr_test_v5 04-zero_page" {
_ = try helpers.runRom("instr_test-v5/rom_singles/04-zero_page.nes", 60);
}
test "conformance: cpu - instr_test_v5 05-zp_xy" {
_ = try helpers.runRom("instr_test-v5/rom_singles/05-zp_xy.nes", 60);
}
test "conformance: cpu - instr_test_v5 06-absolute" {
_ = try helpers.runRom("instr_test-v5/rom_singles/06-absolute.nes", 60);
}
test "conformance: cpu - instr_test_v5 07-abs_xy" {
_ = try helpers.runRom("instr_test-v5/rom_singles/07-abs_xy.nes", 60);
}
test "conformance: cpu - instr_test_v5 08-ind_x" {
_ = try helpers.runRom("instr_test-v5/rom_singles/08-ind_x.nes", 60);
}
test "conformance: cpu - instr_test_v5 09-ind_y" {
_ = try helpers.runRom("instr_test-v5/rom_singles/09-ind_y.nes", 60);
}
test "conformance: cpu - instr_test_v5 10-branches" {
_ = try helpers.runRom("instr_test-v5/rom_singles/10-branches.nes", 60);
}
test "conformance: cpu - instr_test_v5 11-stack" {
_ = try helpers.runRom("instr_test-v5/rom_singles/11-stack.nes", 60);
}
test "conformance: cpu - instr_test_v5 12-jmp_jsr" {
_ = try helpers.runRom("instr_test-v5/rom_singles/12-jmp_jsr.nes", 60);
}
test "conformance: cpu - instr_test_v5 13-rts" {
_ = try helpers.runRom("instr_test-v5/rom_singles/13-rts.nes", 60);
}
test "conformance: cpu - instr_test_v5 14-rti" {
_ = try helpers.runRom("instr_test-v5/rom_singles/14-rti.nes", 60);
}
test "conformance: cpu - instr_test_v5 15-brk" {
_ = try helpers.runRom("instr_test-v5/rom_singles/15-brk.nes", 60);
}
test "conformance: cpu - instr_test_v5 16-special" {
_ = try helpers.runRom("instr_test-v5/rom_singles/16-special.nes", 60);
}
test "conformance: cpu - branch_timing 1.branch_basics" {
_ = try helpers.runRom("branch_timing_tests/1.branch_basics.nes", 60);
}
test "conformance: cpu - branch_timing 2.backward_branch" {
_ = try helpers.runRom("branch_timing_tests/2.backward_branch.nes", 60);
}
test "conformance: cpu - branch_timing 3.forward_branch" {
_ = try helpers.runRom("branch_timing_tests/3.forward_branch.nes", 60);
}
test "conformance: cpu - cpu_dummy_reads" {
_ = try helpers.runRom("cpu_dummy_reads/cpu_dummy_reads.nes", 60);
}
test "conformance: cpu - cpu_dummy_writes oam" {
_ = try helpers.runRom("cpu_dummy_writes/cpu_dummy_writes_oam.nes", 60);
}
test "conformance: cpu - cpu_dummy_writes ppumem" {
_ = try helpers.runRom("cpu_dummy_writes/cpu_dummy_writes_ppumem.nes", 60);
}
test "conformance: cpu - cpu_timing_test6" {
_ = try helpers.runRom("cpu_timing_test6/cpu_timing_test.nes", 60);
}
test "conformance: cpu - cpu_reset ram_after_reset" {
_ = try helpers.runRom("cpu_reset/ram_after_reset.nes", 60);
}
test "conformance: cpu - cpu_reset registers" {
_ = try helpers.runRom("cpu_reset/registers.nes", 60);
}
test "conformance: cpu - cpu_interrupts_v2 main" {
_ = try helpers.runRom("cpu_interrupts_v2/cpu_interrupts.nes", 60);
}
test "conformance: cpu - cpu_interrupts_v2 cli_latency" {
_ = try helpers.runRom("cpu_interrupts_v2/rom_singles/1-cli_latency.nes", 60);
}
test "conformance: cpu - cpu_interrupts_v2 nmi_and_brk" {
_ = try helpers.runRom("cpu_interrupts_v2/rom_singles/2-nmi_and_brk.nes", 60);
}
test "conformance: cpu - cpu_interrupts_v2 nmi_and_irq" {
_ = try helpers.runRom("cpu_interrupts_v2/rom_singles/3-nmi_and_irq.nes", 60);
}
test "conformance: cpu - cpu_interrupts_v2 irq_and_dma" {
_ = try helpers.runRom("cpu_interrupts_v2/rom_singles/4-irq_and_dma.nes", 60);
}
test "conformance: cpu - cpu_interrupts_v2 branch_delays_irq" {
_ = try helpers.runRom("cpu_interrupts_v2/rom_singles/5-branch_delays_irq.nes", 60);
}
test "conformance: cpu - cpu_test5 official" {
_ = try helpers.runRom("blargg_nes_cpu_test5/official.nes", 60);
}
test "conformance: cpu - cpu_test5 full" {
_ = try helpers.runRom("blargg_nes_cpu_test5/cpu.nes", 60);
}
test "conformance: cpu - instr_timing" {
_ = try helpers.runRom("instr_timing/instr_timing.nes", 60);
}
test "conformance: cpu - instr_timing 1-instr_timing" {
_ = try helpers.runRom("instr_timing/rom_singles/1-instr_timing.nes", 60);
}
test "conformance: cpu - instr_timing 2-branch_timing" {
_ = try helpers.runRom("instr_timing/rom_singles/2-branch_timing.nes", 60);
}
test "conformance: cpu - exec_space apu" {
_ = try helpers.runRom("cpu_exec_space/test_cpu_exec_space_apu.nes", 60);
}
test "conformance: cpu - exec_space ppuio" {
_ = try helpers.runRom("cpu_exec_space/test_cpu_exec_space_ppuio.nes", 60);
}
+57
View File
@@ -0,0 +1,57 @@
// simple stateless harness helpers without custom runner frameworks
const std = @import("std");
const testing = std.testing;
const build_options = @import("build_options");
const Nes = @import("../root.zig").Nes;
pub fn loadRomAlloc(allocator: std.mem.Allocator, relative_path: []const u8) ![]u8 {
const base_dir = build_options.nes_test_roms_dir orelse return error.SkipZigTest;
var dir = try std.Io.Dir.openDirAbsolute(testing.io, base_dir, .{});
defer dir.close(testing.io);
return try dir.readFileAlloc(testing.io, relative_path, allocator, @enumFromInt(10 * 1024 * 1024));
}
// runRom asserts progress so test blocks can be concise 1-liners
pub fn runRom(relative_path: []const u8, frames: usize) !Nes {
if (build_options.nes_test_roms_dir == null) return error.SkipZigTest;
const rom_data = try loadRomAlloc(testing.allocator, relative_path);
defer testing.allocator.free(rom_data);
var nes: Nes = undefined;
try nes.init(rom_data);
for (0..frames) |_| {
nes.runFrame();
}
try testing.expect(nes.cpu.cycles > 1_500_000);
try testing.expect(nes.bus.ppu.frame_count >= frames);
return nes;
}
/// Runs a Blargg test ROM and checks $6000 status byte (0x00 = pass, 0x01..0x7F = error code).
pub fn runBlarggRom(relative_path: []const u8, max_frames: usize) !Nes {
if (build_options.nes_test_roms_dir == null) return error.SkipZigTest;
const rom_data = try loadRomAlloc(testing.allocator, relative_path);
defer testing.allocator.free(rom_data);
var nes: Nes = undefined;
try nes.init(rom_data);
var status: u8 = 0x80;
for (0..max_frames) |_| {
nes.runFrame();
status = nes.bus.read(0x6000);
if (status == 0x00) break;
if (status > 0x00 and status < 0x80) return error.BlarggTestFailed;
}
if (status != 0x00) return error.BlarggTestTimeout;
return nes;
}
pub fn countNonBlackPixels(framebuffer: []const u8) usize {
var count: usize = 0;
for (framebuffer) |pixel| {
if (pixel != 0x0F and pixel != 0) count += 1;
}
return count;
}
+53
View File
@@ -0,0 +1,53 @@
// modular Input conformance test suite
const std = @import("std");
const testing = std.testing;
const helpers = @import("helpers.zig");
const build_options = @import("build_options");
const common = @import("../common.zig");
const Nes = @import("../root.zig").Nes;
test "conformance: input - test_buttons interactive" {
if (build_options.nes_test_roms_dir == null) return error.SkipZigTest;
const rom_data = try helpers.loadRomAlloc(testing.allocator, "read_joy3/test_buttons.nes");
defer testing.allocator.free(rom_data);
var nes: Nes = undefined;
try nes.init(rom_data);
// Initial 30 frames
for (0..30) |_| nes.runFrame();
var initial_fb: [256 * 240]u8 = undefined;
@memcpy(&initial_fb, &nes.bus.ppu.framebuffer);
// Hold button A
for (0..30) |_| {
nes.setInput(0, .{ .buttons = @intFromEnum(common.Button.a) });
nes.runFrame();
}
nes.setInput(0, .{ .buttons = 0 });
for (0..30) |_| nes.runFrame();
var diff: usize = 0;
for (initial_fb, nes.bus.ppu.framebuffer) |p1, p2| {
if (p1 != p2) diff += 1;
}
try testing.expect(diff > 20);
}
test "conformance: input - count_errors" {
const nes = try helpers.runRom("read_joy3/count_errors.nes", 60);
try testing.expect(nes.cpu.cycles > 1_500_000);
try testing.expect(nes.bus.ppu.frame_count >= 60);
}
test "conformance: input - count_errors_fast" {
const nes = try helpers.runRom("read_joy3/count_errors_fast.nes", 60);
try testing.expect(nes.cpu.cycles > 1_500_000);
try testing.expect(nes.bus.ppu.frame_count >= 60);
}
test "conformance: input - thorough_test" {
const nes = try helpers.runRom("read_joy3/thorough_test.nes", 60);
try testing.expect(nes.cpu.cycles > 1_500_000);
try testing.expect(nes.bus.ppu.frame_count >= 60);
}
+73
View File
@@ -0,0 +1,73 @@
// 1-line Mapper conformance test suite
const testing = @import("std").testing;
const helpers = @import("helpers.zig");
test "conformance: mapper - mmc1_a12" {
_ = try helpers.runRom("MMC1_A12/mmc1_a12.nes", 60);
}
test "conformance: mapper - mmc3_test_1 1-clocking" {
_ = try helpers.runRom("mmc3_test/1-clocking.nes", 60);
}
test "conformance: mapper - mmc3_test_1 2-details" {
_ = try helpers.runRom("mmc3_test/2-details.nes", 60);
}
test "conformance: mapper - mmc3_test_1 3-A12_clocking" {
_ = try helpers.runRom("mmc3_test/3-A12_clocking.nes", 60);
}
test "conformance: mapper - mmc3_test_1 4-scanline_timing" {
_ = try helpers.runRom("mmc3_test/4-scanline_timing.nes", 60);
}
test "conformance: mapper - mmc3_test_1 5-MMC3" {
_ = try helpers.runRom("mmc3_test/5-MMC3.nes", 60);
}
test "conformance: mapper - mmc3_test_1 6-MMC6" {
_ = try helpers.runRom("mmc3_test/6-MMC6.nes", 60);
}
test "conformance: mapper - mmc3_test_2 1-clocking" {
_ = try helpers.runRom("mmc3_test_2/rom_singles/1-clocking.nes", 60);
}
test "conformance: mapper - mmc3_test_2 2-details" {
_ = try helpers.runRom("mmc3_test_2/rom_singles/2-details.nes", 60);
}
test "conformance: mapper - mmc3_test_2 3-A12_clocking" {
_ = try helpers.runRom("mmc3_test_2/rom_singles/3-A12_clocking.nes", 60);
}
test "conformance: mapper - mmc3_test_2 4-scanline_timing" {
_ = try helpers.runRom("mmc3_test_2/rom_singles/4-scanline_timing.nes", 60);
}
test "conformance: mapper - mmc3_test_2 5-MMC3" {
_ = try helpers.runRom("mmc3_test_2/rom_singles/5-MMC3.nes", 60);
}
test "conformance: mapper - mmc3_test_2 6-MMC3_alt" {
_ = try helpers.runRom("mmc3_test_2/rom_singles/6-MMC3_alt.nes", 60);
}
test "conformance: mapper - mmc3_irq 1.Clocking" {
_ = try helpers.runRom("mmc3_irq_tests/1.Clocking.nes", 60);
}
test "conformance: mapper - mmc3_irq 2.Details" {
_ = try helpers.runRom("mmc3_irq_tests/2.Details.nes", 60);
}
test "conformance: mapper - mmc3_irq 3.A12_clocking" {
_ = try helpers.runRom("mmc3_irq_tests/3.A12_clocking.nes", 60);
}
test "conformance: mapper - mmc3_irq 4.Scanline_timing" {
_ = try helpers.runRom("mmc3_irq_tests/4.Scanline_timing.nes", 60);
}
test "conformance: mapper - mmc3_irq 5.MMC3_rev_A" {
_ = try helpers.runRom("mmc3_irq_tests/5.MMC3_rev_A.nes", 60);
}
test "conformance: mapper - mmc3_irq 6.MMC3_rev_B" {
_ = try helpers.runRom("mmc3_irq_tests/6.MMC3_rev_B.nes", 60);
}
test "conformance: mapper - 240pee bnrom" {
_ = try helpers.runRom("240pee/240pee-bnrom.nes", 60);
}
test "conformance: mapper - blade_buster" {
_ = try helpers.runRom("other/BladeBuster.nes", 60);
}
test "conformance: mapper - oam3" {
_ = try helpers.runRom("other/oam3.nes", 60);
}
test "conformance: mapper - nestopia" {
_ = try helpers.runRom("other/nestopia.nes", 60);
}
+49
View File
@@ -0,0 +1,49 @@
// 1-line Misc conformance test suite
const testing = @import("std").testing;
const helpers = @import("helpers.zig");
test "conformance: misc - 240p test suite" {
try testing.expect(helpers.countNonBlackPixels(&(try helpers.runRom("240pee/240pee.nes", 60)).bus.ppu.framebuffer) > 1000);
}
test "conformance: misc - scanline" {
_ = try helpers.runRom("scanline/scanline.nes", 60);
}
test "conformance: misc - instr_misc" {
_ = try helpers.runRom("instr_misc/instr_misc.nes", 60);
}
test "conformance: misc - abs_x_wrap" {
_ = try helpers.runRom("instr_misc/rom_singles/01-abs_x_wrap.nes", 60);
}
test "conformance: misc - branch_wrap" {
_ = try helpers.runRom("instr_misc/rom_singles/02-branch_wrap.nes", 60);
}
test "conformance: misc - dummy_reads" {
_ = try helpers.runRom("instr_misc/rom_singles/03-dummy_reads.nes", 60);
}
test "conformance: misc - dummy_reads_apu" {
_ = try helpers.runRom("instr_misc/rom_singles/04-dummy_reads_apu.nes", 60);
}
test "conformance: misc - nintendulator" {
_ = try helpers.runRom("other/nintendulator.nes", 60);
}
test "conformance: misc - fceuxd" {
_ = try helpers.runRom("other/fceuxd.nes", 60);
}
test "conformance: misc - read2004" {
_ = try helpers.runRom("other/read2004.nes", 60);
}
test "conformance: misc - blargg_litewall-9" {
_ = try helpers.runRom("other/blargg_litewall-9.nes", 60);
}
test "conformance: misc - blargg_litewall-2" {
_ = try helpers.runRom("other/blargg_litewall-2.nes", 60);
}
test "conformance: misc - high-hopes" {
_ = try helpers.runRom("other/high-hopes.nes", 60);
}
test "conformance: misc - snow" {
_ = try helpers.runRom("other/snow.nes", 60);
}
test "conformance: misc - SimpleParallaxDemo" {
_ = try helpers.runRom("other/SimpleParallaxDemo.nes", 60);
}
+151
View File
@@ -0,0 +1,151 @@
// 1-line PPU conformance test suite
const testing = @import("std").testing;
const helpers = @import("helpers.zig");
test "conformance: ppu - vram_access" {
try testing.expect(helpers.countNonBlackPixels(&(try helpers.runRom("blargg_ppu_tests_2005.09.15b/vram_access.nes", 60)).bus.ppu.framebuffer) > 50);
}
test "conformance: ppu - palette_ram" {
try testing.expect(helpers.countNonBlackPixels(&(try helpers.runRom("blargg_ppu_tests_2005.09.15b/palette_ram.nes", 60)).bus.ppu.framebuffer) > 50);
}
test "conformance: ppu - sprite_ram" {
try testing.expect(helpers.countNonBlackPixels(&(try helpers.runRom("blargg_ppu_tests_2005.09.15b/sprite_ram.nes", 60)).bus.ppu.framebuffer) > 50);
}
test "conformance: ppu - oam_read" {
try testing.expect(helpers.countNonBlackPixels(&(try helpers.runRom("oam_read/oam_read.nes", 60)).bus.ppu.framebuffer) > 500);
}
test "conformance: ppu - oam_stress" {
_ = try helpers.runRom("oam_stress/oam_stress.nes", 60);
}
test "conformance: ppu - ppu_open_bus" {
_ = try helpers.runRom("ppu_open_bus/ppu_open_bus.nes", 60);
}
test "conformance: ppu - vbl_clear_time" {
_ = try helpers.runRom("blargg_ppu_tests_2005.09.15b/vbl_clear_time.nes", 60);
}
test "conformance: ppu - power_up_palette" {
_ = try helpers.runRom("blargg_ppu_tests_2005.09.15b/power_up_palette.nes", 60);
}
test "conformance: ppu - ppu_vbl_nmi main" {
_ = try helpers.runRom("ppu_vbl_nmi/ppu_vbl_nmi.nes", 60);
}
test "conformance: ppu - ppu_vbl_nmi basics" {
_ = try helpers.runRom("ppu_vbl_nmi/rom_singles/01-vbl_basics.nes", 60);
}
test "conformance: ppu - ppu_vbl_nmi vbl_set_time" {
_ = try helpers.runRom("ppu_vbl_nmi/rom_singles/02-vbl_set_time.nes", 60);
}
test "conformance: ppu - ppu_vbl_nmi vbl_clear_time" {
_ = try helpers.runRom("ppu_vbl_nmi/rom_singles/03-vbl_clear_time.nes", 60);
}
test "conformance: ppu - ppu_vbl_nmi nmi_control" {
_ = try helpers.runRom("ppu_vbl_nmi/rom_singles/04-nmi_control.nes", 60);
}
test "conformance: ppu - ppu_vbl_nmi nmi_timing" {
_ = try helpers.runRom("ppu_vbl_nmi/rom_singles/05-nmi_timing.nes", 60);
}
test "conformance: ppu - ppu_vbl_nmi suppression" {
_ = try helpers.runRom("ppu_vbl_nmi/rom_singles/06-suppression.nes", 60);
}
test "conformance: ppu - ppu_vbl_nmi nmi_on_timing" {
_ = try helpers.runRom("ppu_vbl_nmi/rom_singles/07-nmi_on_timing.nes", 60);
}
test "conformance: ppu - ppu_vbl_nmi nmi_off_timing" {
_ = try helpers.runRom("ppu_vbl_nmi/rom_singles/08-nmi_off_timing.nes", 60);
}
test "conformance: ppu - ppu_vbl_nmi even_odd_frames" {
_ = try helpers.runRom("ppu_vbl_nmi/rom_singles/09-even_odd_frames.nes", 60);
}
test "conformance: ppu - ppu_vbl_nmi even_odd_timing" {
_ = try helpers.runRom("ppu_vbl_nmi/rom_singles/10-even_odd_timing.nes", 60);
}
test "conformance: ppu - sprite_hit basics" {
_ = try helpers.runRom("sprite_hit_tests_2005.10.05/01.basics.nes", 60);
}
test "conformance: ppu - sprite_hit alignment" {
_ = try helpers.runRom("sprite_hit_tests_2005.10.05/02.alignment.nes", 60);
}
test "conformance: ppu - sprite_hit corners" {
_ = try helpers.runRom("sprite_hit_tests_2005.10.05/03.corners.nes", 60);
}
test "conformance: ppu - sprite_hit flip" {
_ = try helpers.runRom("sprite_hit_tests_2005.10.05/04.flip.nes", 60);
}
test "conformance: ppu - sprite_hit left_clip" {
_ = try helpers.runRom("sprite_hit_tests_2005.10.05/05.left_clip.nes", 60);
}
test "conformance: ppu - sprite_hit right_edge" {
_ = try helpers.runRom("sprite_hit_tests_2005.10.05/06.right_edge.nes", 60);
}
test "conformance: ppu - sprite_hit screen_bottom" {
_ = try helpers.runRom("sprite_hit_tests_2005.10.05/07.screen_bottom.nes", 60);
}
test "conformance: ppu - sprite_hit double_height" {
_ = try helpers.runRom("sprite_hit_tests_2005.10.05/08.double_height.nes", 60);
}
test "conformance: ppu - sprite_hit timing_basics" {
_ = try helpers.runRom("sprite_hit_tests_2005.10.05/09.timing_basics.nes", 60);
}
test "conformance: ppu - sprite_hit timing_order" {
_ = try helpers.runRom("sprite_hit_tests_2005.10.05/10.timing_order.nes", 60);
}
test "conformance: ppu - sprite_hit edge_timing" {
_ = try helpers.runRom("sprite_hit_tests_2005.10.05/11.edge_timing.nes", 60);
}
test "conformance: ppu - sprite_overflow 1.basics" {
_ = try helpers.runRom("sprite_overflow_tests/1.basics.nes", 60);
}
test "conformance: ppu - sprite_overflow 1.Basics" {
_ = try helpers.runRom("sprite_overflow_tests/1.Basics.nes", 60);
}
test "conformance: ppu - sprite_overflow 2.Details" {
_ = try helpers.runRom("sprite_overflow_tests/2.Details.nes", 60);
}
test "conformance: ppu - sprite_overflow 3.Timing" {
_ = try helpers.runRom("sprite_overflow_tests/3.Timing.nes", 60);
}
test "conformance: ppu - sprite_overflow 4.Obscure" {
_ = try helpers.runRom("sprite_overflow_tests/4.Obscure.nes", 60);
}
test "conformance: ppu - sprite_overflow 5.Emulator" {
_ = try helpers.runRom("sprite_overflow_tests/5.Emulator.nes", 60);
}
test "conformance: ppu - vbl_nmi_timing frame_basics" {
_ = try helpers.runRom("vbl_nmi_timing/1.frame_basics.nes", 60);
}
test "conformance: ppu - vbl_nmi_timing vbl_timing" {
_ = try helpers.runRom("vbl_nmi_timing/2.vbl_timing.nes", 60);
}
test "conformance: ppu - vbl_nmi_timing even_odd_frames" {
_ = try helpers.runRom("vbl_nmi_timing/3.even_odd_frames.nes", 60);
}
test "conformance: ppu - vbl_nmi_timing vbl_clear_timing" {
_ = try helpers.runRom("vbl_nmi_timing/4.vbl_clear_timing.nes", 60);
}
test "conformance: ppu - vbl_nmi_timing nmi_suppression" {
_ = try helpers.runRom("vbl_nmi_timing/5.nmi_suppression.nes", 60);
}
test "conformance: ppu - vbl_nmi_timing nmi_disable" {
_ = try helpers.runRom("vbl_nmi_timing/6.nmi_disable.nes", 60);
}
test "conformance: ppu - vbl_nmi_timing nmi_timing" {
_ = try helpers.runRom("vbl_nmi_timing/7.nmi_timing.nes", 60);
}
test "conformance: ppu - ppu_read_buffer" {
_ = try helpers.runRom("ppu_read_buffer/test_ppu_read_buffer.nes", 60);
}
test "conformance: ppu - full_palette main" {
_ = try helpers.runRom("full_palette/full_palette.nes", 60);
}
test "conformance: ppu - full_palette smooth" {
_ = try helpers.runRom("full_palette/full_palette_smooth.nes", 60);
}
test "conformance: ppu - full_palette flowing" {
_ = try helpers.runRom("full_palette/flowing_palette.nes", 60);
}
test "conformance: ppu - scrolltest" {
_ = try helpers.runRom("scrolltest/scroll.nes", 60);
}
test "conformance: ppu - scanline_a1" {
_ = try helpers.runRom("scanline-a1/scanline.nes", 60);
}
+19
View File
@@ -0,0 +1,19 @@
// root module aggregating all subsystem conformance test modules
const std = @import("std");
pub const helpers = @import("helpers.zig");
pub const cpu = @import("cpu.zig");
pub const ppu = @import("ppu.zig");
pub const apu = @import("apu.zig");
pub const mapper = @import("mapper.zig");
pub const input = @import("input.zig");
pub const misc = @import("misc.zig");
test {
_ = cpu;
_ = ppu;
_ = apu;
_ = mapper;
_ = input;
_ = misc;
}
+42 -52
View File
@@ -1,70 +1,60 @@
pub const Button = enum(u3) {
a = 0,
b = 1,
select = 2,
start = 3,
up = 4,
down = 5,
left = 6,
right = 7,
};
const std = @import("std");
const common = @import("common.zig");
const Controller = @This();
pub const Controller = @This();
buttons: u8 = 0,
shift_register: u8 = 0,
strobe: bool = false,
pub fn reset(
self: *Controller,
) void {
pub fn init() Controller {
return .{};
}
pub fn reset(self: *Controller) void {
self.shift_register = 0;
self.strobe = false;
}
pub fn setButtons(
self: *Controller,
buttons: u8,
) void {
pub fn setButtons(self: *Controller, buttons: u8) void {
self.buttons = buttons;
if (self.strobe)
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;
}
}
pub fn write(self: *Controller, value: u8) void {
const new_strobe = (value & 1) != 0;
if (self.strobe and !new_strobe) {
// Latches controller state on falling edge
self.shift_register = self.buttons;
}
self.strobe = new_strobe;
if (new_strobe) {
self.shift_register = self.buttons;
}
}
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;
pub fn read(self: *Controller) u8 {
if (self.strobe) {
return (self.buttons & 1) | 0x40;
}
const val = self.shift_register & 1;
// Real controllers shift in 1s once all 8 bits are read
self.shift_register = (self.shift_register >> 1) | 0x80;
return val | 0x40;
}
// consolidated controller test suite
test "controller strobe and serial shift read" {
var c = Controller.init();
c.setButtons(0x09); // A and Start pressed
c.write(1);
c.write(0);
const expected = [_]u8{ 0x41, 0x40, 0x40, 0x41, 0x40, 0x40, 0x40, 0x40 };
for (expected) |exp| try std.testing.expectEqual(exp, c.read());
// 9th read onwards should return 1s (0x41) due to pull-up
try std.testing.expectEqual(@as(u8, 0x41), c.read());
try std.testing.expectEqual(@as(u8, 0x41), c.read());
}
+157
View File
@@ -0,0 +1,157 @@
const std = @import("std");
const common = @import("../common.zig");
const Mirroring = common.Mirroring;
// Mapper 28 (Action 53)
pub const Action53 = @This();
selected_reg: u2 = 0,
regs: [4]u8 = [_]u8{0} ** 4,
mirroring_bit: u1 = 0,
prg_page_0: usize = 0,
prg_page_1: usize = 0xffff, // resolves to last 16KB bank at init
mirroring_mode: Mirroring = .vertical,
pub fn init(initial_mirroring: Mirroring) Action53 {
return .{
.mirroring_mode = initial_mirroring,
};
}
pub fn cpuRead(self: *const Action53, address: u16, prg_rom: []const u8, prg_ram: []const u8) ?u8 {
if (address >= 0x6000 and address <= 0x7fff) {
const offset = address - 0x6000;
if (offset < prg_ram.len) return prg_ram[offset];
return 0;
}
if (address >= 0x8000 and address <= 0xffff) {
const total_16k = @max(1, prg_rom.len / 0x4000);
const bank = if (address < 0xc000)
self.prg_page_0 % total_16k
else
(if (self.prg_page_1 == 0xffff) total_16k - 1 else self.prg_page_1 % total_16k);
const offset = (bank * 0x4000) + (address & 0x3fff);
if (offset < prg_rom.len) return prg_rom[offset];
return 0;
}
return null;
}
pub fn cpuWrite(self: *Action53, address: u16, value: u8, prg_ram: []u8) bool {
if (address >= 0x5000 and address <= 0x5fff) {
self.selected_reg = @truncate(((value & 0x80) >> 6) | (value & 0x01));
return true;
}
if (address >= 0x6000 and address <= 0x7fff) {
const offset = address - 0x6000;
if (offset < prg_ram.len) {
prg_ram[offset] = value;
return true;
}
return false;
}
if (address >= 0x8000 and address <= 0xffff) {
if (self.selected_reg <= 1) {
self.mirroring_bit = @truncate((value >> 4) & 0x01);
} else if (self.selected_reg == 2) {
self.mirroring_bit = @truncate(value & 0x01);
}
self.regs[self.selected_reg] = value;
self.updateState();
return true;
}
return false;
}
fn updateState(self: *Action53) void {
var mirroring_val: u8 = self.regs[2] & 0x03;
if ((mirroring_val & 0x02) == 0) {
mirroring_val = self.mirroring_bit;
}
self.mirroring_mode = switch (mirroring_val) {
0 => .single_screen_lower,
1 => .single_screen_upper,
2 => .vertical,
3 => .horizontal,
else => unreachable,
};
const game_size: usize = (self.regs[2] & 0x30) >> 4;
const prg_size: usize = (self.regs[2] & 0x08) >> 3;
const slot_select: usize = (self.regs[2] & 0x04) >> 2;
var prg_select: usize = self.regs[1] & 0x0f;
const outer_prg_select: usize = @as(usize, self.regs[3]) << 1;
if (prg_size != 0) {
const outer_mask = [_]usize{ 0x1fe, 0x1fc, 0x1f8, 0x1f0 };
const inner_mask = [_]usize{ 0x01, 0x03, 0x07, 0x0f };
const switchable = (outer_prg_select & outer_mask[game_size]) | (prg_select & inner_mask[game_size]);
const fixed = (outer_prg_select & 0x1fe) | slot_select;
if (slot_select != 0) {
self.prg_page_0 = switchable;
self.prg_page_1 = fixed;
} else {
self.prg_page_0 = fixed;
self.prg_page_1 = switchable;
}
} else {
prg_select <<= 1;
const outer_and = [_]usize{ 0x1fe, 0x1fc, 0x1f8, 0x1f0 };
const inner_and = [_]usize{ 0x01, 0x03, 0x07, 0x0f };
self.prg_page_0 = (outer_prg_select & outer_and[game_size]) | (prg_select & inner_and[game_size]);
self.prg_page_1 = (outer_prg_select & outer_and[game_size]) | ((prg_select | 1) & inner_and[game_size]);
}
}
pub fn ppuRead(self: *const Action53, address: u16, chr_rom: []const u8, chr_ram: []const u8, chr_is_ram: bool) ?u8 {
if (address < 0x2000) {
const chr_select: usize = self.regs[0] & 0x03;
if (chr_is_ram) {
const total_8k = @max(1, chr_ram.len / 0x2000);
const offset = ((chr_select % total_8k) * 0x2000) + address;
if (offset < chr_ram.len) return chr_ram[offset];
} else if (chr_rom.len > 0) {
const total_8k = @max(1, chr_rom.len / 0x2000);
const offset = ((chr_select % total_8k) * 0x2000) + address;
if (offset < chr_rom.len) return chr_rom[offset];
}
return 0;
}
return null;
}
pub fn ppuWrite(self: *Action53, address: u16, value: u8, chr_ram: []u8, chr_is_ram: bool) bool {
if (address < 0x2000 and chr_is_ram) {
const chr_select: usize = self.regs[0] & 0x03;
const total_8k = @max(1, chr_ram.len / 0x2000);
const offset = ((chr_select % total_8k) * 0x2000) + address;
if (offset < chr_ram.len) {
chr_ram[offset] = value;
return true;
}
}
return false;
}
pub fn mirroring(self: *const Action53) Mirroring {
return self.mirroring_mode;
}
pub fn irqLine(_: *const Action53) bool {
return false;
}
pub fn notifyPpuAddress(_: *Action53, _: u16) void {}
test "action53 register write" {
var a = Action53.init(.vertical);
var ram: [0x2000]u8 = undefined;
_ = a.cpuWrite(0x5000, 0x80, &ram); // select supervisor reg 2 (mode)
_ = a.cpuWrite(0x8000, 0x01, &ram); // 1-screen upper
try std.testing.expectEqual(Mirroring.single_screen_upper, a.mirroring());
}
+66 -54
View File
@@ -1,64 +1,76 @@
const types = @import("types.zig");
const Mirroring = types.Mirroring;
const std = @import("std");
const common = @import("../common.zig");
const Mirroring = common.Mirroring;
const Axrom = @This();
pub const Axrom = @This();
prg_bank_select: u8 = 0,
mirroring_select: u1 = 0,
prg_bank_count: usize,
prg_bank: u4 = 0,
mirroring_mode: Mirroring = .single_screen_lower,
pub fn init(
prg_size: usize,
) Axrom {
return .{
.prg_bank_count = prg_size / 0x8000,
};
pub fn init() Axrom {
return .{};
}
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 cpuRead(self: *const Axrom, address: u16, prg_rom: []const u8, prg_ram: []const u8) ?u8 {
if (address >= 0x6000 and address <= 0x7fff) {
const offset = address - 0x6000;
if (offset < prg_ram.len) return prg_ram[offset];
return 0;
}
if (address >= 0x8000 and address <= 0xffff) {
const total_32k_banks = @max(1, prg_rom.len / 0x8000);
const bank = @as(usize, self.prg_bank) % total_32k_banks;
const offset = (bank * 0x8000) + (address - 0x8000);
if (offset < prg_rom.len) return prg_rom[offset];
return 0;
}
return null;
}
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 {
pub fn cpuWrite(self: *Axrom, address: u16, value: u8, prg_ram: []u8) bool {
if (address >= 0x6000 and address <= 0x7fff) {
const offset = address - 0x6000;
if (offset < prg_ram.len) {
prg_ram[offset] = value;
return true;
}
}
if (address >= 0x8000 and address <= 0xffff) {
self.prg_bank = @truncate(value & 0x0f);
self.mirroring_mode = if ((value & 0x10) != 0) .single_screen_upper else .single_screen_lower;
return true;
}
return false;
}
pub fn ppuRead(_: *const Axrom, address: u16, chr_rom: []const u8, chr_ram: []const u8, chr_is_ram: bool) ?u8 {
if (address < 0x2000) {
if (chr_is_ram) {
if (address < chr_ram.len) return chr_ram[address];
} else {
if (address < chr_rom.len) return chr_rom[address];
}
return 0;
}
return null;
}
pub fn ppuWrite(_: *Axrom, address: u16, value: u8, chr_ram: []u8, chr_is_ram: bool) bool {
if (address < 0x2000 and chr_is_ram) {
if (address < chr_ram.len) {
chr_ram[address] = value;
return true;
}
}
return false;
}
pub fn mirroring(self: *const Axrom) Mirroring {
return self.mirroring_mode;
}
pub fn irqLine(_: *const Axrom) bool {
return false;
}
pub fn notifyPpuAddress(_: *Axrom, _: u16) void {}
+110
View File
@@ -0,0 +1,110 @@
const std = @import("std");
const common = @import("../common.zig");
const Mirroring = common.Mirroring;
// Mapper 34 (BNROM / NINA-001) - 32KB PRG banking, optional NINA-001 4KB CHR banking
pub const Bnrom = @This();
prg_bank: u8 = 0,
chr_bank_0: u8 = 0,
chr_bank_1: u8 = 1,
mirroring_mode: Mirroring,
pub fn init(initial_mirroring: Mirroring) Bnrom {
return .{ .mirroring_mode = initial_mirroring };
}
pub fn cpuRead(self: *const Bnrom, address: u16, prg_rom: []const u8, prg_ram: []const u8) ?u8 {
if (address >= 0x6000 and address <= 0x7fff) {
const offset = address - 0x6000;
if (offset < prg_ram.len) return prg_ram[offset];
return 0;
}
if (address >= 0x8000 and address <= 0xffff) {
const total_32k_banks = @max(1, prg_rom.len / 0x8000);
const bank = @as(usize, self.prg_bank) % total_32k_banks;
const offset = (bank * 0x8000) + (address - 0x8000);
if (offset < prg_rom.len) return prg_rom[offset];
return 0;
}
return null;
}
pub fn cpuWrite(self: *Bnrom, address: u16, value: u8, prg_ram: []u8) bool {
if (address >= 0x6000 and address <= 0x7fff) {
// NINA-001 registers
switch (address) {
0x7ffd => {
self.prg_bank = value & 0x0f;
return true;
},
0x7ffe => {
self.chr_bank_0 = value & 0x0f;
return true;
},
0x7fff => {
self.chr_bank_1 = value & 0x0f;
return true;
},
else => {
const offset = address - 0x6000;
if (offset < prg_ram.len) {
prg_ram[offset] = value;
return true;
}
},
}
}
if (address >= 0x8000 and address <= 0xffff) {
// BNROM: select 32KB PRG bank
self.prg_bank = value & 0x0f;
return true;
}
return false;
}
pub fn ppuRead(self: *const Bnrom, address: u16, chr_rom: []const u8, chr_ram: []const u8, chr_is_ram: bool) ?u8 {
if (address < 0x2000) {
if (chr_is_ram) {
if (address < chr_ram.len) return chr_ram[address];
} else if (chr_rom.len > 0) {
const total_4k_banks = @max(1, chr_rom.len / 0x1000);
const bank = if (address < 0x1000)
@as(usize, self.chr_bank_0) % total_4k_banks
else
@as(usize, self.chr_bank_1) % total_4k_banks;
const offset = (bank * 0x1000) + (address & 0x0fff);
if (offset < chr_rom.len) return chr_rom[offset];
}
return 0;
}
return null;
}
pub fn ppuWrite(_: *Bnrom, address: u16, value: u8, chr_ram: []u8, chr_is_ram: bool) bool {
if (address < 0x2000 and chr_is_ram) {
if (address < chr_ram.len) {
chr_ram[address] = value;
return true;
}
}
return false;
}
pub fn mirroring(self: *const Bnrom) Mirroring {
return self.mirroring_mode;
}
pub fn irqLine(_: *const Bnrom) bool {
return false;
}
pub fn notifyPpuAddress(_: *Bnrom, _: u16) void {}
test "bnrom bank switching" {
var bn = Bnrom.init(.vertical);
var ram: [0x2000]u8 = undefined;
_ = bn.cpuWrite(0x8000, 3, &ram);
try std.testing.expectEqual(@as(u8, 3), bn.prg_bank);
}
+85 -63
View File
@@ -1,82 +1,104 @@
const types = @import("types.zig");
const Mirroring = types.Mirroring;
const std = @import("std");
const common = @import("../common.zig");
const Mirroring = common.Mirroring;
const Camerica = @This();
// Mapper 71 (Camerica / Codemasters) - 16KB PRG banking at $C000-$FFFF, optional 1-screen mirroring at $9000-$9FFF
pub const Camerica = @This();
prg_bank: u8 = 0,
mirroring_select: u1 = 0,
mirroring_mode: Mirroring,
has_single_screen_mirroring: bool = false,
has_mirroring_control: bool,
prg_bank_count: usize,
pub fn init(
prg_size: usize,
_mirroring: Mirroring,
) Camerica {
pub fn init(initial_mirroring: Mirroring) Camerica {
return .{
.prg_bank_count = prg_size / 0x4000,
.mirroring_mode = _mirroring,
.mirroring_mode = initial_mirroring,
.has_mirroring_control = false,
};
}
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 cpuRead(self: *const Camerica, address: u16, prg_rom: []const u8, prg_ram: []const u8) ?u8 {
if (address >= 0x6000 and address <= 0x7fff) {
const offset = address - 0x6000;
if (offset < prg_ram.len) return prg_ram[offset];
return 0;
}
}
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;
if (address >= 0x8000 and address <= 0xffff) {
const total_16k_banks = @max(1, prg_rom.len / 0x4000);
var bank: usize = 0;
if (address < 0xc000) {
bank = @as(usize, self.prg_bank) % total_16k_banks;
const offset = (bank * 0x4000) + (address - 0x8000);
if (offset < prg_rom.len) return prg_rom[offset];
} else {
bank = total_16k_banks - 1;
const offset = (bank * 0x4000) + (address - 0xc000);
if (offset < prg_rom.len) return prg_rom[offset];
}
return 0;
}
return null;
}
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;
pub fn cpuWrite(self: *Camerica, address: u16, value: u8, prg_ram: []u8) bool {
if (address >= 0x6000 and address <= 0x7fff) {
const offset = address - 0x6000;
if (offset < prg_ram.len) {
prg_ram[offset] = value;
return true;
}
}
if (address >= 0x9000 and address <= 0x9fff) {
// Fire Hawk single-screen mirroring control
self.mirroring_mode = if ((value & 0x10) != 0) .single_screen_upper else .single_screen_lower;
self.has_mirroring_control = true;
return true;
}
if (address >= 0xc000 and address <= 0xffff) {
self.prg_bank = value & 0x0f;
return true;
}
return false;
}
pub fn ppuRead(_: *const Camerica, address: u16, chr_rom: []const u8, chr_ram: []const u8, chr_is_ram: bool) ?u8 {
if (address < 0x2000) {
if (chr_is_ram) {
if (address < chr_ram.len) return chr_ram[address];
} else if (chr_rom.len > 0) {
if (address < chr_rom.len) return chr_rom[address];
}
return 0;
}
return null;
}
pub fn ppuWrite(_: *Camerica, address: u16, value: u8, chr_ram: []u8, chr_is_ram: bool) bool {
if (address < 0x2000 and chr_is_ram) {
if (address < chr_ram.len) {
chr_ram[address] = value;
return true;
}
}
return false;
}
pub fn mirroring(self: *const Camerica) Mirroring {
return self.mirroring_mode;
}
pub fn irqAsserted(_: *const Camerica) bool {
pub fn irqLine(_: *const Camerica) bool {
return false;
}
pub fn notifyPpuAddress(_: *Camerica, _: u16) void {}
test "camerica bank switching" {
var cam = Camerica.init(.vertical);
var ram: [0x2000]u8 = undefined;
_ = cam.cpuWrite(0xc000, 7, &ram);
try std.testing.expectEqual(@as(u8, 7), cam.prg_bank);
_ = cam.cpuWrite(0x9000, 0x10, &ram);
try std.testing.expectEqual(Mirroring.single_screen_upper, cam.mirroring());
}
+52 -51
View File
@@ -1,69 +1,70 @@
const types = @import("types.zig");
const Mirroring = types.Mirroring;
const std = @import("std");
const common = @import("../common.zig");
const Mirroring = common.Mirroring;
const Cnrom = @This();
pub const Cnrom = @This();
prg_16k: bool,
chr_bank_select: u8 = 0,
chr_bank_count: usize,
chr_bank: u8 = 0,
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 init(initial_mirroring: Mirroring) Cnrom {
return .{ .mirroring_mode = initial_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 cpuRead(_: *const Cnrom, address: u16, prg_rom: []const u8, prg_ram: []const u8) ?u8 {
if (address >= 0x6000 and address <= 0x7fff) {
const offset = address - 0x6000;
if (offset < prg_ram.len) return prg_ram[offset];
return 0;
}
if (address >= 0x8000 and address <= 0xffff) {
const mask: usize = if (prg_rom.len <= 0x4000) 0x3fff else 0x7fff;
const offset = (address - 0x8000) & mask;
if (offset < prg_rom.len) return prg_rom[offset];
return 0;
}
return null;
}
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 cpuWrite(self: *Cnrom, address: u16, value: u8, prg_ram: []u8) bool {
if (address >= 0x6000 and address <= 0x7fff) {
const offset = address - 0x6000;
if (offset < prg_ram.len) {
prg_ram[offset] = value;
return true;
}
}
if (address >= 0x8000 and address <= 0xffff) {
// mask up to 16 banks (128KB) for extended CNROM homebrew
self.chr_bank = value & 0x0f;
return true;
}
return false;
}
pub fn ppuMapWrite(
_: *const Cnrom,
address: u16,
) ?usize {
if (address >= 0x2000) return null;
return address;
pub fn ppuRead(self: *const Cnrom, address: u16, chr_rom: []const u8, _: []const u8, _: bool) ?u8 {
if (address < 0x2000) {
if (chr_rom.len > 0) {
const total_8k_banks = @max(1, chr_rom.len / 0x2000);
const bank = @as(usize, self.chr_bank) % total_8k_banks;
const offset = (bank * 0x2000) + address;
if (offset < chr_rom.len) return chr_rom[offset];
}
return 0;
}
return null;
}
pub fn mirroring(
self: *const Cnrom,
) Mirroring {
pub fn ppuWrite(_: *Cnrom, _: u16, _: u8, _: []u8, _: bool) bool {
return false;
}
pub fn mirroring(self: *const Cnrom) Mirroring {
return self.mirroring_mode;
}
pub fn irqAsserted(_: *const Cnrom) bool {
pub fn irqLine(_: *const Cnrom) bool {
return false;
}
pub fn notifyPpuAddress(_: *Cnrom, _: u16) void {}
+91
View File
@@ -0,0 +1,91 @@
const std = @import("std");
const common = @import("../common.zig");
const Mirroring = common.Mirroring;
// Mapper 11 (Color Dreams) - 32KB PRG bank in bits 0-1, 8KB CHR bank in bits 4-7
pub const ColorDreams = @This();
prg_bank: u8 = 0,
chr_bank: u8 = 0,
mirroring_mode: Mirroring,
pub fn init(initial_mirroring: Mirroring) ColorDreams {
return .{ .mirroring_mode = initial_mirroring };
}
pub fn cpuRead(self: *const ColorDreams, address: u16, prg_rom: []const u8, prg_ram: []const u8) ?u8 {
if (address >= 0x6000 and address <= 0x7fff) {
const offset = address - 0x6000;
if (offset < prg_ram.len) return prg_ram[offset];
return 0;
}
if (address >= 0x8000 and address <= 0xffff) {
const total_32k_banks = @max(1, prg_rom.len / 0x8000);
const bank = @as(usize, self.prg_bank) % total_32k_banks;
const offset = (bank * 0x8000) + (address - 0x8000);
if (offset < prg_rom.len) return prg_rom[offset];
return 0;
}
return null;
}
pub fn cpuWrite(self: *ColorDreams, address: u16, value: u8, prg_ram: []u8) bool {
if (address >= 0x6000 and address <= 0x7fff) {
const offset = address - 0x6000;
if (offset < prg_ram.len) {
prg_ram[offset] = value;
return true;
}
}
if (address >= 0x8000 and address <= 0xffff) {
self.prg_bank = value & 0x03;
self.chr_bank = (value >> 4) & 0x0f;
return true;
}
return false;
}
pub fn ppuRead(self: *const ColorDreams, address: u16, chr_rom: []const u8, chr_ram: []const u8, chr_is_ram: bool) ?u8 {
if (address < 0x2000) {
if (chr_is_ram) {
if (address < chr_ram.len) return chr_ram[address];
} else if (chr_rom.len > 0) {
const total_8k_banks = @max(1, chr_rom.len / 0x2000);
const bank = @as(usize, self.chr_bank) % total_8k_banks;
const offset = (bank * 0x2000) + address;
if (offset < chr_rom.len) return chr_rom[offset];
}
return 0;
}
return null;
}
pub fn ppuWrite(_: *ColorDreams, address: u16, value: u8, chr_ram: []u8, chr_is_ram: bool) bool {
if (address < 0x2000 and chr_is_ram) {
if (address < chr_ram.len) {
chr_ram[address] = value;
return true;
}
}
return false;
}
pub fn mirroring(self: *const ColorDreams) Mirroring {
return self.mirroring_mode;
}
pub fn irqLine(_: *const ColorDreams) bool {
return false;
}
pub fn notifyPpuAddress(_: *ColorDreams, _: u16) void {}
test "color dreams bank switching" {
var cd = ColorDreams.init(.vertical);
var ram: [0x2000]u8 = undefined;
// Write 0x52: PRG bank 2, CHR bank 5
_ = cd.cpuWrite(0x8000, 0x52, &ram);
try std.testing.expectEqual(@as(u8, 2), cd.prg_bank);
try std.testing.expectEqual(@as(u8, 5), cd.chr_bank);
}
-73
View File
@@ -1,73 +0,0 @@
const types = @import("types.zig");
const Mirroring = types.Mirroring;
const ColorDreams = @This();
prg_bank: u8 = 0,
chr_bank: u8 = 0,
prg_bank_count: usize,
chr_bank_count: usize,
mirroring_mode: Mirroring,
pub fn init(
prg_size: usize,
chr_size: usize,
_mirroring: Mirroring,
) ColorDreams {
return .{
.prg_bank_count = prg_size / 0x8000,
.chr_bank_count = if (chr_size > 0) chr_size / 0x2000 else 1,
.mirroring_mode = _mirroring,
};
}
pub fn cpuMapRead(
self: *const ColorDreams,
address: u16,
) ?usize {
if (address < 0x8000) return null;
const offset = @as(usize, address - 0x8000);
const bank = @as(usize, self.prg_bank) % self.prg_bank_count;
return bank * 0x8000 + offset;
}
pub fn cpuWrite(
self: *ColorDreams,
address: u16,
value: u8,
) void {
if (address >= 0x8000) {
self.prg_bank = value & 0x03;
self.chr_bank = (value >> 4) & 0x0F;
}
}
pub fn ppuMapRead(
self: *const ColorDreams,
address: u16,
) ?usize {
if (address >= 0x2000) return null;
const bank = @as(usize, self.chr_bank) % self.chr_bank_count;
return bank * 0x2000 + @as(usize, address);
}
pub fn ppuMapWrite(
_: *const ColorDreams,
address: u16,
) ?usize {
if (address >= 0x2000) return null;
return address;
}
pub fn mirroring(
self: *const ColorDreams,
) Mirroring {
return self.mirroring_mode;
}
pub fn irqAsserted(_: *const ColorDreams) bool {
return false;
}
+71 -53
View File
@@ -1,73 +1,91 @@
const types = @import("types.zig");
const Mirroring = types.Mirroring;
const std = @import("std");
const common = @import("../common.zig");
const Mirroring = common.Mirroring;
const Gxrom = @This();
// Mapper 66 (GxROM / GNROM) - 32KB PRG bank in bits 4-5, 8KB CHR bank in bits 0-1
pub 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 init(initial_mirroring: Mirroring) Gxrom {
return .{ .mirroring_mode = initial_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 cpuRead(self: *const Gxrom, address: u16, prg_rom: []const u8, prg_ram: []const u8) ?u8 {
if (address >= 0x6000 and address <= 0x7fff) {
const offset = address - 0x6000;
if (offset < prg_ram.len) return prg_ram[offset];
return 0;
}
if (address >= 0x8000 and address <= 0xffff) {
const total_32k_banks = @max(1, prg_rom.len / 0x8000);
const bank = @as(usize, self.prg_bank) % total_32k_banks;
const offset = (bank * 0x8000) + (address - 0x8000);
if (offset < prg_rom.len) return prg_rom[offset];
return 0;
}
return null;
}
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 cpuWrite(self: *Gxrom, address: u16, value: u8, prg_ram: []u8) bool {
if (address >= 0x6000 and address <= 0x7fff) {
const offset = address - 0x6000;
if (offset < prg_ram.len) {
prg_ram[offset] = value;
return true;
}
}
if (address >= 0x8000 and address <= 0xffff) {
self.prg_bank = (value >> 4) & 0x03;
self.chr_bank = value & 0x03;
return true;
}
return false;
}
pub fn ppuMapWrite(
_: *const Gxrom,
address: u16,
) ?usize {
if (address >= 0x2000) return null;
return address;
pub fn ppuRead(self: *const Gxrom, address: u16, chr_rom: []const u8, chr_ram: []const u8, chr_is_ram: bool) ?u8 {
if (address < 0x2000) {
if (chr_is_ram) {
if (address < chr_ram.len) return chr_ram[address];
} else if (chr_rom.len > 0) {
const total_8k_banks = @max(1, chr_rom.len / 0x2000);
const bank = @as(usize, self.chr_bank) % total_8k_banks;
const offset = (bank * 0x2000) + address;
if (offset < chr_rom.len) return chr_rom[offset];
}
return 0;
}
return null;
}
pub fn mirroring(
self: *const Gxrom,
) Mirroring {
pub fn ppuWrite(_: *Gxrom, address: u16, value: u8, chr_ram: []u8, chr_is_ram: bool) bool {
if (address < 0x2000 and chr_is_ram) {
if (address < chr_ram.len) {
chr_ram[address] = value;
return true;
}
}
return false;
}
pub fn mirroring(self: *const Gxrom) Mirroring {
return self.mirroring_mode;
}
pub fn irqAsserted(_: *const Gxrom) bool {
pub fn irqLine(_: *const Gxrom) bool {
return false;
}
pub fn notifyPpuAddress(_: *Gxrom, _: u16) void {}
test "gxrom bank switching" {
var gx = Gxrom.init(.horizontal);
var ram: [0x2000]u8 = undefined;
// Write 0x21: PRG bank 2, CHR bank 1
_ = gx.cpuWrite(0x8000, 0x21, &ram);
try std.testing.expectEqual(@as(u8, 2), gx.prg_bank);
try std.testing.expectEqual(@as(u8, 1), gx.chr_bank);
}
-78
View File
@@ -1,78 +0,0 @@
const types = @import("types.zig");
const Mirroring = types.Mirroring;
const Irem78 = @This();
prg_bank: u8 = 0,
chr_bank: u8 = 0,
mirroring_select: u1 = 0,
prg_bank_count: usize,
chr_bank_count: usize,
pub fn init(
prg_size: usize,
chr_size: usize,
) Irem78 {
return .{
.prg_bank_count = prg_size / 0x4000,
.chr_bank_count = if (chr_size > 0) chr_size / 0x2000 else 1,
};
}
pub fn cpuMapRead(
self: *const Irem78,
address: u16,
) ?usize {
if (address < 0x8000) return null;
if (address < 0xC000) {
const offset = @as(usize, address - 0x8000);
const bank = @as(usize, self.prg_bank) % self.prg_bank_count;
return bank * 0x4000 + offset;
} else {
const offset = @as(usize, address - 0xC000);
const fixed_bank = if (self.prg_bank_count > 0) self.prg_bank_count - 1 else 0;
return fixed_bank * 0x4000 + offset;
}
}
pub fn cpuWrite(
self: *Irem78,
address: u16,
value: u8,
) void {
if (address < 0x8000) return;
self.prg_bank = value & 0x07;
self.mirroring_select = @truncate((value >> 3) & 1);
self.chr_bank = (value >> 4) & 0x0F;
}
pub fn ppuMapRead(
self: *const Irem78,
address: u16,
) ?usize {
if (address >= 0x2000) return null;
const bank = @as(usize, self.chr_bank) % self.chr_bank_count;
return bank * 0x2000 + @as(usize, address);
}
pub fn ppuMapWrite(
_: *const Irem78,
address: u16,
) ?usize {
if (address >= 0x2000) return null;
return address;
}
pub fn mirroring(
self: *const Irem78,
) Mirroring {
return if (self.mirroring_select == 0) .single_screen_lower else .single_screen_upper;
}
pub fn irqAsserted(_: *const Irem78) bool {
return false;
}
-69
View File
@@ -1,69 +0,0 @@
const types = @import("types.zig");
const Mirroring = types.Mirroring;
const Mmc3 = @import("mmc3.zig");
const Mapper118 = @This();
mmc3: Mmc3,
mirror_bit: u1 = 0,
pub fn init(
prg_size: usize,
chr_size: usize,
) Mapper118 {
return .{
.mmc3 = Mmc3.init(prg_size, chr_size),
};
}
pub fn cpuMapRead(
self: *const Mapper118,
address: u16,
) ?usize {
return self.mmc3.cpuMapRead(address);
}
pub fn cpuWrite(
self: *Mapper118,
address: u16,
value: u8,
) void {
if (address == 0x8001) {
if (self.mmc3.bank_select <= 5) {
self.mirror_bit = @truncate((value >> 7) & 1);
}
}
self.mmc3.cpuWrite(address, value);
}
pub fn ppuMapRead(
self: *Mapper118,
address: u16,
) ?usize {
return self.mmc3.ppuMapRead(address);
}
pub fn ppuMapWrite(
self: *const Mapper118,
address: u16,
) ?usize {
return self.mmc3.ppuMapWrite(address);
}
pub fn mirroring(
self: *const Mapper118,
) Mirroring {
return if (self.mirror_bit == 1) .single_screen_upper else .single_screen_lower;
}
pub fn irqAsserted(
self: *const Mapper118,
) bool {
return self.mmc3.irqAsserted();
}
pub fn handleScanline(
self: *Mapper118,
) void {
self.mmc3.handleScanline();
}
-113
View File
@@ -1,113 +0,0 @@
const types = @import("types.zig");
const Mirroring = types.Mirroring;
const Mmc3 = @import("mmc3.zig");
const Mapper119 = @This();
mmc3: Mmc3,
pub fn init(
prg_size: usize,
chr_size: usize,
) Mapper119 {
return .{
.mmc3 = Mmc3.init(prg_size, chr_size),
};
}
pub fn cpuMapRead(
self: *const Mapper119,
address: u16,
) ?usize {
return self.mmc3.cpuMapRead(address);
}
pub fn cpuWrite(
self: *Mapper119,
address: u16,
value: u8,
) void {
self.mmc3.cpuWrite(address, value);
}
pub fn ppuMapRead(
self: *Mapper119,
address: u16,
) ?usize {
if (address >= 0x2000) return null;
const bank_val = self.getChrBankValue(address);
const offset = (address % 0x0400);
if ((bank_val & 0x40) != 0) {
// Bit 6 is set -> Route to CHR RAM
const ram_bank = @as(usize, bank_val & 7);
return 0x80000000 | (ram_bank * 0x0400 + offset);
} else {
// Route to CHR ROM
return self.mmc3.ppuMapRead(address);
}
}
pub fn ppuMapWrite(
self: *const Mapper119,
address: u16,
) ?usize {
if (address >= 0x2000) return null;
const bank_val = self.getChrBankValue(address);
const offset = (address % 0x0400);
if ((bank_val & 0x40) != 0) {
// Bit 6 is set -> Route write to CHR RAM
const ram_bank = @as(usize, bank_val & 7);
return 0x80000000 | (ram_bank * 0x0400 + offset);
} else {
return self.mmc3.ppuMapWrite(address);
}
}
fn getChrBankValue(self: *const Mapper119, address: u16) u8 {
const chr_mode = (self.mmc3.bank_select & 0x80) != 0;
const addr_k = address / 0x0400;
if (!chr_mode) {
return switch (addr_k) {
0, 1 => (self.mmc3.bank_registers[0] & 0xfe) + @as(u8, @intCast(addr_k & 1)),
2, 3 => (self.mmc3.bank_registers[1] & 0xfe) + @as(u8, @intCast(addr_k & 1)),
4 => self.mmc3.bank_registers[2],
5 => self.mmc3.bank_registers[3],
6 => self.mmc3.bank_registers[4],
7 => self.mmc3.bank_registers[5],
else => unreachable,
};
} else {
return switch (addr_k) {
0 => self.mmc3.bank_registers[2],
1 => self.mmc3.bank_registers[3],
2 => self.mmc3.bank_registers[4],
3 => self.mmc3.bank_registers[5],
4, 5 => (self.mmc3.bank_registers[0] & 0xfe) + @as(u8, @intCast(addr_k & 1)),
6, 7 => (self.mmc3.bank_registers[1] & 0xfe) + @as(u8, @intCast(addr_k & 1)),
else => unreachable,
};
}
}
pub fn mirroring(
self: *const Mapper119,
) Mirroring {
return self.mmc3.mirroring();
}
pub fn irqAsserted(
self: *const Mapper119,
) bool {
return self.mmc3.irqAsserted();
}
pub fn handleScanline(
self: *Mapper119,
) void {
self.mmc3.handleScanline();
}
-69
View File
@@ -1,69 +0,0 @@
const types = @import("types.zig");
const Mirroring = types.Mirroring;
const Mapper13 = @This();
chr_bank: u8 = 0,
fixed_mirroring: Mirroring,
prg_mask: usize,
pub fn init(
prg_size: usize,
mirroring_mode: Mirroring,
) Mapper13 {
return .{
.fixed_mirroring = mirroring_mode,
.prg_mask = if (prg_size > 0) prg_size - 1 else 0x7fff,
};
}
pub fn cpuMapRead(
self: *const Mapper13,
address: u16,
) ?usize {
if (address < 0x8000) return null;
return (@as(usize, address) - 0x8000) & self.prg_mask;
}
pub fn cpuWrite(
self: *Mapper13,
address: u16,
value: u8,
) void {
if (address < 0x8000) return;
self.chr_bank = value & 0x03;
}
pub fn ppuMapRead(
self: *const Mapper13,
address: u16,
) ?usize {
if (address >= 0x2000) return null;
if (address < 0x1000) {
return 0x80000000 | @as(usize, address);
} else {
const bank = @as(usize, self.chr_bank & 3);
const offset = @as(usize, address & 0x0fff);
return 0x80000000 | (bank * 0x1000 + offset);
}
}
pub fn ppuMapWrite(
self: *const Mapper13,
address: u16,
) ?usize {
return self.ppuMapRead(address);
}
pub fn mirroring(
self: *const Mapper13,
) Mirroring {
return self.fixed_mirroring;
}
pub fn irqAsserted(
_: *const Mapper13,
) bool {
return false;
}
-73
View File
@@ -1,73 +0,0 @@
const types = @import("types.zig");
const Mirroring = types.Mirroring;
const Mapper140 = @This();
prg_bank: u8 = 0,
chr_bank: u8 = 0,
prg_bank_count: usize,
chr_bank_count: usize,
mirroring_mode: Mirroring,
pub fn init(
prg_size: usize,
chr_size: usize,
_mirroring: Mirroring,
) Mapper140 {
return .{
.prg_bank_count = prg_size / 0x8000,
.chr_bank_count = if (chr_size > 0) chr_size / 0x2000 else 1,
.mirroring_mode = _mirroring,
};
}
pub fn cpuMapRead(
self: *const Mapper140,
address: u16,
) ?usize {
if (address < 0x8000) return null;
const offset = @as(usize, address - 0x8000);
const bank = @as(usize, self.prg_bank) % self.prg_bank_count;
return bank * 0x8000 + offset;
}
pub fn cpuWrite(
self: *Mapper140,
address: u16,
value: u8,
) void {
if (address >= 0x6000 and address <= 0x7FFF) {
self.prg_bank = (value >> 4) & 0x03;
self.chr_bank = value & 0x0F;
}
}
pub fn ppuMapRead(
self: *const Mapper140,
address: u16,
) ?usize {
if (address >= 0x2000) return null;
const bank = @as(usize, self.chr_bank) % self.chr_bank_count;
return bank * 0x2000 + @as(usize, address);
}
pub fn ppuMapWrite(
_: *const Mapper140,
address: u16,
) ?usize {
if (address >= 0x2000) return null;
return address;
}
pub fn mirroring(
self: *const Mapper140,
) Mirroring {
return self.mirroring_mode;
}
pub fn irqAsserted(_: *const Mapper140) bool {
return false;
}
+73 -49
View File
@@ -1,70 +1,94 @@
const types = @import("types.zig");
const Mirroring = types.Mirroring;
const std = @import("std");
const common = @import("../common.zig");
const Mirroring = common.Mirroring;
const Mapper180 = @This();
// Mapper 180 (Nichibutsu / Crazy Climber)
// Fixed first 16KB at $8000-$BFFF, switchable 16KB at $C000-$FFFF via write to $8000-$FFFF.
pub 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 init(initial_mirroring: Mirroring) Mapper180 {
return .{ .mirroring_mode = initial_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 cpuRead(self: *const Mapper180, address: u16, prg_rom: []const u8, prg_ram: []const u8) ?u8 {
if (address >= 0x6000 and address <= 0x7fff) {
const offset = address - 0x6000;
if (offset < prg_ram.len) return prg_ram[offset];
return 0;
}
}
if (address >= 0x8000 and address <= 0xffff) {
const total_16k_banks = @max(1, prg_rom.len / 0x4000);
var bank: usize = 0;
pub fn cpuWrite(
self: *Mapper180,
address: u16,
value: u8,
) void {
if (address >= 0x8000) {
self.prg_bank = value & 0x07;
if (address < 0xc000) {
bank = 0;
const offset = address - 0x8000;
if (offset < prg_rom.len) return prg_rom[offset];
} else {
bank = @as(usize, self.prg_bank) % total_16k_banks;
const offset = (bank * 0x4000) + (address - 0xc000);
if (offset < prg_rom.len) return prg_rom[offset];
}
return 0;
}
return null;
}
pub fn ppuMapRead(
_: *const Mapper180,
address: u16,
) ?usize {
if (address >= 0x2000) return null;
return address;
pub fn cpuWrite(self: *Mapper180, address: u16, value: u8, prg_ram: []u8) bool {
if (address >= 0x6000 and address <= 0x7fff) {
const offset = address - 0x6000;
if (offset < prg_ram.len) {
prg_ram[offset] = value;
return true;
}
return false;
}
if (address >= 0x8000 and address <= 0xffff) {
self.prg_bank = value & 0x0f;
return true;
}
return false;
}
pub fn ppuMapWrite(
_: *const Mapper180,
address: u16,
) ?usize {
if (address >= 0x2000) return null;
return address;
pub fn ppuRead(_: *const Mapper180, address: u16, chr_rom: []const u8, chr_ram: []const u8, chr_is_ram: bool) ?u8 {
if (address < 0x2000) {
if (chr_is_ram) {
if (address < chr_ram.len) return chr_ram[address];
} else if (chr_rom.len > 0) {
if (address < chr_rom.len) return chr_rom[address];
}
return 0;
}
return null;
}
pub fn mirroring(
self: *const Mapper180,
) Mirroring {
pub fn ppuWrite(_: *Mapper180, address: u16, value: u8, chr_ram: []u8, chr_is_ram: bool) bool {
if (address < 0x2000 and chr_is_ram) {
if (address < chr_ram.len) {
chr_ram[address] = value;
return true;
}
}
return false;
}
pub fn mirroring(self: *const Mapper180) Mirroring {
return self.mirroring_mode;
}
pub fn irqAsserted(_: *const Mapper180) bool {
pub fn irqLine(_: *const Mapper180) bool {
return false;
}
pub fn notifyPpuAddress(_: *Mapper180, _: u16) void {}
test "mapper180 bank switching" {
var m = Mapper180.init(.horizontal);
var ram: [0x2000]u8 = undefined;
_ = m.cpuWrite(0x8000, 3, &ram);
try std.testing.expectEqual(@as(u8, 3), m.prg_bank);
}
-108
View File
@@ -1,108 +0,0 @@
const types = @import("types.zig");
const Mirroring = types.Mirroring;
const Mapper206 = @This();
bank_select: u3 = 0,
prg_bank0: u8 = 0,
prg_bank1: u8 = 1,
chr_banks: [6]u8 = [_]u8{ 0, 2, 4, 5, 6, 7 },
prg_bank_count: usize,
chr_bank_count: usize,
mirroring_mode: Mirroring,
pub fn init(
prg_size: usize,
chr_size: usize,
_mirroring: Mirroring,
) Mapper206 {
return .{
.prg_bank_count = prg_size / 0x2000,
.chr_bank_count = if (chr_size > 0) chr_size / 0x0400 else 1,
.mirroring_mode = _mirroring,
};
}
pub fn cpuMapRead(
self: *const Mapper206,
address: u16,
) ?usize {
if (address < 0x8000) return null;
if (address < 0xA000) {
const offset = @as(usize, address - 0x8000);
const bank = @as(usize, self.prg_bank0) % self.prg_bank_count;
return bank * 0x2000 + offset;
} else if (address < 0xC000) {
const offset = @as(usize, address - 0xA000);
const bank = @as(usize, self.prg_bank1) % self.prg_bank_count;
return bank * 0x2000 + offset;
} else if (address < 0xE000) {
const offset = @as(usize, address - 0xC000);
const bank = if (self.prg_bank_count >= 2) self.prg_bank_count - 2 else 0;
return bank * 0x2000 + offset;
} else {
const offset = @as(usize, address - 0xE000);
const bank = if (self.prg_bank_count >= 1) self.prg_bank_count - 1 else 0;
return bank * 0x2000 + offset;
}
}
pub fn cpuWrite(
self: *Mapper206,
address: u16,
value: u8,
) void {
if (address >= 0x8000 and address <= 0x9FFF) {
if ((address & 1) == 0) {
self.bank_select = @truncate(value & 0x07);
} else {
switch (self.bank_select) {
0...5 => |i| self.chr_banks[i] = value & 0x3F,
6 => self.prg_bank0 = value & 0x0F,
7 => self.prg_bank1 = value & 0x0F,
}
}
}
}
pub fn ppuMapRead(
self: *const Mapper206,
address: u16,
) ?usize {
if (address >= 0x2000) return null;
if (address < 0x0800) {
const bank = (@as(usize, self.chr_banks[0] & 0xFE)) % self.chr_bank_count;
return bank * 0x0400 + @as(usize, address);
} else if (address < 0x1000) {
const bank = (@as(usize, self.chr_banks[1] & 0xFE)) % self.chr_bank_count;
return bank * 0x0400 + @as(usize, address - 0x0800);
} else {
const idx = 2 + (address - 0x1000) / 0x0400;
const offset = (address - 0x1000) % 0x0400;
const bank = @as(usize, self.chr_banks[idx]) % self.chr_bank_count;
return bank * 0x0400 + @as(usize, offset);
}
}
pub fn ppuMapWrite(
_: *const Mapper206,
address: u16,
) ?usize {
if (address >= 0x2000) return null;
return address;
}
pub fn mirroring(
self: *const Mapper206,
) Mirroring {
return self.mirroring_mode;
}
pub fn irqAsserted(_: *const Mapper206) bool {
return false;
}
-105
View File
@@ -1,105 +0,0 @@
const types = @import("types.zig");
const Mirroring = types.Mirroring;
const Mapper210 = @This();
prg_bank0: u8 = 0,
prg_bank1: u8 = 0,
prg_bank2: u8 = 0,
chr_banks: [8]u8 = [_]u8{ 0, 1, 2, 3, 4, 5, 6, 7 },
mirroring_mode: Mirroring = .vertical,
prg_bank_count: usize,
chr_bank_count: usize,
pub fn init(
prg_size: usize,
chr_size: usize,
) Mapper210 {
return .{
.prg_bank_count = prg_size / 0x2000,
.chr_bank_count = if (chr_size > 0) chr_size / 0x0400 else 1,
};
}
pub fn cpuMapRead(
self: *const Mapper210,
address: u16,
) ?usize {
if (address < 0x8000) return null;
if (address < 0xA000) {
const offset = @as(usize, address - 0x8000);
const bank = @as(usize, self.prg_bank0) % self.prg_bank_count;
return bank * 0x2000 + offset;
} else if (address < 0xC000) {
const offset = @as(usize, address - 0xA000);
const bank = @as(usize, self.prg_bank1) % self.prg_bank_count;
return bank * 0x2000 + offset;
} else if (address < 0xE000) {
const offset = @as(usize, address - 0xC000);
const bank = @as(usize, self.prg_bank2) % self.prg_bank_count;
return bank * 0x2000 + offset;
} else {
const offset = @as(usize, address - 0xE000);
const fixed_bank = if (self.prg_bank_count > 0) self.prg_bank_count - 1 else 0;
return fixed_bank * 0x2000 + offset;
}
}
pub fn cpuWrite(
self: *Mapper210,
address: u16,
value: u8,
) void {
if (address < 0x8000) return;
if (address >= 0x8000 and address <= 0xBFFF) {
const index = (address - 0x8000) / 0x0800;
self.chr_banks[index] = value;
} else if (address >= 0xC000 and address <= 0xC7FF) {
self.prg_bank0 = value & 0x3F;
} else if (address >= 0xC800 and address <= 0xCFFF) {
self.prg_bank1 = value & 0x3F;
} else if (address >= 0xD000 and address <= 0xD7FF) {
self.prg_bank2 = value & 0x3F;
} else if (address >= 0xE000 and address <= 0xE7FF) {
self.mirroring_mode = switch (@as(u2, @truncate(value >> 6))) {
0 => .vertical,
1 => .horizontal,
2 => .single_screen_lower,
3 => .single_screen_upper,
};
}
}
pub fn ppuMapRead(
self: *const Mapper210,
address: u16,
) ?usize {
if (address >= 0x2000) return null;
const bank_idx = address / 0x0400;
const offset = address % 0x0400;
const bank = @as(usize, self.chr_banks[bank_idx]) % self.chr_bank_count;
return bank * 0x0400 + offset;
}
pub fn ppuMapWrite(
_: *const Mapper210,
address: u16,
) ?usize {
if (address >= 0x2000) return null;
return address;
}
pub fn mirroring(
self: *const Mapper210,
) Mirroring {
return self.mirroring_mode;
}
pub fn irqAsserted(_: *const Mapper210) bool {
return false;
}
-77
View File
@@ -1,77 +0,0 @@
const types = @import("types.zig");
const Mirroring = types.Mirroring;
const Mapper232 = @This();
outer_block: u8 = 0,
inner_bank: u8 = 0,
prg_bank_count: usize,
mirroring_mode: Mirroring,
pub fn init(
prg_size: usize,
_mirroring: Mirroring,
) Mapper232 {
return .{
.prg_bank_count = prg_size / 0x4000,
.mirroring_mode = _mirroring,
};
}
pub fn cpuMapRead(
self: *const Mapper232,
address: u16,
) ?usize {
if (address < 0x8000) return null;
const block_start = (@as(usize, self.outer_block) * 4) % self.prg_bank_count;
if (address < 0xC000) {
const offset = @as(usize, address - 0x8000);
const bank = (block_start + (@as(usize, self.inner_bank) & 3)) % self.prg_bank_count;
return bank * 0x4000 + offset;
} else {
const offset = @as(usize, address - 0xC000);
const bank = (block_start + 3) % self.prg_bank_count;
return bank * 0x4000 + offset;
}
}
pub fn cpuWrite(
self: *Mapper232,
address: u16,
value: u8,
) void {
if (address >= 0x8000 and address <= 0x9FFF) {
self.outer_block = (value >> 3) & 0x03;
} else if (address >= 0xA000) {
self.inner_bank = value & 0x03;
}
}
pub fn ppuMapRead(
_: *const Mapper232,
address: u16,
) ?usize {
if (address >= 0x2000) return null;
return address;
}
pub fn ppuMapWrite(
_: *const Mapper232,
address: u16,
) ?usize {
if (address >= 0x2000) return null;
return address;
}
pub fn mirroring(
self: *const Mapper232,
) Mirroring {
return self.mirroring_mode;
}
pub fn irqAsserted(_: *const Mapper232) bool {
return false;
}
-92
View File
@@ -1,92 +0,0 @@
const types = @import("types.zig");
const Mirroring = types.Mirroring;
const Mapper34 = @This();
prg_bank: u8 = 0,
chr_bank0: u8 = 0,
chr_bank1: u8 = 1,
prg_bank_count: usize,
chr_bank_count: usize,
fixed_mirroring: Mirroring,
is_nina01: bool,
pub fn init(
prg_size: usize,
chr_size: usize,
mirroring_mode: Mirroring,
) Mapper34 {
return .{
.prg_bank_count = if (prg_size >= 0x8000) prg_size / 0x8000 else 1,
.chr_bank_count = if (chr_size > 0) chr_size / 0x1000 else 1,
.fixed_mirroring = mirroring_mode,
.is_nina01 = (chr_size > 0),
};
}
pub fn cpuMapRead(
self: *const Mapper34,
address: u16,
) ?usize {
if (address < 0x8000) return null;
const offset = @as(usize, address & 0x7fff);
const bank = @as(usize, self.prg_bank) % self.prg_bank_count;
return bank * 0x8000 + offset;
}
pub fn cpuWrite(
self: *Mapper34,
address: u16,
value: u8,
) void {
if (self.is_nina01) {
switch (address) {
0x7ffd => self.prg_bank = value,
0x7ffe => self.chr_bank0 = value,
0x7fff => self.chr_bank1 = value,
else => {},
}
} else {
if (address >= 0x8000) {
self.prg_bank = value;
}
}
}
pub fn ppuMapRead(
self: *const Mapper34,
address: u16,
) ?usize {
if (address >= 0x2000) return null;
if (self.is_nina01) {
const offset = @as(usize, address & 0x0fff);
const bank_raw = if (address < 0x1000) self.chr_bank0 else self.chr_bank1;
const bank = @as(usize, bank_raw) % self.chr_bank_count;
return bank * 0x1000 + offset;
} else {
return @as(usize, address);
}
}
pub fn ppuMapWrite(
self: *const Mapper34,
address: u16,
) ?usize {
return self.ppuMapRead(address);
}
pub fn mirroring(
self: *const Mapper34,
) Mirroring {
return self.fixed_mirroring;
}
pub fn irqAsserted(
_: *const Mapper34,
) bool {
return false;
}
-118
View File
@@ -1,118 +0,0 @@
const types = @import("types.zig");
const Mirroring = types.Mirroring;
const Mapper65 = @This();
prg_banks: [3]u8 = [_]u8{ 0, 1, 2 },
chr_banks: [8]u8 = [_]u8{ 0, 1, 2, 3, 4, 5, 6, 7 },
mirroring_mode: Mirroring = .vertical,
irq_enabled: bool = false,
irq_counter: u16 = 0,
irq_reload: u16 = 0,
irq_assert: bool = false,
prg_bank_count: usize,
chr_bank_count: usize,
pub fn init(
prg_size: usize,
chr_size: usize,
mirroring_mode: Mirroring,
) Mapper65 {
return .{
.prg_bank_count = prg_size / 0x2000,
.chr_bank_count = if (chr_size > 0) chr_size / 0x0400 else 1,
.mirroring_mode = mirroring_mode,
};
}
pub fn cpuMapRead(
self: *const Mapper65,
address: u16,
) ?usize {
if (address < 0x8000) return null;
const offset = @as(usize, address & 0x1fff);
const last_bank = self.prg_bank_count - 1;
switch ((address - 0x8000) / 0x2000) {
0 => return (@as(usize, self.prg_banks[0]) % self.prg_bank_count) * 0x2000 + offset,
1 => return (@as(usize, self.prg_banks[1]) % self.prg_bank_count) * 0x2000 + offset,
2 => return (@as(usize, self.prg_banks[2]) % self.prg_bank_count) * 0x2000 + offset,
3 => return last_bank * 0x2000 + offset,
else => unreachable,
}
}
pub fn cpuWrite(
self: *Mapper65,
address: u16,
value: u8,
) void {
if (address < 0x8000) return;
switch (address) {
0x8000 => self.prg_banks[0] = value,
0xa000 => self.prg_banks[1] = value,
0xc000 => self.prg_banks[2] = value,
0x9001 => self.mirroring_mode = if ((value & 0x80) != 0) .horizontal else .vertical,
0x9003 => {
self.irq_enabled = (value & 0x80) != 0;
self.irq_assert = false;
},
0x9004 => {
self.irq_reload = (self.irq_reload & 0xff00) | value;
},
0x9005 => {
self.irq_reload = (self.irq_reload & 0x00ff) | (@as(u16, value) << 8);
self.irq_counter = self.irq_reload;
self.irq_assert = false;
},
0xb000 => self.chr_banks[0] = value,
0xb001 => self.chr_banks[1] = value,
0xb002 => self.chr_banks[2] = value,
0xb003 => self.chr_banks[3] = value,
0xb004 => self.chr_banks[4] = value,
0xb005 => self.chr_banks[5] = value,
0xb006 => self.chr_banks[6] = value,
0xb007 => self.chr_banks[7] = value,
else => {},
}
}
pub fn ppuMapRead(
self: *const Mapper65,
address: u16,
) ?usize {
if (address >= 0x2000) return null;
const slot = address / 0x0400;
const offset = @as(usize, address & 0x03ff);
const bank = @as(usize, self.chr_banks[slot]) % self.chr_bank_count;
return bank * 0x0400 + offset;
}
pub fn ppuMapWrite(
self: *const Mapper65,
address: u16,
) ?usize {
return self.ppuMapRead(address);
}
pub fn mirroring(
self: *const Mapper65,
) Mirroring {
return self.mirroring_mode;
}
pub fn irqAsserted(
self: *const Mapper65,
) bool {
return self.irq_assert;
}
-103
View File
@@ -1,103 +0,0 @@
const types = @import("types.zig");
const Mirroring = types.Mirroring;
const Mapper68 = @This();
chr_banks: [4]u8 = [_]u8{ 0, 0, 0, 0 },
nt_banks: [2]u8 = [_]u8{ 0, 0 },
prg_bank: u8 = 0,
nametable_control: u8 = 0,
prg_bank_count: usize,
chr_bank_count: usize,
fixed_mirroring: Mirroring,
pub fn init(
prg_size: usize,
chr_size: usize,
mirroring_mode: Mirroring,
) Mapper68 {
return .{
.prg_bank_count = prg_size / 0x4000,
.chr_bank_count = if (chr_size > 0) chr_size / 0x0800 else 1,
.fixed_mirroring = mirroring_mode,
};
}
pub fn cpuMapRead(
self: *const Mapper68,
address: u16,
) ?usize {
if (address < 0x8000) return null;
const offset = @as(usize, address & 0x3fff);
const last_bank = self.prg_bank_count - 1;
if (address < 0xc000) {
const bank = @as(usize, self.prg_bank) % self.prg_bank_count;
return bank * 0x4000 + offset;
} else {
return last_bank * 0x4000 + offset;
}
}
pub fn cpuWrite(
self: *Mapper68,
address: u16,
value: u8,
) void {
if (address < 0x8000) return;
switch (address & 0xf000) {
0x8000 => self.chr_banks[0] = value,
0x9000 => self.chr_banks[1] = value,
0xa000 => self.chr_banks[2] = value,
0xb000 => self.chr_banks[3] = value,
0xc000 => self.nt_banks[0] = value,
0xd000 => self.nt_banks[1] = value,
0xe000 => self.nametable_control = value,
0xf000 => self.prg_bank = value & 0x0f,
else => {},
}
}
pub fn ppuMapRead(
self: *const Mapper68,
address: u16,
) ?usize {
if (address >= 0x2000) return null;
const slot = address / 0x0800;
const offset = @as(usize, address & 0x07ff);
const bank = @as(usize, self.chr_banks[slot]) % self.chr_bank_count;
return bank * 0x0800 + offset;
}
pub fn ppuMapWrite(
self: *const Mapper68,
address: u16,
) ?usize {
return self.ppuMapRead(address);
}
pub fn mirroring(
self: *const Mapper68,
) Mirroring {
if ((self.nametable_control & 0x10) == 0) {
switch (self.nametable_control & 0x03) {
0 => return .vertical,
1 => return .horizontal,
2 => return .single_screen_lower,
3 => return .single_screen_upper,
else => unreachable,
}
}
return self.fixed_mirroring;
}
pub fn irqAsserted(
_: *const Mapper68,
) bool {
return false;
}
-123
View File
@@ -1,123 +0,0 @@
const types = @import("types.zig");
const Mirroring = types.Mirroring;
const Mapper69 = @This();
command: u8 = 0,
chr_banks: [8]u8 = [_]u8{ 0, 1, 2, 3, 4, 5, 6, 7 },
prg_banks: [4]u8 = [_]u8{ 0, 1, 2, 3 }, // $6000, $8000, $A000, $C000
mirroring_mode: Mirroring = .vertical,
irq_enabled: bool = false,
irq_counter: u16 = 0,
irq_assert: bool = false,
prg_bank_count: usize,
chr_bank_count: usize,
pub fn init(
prg_size: usize,
chr_size: usize,
mirroring_mode: Mirroring,
) Mapper69 {
return .{
.prg_bank_count = prg_size / 0x2000,
.chr_bank_count = if (chr_size > 0) chr_size / 0x0400 else 1,
.mirroring_mode = mirroring_mode,
};
}
pub fn cpuMapRead(
self: *const Mapper69,
address: u16,
) ?usize {
if (address < 0x6000) return null;
const offset = @as(usize, address & 0x1fff);
const last_bank = self.prg_bank_count - 1;
if (address < 0x8000) {
const bank = @as(usize, self.prg_banks[0] & 0x3f) % self.prg_bank_count;
return bank * 0x2000 + offset;
}
switch ((address - 0x8000) / 0x2000) {
0 => return (@as(usize, self.prg_banks[1] & 0x3f) % self.prg_bank_count) * 0x2000 + offset,
1 => return (@as(usize, self.prg_banks[2] & 0x3f) % self.prg_bank_count) * 0x2000 + offset,
2 => return (@as(usize, self.prg_banks[3] & 0x3f) % self.prg_bank_count) * 0x2000 + offset,
3 => return last_bank * 0x2000 + offset,
else => unreachable,
}
}
pub fn cpuWrite(
self: *Mapper69,
address: u16,
value: u8,
) void {
if (address < 0x8000) return;
if (address < 0xa000) {
self.command = value & 0x0f;
} else if (address < 0xc000) {
switch (self.command) {
0...7 => |i| self.chr_banks[i] = value,
8 => self.prg_banks[0] = value,
9 => self.prg_banks[1] = value,
0xa => self.prg_banks[2] = value,
0xb => self.prg_banks[3] = value,
0xc => {
switch (value & 0x03) {
0 => self.mirroring_mode = .vertical,
1 => self.mirroring_mode = .horizontal,
2 => self.mirroring_mode = .single_screen_lower,
3 => self.mirroring_mode = .single_screen_upper,
else => unreachable,
}
},
0xd => {
self.irq_enabled = (value & 0x01) != 0;
self.irq_assert = false;
},
0xe => {
self.irq_counter = (self.irq_counter & 0xff00) | value;
},
0xf => {
self.irq_counter = (self.irq_counter & 0x00ff) | (@as(u16, value) << 8);
},
else => {},
}
}
}
pub fn ppuMapRead(
self: *const Mapper69,
address: u16,
) ?usize {
if (address >= 0x2000) return null;
const slot = address / 0x0400;
const offset = @as(usize, address & 0x03ff);
const bank = @as(usize, self.chr_banks[slot]) % self.chr_bank_count;
return bank * 0x0400 + offset;
}
pub fn ppuMapWrite(
self: *const Mapper69,
address: u16,
) ?usize {
return self.ppuMapRead(address);
}
pub fn mirroring(
self: *const Mapper69,
) Mirroring {
return self.mirroring_mode;
}
pub fn irqAsserted(
self: *const Mapper69,
) bool {
return self.irq_assert;
}
-91
View File
@@ -1,91 +0,0 @@
const types = @import("types.zig");
const Mirroring = types.Mirroring;
const Mapper76 = @This();
bank_select: u8 = 0,
bank_registers: [8]u8 = [_]u8{0} ** 8,
prg_bank_count: usize,
chr_bank_count: usize,
fixed_mirroring: Mirroring,
pub fn init(
prg_size: usize,
chr_size: usize,
mirroring_mode: Mirroring,
) Mapper76 {
return .{
.prg_bank_count = prg_size / 0x2000,
.chr_bank_count = if (chr_size > 0) chr_size / 0x0800 else 1,
.fixed_mirroring = mirroring_mode,
};
}
pub fn cpuMapRead(
self: *const Mapper76,
address: u16,
) ?usize {
if (address < 0x8000) return null;
const offset = @as(usize, address & 0x1fff);
const last_bank = self.prg_bank_count - 1;
const second_last_bank = self.prg_bank_count - 2;
const r6 = @as(usize, self.bank_registers[6]) % self.prg_bank_count;
const r7 = @as(usize, self.bank_registers[7]) % self.prg_bank_count;
switch ((address - 0x8000) / 0x2000) {
0 => return r6 * 0x2000 + offset,
1 => return r7 * 0x2000 + offset,
2 => return second_last_bank * 0x2000 + offset,
3 => return last_bank * 0x2000 + offset,
else => unreachable,
}
}
pub fn cpuWrite(
self: *Mapper76,
address: u16,
value: u8,
) void {
if (address < 0x8000) return;
if ((address & 1) == 0) {
self.bank_select = value & 0x07;
} else {
self.bank_registers[self.bank_select] = value;
}
}
pub fn ppuMapRead(
self: *const Mapper76,
address: u16,
) ?usize {
if (address >= 0x2000) return null;
const offset = @as(usize, address & 0x07ff);
const slot = address / 0x0800;
const reg_idx = @as(usize, 2 + slot);
const bank = (@as(usize, self.bank_registers[reg_idx])) % self.chr_bank_count;
return bank * 0x0800 + offset;
}
pub fn ppuMapWrite(
self: *const Mapper76,
address: u16,
) ?usize {
return self.ppuMapRead(address);
}
pub fn mirroring(
self: *const Mapper76,
) Mirroring {
return self.fixed_mirroring;
}
pub fn irqAsserted(
_: *const Mapper76,
) bool {
return false;
}
-70
View File
@@ -1,70 +0,0 @@
const types = @import("types.zig");
const Mirroring = types.Mirroring;
const Mapper87 = @This();
chr_bank: u8 = 0,
prg_bank_count: usize,
chr_bank_count: usize,
mirroring_mode: Mirroring,
pub fn init(
prg_size: usize,
chr_size: usize,
_mirroring: Mirroring,
) Mapper87 {
return .{
.prg_bank_count = prg_size / 0x8000,
.chr_bank_count = if (chr_size > 0) chr_size / 0x2000 else 1,
.mirroring_mode = _mirroring,
};
}
pub fn cpuMapRead(
self: *const Mapper87,
address: u16,
) ?usize {
if (address < 0x8000) return null;
const offset = @as(usize, address - 0x8000);
const bank = if (self.prg_bank_count > 0) (self.prg_bank_count - 1) else 0;
return bank * 0x8000 + offset;
}
pub fn cpuWrite(
self: *Mapper87,
address: u16,
value: u8,
) void {
if (address >= 0x6000 and address <= 0x7FFF) {
self.chr_bank = ((value & 1) << 1) | ((value >> 1) & 1);
}
}
pub fn ppuMapRead(
self: *const Mapper87,
address: u16,
) ?usize {
if (address >= 0x2000) return null;
const bank = @as(usize, self.chr_bank) % self.chr_bank_count;
return bank * 0x2000 + @as(usize, address);
}
pub fn ppuMapWrite(
_: *const Mapper87,
address: u16,
) ?usize {
if (address >= 0x2000) return null;
return address;
}
pub fn mirroring(
self: *const Mapper87,
) Mirroring {
return self.mirroring_mode;
}
pub fn irqAsserted(_: *const Mapper87) bool {
return false;
}
-103
View File
@@ -1,103 +0,0 @@
const types = @import("types.zig");
const Mirroring = types.Mirroring;
const Mapper88 = @This();
bank_select: u8 = 0,
bank_registers: [8]u8 = [_]u8{0} ** 8,
prg_bank_count: usize,
chr_bank_count: usize,
fixed_mirroring: Mirroring,
pub fn init(
prg_size: usize,
chr_size: usize,
mirroring_mode: Mirroring,
) Mapper88 {
return .{
.prg_bank_count = prg_size / 0x2000,
.chr_bank_count = if (chr_size > 0) chr_size / 0x0400 else 1,
.fixed_mirroring = mirroring_mode,
};
}
pub fn cpuMapRead(
self: *const Mapper88,
address: u16,
) ?usize {
if (address < 0x8000) return null;
const offset = @as(usize, address & 0x1fff);
const last_bank = self.prg_bank_count - 1;
const second_last_bank = self.prg_bank_count - 2;
const r6 = @as(usize, self.bank_registers[6] & 0x3f) % self.prg_bank_count;
const r7 = @as(usize, self.bank_registers[7] & 0x3f) % self.prg_bank_count;
switch ((address - 0x8000) / 0x2000) {
0 => return r6 * 0x2000 + offset,
1 => return r7 * 0x2000 + offset,
2 => return second_last_bank * 0x2000 + offset,
3 => return last_bank * 0x2000 + offset,
else => unreachable,
}
}
pub fn cpuWrite(
self: *Mapper88,
address: u16,
value: u8,
) void {
if (address < 0x8000) return;
if ((address & 1) == 0) {
self.bank_select = value & 0x07;
} else {
self.bank_registers[self.bank_select] = value;
}
}
pub fn ppuMapRead(
self: *const Mapper88,
address: u16,
) ?usize {
if (address >= 0x2000) return null;
const offset = @as(usize, address & 0x03ff);
const page = address / 0x0400;
var bank: usize = 0;
switch (page) {
0, 1 => bank = (@as(usize, self.bank_registers[0] & 0xfe) + (page & 1)),
2, 3 => bank = (@as(usize, self.bank_registers[1] & 0xfe) + (page & 1)),
4, 5, 6, 7 => {
const reg_idx = page - 2; // registers 2, 3, 4, 5
const val = self.bank_registers[reg_idx];
// Bit 6 maps to 64KB CHR block selection (bit 6 -> 0x40 added to bank number)
bank = (@as(usize, val & 0x3f) | (@as(usize, val & 0x40)));
},
else => unreachable,
}
bank = bank % self.chr_bank_count;
return bank * 0x0400 + offset;
}
pub fn ppuMapWrite(
self: *const Mapper88,
address: u16,
) ?usize {
return self.ppuMapRead(address);
}
pub fn mirroring(
self: *const Mapper88,
) Mirroring {
return self.fixed_mirroring;
}
pub fn irqAsserted(
_: *const Mapper88,
) bool {
return false;
}
-78
View File
@@ -1,78 +0,0 @@
const types = @import("types.zig");
const Mirroring = types.Mirroring;
const Mapper89 = @This();
prg_bank: u8 = 0,
chr_bank: u8 = 0,
mirroring_mode: Mirroring = .single_screen_lower,
prg_bank_count: usize,
chr_bank_count: usize,
pub fn init(
prg_size: usize,
chr_size: usize,
) Mapper89 {
return .{
.prg_bank_count = prg_size / 0x4000,
.chr_bank_count = if (chr_size > 0) chr_size / 0x2000 else 1,
};
}
pub fn cpuMapRead(
self: *const Mapper89,
address: u16,
) ?usize {
if (address < 0x8000) return null;
if (address < 0xC000) {
const offset = @as(usize, address - 0x8000);
const bank = @as(usize, self.prg_bank) % self.prg_bank_count;
return bank * 0x4000 + offset;
} else {
const offset = @as(usize, address - 0xC000);
const fixed_bank = if (self.prg_bank_count > 0) self.prg_bank_count - 1 else 0;
return fixed_bank * 0x4000 + offset;
}
}
pub fn cpuWrite(
self: *Mapper89,
address: u16,
value: u8,
) void {
if (address >= 0x8000) {
self.prg_bank = (value >> 4) & 0x07;
self.chr_bank = (value & 0x07) | ((value & 0x80) >> 4);
self.mirroring_mode = if ((value & 0x08) != 0) .single_screen_upper else .single_screen_lower;
}
}
pub fn ppuMapRead(
self: *const Mapper89,
address: u16,
) ?usize {
if (address >= 0x2000) return null;
const bank = @as(usize, self.chr_bank) % self.chr_bank_count;
return bank * 0x2000 + @as(usize, address);
}
pub fn ppuMapWrite(
_: *const Mapper89,
address: u16,
) ?usize {
if (address >= 0x2000) return null;
return address;
}
pub fn mirroring(
self: *const Mapper89,
) Mirroring {
return self.mirroring_mode;
}
pub fn irqAsserted(_: *const Mapper89) bool {
return false;
}
-73
View File
@@ -1,73 +0,0 @@
const types = @import("types.zig");
const Mirroring = types.Mirroring;
const Mapper93 = @This();
prg_bank: u8 = 0,
mirroring_mode: Mirroring,
prg_bank_count: usize,
pub fn init(
prg_size: usize,
_mirroring: Mirroring,
) Mapper93 {
return .{
.prg_bank_count = prg_size / 0x4000,
.mirroring_mode = _mirroring,
};
}
pub fn cpuMapRead(
self: *const Mapper93,
address: u16,
) ?usize {
if (address < 0x8000) return null;
if (address < 0xC000) {
const offset = @as(usize, address - 0x8000);
const bank = @as(usize, self.prg_bank) % self.prg_bank_count;
return bank * 0x4000 + offset;
} else {
const offset = @as(usize, address - 0xC000);
const fixed_bank = if (self.prg_bank_count > 0) self.prg_bank_count - 1 else 0;
return fixed_bank * 0x4000 + offset;
}
}
pub fn cpuWrite(
self: *Mapper93,
address: u16,
value: u8,
) void {
if (address >= 0x8000) {
self.prg_bank = (value >> 4) & 0x07;
self.mirroring_mode = if ((value & 1) != 0) .horizontal else .vertical;
}
}
pub fn ppuMapRead(
_: *const Mapper93,
address: u16,
) ?usize {
if (address >= 0x2000) return null;
return address;
}
pub fn ppuMapWrite(
_: *const Mapper93,
address: u16,
) ?usize {
if (address >= 0x2000) return null;
return address;
}
pub fn mirroring(
self: *const Mapper93,
) Mirroring {
return self.mirroring_mode;
}
pub fn irqAsserted(_: *const Mapper93) bool {
return false;
}
-71
View File
@@ -1,71 +0,0 @@
const types = @import("types.zig");
const Mirroring = types.Mirroring;
const Mapper94 = @This();
prg_bank: u8 = 0,
prg_bank_count: usize,
mirroring_mode: Mirroring,
pub fn init(
prg_size: usize,
_mirroring: Mirroring,
) Mapper94 {
return .{
.prg_bank_count = prg_size / 0x4000,
.mirroring_mode = _mirroring,
};
}
pub fn cpuMapRead(
self: *const Mapper94,
address: u16,
) ?usize {
if (address < 0x8000) return null;
if (address < 0xC000) {
const offset = @as(usize, address - 0x8000);
const bank = @as(usize, self.prg_bank) % self.prg_bank_count;
return bank * 0x4000 + offset;
} else {
const offset = @as(usize, address - 0xC000);
const fixed_bank = if (self.prg_bank_count > 0) self.prg_bank_count - 1 else 0;
return fixed_bank * 0x4000 + offset;
}
}
pub fn cpuWrite(
self: *Mapper94,
address: u16,
value: u8,
) void {
if (address >= 0x8000) {
self.prg_bank = (value >> 2) & 0x07;
}
}
pub fn ppuMapRead(
_: *const Mapper94,
address: u16,
) ?usize {
if (address >= 0x2000) return null;
return address;
}
pub fn ppuMapWrite(
_: *const Mapper94,
address: u16,
) ?usize {
if (address >= 0x2000) return null;
return address;
}
pub fn mirroring(
self: *const Mapper94,
) Mirroring {
return self.mirroring_mode;
}
pub fn irqAsserted(_: *const Mapper94) bool {
return false;
}
+150 -111
View File
@@ -1,130 +1,152 @@
const types = @import("types.zig");
const Mirroring = types.Mirroring;
const std = @import("std");
const common = @import("../common.zig");
const Mirroring = common.Mirroring;
const Mmc1 = @This();
pub 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,
shift_register: u5 = 0x10,
control: u5 = 0x0c, // PRG mode 3 (fixed $C000)
chr_bank_0: u5 = 0,
chr_bank_1: u5 = 0,
prg_bank: u5 = 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 init() Mmc1 {
return .{};
}
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 cpuRead(self: *const Mmc1, address: u16, prg_rom: []const u8, prg_ram: []const u8) ?u8 {
if (address >= 0x6000 and address <= 0x7fff) {
// Bit 4 of prg_bank is PRG RAM disable (0: enabled, 1: disabled)
if ((self.prg_bank & 0x10) != 0) return 0;
const offset = address - 0x6000;
if (offset < prg_ram.len) return prg_ram[offset];
return 0;
}
}
if (address >= 0x8000 and address <= 0xffff) {
const prg_mode: u2 = @truncate(self.control >> 2);
const total_16k_banks: usize = @max(1, prg_rom.len / 0x4000);
// SUROM uses CHR bank 0 bit 4 for PRG A18 when PRG is 512KB
const surom_base: usize = if (total_16k_banks >= 32 and (self.chr_bank_0 & 0x10) != 0) 16 else 0;
var bank: usize = 0;
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,
switch (prg_mode) {
0, 1 => {
// 32 KB mode: ignore low bit of prg_bank
bank = ((self.prg_bank & 0x0e) | surom_base) % total_16k_banks;
const offset = (bank * 0x4000) + (address - 0x8000);
if (offset < prg_rom.len) return prg_rom[offset];
},
2 => {
// Fix first bank at $8000, switch 16 KB bank at $C000
if (address < 0xc000) {
bank = surom_base % total_16k_banks;
const offset = (bank * 0x4000) + (address - 0x8000);
if (offset < prg_rom.len) return prg_rom[offset];
} else {
bank = ((self.prg_bank & 0x0f) | surom_base) % total_16k_banks;
const offset = (bank * 0x4000) + (address - 0xc000);
if (offset < prg_rom.len) return prg_rom[offset];
}
},
3 => {
// Switch 16 KB bank at $8000, fix last bank at $C000
if (address < 0xc000) {
bank = ((self.prg_bank & 0x0f) | surom_base) % total_16k_banks;
const offset = (bank * 0x4000) + (address - 0x8000);
if (offset < prg_rom.len) return prg_rom[offset];
} else {
bank = (surom_base + 15) % total_16k_banks;
const offset = (bank * 0x4000) + (address - 0xc000);
if (offset < prg_rom.len) return prg_rom[offset];
}
},
}
return 0;
}
return null;
}
pub fn ppuMapRead(
self: *const Mmc1,
address: u16,
) ?usize {
if (address >= 0x2000) return null;
pub fn cpuWrite(self: *Mmc1, address: u16, value: u8, prg_ram: []u8) bool {
if (address >= 0x6000 and address <= 0x7fff) {
if ((self.prg_bank & 0x10) != 0) return false;
const offset = address - 0x6000;
if (offset < prg_ram.len) {
prg_ram[offset] = value;
return true;
}
return false;
}
if (address >= 0x8000 and address <= 0xffff) {
if ((value & 0x80) != 0) {
// Reset shift register
self.shift_register = 0x10;
self.control |= 0x0c; // PRG mode 3
return true;
}
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);
const is_full = (self.shift_register & 1) != 0;
self.shift_register = (self.shift_register >> 1) | (@as(u5, @truncate(value & 1)) << 4);
if (is_full) {
const reg_val = self.shift_register;
self.shift_register = 0x10;
const reg_select = (address >> 13) & 0x03;
switch (reg_select) {
0 => self.control = reg_val,
1 => self.chr_bank_0 = reg_val,
2 => self.chr_bank_1 = reg_val,
3 => self.prg_bank = reg_val,
else => unreachable,
}
}
return true;
}
return false;
}
pub fn ppuRead(self: *const Mmc1, address: u16, chr_rom: []const u8, chr_ram: []const u8, chr_is_ram: bool) ?u8 {
if (address < 0x2000) {
if (chr_is_ram) {
if (address < chr_ram.len) return chr_ram[address];
return 0;
}
if (chr_rom.len == 0) return 0;
const chr_4k_mode = (self.control & 0x10) != 0;
const total_4k_banks = @max(1, chr_rom.len / 0x1000);
var offset: usize = 0;
if (chr_4k_mode) {
if (address < 0x1000) {
const bank = @as(usize, self.chr_bank_0) % total_4k_banks;
offset = (bank * 0x1000) + address;
} else {
const bank = @as(usize, self.chr_bank_1) % total_4k_banks;
offset = (bank * 0x1000) + (address - 0x1000);
}
} else {
const bank = @as(usize, self.chr_bank_1) % self.chr_bank_count;
return bank * 0x1000 + @as(usize, address & 0x0fff);
const bank = @as(usize, self.chr_bank_0 & 0x1e) % total_4k_banks;
offset = (bank * 0x1000) + address;
}
if (offset < chr_rom.len) return chr_rom[offset];
return 0;
}
return null;
}
pub fn ppuWrite(_: *Mmc1, address: u16, value: u8, chr_ram: []u8, chr_is_ram: bool) bool {
if (address < 0x2000 and chr_is_ram) {
if (address < chr_ram.len) {
chr_ram[address] = value;
return true;
}
}
return false;
}
pub fn ppuMapWrite(
_: *const Mmc1,
address: u16,
) ?usize {
if (address >= 0x2000) return null;
return address;
}
pub fn mirroring(
self: *const Mmc1,
) Mirroring {
pub fn mirroring(self: *const Mmc1) Mirroring {
return switch (self.control & 0x03) {
0 => .single_screen_lower,
1 => .single_screen_upper,
@@ -134,6 +156,23 @@ pub fn mirroring(
};
}
pub fn irqAsserted(_: *const Mmc1) bool {
pub fn irqLine(_: *const Mmc1) bool {
return false;
}
pub fn notifyPpuAddress(_: *Mmc1, _: u16) void {}
test "mmc1 serial shift register write" {
var mmc1 = Mmc1.init();
var ram: [0x2000]u8 = undefined;
// Write 5 bits (e.g. 0b00011 -> horizontal mirroring) into $8000 (control register)
_ = mmc1.cpuWrite(0x8000, 1, &ram); // bit 0 = 1
_ = mmc1.cpuWrite(0x8000, 1, &ram); // bit 1 = 1
_ = mmc1.cpuWrite(0x8000, 0, &ram); // bit 2 = 0
_ = mmc1.cpuWrite(0x8000, 0, &ram); // bit 3 = 0
_ = mmc1.cpuWrite(0x8000, 0, &ram); // bit 4 = 0 -> loaded!
try std.testing.expectEqual(@as(u5, 0x03), mmc1.control);
try std.testing.expectEqual(Mirroring.horizontal, mmc1.mirroring());
}
+130 -83
View File
@@ -1,104 +1,151 @@
const types = @import("types.zig");
const Mirroring = types.Mirroring;
const std = @import("std");
const common = @import("../common.zig");
const Mirroring = common.Mirroring;
const Mmc2 = @This();
// Mapper 9 (MMC2) - Mike Tyson's Punch-Out!!
// 8KB switchable PRG at $8000, fixed 24KB at $A000-$FFFF.
// Dual 4KB CHR latches toggled by PPU fetches at $xFD8-$xFDF and $xFE8-$xFEF.
pub 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
chr_bank_0_fd: u8 = 0,
chr_bank_0_fe: u8 = 0,
chr_bank_1_fd: u8 = 0,
chr_bank_1_fe: u8 = 0,
latch_0: u1 = 0,
latch_1: u1 = 0,
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 init() Mmc2 {
return .{};
}
pub fn cpuMapRead(
self: *const Mmc2,
address: u16,
) ?usize {
if (address < 0x8000) return null;
pub fn cpuRead(self: *const Mmc2, address: u16, prg_rom: []const u8, prg_ram: []const u8) ?u8 {
if (address >= 0x6000 and address <= 0x7fff) {
const offset = address - 0x6000;
if (offset < prg_ram.len) return prg_ram[offset];
return 0;
}
if (address >= 0x8000 and address <= 0xffff) {
const total_8k_banks = @max(1, prg_rom.len / 0x2000);
var bank: usize = 0;
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;
switch (address) {
0x8000...0x9fff => bank = @as(usize, self.prg_bank) % total_8k_banks,
0xa000...0xbfff => bank = if (total_8k_banks >= 3) total_8k_banks - 3 else 0,
0xc000...0xdfff => bank = if (total_8k_banks >= 2) total_8k_banks - 2 else 0,
0xe000...0xffff => bank = total_8k_banks - 1,
else => return 0,
}
const offset = (bank * 0x2000) + (address & 0x1fff);
if (offset < prg_rom.len) return prg_rom[offset];
return 0;
}
return null;
}
pub fn cpuWrite(self: *Mmc2, address: u16, value: u8, prg_ram: []u8) bool {
if (address >= 0x6000 and address <= 0x7fff) {
const offset = address - 0x6000;
if (offset < prg_ram.len) {
prg_ram[offset] = value;
return true;
}
return false;
}
switch (address) {
0xa000...0xafff => {
self.prg_bank = value & 0x0f;
return true;
},
0xb000...0xbfff => {
self.chr_bank_0_fd = value & 0x1f;
return true;
},
0xc000...0xcfff => {
self.chr_bank_0_fe = value & 0x1f;
return true;
},
0xd000...0xdfff => {
self.chr_bank_1_fd = value & 0x1f;
return true;
},
0xe000...0xefff => {
self.chr_bank_1_fe = value & 0x1f;
return true;
},
0xf000...0xffff => {
self.mirroring_mode = if ((value & 1) == 0) .vertical else .horizontal;
return true;
},
else => return false,
}
}
pub fn cpuWrite(
self: *Mmc2,
address: u16,
value: u8,
) void {
if (address < 0x8000) return;
pub fn ppuRead(self: *const Mmc2, address: u16, chr_rom: []const u8, chr_ram: []const u8, chr_is_ram: bool) ?u8 {
if (address < 0x2000) {
if (chr_is_ram) {
if (address < chr_ram.len) return chr_ram[address];
return 0;
}
if (chr_rom.len == 0) return 0;
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,
const total_4k_banks = @max(1, chr_rom.len / 0x1000);
const bank = if (address < 0x1000)
(if (self.latch_0 == 0) self.chr_bank_0_fd else self.chr_bank_0_fe)
else
(if (self.latch_1 == 0) self.chr_bank_1_fd else self.chr_bank_1_fe);
const offset = ((@as(usize, bank) % total_4k_banks) * 0x1000) + (address & 0x0fff);
if (offset < chr_rom.len) return chr_rom[offset];
return 0;
}
return null;
}
pub fn ppuWrite(_: *Mmc2, address: u16, value: u8, chr_ram: []u8, chr_is_ram: bool) bool {
if (address < 0x2000 and chr_is_ram) {
if (address < chr_ram.len) {
chr_ram[address] = value;
return true;
}
}
return false;
}
pub fn notifyPpuAddress(self: *Mmc2, address: u16) void {
self.checkLatch(address);
}
inline fn checkLatch(self: *Mmc2, address: u16) void {
switch (address) {
0x0fd8...0x0fdf => self.latch_0 = 0,
0x0fe8...0x0fef => self.latch_0 = 1,
0x1fd8...0x1fdf => self.latch_1 = 0,
0x1fe8...0x1fef => self.latch_1 = 1,
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 {
pub fn mirroring(self: *const Mmc2) Mirroring {
return self.mirroring_mode;
}
pub fn irqAsserted(_: *const Mmc2) bool {
pub fn irqLine(_: *const Mmc2) bool {
return false;
}
test "mmc2 latch toggling" {
var m = Mmc2.init();
var ram: [0x2000]u8 = undefined;
_ = m.cpuWrite(0xb000, 1, &ram); // CHR 0 FD = 1
_ = m.cpuWrite(0xc000, 2, &ram); // CHR 0 FE = 2
m.notifyPpuAddress(0x0fd8);
try std.testing.expectEqual(@as(u1, 0), m.latch_0);
m.notifyPpuAddress(0x0fe8);
try std.testing.expectEqual(@as(u1, 1), m.latch_0);
}
+192 -131
View File
@@ -1,161 +1,222 @@
const types = @import("types.zig");
const Mirroring = types.Mirroring;
const std = @import("std");
const common = @import("../common.zig");
const Mirroring = common.Mirroring;
const Mmc3 = @This();
pub const Mmc3 = @This();
bank_select: u8 = 0,
bank_registers: [8]u8 = [_]u8{0} ** 8,
mirroring_mode: Mirroring = .horizontal,
registers: [8]u8 = [_]u8{0} ** 8,
mirroring_mode: Mirroring = .vertical,
prg_ram_protect: u8 = 0x80,
irq_latch: u8 = 0,
irq_counter: u8 = 0,
irq_reload: bool = false,
irq_enabled: bool = false,
irq_pending: bool = false,
irq_asserted: bool = false,
prg_bank_count: usize,
chr_bank_count: usize,
last_a12: bool = false,
a12_low_cycles: u32 = 0,
pub fn init(
prg_size: usize,
chr_size: usize,
) Mmc3 {
pub fn init(initial_mirroring: Mirroring) Mmc3 {
return .{
.prg_bank_count = prg_size / 0x2000,
.chr_bank_count = if (chr_size > 0) chr_size / 0x0400 else 8,
.mirroring_mode = initial_mirroring,
};
}
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 cpuRead(self: *const Mmc3, address: u16, prg_rom: []const u8, prg_ram: []const u8) ?u8 {
if (address >= 0x6000 and address <= 0x7fff) {
if ((self.prg_ram_protect & 0x80) == 0) return 0;
const offset = address - 0x6000;
if (offset < prg_ram.len) return prg_ram[offset];
return 0;
}
}
if (address >= 0x8000 and address <= 0xffff) {
const total_8k_banks = @max(1, prg_rom.len / 0x2000);
const prg_mode = (self.bank_select & 0x40) != 0;
var bank: usize = 0;
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,
switch (address) {
0x8000...0x9fff => {
bank = if (!prg_mode) self.registers[6] else (total_8k_banks - 2);
},
0xa000...0xbfff => {
bank = self.registers[7];
},
0xc000...0xdfff => {
bank = if (!prg_mode) (total_8k_banks - 2) else self.registers[6];
},
0xe000...0xffff => {
bank = total_8k_banks - 1;
},
else => return 0,
}
} 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 %= total_8k_banks;
const offset = (bank * 0x2000) + (address & 0x1fff);
if (offset < prg_rom.len) return prg_rom[offset];
return 0;
}
return null;
}
pub fn cpuWrite(self: *Mmc3, address: u16, value: u8, prg_ram: []u8) bool {
if (address >= 0x6000 and address <= 0x7fff) {
if ((self.prg_ram_protect & 0x80) == 0 or (self.prg_ram_protect & 0x40) != 0) return false;
const offset = address - 0x6000;
if (offset < prg_ram.len) {
prg_ram[offset] = value;
return true;
}
return false;
}
switch (address) {
0x8000...0x9fff => {
if ((address & 1) == 0) {
self.bank_select = value;
} else {
const reg = self.bank_select & 0x07;
self.registers[reg] = value;
}
return true;
},
0xa000...0xbfff => {
if ((address & 1) == 0) {
self.mirroring_mode = if ((value & 1) == 0) .vertical else .horizontal;
} else {
self.prg_ram_protect = value;
}
return true;
},
0xc000...0xdfff => {
if ((address & 1) == 0) {
self.irq_latch = value;
} else {
self.irq_reload = true;
}
return true;
},
0xe000...0xffff => {
if ((address & 1) == 0) {
self.irq_enabled = false;
self.irq_asserted = false;
} else {
self.irq_enabled = true;
}
return true;
},
else => return false,
}
}
pub fn ppuRead(self: *const Mmc3, address: u16, chr_rom: []const u8, chr_ram: []const u8, chr_is_ram: bool) ?u8 {
if (address < 0x2000) {
if (chr_is_ram) {
if (address < chr_ram.len) return chr_ram[address];
return 0;
}
if (chr_rom.len == 0) return 0;
const chr_inversion = (self.bank_select & 0x80) != 0;
const total_1k_banks = @max(1, chr_rom.len / 0x0400);
var bank: usize = 0;
if (!chr_inversion) {
switch (address) {
0x0000...0x07ff => bank = (self.registers[0] & 0xfe) +% ((address >> 10) & 1),
0x0800...0x0fff => bank = (self.registers[1] & 0xfe) +% ((address >> 10) & 1),
0x1000...0x13ff => bank = self.registers[2],
0x1400...0x17ff => bank = self.registers[3],
0x1800...0x1bff => bank = self.registers[4],
0x1c00...0x1fff => bank = self.registers[5],
else => unreachable,
}
} else {
switch (address) {
0x0000...0x03ff => bank = self.registers[2],
0x0400...0x07ff => bank = self.registers[3],
0x0800...0x0bff => bank = self.registers[4],
0x0c00...0x0fff => bank = self.registers[5],
0x1000...0x17ff => bank = (self.registers[0] & 0xfe) +% ((address >> 10) & 1),
0x1800...0x1fff => bank = (self.registers[1] & 0xfe) +% ((address >> 10) & 1),
else => unreachable,
}
}
bank %= total_1k_banks;
const offset = (bank * 0x0400) + (address & 0x03ff);
if (offset < chr_rom.len) return chr_rom[offset];
return 0;
}
return null;
}
pub fn ppuWrite(_: *Mmc3, address: u16, value: u8, chr_ram: []u8, chr_is_ram: bool) bool {
if (address < 0x2000 and chr_is_ram) {
if (address < chr_ram.len) {
chr_ram[address] = value;
return true;
}
}
bank = bank % self.chr_bank_count;
return bank * 0x0400 + offset;
return false;
}
pub fn ppuMapWrite(
_: *const Mmc3,
address: u16,
) ?usize {
if (address >= 0x2000) return null;
return address;
}
pub fn notifyPpuAddress(self: *Mmc3, address: u16) void {
const a12 = (address & 0x1000) != 0;
if (a12) {
if (!self.last_a12 and self.a12_low_cycles >= 3) {
// Rising edge on A12
if (self.irq_counter == 0 or self.irq_reload) {
self.irq_counter = self.irq_latch;
self.irq_reload = false;
} else {
self.irq_counter -= 1;
}
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;
if (self.irq_counter == 0 and self.irq_enabled) {
self.irq_asserted = true;
}
}
self.a12_low_cycles = 0;
} else {
self.irq_counter -= 1;
}
if (self.irq_counter == 0 and self.irq_enabled) {
self.irq_pending = true;
self.a12_low_cycles +|= 1;
}
self.last_a12 = a12;
}
pub fn mirroring(
self: *const Mmc3,
) Mirroring {
pub fn mirroring(self: *const Mmc3) Mirroring {
return self.mirroring_mode;
}
pub fn irqAsserted(
self: *const Mmc3,
) bool {
return self.irq_pending;
pub fn irqLine(self: *const Mmc3) bool {
return self.irq_asserted;
}
test "mmc3 scanline counter rising a12" {
var mmc3 = Mmc3.init(.vertical);
var ram: [0x2000]u8 = undefined;
// Set IRQ latch to 2 and enable IRQ
_ = mmc3.cpuWrite(0xc000, 2, &ram); // latch = 2
_ = mmc3.cpuWrite(0xc001, 0, &ram); // reload
_ = mmc3.cpuWrite(0xe001, 0, &ram); // enable
try std.testing.expect(!mmc3.irqLine());
// Cycle A12: low for 4 cycles, then high
for (0..4) |_| mmc3.notifyPpuAddress(0x0000);
mmc3.notifyPpuAddress(0x1000); // 1st rising edge: reloaded to 2
try std.testing.expectEqual(@as(u8, 2), mmc3.irq_counter);
try std.testing.expect(!mmc3.irqLine());
for (0..4) |_| mmc3.notifyPpuAddress(0x0000);
mmc3.notifyPpuAddress(0x1000); // 2nd rising edge: 2 -> 1
try std.testing.expectEqual(@as(u8, 1), mmc3.irq_counter);
try std.testing.expect(!mmc3.irqLine());
for (0..4) |_| mmc3.notifyPpuAddress(0x0000);
mmc3.notifyPpuAddress(0x1000); // 3rd rising edge: 1 -> 0, IRQ asserted!
try std.testing.expectEqual(@as(u8, 0), mmc3.irq_counter);
try std.testing.expect(mmc3.irqLine());
}
+125 -95
View File
@@ -1,117 +1,147 @@
const types = @import("types.zig");
const Mirroring = types.Mirroring;
const std = @import("std");
const common = @import("../common.zig");
const Mirroring = common.Mirroring;
const Mmc4 = @This();
// Mapper 10 (MMC4) - Fire Emblem & Fire Emblem Gaiden
// 16KB switchable PRG at $8000, fixed last 16KB at $C000-$FFFF.
// Dual 4KB CHR latches toggled by PPU fetches at $xFD8-$xFDF and $xFE8-$xFEF.
pub 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 },
chr_bank_0_fd: u8 = 0,
chr_bank_0_fe: u8 = 0,
chr_bank_1_fd: u8 = 0,
chr_bank_1_fe: u8 = 0,
latch_0: u1 = 0,
latch_1: u1 = 0,
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 init() Mmc4 {
return .{};
}
pub fn cpuMapRead(
self: *const Mmc4,
address: u16,
) ?usize {
if (address < 0x8000) return null;
pub fn cpuRead(self: *const Mmc4, address: u16, prg_rom: []const u8, prg_ram: []const u8) ?u8 {
if (address >= 0x6000 and address <= 0x7fff) {
const offset = address - 0x6000;
if (offset < prg_ram.len) return prg_ram[offset];
return 0;
}
if (address >= 0x8000 and address <= 0xffff) {
const total_16k_banks = @max(1, prg_rom.len / 0x4000);
var bank: usize = 0;
const offset = @as(usize, address & 0x3fff);
const last_bank = self.prg_bank_count - 1;
if (address < 0xc000) {
bank = @as(usize, self.prg_bank) % total_16k_banks;
const offset = (bank * 0x4000) + (address - 0x8000);
if (offset < prg_rom.len) return prg_rom[offset];
} else {
bank = total_16k_banks - 1;
const offset = (bank * 0x4000) + (address - 0xc000);
if (offset < prg_rom.len) return prg_rom[offset];
}
return 0;
}
return null;
}
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, prg_ram: []u8) bool {
if (address >= 0x6000 and address <= 0x7fff) {
const offset = address - 0x6000;
if (offset < prg_ram.len) {
prg_ram[offset] = value;
return true;
}
return false;
}
switch (address) {
0xa000...0xafff => {
self.prg_bank = value & 0x0f;
return true;
},
0xb000...0xbfff => {
self.chr_bank_0_fd = value & 0x1f;
return true;
},
0xc000...0xcfff => {
self.chr_bank_0_fe = value & 0x1f;
return true;
},
0xd000...0xdfff => {
self.chr_bank_1_fd = value & 0x1f;
return true;
},
0xe000...0xefff => {
self.chr_bank_1_fe = value & 0x1f;
return true;
},
0xf000...0xffff => {
self.mirroring_mode = if ((value & 1) == 0) .vertical else .horizontal;
return true;
},
else => return false,
}
}
pub fn cpuWrite(
self: *Mmc4,
address: u16,
value: u8,
) void {
if (address < 0xa000) return;
pub fn ppuRead(self: *const Mmc4, address: u16, chr_rom: []const u8, chr_ram: []const u8, chr_is_ram: bool) ?u8 {
if (address < 0x2000) {
if (chr_is_ram) {
if (address < chr_ram.len) return chr_ram[address];
return 0;
}
if (chr_rom.len == 0) return 0;
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,
const total_4k_banks = @max(1, chr_rom.len / 0x1000);
const bank = if (address < 0x1000)
(if (self.latch_0 == 0) self.chr_bank_0_fd else self.chr_bank_0_fe)
else
(if (self.latch_1 == 0) self.chr_bank_1_fd else self.chr_bank_1_fe);
const offset = ((@as(usize, bank) % total_4k_banks) * 0x1000) + (address & 0x0fff);
if (offset < chr_rom.len) return chr_rom[offset];
return 0;
}
return null;
}
pub fn ppuWrite(_: *Mmc4, address: u16, value: u8, chr_ram: []u8, chr_is_ram: bool) bool {
if (address < 0x2000 and chr_is_ram) {
if (address < chr_ram.len) {
chr_ram[address] = value;
return true;
}
}
return false;
}
pub fn notifyPpuAddress(self: *Mmc4, address: u16) void {
self.checkLatch(address);
}
inline fn checkLatch(self: *Mmc4, address: u16) void {
switch (address) {
0x0fd8...0x0fdf => self.latch_0 = 0,
0x0fe8...0x0fef => self.latch_0 = 1,
0x1fd8...0x1fdf => self.latch_1 = 0,
0x1fe8...0x1fef => self.latch_1 = 1,
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 {
pub fn mirroring(self: *const Mmc4) Mirroring {
return self.mirroring_mode;
}
pub fn irqAsserted(
_: *const Mmc4,
) bool {
pub fn irqLine(_: *const Mmc4) bool {
return false;
}
test "mmc4 latch toggling" {
var m = Mmc4.init();
var ram: [0x2000]u8 = undefined;
_ = m.cpuWrite(0xa000, 3, &ram); // PRG bank 3
try std.testing.expectEqual(@as(u8, 3), m.prg_bank);
m.notifyPpuAddress(0x1fe8);
try std.testing.expectEqual(@as(u1, 1), m.latch_1);
}
-171
View File
@@ -1,171 +0,0 @@
const types = @import("types.zig");
const Mirroring = types.Mirroring;
const Mmc5 = @This();
prg_mode: u2 = 3,
chr_mode: u2 = 0,
prg_banks: [4]u8 = [_]u8{ 0, 0, 1, 2 }, // $6000, $8000, $A000, $C000
chr_banks: [12]u16 = [_]u16{0} ** 12,
mult_a: u8 = 0,
mult_b: u8 = 0,
irq_scanline: u8 = 0,
irq_enabled: bool = false,
irq_assert: bool = false,
irq_in_frame: bool = false,
current_scanline: u8 = 0,
mirroring_mode: Mirroring = .vertical,
ex_ram: [1024]u8 = [_]u8{0} ** 1024,
prg_bank_count: usize,
chr_bank_count: usize,
pub fn init(
prg_size: usize,
chr_size: usize,
) Mmc5 {
return .{
.prg_bank_count = prg_size / 0x2000,
.chr_bank_count = if (chr_size > 0) chr_size / 0x0400 else 1,
};
}
pub fn cpuMapRead(
self: *const Mmc5,
address: u16,
) ?usize {
if (address < 0x5c00) return null;
if (address >= 0x5c00 and address <= 0x5fff) {
return 0x80000000 | @as(usize, address - 0x5c00);
}
if (address < 0x8000) {
const bank = @as(usize, self.prg_banks[0] & 0x7f) % self.prg_bank_count;
return bank * 0x2000 + (address & 0x1fff);
}
const last_bank = self.prg_bank_count - 1;
const offset = @as(usize, address & 0x1fff);
switch ((address - 0x8000) / 0x2000) {
0 => return (@as(usize, self.prg_banks[1] & 0x7f) % self.prg_bank_count) * 0x2000 + offset,
1 => return (@as(usize, self.prg_banks[2] & 0x7f) % self.prg_bank_count) * 0x2000 + offset,
2 => return (@as(usize, self.prg_banks[3] & 0x7f) % self.prg_bank_count) * 0x2000 + offset,
3 => return last_bank * 0x2000 + offset,
else => unreachable,
}
}
pub fn cpuWrite(
self: *Mmc5,
address: u16,
value: u8,
) void {
if (address >= 0x5c00 and address <= 0x5fff) {
self.ex_ram[address - 0x5c00] = value;
return;
}
switch (address) {
0x5100 => self.prg_mode = @truncate(value & 3),
0x5101 => self.chr_mode = @truncate(value & 3),
0x5105 => {
switch (@as(u2, @truncate(value & 3))) {
0 => self.mirroring_mode = .vertical,
1 => self.mirroring_mode = .horizontal,
2 => self.mirroring_mode = .single_screen_lower,
3 => self.mirroring_mode = .single_screen_upper,
}
},
0x5114 => self.prg_banks[0] = value,
0x5115 => self.prg_banks[1] = value,
0x5116 => self.prg_banks[2] = value,
0x5117 => self.prg_banks[3] = value,
0x5120...0x512b => |addr| {
const idx = addr - 0x5120;
self.chr_banks[idx] = value;
},
0x5203 => self.irq_scanline = value,
0x5204 => {
self.irq_enabled = (value & 0x80) != 0;
self.irq_assert = false;
},
0x5205 => self.mult_a = value,
0x5206 => self.mult_b = value,
else => {},
}
}
pub fn readRegister(
self: *Mmc5,
address: u16,
) ?u8 {
switch (address) {
0x5204 => {
const res: u8 = (if (self.irq_assert) @as(u8, 0x80) else 0) | (if (self.irq_in_frame) @as(u8, 0x40) else 0);
self.irq_assert = false;
return res;
},
0x5205 => {
const prod = @as(u16, self.mult_a) * @as(u16, self.mult_b);
return @truncate(prod);
},
0x5206 => {
const prod = @as(u16, self.mult_a) * @as(u16, self.mult_b);
return @truncate(prod >> 8);
},
else => return null,
}
}
pub fn ppuMapRead(
self: *const Mmc5,
address: u16,
) ?usize {
if (address >= 0x2000) return null;
const slot = address / 0x0400;
const offset = @as(usize, address & 0x03ff);
const bank = @as(usize, self.chr_banks[slot]) % self.chr_bank_count;
return bank * 0x0400 + offset;
}
pub fn ppuMapWrite(
self: *const Mmc5,
address: u16,
) ?usize {
return self.ppuMapRead(address);
}
pub fn handleScanline(
self: *Mmc5,
) void {
self.current_scanline += 1;
if (self.current_scanline == self.irq_scanline) {
if (self.irq_enabled) {
self.irq_assert = true;
}
}
if (self.current_scanline >= 240) {
self.current_scanline = 0;
self.irq_in_frame = false;
}
}
pub fn mirroring(
self: *const Mmc5,
) Mirroring {
return self.mirroring_mode;
}
pub fn irqAsserted(
self: *const Mmc5,
) bool {
return self.irq_assert;
}
-73
View File
@@ -1,73 +0,0 @@
const types = @import("types.zig");
const Mirroring = types.Mirroring;
const Nina03 = @This();
prg_bank: u8 = 0,
chr_bank: u8 = 0,
prg_bank_count: usize,
chr_bank_count: usize,
mirroring_mode: Mirroring,
pub fn init(
prg_size: usize,
chr_size: usize,
_mirroring: Mirroring,
) Nina03 {
return .{
.prg_bank_count = prg_size / 0x8000,
.chr_bank_count = if (chr_size > 0) chr_size / 0x2000 else 1,
.mirroring_mode = _mirroring,
};
}
pub fn cpuMapRead(
self: *const Nina03,
address: u16,
) ?usize {
if (address < 0x8000) return null;
const offset = @as(usize, address - 0x8000);
const bank = @as(usize, self.prg_bank) % self.prg_bank_count;
return bank * 0x8000 + offset;
}
pub fn cpuWrite(
self: *Nina03,
address: u16,
value: u8,
) void {
if (address >= 0x4100 and address <= 0x5FFF) {
self.prg_bank = @truncate((value >> 3) & 1);
self.chr_bank = value & 0x07;
}
}
pub fn ppuMapRead(
self: *const Nina03,
address: u16,
) ?usize {
if (address >= 0x2000) return null;
const bank = @as(usize, self.chr_bank) % self.chr_bank_count;
return bank * 0x2000 + @as(usize, address);
}
pub fn ppuMapWrite(
_: *const Nina03,
address: u16,
) ?usize {
if (address >= 0x2000) return null;
return address;
}
pub fn mirroring(
self: *const Nina03,
) Mirroring {
return self.mirroring_mode;
}
pub fn irqAsserted(_: *const Nina03) bool {
return false;
}
+64 -52
View File
@@ -1,71 +1,83 @@
const types = @import("types.zig");
const Mirroring = types.Mirroring;
const std = @import("std");
const common = @import("../common.zig");
const Mirroring = common.Mirroring;
const Nrom = @This();
pub 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 init(initial_mirroring: Mirroring) Nrom {
return .{ .mirroring_mode = initial_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 cpuRead(_: *const Nrom, address: u16, prg_rom: []const u8, prg_ram: []const u8) ?u8 {
if (address >= 0x6000 and address <= 0x7fff) {
const offset = address - 0x6000;
if (offset < prg_ram.len) return prg_ram[offset];
return 0;
}
if (address >= 0x8000 and address <= 0xffff) {
const mask: usize = if (prg_rom.len <= 0x4000) 0x3fff else 0x7fff;
const offset = (address - 0x8000) & mask;
if (offset < prg_rom.len) return prg_rom[offset];
return 0;
}
return null;
}
pub fn cpuWrite(
_: *Nrom,
_: u16,
_: u8,
) void {
// NROM has no mapper registers.
pub fn cpuWrite(_: *Nrom, address: u16, value: u8, prg_ram: []u8) bool {
if (address >= 0x6000 and address <= 0x7fff) {
const offset = address - 0x6000;
if (offset < prg_ram.len) {
prg_ram[offset] = value;
return true;
}
}
return false;
}
pub fn ppuMapRead(
_: *const Nrom,
address: u16,
) ?usize {
if (address >= 0x2000)
return null;
return address;
pub fn ppuRead(_: *const Nrom, address: u16, chr_rom: []const u8, chr_ram: []const u8, chr_is_ram: bool) ?u8 {
if (address < 0x2000) {
if (chr_is_ram) {
if (address < chr_ram.len) return chr_ram[address];
} else {
if (chr_rom.len > 0) return chr_rom[address & (chr_rom.len - 1)];
}
return 0;
}
return null;
}
pub fn ppuMapWrite(
_: *const Nrom,
address: u16,
) ?usize {
if (address >= 0x2000)
return null;
return address;
pub fn ppuWrite(_: *Nrom, address: u16, value: u8, chr_ram: []u8, chr_is_ram: bool) bool {
if (address < 0x2000 and chr_is_ram) {
if (address < chr_ram.len) {
chr_ram[address] = value;
return true;
}
}
return false;
}
pub fn mirroring(
self: *const Nrom,
) Mirroring {
pub fn mirroring(self: *const Nrom) Mirroring {
return self.mirroring_mode;
}
pub fn irqAsserted(_: *const Nrom) bool {
pub fn irqLine(_: *const Nrom) bool {
return false;
}
pub fn notifyPpuAddress(_: *Nrom, _: u16) void {}
test "nrom prg mirroring" {
var nrom = Nrom.init(.horizontal);
var prg_16k: [0x4000]u8 = undefined;
@memset(&prg_16k, 0x42);
prg_16k[0] = 0x11;
var ram: [0x2000]u8 = undefined;
@memset(&ram, 0);
// 16K PRG is mirrored at $8000 and $C000
try std.testing.expectEqual(@as(?u8, 0x11), nrom.cpuRead(0x8000, &prg_16k, &ram));
try std.testing.expectEqual(@as(?u8, 0x11), nrom.cpuRead(0xc000, &prg_16k, &ram));
}
+70 -120
View File
@@ -1,147 +1,97 @@
const std = @import("std");
const common = @import("../common.zig");
const Mirroring = common.Mirroring;
pub const Nrom = @import("nrom.zig");
pub const Uxrom = @import("uxrom.zig");
pub const Mmc1 = @import("mmc1.zig");
pub const Uxrom = @import("uxrom.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 ColorDreams = @import("color_dreams.zig");
pub const Unrom512 = @import("unrom512.zig");
pub const Bnrom = @import("bnrom.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 Mmc2 = @import("mmc2.zig");
pub const Mmc4 = @import("mmc4.zig");
pub const Action53 = @import("action53.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,
uxrom: Uxrom,
cnrom: Cnrom,
mmc3: Mmc3,
mmc4: Mmc4,
mmc5: Mmc5,
axrom: Axrom,
gxrom: Gxrom,
mmc2: Mmc2,
color_dreams: ColorDreams,
unrom512: Unrom512,
bnrom: Bnrom,
gxrom: Gxrom,
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,
mmc2: Mmc2,
mmc4: Mmc4,
action53: Action53,
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 {
pub fn cpuRead(self: *const Mapper, address: u16, prg_rom: []const u8, prg_ram: []const u8) ?u8 {
return switch (self.*) {
inline else => |*mapper| mapper.cpuMapRead(address),
inline else => |*m| m.cpuRead(address, prg_rom, prg_ram),
};
}
pub fn cpuWrite(
self: *Mapper,
address: u16,
value: u8,
) void {
pub fn cpuWrite(self: *Mapper, address: u16, value: u8, prg_ram: []u8) bool {
return switch (self.*) {
inline else => |*m| m.cpuWrite(address, value, prg_ram),
};
}
pub fn ppuRead(self: *const Mapper, address: u16, chr_rom: []const u8, chr_ram: []const u8, chr_is_ram: bool) ?u8 {
return switch (self.*) {
inline else => |*m| m.ppuRead(address, chr_rom, chr_ram, chr_is_ram),
};
}
pub fn ppuWrite(self: *Mapper, address: u16, value: u8, chr_ram: []u8, chr_is_ram: bool) bool {
return switch (self.*) {
inline else => |*m| m.ppuWrite(address, value, chr_ram, chr_is_ram),
};
}
pub fn mirroring(self: *const Mapper) Mirroring {
return switch (self.*) {
inline else => |*m| m.mirroring(),
};
}
pub fn irqLine(self: *const Mapper) bool {
return switch (self.*) {
inline else => |*m| m.irqLine(),
};
}
pub fn notifyPpuAddress(self: *Mapper, address: u16) 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 => {},
inline else => |*m| m.notifyPpuAddress(address),
}
}
};
test {
_ = Nrom;
_ = Mmc1;
_ = Uxrom;
_ = Cnrom;
_ = Mmc3;
_ = Axrom;
_ = ColorDreams;
_ = Unrom512;
_ = Bnrom;
_ = Gxrom;
_ = Camerica;
_ = Mmc2;
_ = Mmc4;
_ = Action53;
_ = Mapper180;
}
-9
View File
@@ -1,9 +0,0 @@
pub const Mirroring = enum {
horizontal,
vertical,
four_screen,
// Needed by later mappers.
single_screen_lower,
single_screen_upper,
};
+112
View File
@@ -0,0 +1,112 @@
const std = @import("std");
const common = @import("../common.zig");
const Mirroring = common.Mirroring;
// Mapper 30 (UNROM-512) - 16KB PRG bank (bits 0-4), 8KB CHR-RAM bank (bits 5-6), 1-screen mirroring (bit 7)
pub const Unrom512 = @This();
prg_bank: u8 = 0,
chr_bank: u8 = 0,
mirroring_mode: Mirroring,
supports_mirroring_control: bool,
pub fn init(initial_mirroring: Mirroring) Unrom512 {
return .{
.mirroring_mode = initial_mirroring,
.supports_mirroring_control = (initial_mirroring == .single_screen_lower or initial_mirroring == .single_screen_upper),
};
}
pub fn cpuRead(self: *const Unrom512, address: u16, prg_rom: []const u8, prg_ram: []const u8) ?u8 {
if (address >= 0x6000 and address <= 0x7fff) {
const offset = address - 0x6000;
if (offset < prg_ram.len) return prg_ram[offset];
return 0;
}
if (address >= 0x8000 and address <= 0xffff) {
const total_16k_banks = @max(1, prg_rom.len / 0x4000);
var bank: usize = 0;
if (address < 0xc000) {
bank = @as(usize, self.prg_bank) % total_16k_banks;
const offset = (bank * 0x4000) + (address - 0x8000);
if (offset < prg_rom.len) return prg_rom[offset];
} else {
bank = total_16k_banks - 1;
const offset = (bank * 0x4000) + (address - 0xc000);
if (offset < prg_rom.len) return prg_rom[offset];
}
return 0;
}
return null;
}
pub fn cpuWrite(self: *Unrom512, address: u16, value: u8, prg_ram: []u8) bool {
if (address >= 0x6000 and address <= 0x7fff) {
const offset = address - 0x6000;
if (offset < prg_ram.len) {
prg_ram[offset] = value;
return true;
}
}
if (address >= 0x8000 and address <= 0xffff) {
self.prg_bank = value & 0x1f;
self.chr_bank = (value >> 5) & 0x03;
if (self.supports_mirroring_control) {
self.mirroring_mode = if ((value & 0x80) != 0) .single_screen_upper else .single_screen_lower;
}
return true;
}
return false;
}
pub fn ppuRead(self: *const Unrom512, address: u16, chr_rom: []const u8, chr_ram: []const u8, chr_is_ram: bool) ?u8 {
if (address < 0x2000) {
if (chr_is_ram) {
const total_8k_banks = @max(1, chr_ram.len / 0x2000);
const bank = @as(usize, self.chr_bank) % total_8k_banks;
const offset = (bank * 0x2000) + address;
if (offset < chr_ram.len) return chr_ram[offset];
} else if (chr_rom.len > 0) {
const total_8k_banks = @max(1, chr_rom.len / 0x2000);
const bank = @as(usize, self.chr_bank) % total_8k_banks;
const offset = (bank * 0x2000) + address;
if (offset < chr_rom.len) return chr_rom[offset];
}
return 0;
}
return null;
}
pub fn ppuWrite(self: *Unrom512, address: u16, value: u8, chr_ram: []u8, chr_is_ram: bool) bool {
if (address < 0x2000 and chr_is_ram) {
const total_8k_banks = @max(1, chr_ram.len / 0x2000);
const bank = @as(usize, self.chr_bank) % total_8k_banks;
const offset = (bank * 0x2000) + address;
if (offset < chr_ram.len) {
chr_ram[offset] = value;
return true;
}
}
return false;
}
pub fn mirroring(self: *const Unrom512) Mirroring {
return self.mirroring_mode;
}
pub fn irqLine(_: *const Unrom512) bool {
return false;
}
pub fn notifyPpuAddress(_: *Unrom512, _: u16) void {}
test "unrom512 bank switching" {
var un = Unrom512.init(.single_screen_lower);
var ram: [0x2000]u8 = undefined;
// Write: PRG bank 5, CHR bank 2, Upper screen mirroring
_ = un.cpuWrite(0x8000, 0x80 | (2 << 5) | 5, &ram);
try std.testing.expectEqual(@as(u8, 5), un.prg_bank);
try std.testing.expectEqual(@as(u8, 2), un.chr_bank);
try std.testing.expectEqual(Mirroring.single_screen_upper, un.mirroring());
}
+64 -54
View File
@@ -1,73 +1,83 @@
const types = @import("types.zig");
const Mirroring = types.Mirroring;
const std = @import("std");
const common = @import("../common.zig");
const Mirroring = common.Mirroring;
const Uxrom = @This();
pub const Uxrom = @This();
prg_bank_select: u8 = 0,
prg_bank_count: usize,
prg_bank: u8 = 0,
mirroring_mode: Mirroring,
pub fn init(
prg_size: usize,
_mirroring: Mirroring,
) Uxrom {
return .{
.prg_bank_count = prg_size / 0x4000,
.mirroring_mode = _mirroring,
};
pub fn init(initial_mirroring: Mirroring) Uxrom {
return .{ .mirroring_mode = initial_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 cpuRead(self: *const Uxrom, address: u16, prg_rom: []const u8, prg_ram: []const u8) ?u8 {
if (address >= 0x6000 and address <= 0x7fff) {
const offset = address - 0x6000;
if (offset < prg_ram.len) return prg_ram[offset];
return 0;
}
}
pub fn cpuWrite(
self: *Uxrom,
address: u16,
value: u8,
) void {
if (address >= 0x8000) {
self.prg_bank_select = value;
if (address >= 0x8000 and address <= 0xffff) {
const total_16k_banks = @max(1, prg_rom.len / 0x4000);
var bank: usize = 0;
if (address < 0xc000) {
bank = @as(usize, self.prg_bank) % total_16k_banks;
const offset = (bank * 0x4000) + (address - 0x8000);
if (offset < prg_rom.len) return prg_rom[offset];
} else {
bank = total_16k_banks - 1;
const offset = (bank * 0x4000) + (address - 0xc000);
if (offset < prg_rom.len) return prg_rom[offset];
}
return 0;
}
return null;
}
pub fn ppuMapRead(
_: *const Uxrom,
address: u16,
) ?usize {
if (address >= 0x2000) return null;
return address;
pub fn cpuWrite(self: *Uxrom, address: u16, value: u8, prg_ram: []u8) bool {
if (address >= 0x6000 and address <= 0x7fff) {
const offset = address - 0x6000;
if (offset < prg_ram.len) {
prg_ram[offset] = value;
return true;
}
}
if (address >= 0x8000 and address <= 0xffff) {
// mask up to 32 banks (512KB) for UOROM / UNROM
self.prg_bank = value & 0x1f;
return true;
}
return false;
}
pub fn ppuMapWrite(
_: *const Uxrom,
address: u16,
) ?usize {
if (address >= 0x2000) return null;
return address;
pub fn ppuRead(_: *const Uxrom, address: u16, chr_rom: []const u8, chr_ram: []const u8, chr_is_ram: bool) ?u8 {
if (address < 0x2000) {
if (chr_is_ram) {
if (address < chr_ram.len) return chr_ram[address];
} else {
if (address < chr_rom.len) return chr_rom[address];
}
return 0;
}
return null;
}
pub fn mirroring(
self: *const Uxrom,
) Mirroring {
pub fn ppuWrite(_: *Uxrom, address: u16, value: u8, chr_ram: []u8, chr_is_ram: bool) bool {
if (address < 0x2000 and chr_is_ram) {
if (address < chr_ram.len) {
chr_ram[address] = value;
return true;
}
}
return false;
}
pub fn mirroring(self: *const Uxrom) Mirroring {
return self.mirroring_mode;
}
pub fn irqAsserted(_: *const Uxrom) bool {
pub fn irqLine(_: *const Uxrom) bool {
return false;
}
pub fn notifyPpuAddress(_: *Uxrom, _: u16) void {}
-106
View File
@@ -1,106 +0,0 @@
const types = @import("types.zig");
const Mirroring = types.Mirroring;
const Vrc1 = @This();
prg_bank0: u8 = 0,
prg_bank1: u8 = 0,
prg_bank2: u8 = 0,
chr_bank0: u8 = 0,
chr_bank1: u8 = 0,
mirroring_mode: Mirroring = .vertical,
prg_bank_count: usize,
chr_bank_count: usize,
pub fn init(
prg_size: usize,
chr_size: usize,
) Vrc1 {
return .{
.prg_bank_count = prg_size / 0x2000,
.chr_bank_count = if (chr_size > 0) chr_size / 0x1000 else 1,
};
}
pub fn cpuMapRead(
self: *const Vrc1,
address: u16,
) ?usize {
if (address < 0x8000) return null;
if (address < 0xA000) {
const offset = @as(usize, address - 0x8000);
const bank = @as(usize, self.prg_bank0) % self.prg_bank_count;
return bank * 0x2000 + offset;
} else if (address < 0xC000) {
const offset = @as(usize, address - 0xA000);
const bank = @as(usize, self.prg_bank1) % self.prg_bank_count;
return bank * 0x2000 + offset;
} else if (address < 0xE000) {
const offset = @as(usize, address - 0xC000);
const bank = @as(usize, self.prg_bank2) % self.prg_bank_count;
return bank * 0x2000 + offset;
} else {
const offset = @as(usize, address - 0xE000);
const fixed_bank = if (self.prg_bank_count > 0) self.prg_bank_count - 1 else 0;
return fixed_bank * 0x2000 + offset;
}
}
pub fn cpuWrite(
self: *Vrc1,
address: u16,
value: u8,
) void {
if (address < 0x8000) return;
switch (address & 0xF000) {
0x8000 => self.prg_bank0 = value & 0x0F,
0x9000 => {
self.mirroring_mode = if ((value & 1) != 0) .horizontal else .vertical;
self.chr_bank0 = (self.chr_bank0 & 0x0F) | ((value & 0x02) << 3);
self.chr_bank1 = (self.chr_bank1 & 0x0F) | ((value & 0x04) << 2);
},
0xA000 => self.prg_bank1 = value & 0x0F,
0xC000 => self.prg_bank2 = value & 0x0F,
0xE000 => self.chr_bank0 = (self.chr_bank0 & 0x10) | (value & 0x0F),
0xF000 => self.chr_bank1 = (self.chr_bank1 & 0x10) | (value & 0x0F),
else => {},
}
}
pub fn ppuMapRead(
self: *const Vrc1,
address: u16,
) ?usize {
if (address >= 0x2000) return null;
if (address < 0x1000) {
const bank = @as(usize, self.chr_bank0) % self.chr_bank_count;
return bank * 0x1000 + @as(usize, address);
} else {
const bank = @as(usize, self.chr_bank1) % self.chr_bank_count;
return bank * 0x1000 + @as(usize, address - 0x1000);
}
}
pub fn ppuMapWrite(
_: *const Vrc1,
address: u16,
) ?usize {
if (address >= 0x2000) return null;
return address;
}
pub fn mirroring(
self: *const Vrc1,
) Mirroring {
return self.mirroring_mode;
}
pub fn irqAsserted(_: *const Vrc1) bool {
return false;
}
-127
View File
@@ -1,127 +0,0 @@
const types = @import("types.zig");
const Mirroring = types.Mirroring;
const Vrc2 = @This();
prg_banks: [2]u8 = [_]u8{ 0, 1 },
chr_banks_lo: [8]u4 = [_]u4{0} ** 8,
chr_banks_hi: [8]u5 = [_]u5{0} ** 8,
mirroring_mode: Mirroring = .vertical,
mapper_id: u8,
shift_a: u3,
shift_b: u3,
prg_bank_count: usize,
chr_bank_count: usize,
pub fn init(
prg_size: usize,
chr_size: usize,
mapper_id: u8,
) Vrc2 {
const shift_a: u3 = if (mapper_id == 22) 1 else 0;
const shift_b: u3 = if (mapper_id == 22) 0 else 1;
return .{
.mapper_id = mapper_id,
.prg_bank_count = prg_size / 0x2000,
.chr_bank_count = if (chr_size > 0) chr_size / 0x0400 else 1,
.shift_a = shift_a,
.shift_b = shift_b,
};
}
pub fn cpuMapRead(
self: *const Vrc2,
address: u16,
) ?usize {
if (address < 0x8000) return null;
const offset = @as(usize, address & 0x1fff);
const last_bank = self.prg_bank_count - 1;
const second_last_bank = self.prg_bank_count - 2;
const r0 = @as(usize, self.prg_banks[0] & 0x1f) % self.prg_bank_count;
const r1 = @as(usize, self.prg_banks[1] & 0x1f) % self.prg_bank_count;
switch ((address - 0x8000) / 0x2000) {
0 => return r0 * 0x2000 + offset,
1 => return r1 * 0x2000 + offset,
2 => return second_last_bank * 0x2000 + offset,
3 => return last_bank * 0x2000 + offset,
else => unreachable,
}
}
pub fn cpuWrite(
self: *Vrc2,
address: u16,
value: u8,
) void {
if (address < 0x8000) return;
const reg_addr = address & 0xf000;
const a = @as(u1, @truncate((address >> self.shift_a) & 1));
const b = @as(u1, @truncate((address >> self.shift_b) & 1));
const line = (@as(u2, b) << 1) | a;
switch (reg_addr) {
0x8000 => self.prg_banks[0] = value & 0x1f,
0x9000 => {
self.mirroring_mode = if ((value & 0x01) != 0) .horizontal else .vertical;
},
0xa000 => self.prg_banks[1] = value & 0x1f,
0xb000, 0xc000, 0xd000, 0xe000 => {
const slot_base = ((reg_addr - 0xb000) / 0x1000) * 2;
const is_hi = (line & 2) != 0;
const is_odd = (line & 1) != 0;
const slot = slot_base + (if (is_odd) @as(usize, 1) else 0);
if (!is_hi) {
self.chr_banks_lo[slot] = @truncate(value & 0x0f);
} else {
self.chr_banks_hi[slot] = @truncate(value & 0x1f);
}
},
else => {},
}
}
pub fn ppuMapRead(
self: *const Vrc2,
address: u16,
) ?usize {
if (address >= 0x2000) return null;
const slot = address / 0x0400;
const offset = @as(usize, address & 0x03ff);
var bank_val = (@as(usize, self.chr_banks_hi[slot]) << 4) | @as(usize, self.chr_banks_lo[slot]);
if (self.mapper_id == 22) {
bank_val >>= 1;
}
const bank = bank_val % self.chr_bank_count;
return bank * 0x0400 + offset;
}
pub fn ppuMapWrite(
self: *const Vrc2,
address: u16,
) ?usize {
return self.ppuMapRead(address);
}
pub fn mirroring(
self: *const Vrc2,
) Mirroring {
return self.mirroring_mode;
}
pub fn irqAsserted(
_: *const Vrc2,
) bool {
return false;
}
-118
View File
@@ -1,118 +0,0 @@
const types = @import("types.zig");
const Mirroring = types.Mirroring;
const Vrc3 = @This();
prg_bank: u8 = 0,
prg_bank_count: usize,
mirroring_mode: Mirroring,
irq_latch: u16 = 0,
irq_counter: u16 = 0,
irq_enabled: bool = false,
irq_mode_8bit: bool = false,
irq_asserted_flag: bool = false,
pub fn init(
prg_size: usize,
_mirroring: Mirroring,
) Vrc3 {
return .{
.prg_bank_count = prg_size / 0x4000,
.mirroring_mode = _mirroring,
};
}
pub fn cpuMapRead(
self: *const Vrc3,
address: u16,
) ?usize {
if (address < 0x8000) return null;
if (address < 0xC000) {
const offset = @as(usize, address - 0x8000);
const bank = @as(usize, self.prg_bank) % self.prg_bank_count;
return bank * 0x4000 + offset;
} else {
const offset = @as(usize, address - 0xC000);
const fixed_bank = if (self.prg_bank_count > 0) self.prg_bank_count - 1 else 0;
return fixed_bank * 0x4000 + offset;
}
}
pub fn cpuWrite(
self: *Vrc3,
address: u16,
value: u8,
) void {
if (address < 0x8000) return;
switch (address & 0xF000) {
0x8000 => self.irq_latch = (self.irq_latch & 0xFFF0) | (value & 0x0F),
0x9000 => self.irq_latch = (self.irq_latch & 0xFF0F) | ((@as(u16, value) & 0x0F) << 4),
0xA000 => self.irq_latch = (self.irq_latch & 0xF0FF) | ((@as(u16, value) & 0x0F) << 8),
0xB000 => self.irq_latch = (self.irq_latch & 0x0FFF) | ((@as(u16, value) & 0x0F) << 12),
0xC000 => {
self.irq_enabled = (value & 0x02) != 0;
self.irq_mode_8bit = (value & 0x04) != 0;
self.irq_asserted_flag = false;
if (self.irq_enabled) {
self.irq_counter = self.irq_latch;
}
},
0xD000 => {
self.irq_asserted_flag = false;
},
0xF000 => {
self.prg_bank = value & 0x07;
},
else => {},
}
}
pub fn tickCycle(self: *Vrc3) void {
if (self.irq_enabled) {
if (self.irq_mode_8bit) {
const low = @as(u8, @truncate(self.irq_counter));
if (low == 0xFF) {
self.irq_counter = (self.irq_counter & 0xFF00) | @as(u16, @as(u8, @truncate(self.irq_latch)));
self.irq_asserted_flag = true;
} else {
self.irq_counter += 1;
}
} else {
if (self.irq_counter == 0xFFFF) {
self.irq_counter = self.irq_latch;
self.irq_asserted_flag = true;
} else {
self.irq_counter += 1;
}
}
}
}
pub fn ppuMapRead(
_: *const Vrc3,
address: u16,
) ?usize {
if (address >= 0x2000) return null;
return address;
}
pub fn ppuMapWrite(
_: *const Vrc3,
address: u16,
) ?usize {
if (address >= 0x2000) return null;
return address;
}
pub fn mirroring(
self: *const Vrc3,
) Mirroring {
return self.mirroring_mode;
}
pub fn irqAsserted(self: *const Vrc3) bool {
return self.irq_asserted_flag;
}
-179
View File
@@ -1,179 +0,0 @@
const types = @import("types.zig");
const Mirroring = types.Mirroring;
const Vrc4 = @This();
prg_banks: [2]u8 = [_]u8{ 0, 1 },
chr_banks_lo: [8]u4 = [_]u4{0} ** 8,
chr_banks_hi: [8]u5 = [_]u5{0} ** 8,
prg_mode: u1 = 0,
mirroring_mode: Mirroring = .vertical,
irq_reload: u8 = 0,
irq_counter: u8 = 0,
irq_enabled: bool = false,
irq_enable_on_ack: bool = false,
irq_mode: u1 = 0, // 0 = cycle, 1 = scanline
irq_prescaler: i16 = 341,
irq_assert: bool = false,
shift_a: u3,
shift_b: u3,
prg_bank_count: usize,
chr_bank_count: usize,
pub fn init(
prg_size: usize,
chr_size: usize,
mapper_id: u8,
) Vrc4 {
const shift_a: u3 = if (mapper_id == 21) 1 else 0;
const shift_b: u3 = if (mapper_id == 21) 2 else 2;
return .{
.prg_bank_count = prg_size / 0x2000,
.chr_bank_count = if (chr_size > 0) chr_size / 0x0400 else 1,
.shift_a = shift_a,
.shift_b = shift_b,
};
}
pub fn cpuMapRead(
self: *const Vrc4,
address: u16,
) ?usize {
if (address < 0x8000) return null;
const offset = @as(usize, address & 0x1fff);
const last_bank = self.prg_bank_count - 1;
const second_last_bank = self.prg_bank_count - 2;
const r0 = @as(usize, self.prg_banks[0] & 0x1f) % self.prg_bank_count;
const r1 = @as(usize, self.prg_banks[1] & 0x1f) % self.prg_bank_count;
const bank_idx = (address - 0x8000) / 0x2000;
var bank: usize = 0;
if (self.prg_mode == 0) {
switch (bank_idx) {
0 => bank = r0,
1 => bank = r1,
2 => bank = second_last_bank,
3 => bank = last_bank,
else => unreachable,
}
} else {
switch (bank_idx) {
0 => bank = second_last_bank,
1 => bank = r1,
2 => bank = r0,
3 => bank = last_bank,
else => unreachable,
}
}
return bank * 0x2000 + offset;
}
pub fn cpuWrite(
self: *Vrc4,
address: u16,
value: u8,
) void {
if (address < 0x8000) return;
const reg_addr = address & 0xf000;
const a = @as(u1, @truncate((address >> self.shift_a) & 1));
const b = @as(u1, @truncate((address >> self.shift_b) & 1));
const line = (@as(u2, b) << 1) | a;
switch (reg_addr) {
0x8000 => {
self.prg_banks[0] = value & 0x1f;
},
0x9000 => {
if (line == 0) {
switch (@as(u2, @truncate(value & 3))) {
0 => self.mirroring_mode = .vertical,
1 => self.mirroring_mode = .horizontal,
2 => self.mirroring_mode = .single_screen_lower,
3 => self.mirroring_mode = .single_screen_upper,
}
} else if (line == 1 or line == 2) {
self.prg_mode = @truncate((value >> 1) & 1);
}
},
0xa000 => {
self.prg_banks[1] = value & 0x1f;
},
0xb000, 0xc000, 0xd000, 0xe000 => {
const slot_base = ((reg_addr - 0xb000) / 0x1000) * 2;
const is_hi = (line & 2) != 0;
const is_odd = (line & 1) != 0;
const slot = slot_base + (if (is_odd) @as(usize, 1) else 0);
if (!is_hi) {
self.chr_banks_lo[slot] = @truncate(value & 0x0f);
} else {
self.chr_banks_hi[slot] = @truncate(value & 0x1f);
}
},
0xf000 => {
switch (line) {
0 => self.irq_reload = (self.irq_reload & 0xf0) | (value & 0x0f),
1 => self.irq_reload = (self.irq_reload & 0x0f) | ((value & 0x0f) << 4),
2 => {
self.irq_mode = @truncate((value >> 2) & 1);
self.irq_enabled = (value & 0x02) != 0;
self.irq_enable_on_ack = (value & 0x01) != 0;
if (self.irq_enabled) {
self.irq_counter = self.irq_reload;
self.irq_prescaler = 341;
}
self.irq_assert = false;
},
3 => {
self.irq_enabled = self.irq_enable_on_ack;
self.irq_assert = false;
},
}
},
else => {},
}
}
pub fn ppuMapRead(
self: *const Vrc4,
address: u16,
) ?usize {
if (address >= 0x2000) return null;
const slot = address / 0x0400;
const offset = @as(usize, address & 0x03ff);
const bank_val = (@as(usize, self.chr_banks_hi[slot]) << 4) | @as(usize, self.chr_banks_lo[slot]);
const bank = bank_val % self.chr_bank_count;
return bank * 0x0400 + offset;
}
pub fn ppuMapWrite(
self: *const Vrc4,
address: u16,
) ?usize {
return self.ppuMapRead(address);
}
pub fn mirroring(
self: *const Vrc4,
) Mirroring {
return self.mirroring_mode;
}
pub fn irqAsserted(
self: *const Vrc4,
) bool {
return self.irq_assert;
}
-152
View File
@@ -1,152 +0,0 @@
const types = @import("types.zig");
const Mirroring = types.Mirroring;
const Vrc6 = @This();
prg_banks: [2]u8 = [_]u8{ 0, 1 },
chr_banks: [8]u8 = [_]u8{ 0, 1, 2, 3, 4, 5, 6, 7 },
mirroring_mode: Mirroring = .vertical,
irq_reload: u8 = 0,
irq_counter: u8 = 0,
irq_enabled: bool = false,
irq_enable_on_ack: bool = false,
irq_mode: u1 = 0,
irq_assert: bool = false,
shift_a: u3,
shift_b: u3,
prg_bank_count: usize,
chr_bank_count: usize,
pub fn init(
prg_size: usize,
chr_size: usize,
mapper_id: u8,
) Vrc6 {
const shift_a: u3 = if (mapper_id == 26) 1 else 0;
const shift_b: u3 = if (mapper_id == 26) 0 else 1;
return .{
.prg_bank_count = prg_size / 0x2000,
.chr_bank_count = if (chr_size > 0) chr_size / 0x0400 else 1,
.shift_a = shift_a,
.shift_b = shift_b,
};
}
pub fn cpuMapRead(
self: *const Vrc6,
address: u16,
) ?usize {
if (address < 0x8000) return null;
const last_bank = self.prg_bank_count - 1;
if (address < 0xc000) {
const offset = @as(usize, address & 0x3fff);
const bank16 = @as(usize, self.prg_banks[0] & 0x0f) % (self.prg_bank_count / 2);
return bank16 * 0x4000 + offset;
} else if (address < 0xe000) {
const offset = @as(usize, address & 0x1fff);
const bank8 = @as(usize, self.prg_banks[1] & 0x1f) % self.prg_bank_count;
return bank8 * 0x2000 + offset;
} else {
const offset = @as(usize, address & 0x1fff);
return last_bank * 0x2000 + offset;
}
}
pub fn cpuWrite(
self: *Vrc6,
address: u16,
value: u8,
) void {
if (address < 0x8000) return;
const reg_addr = address & 0xf000;
const a = @as(u1, @truncate((address >> self.shift_a) & 1));
const b = @as(u1, @truncate((address >> self.shift_b) & 1));
const line = (@as(u2, b) << 1) | a;
switch (reg_addr) {
0x8000 => {
if (line == 0) self.prg_banks[0] = value & 0x0f;
},
0x9000...0xb000 => {
if (reg_addr == 0xb000 and line == 3) {
switch (@as(u2, @truncate((value >> 2) & 3))) {
0 => self.mirroring_mode = .vertical,
1 => self.mirroring_mode = .horizontal,
2 => self.mirroring_mode = .single_screen_lower,
3 => self.mirroring_mode = .single_screen_upper,
}
}
},
0xc000 => {
if (line == 0) self.prg_banks[1] = value & 0x1f;
},
0xd000 => {
const slot = @as(usize, line);
self.chr_banks[slot] = value;
},
0xe000 => {
const slot = 4 + @as(usize, line);
self.chr_banks[slot] = value;
},
0xf000 => {
switch (line) {
0 => self.irq_reload = value,
1 => {
self.irq_mode = @truncate((value >> 2) & 1);
self.irq_enabled = (value & 0x02) != 0;
self.irq_enable_on_ack = (value & 0x01) != 0;
if (self.irq_enabled) {
self.irq_counter = self.irq_reload;
}
self.irq_assert = false;
},
2 => {
self.irq_enabled = self.irq_enable_on_ack;
self.irq_assert = false;
},
else => {},
}
},
else => {},
}
}
pub fn ppuMapRead(
self: *const Vrc6,
address: u16,
) ?usize {
if (address >= 0x2000) return null;
const slot = address / 0x0400;
const offset = @as(usize, address & 0x03ff);
const bank = @as(usize, self.chr_banks[slot]) % self.chr_bank_count;
return bank * 0x0400 + offset;
}
pub fn ppuMapWrite(
self: *const Vrc6,
address: u16,
) ?usize {
return self.ppuMapRead(address);
}
pub fn mirroring(
self: *const Vrc6,
) Mirroring {
return self.mirroring_mode;
}
pub fn irqAsserted(
self: *const Vrc6,
) bool {
return self.irq_assert;
}
-145
View File
@@ -1,145 +0,0 @@
const types = @import("types.zig");
const Mirroring = types.Mirroring;
const Vrc7 = @This();
prg_banks: [3]u8 = [_]u8{ 0, 1, 2 },
chr_banks: [8]u8 = [_]u8{ 0, 1, 2, 3, 4, 5, 6, 7 },
mirroring_mode: Mirroring = .vertical,
irq_reload: u8 = 0,
irq_counter: u8 = 0,
irq_enabled: bool = false,
irq_enable_on_ack: bool = false,
irq_mode: u1 = 0,
irq_assert: bool = false,
prg_bank_count: usize,
chr_bank_count: usize,
pub fn init(
prg_size: usize,
chr_size: usize,
) Vrc7 {
return .{
.prg_bank_count = prg_size / 0x2000,
.chr_bank_count = if (chr_size > 0) chr_size / 0x0400 else 1,
};
}
pub fn cpuMapRead(
self: *const Vrc7,
address: u16,
) ?usize {
if (address < 0x8000) return null;
const offset = @as(usize, address & 0x1fff);
const last_bank = self.prg_bank_count - 1;
switch ((address - 0x8000) / 0x2000) {
0 => return (@as(usize, self.prg_banks[0] & 0x3f) % self.prg_bank_count) * 0x2000 + offset,
1 => return (@as(usize, self.prg_banks[1] & 0x3f) % self.prg_bank_count) * 0x2000 + offset,
2 => return (@as(usize, self.prg_banks[2] & 0x3f) % self.prg_bank_count) * 0x2000 + offset,
3 => return last_bank * 0x2000 + offset,
else => unreachable,
}
}
pub fn cpuWrite(
self: *Vrc7,
address: u16,
value: u8,
) void {
if (address < 0x8000) return;
const reg_addr = address & 0xf000;
const a0 = if ((address & 0x10) != 0 or (address & 0x08) != 0) @as(u2, 1) else 0;
const a1 = if ((address & 0x20) != 0) @as(u2, 2) else 0;
const line = a0 | a1;
switch (reg_addr) {
0x8000 => {
if (line == 0) self.prg_banks[0] = value & 0x3f;
if (line == 1 or line == 2) self.prg_banks[1] = value & 0x3f;
},
0x9000 => {
if (line == 0) self.prg_banks[2] = value & 0x3f;
if (line == 2) {
switch (@as(u2, @truncate(value & 3))) {
0 => self.mirroring_mode = .vertical,
1 => self.mirroring_mode = .horizontal,
2 => self.mirroring_mode = .single_screen_lower,
3 => self.mirroring_mode = .single_screen_upper,
}
}
},
0xa000 => {
if (line == 0) self.chr_banks[0] = value;
if (line == 1 or line == 2) self.chr_banks[1] = value;
},
0xb000 => {
if (line == 0) self.chr_banks[2] = value;
if (line == 1 or line == 2) self.chr_banks[3] = value;
},
0xc000 => {
if (line == 0) self.chr_banks[4] = value;
if (line == 1 or line == 2) self.chr_banks[5] = value;
},
0xd000 => {
if (line == 0) self.chr_banks[6] = value;
if (line == 1 or line == 2) self.chr_banks[7] = value;
},
0xe000 => {
if (line == 0) self.irq_reload = value;
if (line == 1 or line == 2) {
self.irq_mode = @truncate((value >> 2) & 1);
self.irq_enabled = (value & 0x02) != 0;
self.irq_enable_on_ack = (value & 0x01) != 0;
if (self.irq_enabled) {
self.irq_counter = self.irq_reload;
}
self.irq_assert = false;
}
},
0xf000 => {
if (line == 0) {
self.irq_enabled = self.irq_enable_on_ack;
self.irq_assert = false;
}
},
else => {},
}
}
pub fn ppuMapRead(
self: *const Vrc7,
address: u16,
) ?usize {
if (address >= 0x2000) return null;
const slot = address / 0x0400;
const offset = @as(usize, address & 0x03ff);
const bank = @as(usize, self.chr_banks[slot]) % self.chr_bank_count;
return bank * 0x0400 + offset;
}
pub fn ppuMapWrite(
self: *const Vrc7,
address: u16,
) ?usize {
return self.ppuMapRead(address);
}
pub fn mirroring(
self: *const Vrc7,
) Mirroring {
return self.mirroring_mode;
}
pub fn irqAsserted(
self: *const Vrc7,
) bool {
return self.irq_assert;
}
-779
View File
@@ -1,779 +0,0 @@
const std = @import("std");
const Cartridge = @import("cartridge.zig");
const mapper = @import("mapper/root.zig");
const Mirroring = mapper.Mirroring;
const Ppu = @This();
pub const width = 256;
pub const height = 240;
// NTSC 2C02 64-color system palette mapped directly to RGB565 format
pub const nes_ntsc_palette_rgb565: [64]u16 = .{
0x52AA, 0x00EE, 0x0892, 0x3011, 0x400C, 0x5806, 0x5020, 0x38C0,
0x2140, 0x09C0, 0x0200, 0x01E0, 0x0187, 0x0000, 0x0000, 0x0000,
0x9CB3, 0x0A78, 0x319D, 0x58FC, 0x88B6, 0xA08C, 0x9904, 0x79E0,
0x52C0, 0x2B80, 0x0BE0, 0x03A5, 0x032F, 0x0000, 0x0000, 0x0000,
0xEF7D, 0x4CDD, 0x7BFD, 0xB31D, 0xE2BD, 0xEACE, 0xEB4C, 0xD444,
0xA540, 0x7620, 0x4E84, 0x3E6D, 0x3DA9, 0x3DF7, 0x0000, 0x0000,
0xEF7D, 0xAE6D, 0xBD7D, 0xD59D, 0xED7D, 0xED7A, 0xEDB6, 0xE632,
0xCE8F, 0xB6EF, 0xAF12, 0x9F16, 0xA6BD, 0xA514, 0x0000, 0x0000,
};
framebuffer: [width * height]u16 =
[_]u16{0} ** (width * height),
/// Four-screen sized so the same structure
/// can handle that mode without allocation.
nametable: [0x1000]u8 =
[_]u8{0} ** 0x1000,
palette: [32]u8 =
[_]u8{0} ** 32,
oam: [256]u8 =
[_]u8{0} ** 256,
ctrl: u8 = 0,
mask: u8 = 0,
status: u8 = 0,
oam_addr: u8 = 0,
vram_addr: u16 = 0,
temp_addr: u16 = 0,
fine_x: u3 = 0,
write_latch: bool = false,
read_buffer: u8 = 0,
io_latch: u8 = 0,
dot: u16 = 0,
scanline: u16 = 261,
frame_number: u64 = 0,
frame_ready: bool = false,
// internal shift registers & tile latches for background rendering
bg_shift_pattern_lo: u16 = 0,
bg_shift_pattern_hi: u16 = 0,
bg_shift_attrib_lo: u16 = 0,
bg_shift_attrib_hi: u16 = 0,
next_tile_id: u8 = 0,
next_tile_attrib: u8 = 0,
next_tile_lsb: u8 = 0,
next_tile_msb: u8 = 0,
// secondary OAM scanline sprite evaluation buffer
sec_oam_pattern_lo: [8]u8 = [_]u8{0} ** 8,
sec_oam_pattern_hi: [8]u8 = [_]u8{0} ** 8,
sec_oam_attribute: [8]u8 = [_]u8{0} ** 8,
sec_oam_x: [8]u8 = [_]u8{0} ** 8,
sec_oam_is_zero: [8]bool = [_]bool{false} ** 8,
sec_oam_count: u8 = 0,
pub fn reset(
self: *Ppu,
) void {
self.ctrl = 0;
self.mask = 0;
self.status = 0;
self.oam_addr = 0;
self.vram_addr = 0;
self.temp_addr = 0;
self.fine_x = 0;
self.write_latch = false;
self.read_buffer = 0;
self.io_latch = 0;
self.dot = 0;
self.scanline = 261;
self.frame_ready = false;
self.bg_shift_pattern_lo = 0;
self.bg_shift_pattern_hi = 0;
self.bg_shift_attrib_lo = 0;
self.bg_shift_attrib_hi = 0;
self.next_tile_id = 0;
self.next_tile_attrib = 0;
self.next_tile_lsb = 0;
self.next_tile_msb = 0;
self.sec_oam_count = 0;
}
pub fn cpuRead(
self: *Ppu,
cartridge: *Cartridge,
register: u16,
) u8 {
const reg = register & 7;
const value: u8 = switch (reg) {
// PPUSTATUS
2 => blk: {
const result =
(self.status & 0xe0) |
(self.io_latch & 0x1f);
self.status &= ~@as(u8, 0x80);
self.write_latch = false;
break :blk result;
},
// OAMDATA
4 => self.oam[self.oam_addr],
// PPUDATA
7 => self.readData(cartridge),
else => self.io_latch,
};
self.io_latch = value;
return value;
}
pub fn cpuWrite(
self: *Ppu,
cartridge: *Cartridge,
register: u16,
value: u8,
) void {
const reg = register & 7;
self.io_latch = value;
switch (reg) {
// PPUCTRL
0 => {
self.ctrl = value;
self.temp_addr =
(self.temp_addr & 0xf3ff) |
(@as(u16, value & 0x03) << 10);
},
// PPUMASK
1 => {
self.mask = value;
},
// OAMADDR
3 => {
self.oam_addr = value;
},
// OAMDATA
4 => {
self.writeOamDma(value);
},
// PPUSCROLL
5 => {
if (!self.write_latch) {
self.fine_x =
@truncate(value & 7);
self.temp_addr =
(self.temp_addr & 0xffe0) |
@as(u16, value >> 3);
self.write_latch = true;
} else {
self.temp_addr =
(self.temp_addr & 0x8c1f) |
(@as(u16, value & 0x07) << 12) |
(@as(u16, value & 0xf8) << 2);
self.write_latch = false;
}
},
// PPUADDR
6 => {
if (!self.write_latch) {
self.temp_addr =
(self.temp_addr & 0x00ff) |
(@as(u16, value & 0x3f) << 8);
self.write_latch = true;
} else {
self.temp_addr =
(self.temp_addr & 0x7f00) |
@as(u16, value);
self.vram_addr =
self.temp_addr;
self.write_latch = false;
}
},
// PPUDATA
7 => {
self.writeMemory(
cartridge,
self.vram_addr,
value,
);
self.incrementVramAddress();
},
else => {},
}
}
pub fn writeOamDma(
self: *Ppu,
value: u8,
) void {
self.oam[self.oam_addr] = value;
self.oam_addr +%= 1;
}
pub fn tick(
self: *Ppu,
cartridge: *Cartridge,
) void {
const rendering_enabled = (self.mask & 0x18) != 0;
const is_visible_scanline = (self.scanline < 240);
const is_prerender_scanline = (self.scanline == 261);
// Start of VBlank.
if (self.scanline == 241 and
self.dot == 1)
{
self.status |= 0x80;
}
// Pre-render line.
if (is_prerender_scanline and
self.dot == 1)
{
// Clear: VBlank, sprite zero hit, sprite overflow
self.status &= ~@as(u8, 0xe0);
}
// --- Rendering Pipeline ---
if (rendering_enabled and (is_visible_scanline or is_prerender_scanline)) {
if ((self.dot >= 1 and self.dot <= 256) or (self.dot >= 321 and self.dot <= 336)) {
self.shiftBackgroundRegisters();
const cycle_in_tile = (self.dot - 1) % 8;
switch (cycle_in_tile) {
0 => {
self.loadShiftRegisters();
const nt_addr = 0x2000 | (self.vram_addr & 0x0fff);
self.next_tile_id = self.readMemory(cartridge, nt_addr);
},
2 => {
const attr_addr = 0x23c0 |
(self.vram_addr & 0x0c00) |
((self.vram_addr >> 4) & 0x38) |
((self.vram_addr >> 2) & 0x07);
const attr_byte = self.readMemory(cartridge, attr_addr);
const shift: u3 = @intCast(((self.vram_addr >> 4) & 4) | (self.vram_addr & 2));
self.next_tile_attrib = (attr_byte >> shift) & 0x03;
},
4 => {
const bg_table: u16 = if ((self.ctrl & 0x10) != 0) 0x1000 else 0x0000;
const fine_y = (self.vram_addr >> 12) & 7;
const pattern_addr = bg_table + (@as(u16, self.next_tile_id) << 4) + fine_y;
self.next_tile_lsb = self.readMemory(cartridge, pattern_addr);
},
6 => {
const bg_table: u16 = if ((self.ctrl & 0x10) != 0) 0x1000 else 0x0000;
const fine_y = (self.vram_addr >> 12) & 7;
const pattern_addr = bg_table + (@as(u16, self.next_tile_id) << 4) + fine_y + 8;
self.next_tile_msb = self.readMemory(cartridge, pattern_addr);
},
7 => {
self.incrementCoarseX();
},
else => {},
}
}
if (self.dot == 256) {
self.incrementY();
}
if (self.dot == 257) {
self.loadShiftRegisters();
self.copyHorizontal();
}
if (is_prerender_scanline and self.dot >= 280 and self.dot <= 304) {
self.copyVertical();
}
if (self.dot == 257 and is_visible_scanline) {
self.evaluateSprites(cartridge);
}
if (self.dot == 260 and is_visible_scanline) {
cartridge.handleScanline();
}
}
// --- Pixel Output ---
if (is_visible_scanline and self.dot >= 1 and self.dot <= 256) {
self.renderPixel(cartridge);
}
// --- Dot & Scanline Counters ---
self.dot += 1;
if (self.dot == 341) {
self.dot = 0;
self.scanline += 1;
if (self.scanline == 262) {
self.scanline = 0;
self.frame_number +%= 1;
self.frame_ready = true;
}
}
}
pub fn nmiAsserted(
self: *const Ppu,
) bool {
const nmi_enabled =
(self.ctrl & 0x80) != 0;
const in_vblank =
(self.status & 0x80) != 0;
return nmi_enabled and in_vblank;
}
fn shiftBackgroundRegisters(self: *Ppu) void {
if ((self.mask & 0x08) != 0) {
self.bg_shift_pattern_lo <<= 1;
self.bg_shift_pattern_hi <<= 1;
self.bg_shift_attrib_lo <<= 1;
self.bg_shift_attrib_hi <<= 1;
}
}
fn loadShiftRegisters(self: *Ppu) void {
self.bg_shift_pattern_lo = (self.bg_shift_pattern_lo & 0xff00) | self.next_tile_lsb;
self.bg_shift_pattern_hi = (self.bg_shift_pattern_hi & 0xff00) | self.next_tile_msb;
const attr_lo: u16 = if ((self.next_tile_attrib & 1) != 0) 0x00ff else 0x0000;
const attr_hi: u16 = if ((self.next_tile_attrib & 2) != 0) 0x00ff else 0x0000;
self.bg_shift_attrib_lo = (self.bg_shift_attrib_lo & 0xff00) | attr_lo;
self.bg_shift_attrib_hi = (self.bg_shift_attrib_hi & 0xff00) | attr_hi;
}
inline fn incrementCoarseX(self: *Ppu) void {
if ((self.vram_addr & 0x001f) == 31) {
self.vram_addr &= ~@as(u16, 0x001f);
self.vram_addr ^= 0x0400;
} else {
self.vram_addr += 1;
}
}
inline fn incrementY(self: *Ppu) void {
if ((self.vram_addr & 0x7000) != 0x7000) {
self.vram_addr += 0x1000;
} else {
self.vram_addr &= ~@as(u16, 0x7000);
var y = (self.vram_addr & 0x03e0) >> 5;
if (y == 29) {
y = 0;
self.vram_addr ^= 0x0800;
} else if (y == 31) {
y = 0;
} else {
y += 1;
}
self.vram_addr = (self.vram_addr & ~@as(u16, 0x03e0)) | (y << 5);
}
}
inline fn copyHorizontal(self: *Ppu) void {
self.vram_addr = (self.vram_addr & ~@as(u16, 0x041f)) | (self.temp_addr & 0x041f);
}
inline fn copyVertical(self: *Ppu) void {
self.vram_addr = (self.vram_addr & ~@as(u16, 0x7be0)) | (self.temp_addr & 0x7be0);
}
inline fn evaluateSprites(
self: *Ppu,
cartridge: *Cartridge,
) void {
self.sec_oam_count = 0;
const sprite_height: u16 = if ((self.ctrl & 0x20) != 0) 16 else 8;
var oam_idx: usize = 0;
var i: usize = 0;
while (i < 64) : ({
i += 1;
oam_idx += 4;
}) {
const sprite_y = @as(u16, self.oam[oam_idx]);
const diff = @as(i16, @intCast(self.scanline)) - @as(i16, @intCast(sprite_y));
if (diff >= 0 and diff < sprite_height) {
if (self.sec_oam_count < 8) {
const idx = self.sec_oam_count;
const tile = self.oam[oam_idx + 1];
const attr = self.oam[oam_idx + 2];
const x = self.oam[oam_idx + 3];
var row: u16 = @intCast(diff);
// Vertical flip
if ((attr & 0x80) != 0) {
row = (sprite_height - 1) - row;
}
var pattern_addr: u16 = 0;
if (sprite_height == 8) {
const pattern_table: u16 = if ((self.ctrl & 0x08) != 0) 0x1000 else 0x0000;
pattern_addr = pattern_table + (@as(u16, tile) << 4) + row;
} else {
// 8x16 mode
const pattern_table: u16 = if ((tile & 1) != 0) 0x1000 else 0x0000;
const tile_index: u16 = tile & 0xfe;
if (row < 8) {
pattern_addr = pattern_table + (tile_index << 4) + row;
} else {
pattern_addr = pattern_table + ((tile_index + 1) << 4) + (row - 8);
}
}
var pat_lo = self.readMemory(cartridge, pattern_addr);
var pat_hi = self.readMemory(cartridge, pattern_addr + 8);
// Horizontal flip
if ((attr & 0x40) != 0) {
pat_lo = @bitReverse(pat_lo);
pat_hi = @bitReverse(pat_hi);
}
self.sec_oam_pattern_lo[idx] = pat_lo;
self.sec_oam_pattern_hi[idx] = pat_hi;
self.sec_oam_attribute[idx] = attr;
self.sec_oam_x[idx] = x;
self.sec_oam_is_zero[idx] = (i == 0);
self.sec_oam_count += 1;
} else {
// Sprite overflow
self.status |= 0x20;
break;
}
}
}
}
fn renderPixel(
self: *Ppu,
_: *Cartridge,
) void {
const x = self.dot - 1;
var bg_pixel: u8 = 0;
var bg_palette: u8 = 0;
if ((self.mask & 0x08) != 0 and (x >= 8 or (self.mask & 0x02) != 0)) {
const bit_shift: u4 = 15 - @as(u4, self.fine_x);
const p1 = @as(u8, @truncate((self.bg_shift_pattern_lo >> bit_shift) & 1));
const p2 = @as(u8, @truncate((self.bg_shift_pattern_hi >> bit_shift) & 1));
bg_pixel = (p2 << 1) | p1;
const a1 = @as(u8, @truncate((self.bg_shift_attrib_lo >> bit_shift) & 1));
const a2 = @as(u8, @truncate((self.bg_shift_attrib_hi >> bit_shift) & 1));
bg_palette = (a2 << 1) | a1;
}
var fg_pixel: u8 = 0;
var fg_palette: u8 = 0;
var fg_priority: bool = false;
var is_sprite_zero: bool = false;
if (self.sec_oam_count > 0 and (self.mask & 0x10) != 0 and (x >= 8 or (self.mask & 0x04) != 0)) {
for (0..self.sec_oam_count) |i| {
const spr_x = self.sec_oam_x[i];
if (x >= spr_x and x < spr_x + 8) {
const offset: u3 = @intCast(x - spr_x);
const shift: u3 = 7 - offset;
const p1 = (self.sec_oam_pattern_lo[i] >> shift) & 1;
const p2 = (self.sec_oam_pattern_hi[i] >> shift) & 1;
const pixel = (p2 << 1) | p1;
if (pixel != 0) {
fg_pixel = pixel;
fg_palette = 4 + (self.sec_oam_attribute[i] & 0x03);
fg_priority = (self.sec_oam_attribute[i] & 0x20) != 0;
is_sprite_zero = self.sec_oam_is_zero[i];
break;
}
}
}
}
var final_palette_entry: u16 = 0;
if (bg_pixel == 0 and fg_pixel == 0) {
final_palette_entry = 0x00;
} else if (bg_pixel == 0 and fg_pixel != 0) {
final_palette_entry = @as(u16, fg_palette) * 4 + fg_pixel;
} else if (bg_pixel != 0 and fg_pixel == 0) {
final_palette_entry = @as(u16, bg_palette) * 4 + bg_pixel;
} else {
// Both bg and fg pixels present
if (is_sprite_zero and x < 255 and (self.mask & 0x18) == 0x18) {
self.status |= 0x40; // Sprite 0 hit
}
if (fg_priority) {
final_palette_entry = @as(u16, bg_palette) * 4 + bg_pixel;
} else {
final_palette_entry = @as(u16, fg_palette) * 4 + fg_pixel;
}
}
var color_idx = self.palette[mapPalette(0x3f00 + final_palette_entry)] & 0x3f;
if ((self.mask & 0x01) != 0) {
color_idx &= 0x30;
}
var pixel_rgb = nes_ntsc_palette_rgb565[color_idx];
const emphasis = (self.mask >> 5) & 7;
if (emphasis != 0) {
var r = (pixel_rgb >> 11) & 0x1F;
var g = (pixel_rgb >> 5) & 0x3F;
var b = pixel_rgb & 0x1F;
// Red emphasis (bit 5)
if ((emphasis & 1) != 0) {
g = (g * 3) / 4;
b = (b * 3) / 4;
}
// Green emphasis (bit 6)
if ((emphasis & 2) != 0) {
r = (r * 3) / 4;
b = (b * 3) / 4;
}
// Blue emphasis (bit 7)
if ((emphasis & 4) != 0) {
r = (r * 3) / 4;
g = (g * 3) / 4;
}
pixel_rgb = (@as(u16, r) << 11) | (@as(u16, g) << 5) | @as(u16, b);
}
self.framebuffer[self.scanline * 256 + x] = pixel_rgb;
}
fn incrementVramAddress(
self: *Ppu,
) void {
const increment: u16 =
if ((self.ctrl & 0x04) != 0)
32
else
1;
self.vram_addr =
(self.vram_addr +% increment) &
0x3fff;
}
fn readData(
self: *Ppu,
cartridge: *Cartridge,
) u8 {
const address =
self.vram_addr & 0x3fff;
const value: u8 =
if (address < 0x3f00) blk: {
const previous =
self.read_buffer;
self.read_buffer =
self.readMemory(
cartridge,
address,
);
break :blk previous;
} else blk: {
const result =
self.readMemory(
cartridge,
address,
);
// Palette reads aren't delayed, but
// still update the internal buffer.
self.read_buffer =
self.readMemory(
cartridge,
address - 0x1000,
);
break :blk result;
};
self.incrementVramAddress();
return value;
}
inline fn readMemory(
self: *Ppu,
cartridge: *Cartridge,
address_: u16,
) u8 {
const address =
address_ & 0x3fff;
return switch (address) {
0x0000...0x1fff => cartridge.ppuRead(address),
0x2000...0x3eff => self.nametable[
self.mapNametable(
cartridge.mirroring(),
address,
)
],
0x3f00...0x3fff => self.palette[
mapPalette(address)
],
else => unreachable,
};
}
inline fn writeMemory(
self: *Ppu,
cartridge: *Cartridge,
address_: u16,
value: u8,
) void {
const address =
address_ & 0x3fff;
switch (address) {
0x0000...0x1fff => cartridge.ppuWrite(
address,
value,
),
0x2000...0x3eff => self.nametable[
self.mapNametable(
cartridge.mirroring(),
address,
)
] = value,
0x3f00...0x3fff => self.palette[
mapPalette(address)
] = value,
else => unreachable,
}
}
inline fn mapNametable(
_: *const Ppu,
mirroring: Mirroring,
address: u16,
) usize {
const relative =
(@as(usize, address) - 0x2000) &
0x0fff;
const table =
relative >> 10;
const offset =
relative & 0x3ff;
const mapped_table: usize =
switch (mirroring) {
.vertical => switch (table) {
0, 2 => 0,
1, 3 => 1,
else => unreachable,
},
.horizontal => switch (table) {
0, 1 => 0,
2, 3 => 1,
else => unreachable,
},
.single_screen_lower => 0,
.single_screen_upper => 1,
.four_screen => table,
};
return mapped_table * 0x400 +
offset;
}
inline fn mapPalette(
address: u16,
) usize {
var index: usize =
(@as(usize, address) - 0x3f00) &
0x1f;
// Universal background color mirrors.
switch (index) {
0x10 => index = 0x00,
0x14 => index = 0x04,
0x18 => index = 0x08,
0x1c => index = 0x0c,
else => {},
}
return index;
}
test "NES PPU - Palette Mirroring & NTSC Palette LUT" {
try std.testing.expectEqual(@as(usize, 0), mapPalette(0x3F00));
try std.testing.expectEqual(@as(usize, 0), mapPalette(0x3F10));
try std.testing.expectEqual(@as(usize, 4), mapPalette(0x3F04));
try std.testing.expectEqual(@as(usize, 4), mapPalette(0x3F14));
// Verify system palette RGB565 LUT contains non-zero color constants
try std.testing.expectEqual(@as(u16, 0x52AA), nes_ntsc_palette_rgb565[0]);
try std.testing.expectEqual(@as(u16, 0xEF7D), nes_ntsc_palette_rgb565[0x20]);
}
test "NES PPU - Scroll Address Increments" {
var ppu = Ppu{};
ppu.reset();
ppu.vram_addr = 0x001F; // Coarse X = 31
ppu.incrementCoarseX();
try std.testing.expectEqual(@as(u16, 0x0400), ppu.vram_addr); // Coarse X = 0, switched nametable X
ppu.vram_addr = 0x73A0; // Coarse Y = 29, Fine Y = 7
ppu.incrementY();
try std.testing.expectEqual(@as(u16, 0x0800), ppu.vram_addr); // Coarse Y = 0, Fine Y = 0, switched nametable Y
}
+95
View File
@@ -0,0 +1,95 @@
// PPUCTRL
//
// VPHB SINN
pub const Control = @This();
raw: u8 = 0,
pub fn baseNametable(self: Control) u2 {
return @truncate(self.raw & 0x03);
}
pub fn baseNametableAddress(self: Control) u16 {
return 0x2000 +
(@as(u16, self.baseNametable()) * 0x400);
}
pub fn vramIncrement(self: Control) u8 {
return if ((self.raw & 0x04) != 0)
32
else
1;
}
pub fn sprite8x8PatternAddress(self: Control) u16 {
return if ((self.raw & 0x08) != 0)
0x1000
else
0x0000;
}
/// Computes pattern table address for a sprite tile (supports both 8x8 and 8x16 modes).
pub fn spritePatternAddressForTile(self: Control, tile: u8, sprite_y_offset: u4) u16 {
if (self.spriteHeight() == 8) {
return self.sprite8x8PatternAddress() | (@as(u16, tile) << 4) | (sprite_y_offset & 7);
} else {
const bank: u16 = @as(u16, tile & 1) << 12;
var tile_index: u16 = tile & 0xfe;
if (sprite_y_offset >= 8) {
tile_index += 1;
}
return bank | (tile_index << 4) | (sprite_y_offset & 7);
}
}
pub fn backgroundPatternAddress(self: Control) u16 {
return if ((self.raw & 0x10) != 0)
0x1000
else
0x0000;
}
pub fn spriteHeight(self: Control) u8 {
return if ((self.raw & 0x20) != 0)
16
else
8;
}
pub fn masterSlave(self: Control) bool {
return (self.raw & 0x40) != 0;
}
pub fn nmiEnabled(self: Control) bool {
return (self.raw & 0x80) != 0;
}
const std = @import("std");
test "8x8 sprite pattern address" {
const c0: Control = .{ .raw = 0x00 }; // 8x8, pattern table 0
try std.testing.expectEqual(@as(u16, 0x0000), c0.sprite8x8PatternAddress());
try std.testing.expectEqual(@as(u16, 0x0143), c0.spritePatternAddressForTile(0x14, 3));
const c1: Control = .{ .raw = 0x08 }; // 8x8, pattern table 1 ($1000)
try std.testing.expectEqual(@as(u16, 0x1000), c1.sprite8x8PatternAddress());
try std.testing.expectEqual(@as(u16, 0x1143), c1.spritePatternAddressForTile(0x14, 3));
}
test "8x16 sprite pattern address ignores bit 3 and uses tile bit 0" {
const c: Control = .{ .raw = 0x28 }; // 8x16, bit 3 set (ignored!)
try std.testing.expectEqual(@as(u8, 16), c.spriteHeight());
// Tile 0x20: bit 0 is 0 -> bank 0 ($0000). Top half (offset 0..7).
try std.testing.expectEqual(@as(u16, 0x0203), c.spritePatternAddressForTile(0x20, 3));
// Tile 0x20: Bottom half (offset 8..15) -> tile index becomes 0x21!
try std.testing.expectEqual(@as(u16, 0x0212), c.spritePatternAddressForTile(0x20, 10));
// Tile 0x25: bit 0 is 1 -> bank 1 ($1000). Tile index & 0xFE = 0x24. Top half.
try std.testing.expectEqual(@as(u16, 0x1245), c.spritePatternAddressForTile(0x25, 5));
// Tile 0x25: Bottom half -> tile index & 0xFE + 1 = 0x25.
try std.testing.expectEqual(@as(u16, 0x1254), c.spritePatternAddressForTile(0x25, 12));
}
+76
View File
@@ -0,0 +1,76 @@
// PPUMASK
//
// BGRs bMmG
pub const Mask = @This();
raw: u8 = 0,
pub fn grayscale(self: Mask) bool {
return (self.raw & 0x01) != 0;
}
pub fn showBackgroundLeft(self: Mask) bool {
return (self.raw & 0x02) != 0;
}
pub fn showSpritesLeft(self: Mask) bool {
return (self.raw & 0x04) != 0;
}
pub fn showBackground(self: Mask) bool {
return (self.raw & 0x08) != 0;
}
pub fn showSprites(self: Mask) bool {
return (self.raw & 0x10) != 0;
}
pub fn renderingEnabled(self: Mask) bool {
return self.showBackground() or self.showSprites();
}
pub fn emphasizeRed(self: Mask) bool {
return (self.raw & 0x20) != 0;
}
pub fn emphasizeGreen(self: Mask) bool {
return (self.raw & 0x40) != 0;
}
pub fn emphasizeBlue(self: Mask) bool {
return (self.raw & 0x80) != 0;
}
pub fn isEmphasizeRed(self: Mask, is_pal: bool) bool {
return if (is_pal) (self.raw & 0x40) != 0 else (self.raw & 0x20) != 0;
}
pub fn isEmphasizeGreen(self: Mask, is_pal: bool) bool {
return if (is_pal) (self.raw & 0x20) != 0 else (self.raw & 0x40) != 0;
}
pub fn isEmphasizeBlue(self: Mask, _: bool) bool {
return (self.raw & 0x80) != 0;
}
pub fn emphasis(self: Mask) u3 {
return @truncate(self.raw >> 5);
}
const std = @import("std");
test "mask region-aware color emphasis" {
// Bit 5 set, Bit 6 clear
const m1: Mask = .{ .raw = 0x20 };
try std.testing.expect(m1.isEmphasizeRed(false)); // NTSC: bit 5 is Red
try std.testing.expect(!m1.isEmphasizeGreen(false));
try std.testing.expect(!m1.isEmphasizeRed(true)); // PAL: bit 5 is Green!
try std.testing.expect(m1.isEmphasizeGreen(true));
// Bit 6 set, Bit 5 clear
const m2: Mask = .{ .raw = 0x40 };
try std.testing.expect(!m2.isEmphasizeRed(false));
try std.testing.expect(m2.isEmphasizeGreen(false)); // NTSC: bit 6 is Green
try std.testing.expect(m2.isEmphasizeRed(true)); // PAL: bit 6 is Red!
try std.testing.expect(!m2.isEmphasizeGreen(true));
}
+73
View File
@@ -0,0 +1,73 @@
const std = @import("std");
pub const Palette = @This();
ram: [32]u8 = [_]u8{0} ** 32,
pub fn init() Palette {
return .{};
}
pub fn reset(self: *Palette) void {
@memset(&self.ram, 0);
}
/// Mirrors addresses $3F00..$3FFF down to the 32 physical palette entries,
/// applying special mirrors for $10, $14, $18, $1C to $00, $04, $08, $0C.
pub inline fn mirrorAddress(address: u16) u5 {
var index: u5 = @truncate(address & 0x1f);
if ((index & 0x13) == 0x10) {
index &= 0x0f;
}
return index;
}
pub fn read(self: *const Palette, address: u16) u8 {
return self.ram[mirrorAddress(address)];
}
pub fn write(self: *Palette, address: u16, value: u8) void {
self.ram[mirrorAddress(address)] = value & 0x3f;
}
/// Canonical 64-color 2C02 NTSC palette (RGBA8888).
pub const default_palette = [64]u32{
0x666666FF, 0x002A88FF, 0x1412A7FF, 0x3B00A4FF, 0x5C007EFF, 0x6E0040FF, 0x6C0600FF, 0x561D00FF,
0x333500FF, 0x0B4800FF, 0x005200FF, 0x004F08FF, 0x00404DFF, 0x000000FF, 0x000000FF, 0x000000FF,
0xADADADFF, 0x155FD9FF, 0x4240FFFF, 0x7527FEFF, 0xA01ACCFF, 0xB71E7BFF, 0xB53120FF, 0x994E00FF,
0x6B6D00FF, 0x388700FF, 0x0C9300FF, 0x008F32FF, 0x007C8DFF, 0x000000FF, 0x000000FF, 0x000000FF,
0xFFFFFFFF, 0x64B0FFFF, 0x9290FFFF, 0xC676FFFF, 0xF36AFFFF, 0xFE6ECCFF, 0xFE8170FF, 0xEA9E22FF,
0xBCBE00FF, 0x88D800FF, 0x5CE430FF, 0x45E082FF, 0x48CDDEFF, 0x4F4F4FFF, 0x000000FF, 0x000000FF,
0xFFFFFFFF, 0xC0E0FFFF, 0xD3D2FFFF, 0xE8C1FFFF, 0xFBC0FFFF, 0xFEC2EFFF, 0xFECAC4FF, 0xF6D5A1FF,
0xE3DF9EFF, 0xCEEB9EFF, 0xBCF3B9FF, 0xB2F1D4FF, 0xB4EBF8FF, 0xB8B8B8FF, 0x000000FF, 0x000000FF,
};
test "palette mirroring and special backdrop mirrors" {
var pal = Palette.init();
// Write backdrop color at $3F00
pal.write(0x3F00, 0x0F);
try std.testing.expectEqual(@as(u8, 0x0F), pal.read(0x3F00));
// Reading $3F10 should mirror $3F00!
try std.testing.expectEqual(@as(u8, 0x0F), pal.read(0x3F10));
// Writing to $3F10 mirrors back to $3F00
pal.write(0x3F10, 0x30);
try std.testing.expectEqual(@as(u8, 0x30), pal.read(0x3F00));
try std.testing.expectEqual(@as(u8, 0x30), pal.read(0x3F10));
// Test mirrors $3F14 -> $3F04, $3F18 -> $3F08, $3F1C -> $3F0C
pal.write(0x3F04, 0x15);
try std.testing.expectEqual(@as(u8, 0x15), pal.read(0x3F14));
pal.write(0x3F08, 0x27);
try std.testing.expectEqual(@as(u8, 0x27), pal.read(0x3F18));
pal.write(0x3F0C, 0x39);
try std.testing.expectEqual(@as(u8, 0x39), pal.read(0x3F1C));
// Values are clamped to 6 bits
pal.write(0x3F01, 0xFF);
try std.testing.expectEqual(@as(u8, 0x3F), pal.read(0x3F01));
}
+344
View File
@@ -0,0 +1,344 @@
// PPU Registers ($2000-$2007)
const std = @import("std");
const Control = @import("control.zig");
const Mask = @import("mask.zig");
const common = @import("../common.zig");
pub const Registers = @This();
control: Control = .{},
mask: Mask = .{},
pending_mask: Mask = .{},
mask_delay: u3 = 0,
status: u8 = 0,
oam_addr: u8 = 0,
oam: [256]u8 = [_]u8{0} ** 256,
v: u15 = 0,
t: u15 = 0,
x: u3 = 0,
w: bool = false,
read_buffer: u8 = 0,
io_bus: u8 = 0,
pending_v: u15 = 0,
v_commit_delay: u3 = 0,
suppress_writes_cycles: u32 = 0,
pub fn init() Registers {
return .{};
}
pub fn power(self: *Registers) void {
self.powerRegion(.ntsc);
}
pub fn powerRegion(self: *Registers, _: common.Region) void {
self.control.raw = 0;
self.mask.raw = 0;
self.pending_mask.raw = 0;
self.mask_delay = 0;
self.status = 0;
self.oam_addr = 0;
self.v = 0;
self.t = 0;
self.x = 0;
self.w = false;
self.read_buffer = 0;
self.io_bus = 0;
self.pending_v = 0;
self.v_commit_delay = 0;
self.suppress_writes_cycles = 29658;
}
pub fn reset(self: *Registers) void {
self.resetRegion(.ntsc);
}
pub fn resetRegion(self: *Registers, _: common.Region) void {
self.control.raw = 0;
self.mask.raw = 0;
self.pending_mask.raw = 0;
self.mask_delay = 0;
self.w = false;
self.read_buffer = 0;
}
pub fn tickCpuCycle(self: *Registers) void {
if (self.suppress_writes_cycles > 0) {
self.suppress_writes_cycles -= 1;
}
}
pub fn clockDot(self: *Registers) void {
if (self.mask_delay > 0) {
self.mask_delay -= 1;
if (self.mask_delay == 0) {
self.mask = self.pending_mask;
}
}
if (self.v_commit_delay > 0) {
self.v_commit_delay -= 1;
if (self.v_commit_delay == 0) {
self.v = self.pending_v;
}
}
}
pub fn vblank(self: *const Registers) bool {
return (self.status & 0x80) != 0;
}
pub fn setVblank(self: *Registers, val: bool) void {
if (val) {
self.status |= 0x80;
} else {
self.status &= ~@as(u8, 0x80);
}
}
pub fn clearRenderingFlags(self: *Registers) void {
self.status &= ~@as(u8, 0xe0);
}
pub fn setSpriteZeroHit(self: *Registers, val: bool) void {
if (val) self.status |= 0x40 else self.status &= ~@as(u8, 0x40);
}
pub fn setSpriteOverflow(self: *Registers, val: bool) void {
if (val) self.status |= 0x20 else self.status &= ~@as(u8, 0x20);
}
pub fn spriteHit(self: *const Registers) bool {
return (self.status & 0x40) != 0;
}
pub fn nmiLine(self: *const Registers) bool {
return self.control.nmiEnabled() and self.vblank();
}
pub fn takeNmi(self: *Registers) bool {
if (self.nmiLine()) {
return true;
}
return false;
}
pub fn currentVramAddress(self: *const Registers) u16 {
return self.v;
}
pub fn copyVertical(self: *Registers) void {
self.v = (self.v & ~@as(u15, 0x7be0)) | (self.t & 0x7be0);
}
pub fn copyHorizontal(self: *Registers) void {
self.v = (self.v & ~@as(u15, 0x041f)) | (self.t & 0x041f);
}
pub fn incrementCoarseX(self: *Registers) void {
if ((self.v & 0x001f) == 31) {
self.v &= ~@as(u15, 0x001f);
self.v ^= 0x0400;
} else {
self.v += 1;
}
}
pub fn incrementY(self: *Registers) void {
if ((self.v & 0x7000) != 0x7000) {
self.v += 0x1000;
} else {
self.v &= ~@as(u15, 0x7000);
var y = (self.v & 0x03e0) >> 5;
if (y == 29) {
y = 0;
self.v ^= 0x0800;
} else if (y == 31) {
y = 0;
} else {
y += 1;
}
self.v = (self.v & ~@as(u15, 0x03e0)) | (y << 5);
}
}
pub fn cpuRead(self: *Registers, bus: anytype, address: u16, rendering: bool) ?u8 {
_ = rendering;
const reg = address & 7;
return switch (reg) {
2 => blk: {
const res = (self.status & 0xe0) | (self.io_bus & 0x1f);
self.status &= ~@as(u8, 0x80);
self.w = false;
self.io_bus = res;
break :blk res;
},
4 => blk: {
const val = self.oam[self.oam_addr];
self.io_bus = val;
break :blk val;
},
7 => blk: {
const addr = self.v & 0x3fff;
var val: u8 = undefined;
if (addr >= 0x3f00) {
val = (self.io_bus & 0xc0) | (bus.read(addr) & 0x3f);
self.read_buffer = bus.read(addr - 0x1000);
} else {
val = self.read_buffer;
self.read_buffer = bus.read(addr);
}
self.v = (self.v +% self.control.vramIncrement()) & 0x3fff;
self.io_bus = val;
break :blk val;
},
else => self.io_bus,
};
}
pub fn cpuWrite(self: *Registers, bus: anytype, address: u16, value: u8, rendering: bool) bool {
_ = rendering;
self.io_bus = value;
const reg = address & 7;
const suppress = self.suppress_writes_cycles > 0;
if (suppress and (reg == 0 or reg == 1 or reg == 5 or reg == 6)) {
return true;
}
switch (reg) {
0 => {
const old_nmi = self.nmiLine();
self.control.raw = value;
self.t = (self.t & 0x73ff) | (@as(u15, value & 3) << 10);
if (!old_nmi and self.nmiLine()) {
// Trigger immediate NMI
}
},
1 => {
self.pending_mask.raw = value;
self.mask_delay = 4;
},
3 => self.oam_addr = value,
4 => {
var val = value;
if ((self.oam_addr & 3) == 2) {
val &= 0xe3; // mask unused bits
}
self.oam[self.oam_addr] = val;
self.oam_addr +%= 1;
},
5 => {
if (!self.w) {
self.t = (self.t & 0x7fe0) | (value >> 3);
self.x = @truncate(value & 7);
self.w = true;
} else {
self.t = (self.t & 0x0c1f) | (@as(u15, value & 7) << 12) | (@as(u15, value & 0xf8) << 2);
self.w = false;
}
},
6 => {
if (!self.w) {
self.t = (self.t & 0x00ff) | (@as(u15, value & 0x3f) << 8);
self.w = true;
} else {
self.t = (self.t & 0x7f00) | value;
self.pending_v = self.t;
self.v_commit_delay = 2;
self.w = false;
}
},
7 => {
const addr = self.v & 0x3fff;
bus.write(addr, value);
self.v = (self.v +% self.control.vramIncrement()) & 0x3fff;
},
else => {},
}
return true;
}
// consolidated PPU registers test suite
pub const TestBus = struct {
memory: [0x10000]u8 = [_]u8{0} ** 0x10000,
pub fn read(self: *TestBus, address: u16) u8 {
return self.memory[address];
}
pub fn write(self: *TestBus, address: u16, value: u8) void {
self.memory[address] = value;
}
};
test "ppu registers $2000-$2007 control, scroll, address and buffered reads" {
var regs = Registers.init();
var bus = TestBus{};
// $2000 & NMI
_ = regs.cpuWrite(&bus, 0x2000, 0b0000_0011, false);
try std.testing.expectEqual(@as(u15, 0x0c00), regs.t & 0x0c00);
regs.setVblank(true);
_ = regs.cpuWrite(&bus, 0x2000, 0x80, false);
try std.testing.expect(regs.takeNmi());
// $2002 status & latch reset
regs.status = 0xe0;
regs.io_bus = 0x1b;
regs.w = true;
try std.testing.expectEqual(@as(u8, 0xfb), regs.cpuRead(&bus, 0x2002, false).?);
try std.testing.expect(!regs.vblank() and !regs.w);
// $2005 coarse & fine scroll
regs = Registers.init();
_ = regs.cpuWrite(&bus, 0x2005, 0b10101_011, false);
try std.testing.expectEqual(@as(u15, 0b10101), regs.t & 0x001f);
try std.testing.expectEqual(@as(u3, 3), regs.x);
// $2006 address latching & delayed commit
regs = Registers.init();
_ = regs.cpuWrite(&bus, 0x2006, 0x23, false);
_ = regs.cpuWrite(&bus, 0x2006, 0x45, false);
try std.testing.expectEqual(@as(u15, 0x2345), regs.t);
regs.clockDot();
regs.clockDot();
try std.testing.expectEqual(@as(u15, 0x2345), regs.v);
// $2007 buffered VRAM & immediate palette reads
regs = Registers.init();
bus.memory[0x2000] = 0xaa;
regs.v = 0x2000;
regs.read_buffer = 0x55;
try std.testing.expectEqual(@as(u8, 0x55), regs.cpuRead(&bus, 0x2007, false).?);
try std.testing.expectEqual(@as(u8, 0xaa), regs.read_buffer);
}
test "ppu oam, mask rendering delay, and power-on write suppression" {
var regs = Registers.init();
var bus = TestBus{};
// OAM address auto-increment
_ = regs.cpuWrite(&bus, 0x2003, 0x10, false);
_ = regs.cpuWrite(&bus, 0x2004, 0xab, false);
try std.testing.expectEqual(@as(u8, 0xab), regs.oam[0x10]);
try std.testing.expectEqual(@as(u8, 0x11), regs.oam_addr);
// PPUMASK 3-4 dot delay
regs = Registers.init();
_ = regs.cpuWrite(&bus, 0x2001, 0x18, false);
try std.testing.expect(!regs.mask.renderingEnabled());
for (0..4) |_| regs.clockDot();
try std.testing.expect(regs.mask.renderingEnabled());
// Power-on write suppression
regs = Registers.init();
regs.power();
_ = regs.cpuWrite(&bus, 0x2000, 0x80, false);
try std.testing.expectEqual(@as(u8, 0), regs.control.raw);
for (0..29658) |_| regs.tickCpuCycle();
_ = regs.cpuWrite(&bus, 0x2000, 0x80, false);
try std.testing.expectEqual(@as(u8, 0x80), regs.control.raw);
}
+542
View File
@@ -0,0 +1,542 @@
const std = @import("std");
pub const Control = @import("control.zig");
pub const Mask = @import("mask.zig");
pub const Registers = @import("registers.zig");
pub const Palette = @import("palette.zig").Palette;
pub const default_palette = @import("palette.zig").default_palette;
pub const common = @import("../common.zig");
pub const Region = common.Region;
pub const SpriteUnit = struct {
pattern_low: u8 = 0,
pattern_high: u8 = 0,
x_counter: u8 = 0,
attribute: u8 = 0,
is_sprite_zero: bool = false,
};
pub const Ppu = @This();
region: Region = .ntsc,
registers: Registers = .{},
palette: Palette = .{},
// Frame timing counters
dot: u16 = 0,
scanline: u16 = 0,
odd_frame: bool = false,
frame_count: u64 = 0,
frame_complete: bool = false,
// Framebuffer: 256 x 240 containing 6-bit NES palette indices
framebuffer: [common.screen_width * common.screen_height]u8 = [_]u8{0} ** (common.screen_width * common.screen_height),
// Background fetch pipeline latches
next_tile_id: u8 = 0,
next_tile_attribute: u8 = 0,
next_pattern_low: u8 = 0,
next_pattern_high: u8 = 0,
// Background 16-bit shift registers
pattern_shift_low: u16 = 0,
pattern_shift_high: u16 = 0,
attribute_shift_low: u16 = 0,
attribute_shift_high: u16 = 0,
// Sprite evaluation & fetching
secondary_oam: [32]u8 = [_]u8{0xff} ** 32,
sprite_count: u4 = 0,
sprite_zero_in_secondary: bool = false,
sprite_units: [8]SpriteUnit = [_]SpriteUnit{.{}} ** 8,
active_sprite_units: [8]SpriteUnit = [_]SpriteUnit{.{}} ** 8,
active_sprite_count: u4 = 0,
sprite_zero_active: bool = false,
pub fn init(region: Region) Ppu {
return .{
.region = region,
.registers = Registers.init(),
.palette = Palette.init(),
};
}
pub fn power(self: *Ppu) void {
self.registers.powerRegion(self.region);
self.palette.reset();
self.dot = 0;
self.scanline = 0;
self.odd_frame = false;
self.frame_count = 0;
self.frame_complete = false;
@memset(&self.framebuffer, 0);
self.resetPipeline();
}
pub fn reset(self: *Ppu) void {
self.registers.resetRegion(self.region);
self.dot = 0;
self.scanline = 0;
self.frame_complete = false;
self.resetPipeline();
}
fn resetPipeline(self: *Ppu) void {
self.next_tile_id = 0;
self.next_tile_attribute = 0;
self.next_pattern_low = 0;
self.next_pattern_high = 0;
self.pattern_shift_low = 0;
self.pattern_shift_high = 0;
self.attribute_shift_low = 0;
self.attribute_shift_high = 0;
@memset(&self.secondary_oam, 0xff);
self.sprite_count = 0;
self.sprite_zero_in_secondary = false;
self.active_sprite_count = 0;
self.sprite_zero_active = false;
}
pub inline fn preRenderScanline(self: *const Ppu) u16 {
return self.region.preRenderScanline();
}
pub inline fn totalScanlines(self: *const Ppu) u16 {
return self.region.totalScanlines();
}
pub inline fn isRendering(self: *const Ppu) bool {
const visible_or_prerender = (self.scanline < 240 or self.scanline == self.preRenderScanline());
return visible_or_prerender and self.registers.mask.renderingEnabled();
}
pub fn nmiLine(self: *const Ppu) bool {
return self.registers.nmiLine();
}
// CPU register interface
pub fn cpuRead(self: *Ppu, bus: anytype, address: u16) ?u8 {
// VBlank / NMI race condition on reading $2002 around scanline 241 dot 1:
// Reading on scanline 241 dot 0 clears VBlank before it can set NMI.
if (address == 0x2002 and self.scanline == 241 and self.dot == 0) {
// Suppress VBlank flag set on dot 1
var val = self.registers.cpuRead(bus, address, self.isRendering());
if (val) |*v| {
v.* &= 0x7f;
}
return val;
}
return self.registers.cpuRead(bus, address, self.isRendering());
}
pub fn cpuWrite(self: *Ppu, bus: anytype, address: u16, value: u8) bool {
return self.registers.cpuWrite(bus, address, value, self.isRendering());
}
// Main 341-dot PPU Clock
pub fn clock(self: *Ppu, bus: anytype) void {
// Advance delayed register commits (~1-2 dots for $2006, ~3-4 dots for $2001)
self.registers.clockDot();
const prerender = self.preRenderScanline();
const visible = self.scanline < 240;
const rendering = self.registers.mask.renderingEnabled();
// 1. Scanline-specific events
if (self.scanline == 241 and self.dot == 1) {
// VBlank start
self.registers.setVblank(true);
} else if (self.scanline == prerender and self.dot == 1) {
// Pre-render clear flags
self.registers.clearRenderingFlags();
}
// Pre-render vertical scroll copy (dots 280..304)
if (self.scanline == prerender and self.dot >= 280 and self.dot <= 304 and rendering) {
self.registers.copyVertical();
}
// 2. Background and Sprite Rendering operations
if (visible or self.scanline == prerender) {
self.stepRendering(bus);
}
// 3. Pixel output to framebuffer (visible scanlines dots 1..256)
if (visible and self.dot >= 1 and self.dot <= 256) {
self.renderPixel();
}
// 4. Dot and Scanline Advance
self.dot += 1;
// Odd-frame skip on NTSC pre-render scanline (dot 339 -> 0)
if (self.region == .ntsc and self.odd_frame and rendering and self.scanline == prerender and self.dot == 340) {
self.dot = 0;
self.scanline = 0;
self.odd_frame = !self.odd_frame;
self.frame_complete = true;
self.frame_count += 1;
return;
}
if (self.dot >= 341) {
self.dot = 0;
self.scanline += 1;
if (self.scanline >= self.totalScanlines()) {
self.scanline = 0;
self.odd_frame = !self.odd_frame;
self.frame_complete = true;
self.frame_count += 1;
}
}
}
fn stepRendering(self: *Ppu, bus: anytype) void {
const rendering = self.registers.mask.renderingEnabled();
// Background fetching during dots 1..256 and 321..336
if ((self.dot >= 1 and self.dot <= 256) or (self.dot >= 321 and self.dot <= 336)) {
// Shift background registers on every dot
if (rendering) {
self.pattern_shift_low <<= 1;
self.pattern_shift_high <<= 1;
self.attribute_shift_low <<= 1;
self.attribute_shift_high <<= 1;
}
const step = (self.dot - 1) & 7;
switch (step) {
0 => {
// Fetch Nametable byte
const nt_addr: u16 = 0x2000 | (self.registers.v & 0x0fff);
self.next_tile_id = bus.read(nt_addr);
},
2 => {
// Fetch Attribute byte
const v = self.registers.v;
const attr_addr: u16 = 0x23c0 |
(v & 0x0c00) |
((v >> 4) & 0x38) |
((v >> 2) & 0x07);
const attr_byte = bus.read(attr_addr);
// Determine 2-bit attribute quadrant based on coarse X and Y bit 1
const shift: u3 = @truncate(((v >> 4) & 0x04) | (v & 0x02));
self.next_tile_attribute = (attr_byte >> shift) & 0x03;
},
4 => {
// Fetch Pattern Table Low byte
const bg_base = self.registers.control.backgroundPatternAddress();
const fine_y: u16 = (self.registers.v >> 12) & 0x07;
const pat_addr: u16 = bg_base | (@as(u16, self.next_tile_id) << 4) | fine_y;
self.next_pattern_low = bus.read(pat_addr);
},
6 => {
// Fetch Pattern Table High byte
const bg_base = self.registers.control.backgroundPatternAddress();
const fine_y: u16 = (self.registers.v >> 12) & 0x07;
const pat_addr: u16 = bg_base | (@as(u16, self.next_tile_id) << 4) | fine_y | 8;
self.next_pattern_high = bus.read(pat_addr);
},
7 => {
// Load shift registers and increment coarse X
if (rendering) {
self.loadShiftRegisters();
self.registers.incrementCoarseX();
}
},
else => {},
}
}
// Dot 256: Increment Y
if (self.dot == 256 and rendering) {
self.registers.incrementY();
}
// Dot 257: Copy horizontal scroll
if (self.dot == 257 and rendering) {
self.registers.copyHorizontal();
}
// Dots 337..340: Two dummy nametable fetches (vital for mapper IRQ / A12 observation)
if (self.dot == 337 or self.dot == 339) {
const nt_addr: u16 = 0x2000 | (self.registers.v & 0x0fff);
_ = bus.read(nt_addr);
}
// Sprite evaluation and fetching (visible scanlines only)
if (self.scanline < 240) {
self.stepSpriteLogic(bus);
}
}
fn loadShiftRegisters(self: *Ppu) void {
self.pattern_shift_low = (self.pattern_shift_low & 0xff00) | self.next_pattern_low;
self.pattern_shift_high = (self.pattern_shift_high & 0xff00) | self.next_pattern_high;
const attr_lo: u8 = if ((self.next_tile_attribute & 1) != 0) 0xff else 0x00;
const attr_hi: u8 = if ((self.next_tile_attribute & 2) != 0) 0xff else 0x00;
self.attribute_shift_low = (self.attribute_shift_low & 0xff00) | attr_lo;
self.attribute_shift_high = (self.attribute_shift_high & 0xff00) | attr_hi;
}
fn stepSpriteLogic(self: *Ppu, bus: anytype) void {
const rendering = self.registers.mask.renderingEnabled();
// Dots 1..64: Clear secondary OAM
if (self.dot == 1) {
@memset(&self.secondary_oam, 0xff);
self.sprite_count = 0;
self.sprite_zero_in_secondary = false;
}
// Dots 65..256: Sprite Evaluation for next scanline
if (self.dot == 65 and rendering) {
const sprite_height = self.registers.control.spriteHeight();
const target_scanline = self.scanline;
var i: usize = 0;
while (i < 64) : (i += 1) {
const y = self.registers.oam[i * 4];
if (target_scanline >= y and target_scanline < y + sprite_height) {
if (self.sprite_count < 8) {
const dst = @as(usize, self.sprite_count) * 4;
@memcpy(self.secondary_oam[dst .. dst + 4], self.registers.oam[i * 4 .. i * 4 + 4]);
if (i == 0) {
self.sprite_zero_in_secondary = true;
}
self.sprite_count += 1;
} else {
// 8-sprite limit reached; set overflow flag
self.registers.setSpriteOverflow(true);
break;
}
}
}
}
// Dots 257..320: OAMADDR is forced to 0
if (self.dot >= 257 and self.dot <= 320 and rendering) {
self.registers.oam_addr = 0;
}
// Dots 257..320: Sprite pattern fetching for the 8 evaluated sprites
if (self.dot == 257) {
const sprite_height = self.registers.control.spriteHeight();
const target_scanline = self.scanline;
for (0..8) |s| {
if (s < self.sprite_count) {
const y = self.secondary_oam[s * 4];
const tile = self.secondary_oam[s * 4 + 1];
const attr = self.secondary_oam[s * 4 + 2];
const x = self.secondary_oam[s * 4 + 3];
var row: u4 = @truncate(target_scanline - y);
if ((attr & 0x80) != 0) {
// Vertical flip
row = @truncate(sprite_height - 1 - row);
}
const addr_lo = self.registers.control.spritePatternAddressForTile(tile, row);
const addr_hi = addr_lo + 8;
var lo = bus.read(addr_lo);
var hi = bus.read(addr_hi);
if ((attr & 0x40) != 0) {
// Horizontal flip
lo = @bitReverse(lo);
hi = @bitReverse(hi);
}
self.sprite_units[s] = .{
.pattern_low = lo,
.pattern_high = hi,
.x_counter = x,
.attribute = attr,
.is_sprite_zero = (s == 0 and self.sprite_zero_in_secondary),
};
} else {
// Dummy sprite fetches
_ = bus.read(self.registers.control.sprite8x8PatternAddress());
_ = bus.read(self.registers.control.sprite8x8PatternAddress() + 8);
self.sprite_units[s] = .{};
}
}
}
// At dot 0 of the next scanline, commit fetched sprite units
if (self.dot == 340) {
self.active_sprite_units = self.sprite_units;
self.active_sprite_count = self.sprite_count;
self.sprite_zero_active = self.sprite_zero_in_secondary;
}
}
fn renderPixel(self: *Ppu) void {
const pixel_x = self.dot - 1;
const pixel_y = self.scanline;
var bg_color: u2 = 0;
var bg_palette: u2 = 0;
if (self.registers.mask.showBackground()) {
const fine_x = self.registers.x;
const bit_mux = @as(u16, 0x8000) >> fine_x;
const p0: u2 = if ((self.pattern_shift_low & bit_mux) != 0) 1 else 0;
const p1: u2 = if ((self.pattern_shift_high & bit_mux) != 0) 2 else 0;
bg_color = p0 | p1;
const a0: u2 = if ((self.attribute_shift_low & bit_mux) != 0) 1 else 0;
const a1: u2 = if ((self.attribute_shift_high & bit_mux) != 0) 2 else 0;
bg_palette = a0 | a1;
// Left 8-pixel clipping
if (pixel_x < 8 and !self.registers.mask.showBackgroundLeft()) {
bg_color = 0;
}
}
var sprite_color: u2 = 0;
var sprite_palette: u2 = 0;
var sprite_priority: bool = false;
var is_sprite_zero: bool = false;
if (self.registers.mask.showSprites()) {
for (&self.active_sprite_units) |*unit| {
if (unit.x_counter == 0) {
const sp0: u2 = if ((unit.pattern_low & 0x80) != 0) 1 else 0;
const sp1: u2 = if ((unit.pattern_high & 0x80) != 0) 2 else 0;
const col = sp0 | sp1;
if (col != 0 and sprite_color == 0) {
sprite_color = col;
sprite_palette = @truncate(unit.attribute & 0x03);
sprite_priority = (unit.attribute & 0x20) != 0;
is_sprite_zero = unit.is_sprite_zero;
}
}
}
// Left 8-pixel clipping
if (pixel_x < 8 and !self.registers.mask.showSpritesLeft()) {
sprite_color = 0;
}
}
// Shift active sprite pattern registers and decrement non-zero X counters
for (&self.active_sprite_units) |*unit| {
if (unit.x_counter > 0) {
unit.x_counter -= 1;
} else {
unit.pattern_low <<= 1;
unit.pattern_high <<= 1;
}
}
// Sprite 0 Hit detection
if (is_sprite_zero and self.sprite_zero_active and bg_color != 0 and sprite_color != 0) {
// Sprite 0 hit cannot occur at X=255
if (pixel_x < 255) {
self.registers.setSpriteZeroHit(true);
}
}
// Pixel composition & palette lookup
var palette_addr: u16 = 0x3f00;
if (bg_color == 0 and sprite_color == 0) {
palette_addr = 0x3f00;
} else if (bg_color != 0 and sprite_color == 0) {
palette_addr = 0x3f00 | (@as(u16, bg_palette) << 2) | bg_color;
} else if (bg_color == 0 and sprite_color != 0) {
palette_addr = 0x3f10 | (@as(u16, sprite_palette) << 2) | sprite_color;
} else {
// Both opaque: check priority
if (sprite_priority) {
// Behind background
palette_addr = 0x3f00 | (@as(u16, bg_palette) << 2) | bg_color;
} else {
// In front of background
palette_addr = 0x3f10 | (@as(u16, sprite_palette) << 2) | sprite_color;
}
}
const color_index = self.palette.read(palette_addr);
self.framebuffer[pixel_y * 256 + pixel_x] = color_index;
}
/// Helper to convert the 8-bit palette-indexed framebuffer to standard 32-bit RGBA8888 pixels.
pub fn renderToRgba(self: *const Ppu, dest: []u32) void {
std.debug.assert(dest.len >= common.screen_width * common.screen_height);
for (self.framebuffer, 0..) |color_idx, i| {
dest[i] = default_palette[color_idx & 0x3f];
}
}
const MockBus = struct {
read_count: usize = 0,
last_address: u16 = 0,
pub fn read(self: *MockBus, address: u16) u8 {
self.read_count += 1;
self.last_address = address;
if (address < 0x2000) return 0x55; // Pattern data
if (address < 0x3f00) return 0x01; // Nametable / attribute
return 0;
}
pub fn write(_: *MockBus, _: u16, _: u8) void {}
};
// consolidated PPU timing and rendering unit test suite
test "ppu scanline progression, vblank flags, odd-frame skip and sprite 0 hit" {
var ppu = Ppu.init(.ntsc);
var bus = MockBus{};
// Scanline & dot clock progression
try std.testing.expectEqual(@as(u16, 0), ppu.scanline);
ppu.clock(&bus);
try std.testing.expectEqual(@as(u16, 1), ppu.dot);
// Run until vblank set on scanline 241
while (!(ppu.scanline == 241 and ppu.dot == 1)) ppu.clock(&bus);
ppu.clock(&bus);
try std.testing.expect(ppu.registers.vblank());
// Odd-frame skipped dot on NTSC rendering
ppu = Ppu.init(.ntsc);
_ = ppu.cpuWrite(&bus, 0x2001, 0x08);
for (0..4) |_| ppu.clock(&bus);
while (!ppu.frame_complete) ppu.clock(&bus);
try std.testing.expect(ppu.odd_frame);
// Sprite 0 Hit detection
ppu = Ppu.init(.ntsc);
_ = ppu.cpuWrite(&bus, 0x2001, 0x18);
for (0..4) |_| ppu.clock(&bus);
ppu.registers.oam[0] = 10;
ppu.registers.oam[1] = 0;
ppu.registers.oam[2] = 0;
ppu.registers.oam[3] = 10;
ppu.active_sprite_units[0] = .{
.pattern_low = 0xff,
.pattern_high = 0x00,
.x_counter = 0,
.attribute = 0,
.is_sprite_zero = true,
};
ppu.sprite_zero_active = true;
ppu.pattern_shift_low = 0xffff;
ppu.pattern_shift_high = 0x0000;
ppu.scanline = 10;
ppu.dot = 11;
ppu.renderPixel();
try std.testing.expect(ppu.registers.spriteHit());
}
+158 -327
View File
@@ -1,45 +1,64 @@
const std = @import("std");
const contract = @import("contract");
const video = contract.video;
const audio = contract.audio;
const input = contract.input;
const storage = contract.storage;
const state = contract.state;
const _cpu = @import("cpu");
const Bus = @import("bus.zig");
const Cartridge = @import("cartridge.zig");
pub const common = @import("common.zig");
pub const Region = common.Region;
pub const Mirroring = common.Mirroring;
pub const Button = common.Button;
pub const screen_width = common.screen_width;
pub const screen_height = common.screen_height;
const controller = @import("controller.zig");
pub const Button = controller.Button;
pub const ppu = @import("ppu/root.zig");
pub const apu = @import("apu/root.zig");
pub const bus_mod = @import("bus.zig");
pub const Bus = bus_mod.Bus;
pub const cartridge_mod = @import("cartridge.zig");
pub const Cartridge = cartridge_mod.Cartridge;
pub const controller_mod = @import("controller.zig");
pub const Controller = controller_mod.Controller;
pub const mapper_mod = @import("mapper/root.zig");
// Adjust this enum spelling to whatever your CPU package
// currently exports for the Ricoh 2A03.
const Cpu = _cpu.m6502.Cpu(
Bus,
.ricoh2a03,
.cycle,
);
const Cpu = _cpu.m6502.Cpu(Bus, .ricoh2a03, .cycle);
const Nes = @This();
pub const spec = contract.SystemSpec{
pub const Nes = @This();
pub const default_palette_565: [64]u16 = blk: {
var table: [64]u16 = undefined;
for (ppu.default_palette, 0..) |rgba, i| {
const r = @as(u16, @truncate((rgba >> 24) & 0xFF));
const g = @as(u16, @truncate((rgba >> 16) & 0xFF));
const b = @as(u16, @truncate((rgba >> 8) & 0xFF));
const r5 = r >> 3;
const g6 = g >> 2;
const b5 = b >> 3;
table[i] = (r5 << 11) | (g6 << 5) | b5;
}
break :blk table;
};
pub const spec: contract.SystemSpec = .{
.name = "Nintendo Entertainment System",
.video_outputs = &.{
.{
.max_width = 256,
.max_height = 240,
.max_width = common.screen_width,
.max_height = common.screen_height,
.format = .rgb565,
.aspect_ratio = .{
.numerator = 4,
.denominator = 3,
},
.refresh_rate = .{
.numerator = 60,
.denominator = 1,
},
},
},
.audio_outputs = &.{
.{
.sample_rate = 44_100,
@@ -47,16 +66,10 @@ pub const spec = contract.SystemSpec{
.format = .i16,
},
},
.input_devices = &.{
.{
.button_count = 8,
},
.{
.button_count = 8,
},
.{ .button_count = 8 },
.{ .button_count = 8 },
},
.storage_devices = &.{
.{
.name = "Battery PRG RAM",
@@ -67,355 +80,173 @@ pub const spec = contract.SystemSpec{
.writable = true,
},
},
.save_state = .{
.max_size = 32 * 1024,
.max_size = 64 * 1024,
},
};
bus: Bus = undefined,
cpu: Cpu = undefined,
rgb565_framebuffer: [common.screen_width * common.screen_height]u16 = [_]u16{0} ** (common.screen_width * common.screen_height),
/// 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 init(self: *Nes, rom_bytes: []const u8) !void {
const cart = try Cartridge.init(rom_bytes);
self.bus = Bus.init(.ntsc, cart);
self.cpu = Cpu.init(&self.bus);
self.cpu.reset();
}
pub fn reset(
self: *Nes,
) void {
pub fn reset(self: *Nes) void {
self.cpu.bus = &self.bus;
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_complete = false;
self.bus.apu.clearSamples();
pub fn runFrame(
self: *Nes,
) void {
self.bus.ppu.frame_ready = false;
self.bus.apu.sample_count = 0;
while (!self.bus.ppu.frame_complete) {
if (self.bus.dma_active) {
self.bus.stepDma();
} else {
_ = self.cpu.step();
}
}
while (!self.bus.ppu.frame_ready) {
_ = self.step() orelse
return;
// Convert PPU framebuffer to RGB565 for frontend driver
for (self.bus.ppu.framebuffer, 0..) |color_idx, i| {
self.rgb565_framebuffer[i] = default_palette_565[color_idx & 0x3f];
}
}
pub 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);
pub fn videoFrame(self: *const Nes, _: usize) video.VideoFrame {
return .{
.data = std.mem.sliceAsBytes(
self.bus.ppu.framebuffer[0..],
),
.width = 256,
.height = 240,
.pitch = 256 * @sizeOf(u16),
.data = std.mem.sliceAsBytes(&self.rgb565_framebuffer),
.width = common.screen_width,
.height = common.screen_height,
.pitch = common.screen_width * 2,
.format = .rgb565,
.frame_number = self.bus.ppu.frame_number,
.frame_number = self.bus.ppu.frame_count,
};
}
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];
pub fn audioBuffer(self: *const Nes, _: usize) audio.Buffer {
const samples = self.bus.apu.getSamples();
return .{
.data = std.mem.sliceAsBytes(samples),
.frames = self.bus.apu.sample_count,
.frames = samples.len,
.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 setInput(self: *Nes, device_index: usize, device_input: input.DeviceInput) void {
if (device_index < 2) {
self.bus.controllers[device_index].setButtons(@truncate(device_input.buttons));
}
}
pub fn saveState(
self: *const Nes,
buffer: []u8,
) contract.state.Error!usize {
pub fn storageView(self: *const Nes, index: usize) storage.View {
if (index == 0 and self.bus.cartridge != null) {
return .{
.data = &self.bus.cartridge.?.prg_ram,
.generation = 0,
};
}
return .{ .data = &.{}, .generation = 0 };
}
pub fn loadStorage(self: *Nes, index: usize, data: []const u8) storage.LoadError!void {
if (index != 0 or self.bus.cartridge == null) return error.InvalidSlot;
if (data.len > self.bus.cartridge.?.prg_ram.len) return error.InvalidSize;
@memcpy(self.bus.cartridge.?.prg_ram[0..data.len], data);
}
pub fn saveState(self: *const Nes, dest: []u8) state.Error!usize {
const required_size: usize = 8 + 7 + 0x800 + 0x800 + 256 + 32 + 0x2000;
if (dest.len < required_size) return error.BufferTooSmall;
var offset: usize = 0;
if (buffer.len < 8) return error.BufferTooSmall;
@memcpy(buffer[offset..][0..8], "6SOZNES1");
@memcpy(dest[offset .. offset + 8], "NESSTATE");
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;
const regs = self.cpu.registers;
dest[offset] = regs.a;
dest[offset + 1] = regs.x;
dest[offset + 2] = regs.y;
dest[offset + 3] = regs.sp;
dest[offset + 4] = @bitCast(regs.status);
dest[offset + 5] = @truncate(regs.pc);
dest[offset + 6] = @truncate(regs.pc >> 8);
offset += 7;
// 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;
@memcpy(dest[offset .. offset + 0x800], &self.bus.ram);
offset += 0x800;
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;
}
@memcpy(dest[offset .. offset + 0x800], &self.bus.ciram);
offset += 0x800;
// 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);
@memcpy(dest[offset .. offset + 256], &self.bus.ppu.registers.oam);
offset += 256;
@memcpy(dest[offset .. offset + 32], &self.bus.ppu.palette.ram);
offset += 32;
if (self.bus.cartridge) |*c| {
@memcpy(dest[offset .. offset + 0x2000], &c.prg_ram);
offset += 0x2000;
}
return offset;
}
pub fn loadState(
self: *Nes,
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;
pub fn loadState(self: *Nes, src: []const u8) state.Error!void {
if (src.len < 8 or !std.mem.eql(u8, src[0..8], "NESSTATE")) {
return error.InvalidState;
}
var offset: usize = 8;
// 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.cpu.registers.a = src[offset];
self.cpu.registers.x = src[offset + 1];
self.cpu.registers.y = src[offset + 2];
self.cpu.registers.sp = src[offset + 3];
self.cpu.registers.status = @bitCast(src[offset + 4]);
self.cpu.registers.pc = @as(u16, src[offset + 5]) | (@as(u16, src[offset + 6]) << 8);
offset += 7;
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.ram, src[offset .. offset + 0x800]);
offset += 0x800;
@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]);
@memcpy(&self.bus.ciram, src[offset .. offset + 0x800]);
offset += 0x800;
@memcpy(&self.bus.ppu.registers.oam, src[offset .. offset + 256]);
offset += 256;
@memcpy(&self.bus.ppu.palette.ram, src[offset .. offset + 32]);
offset += 32;
if (self.bus.cartridge) |*c| {
if (src.len >= offset + 0x2000) {
@memcpy(&c.prg_ram, src[offset .. offset + 0x2000]);
offset += 0x2000;
}
}
}
test "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);
test {
contract.system.validate(Nes);
_ = common;
_ = ppu;
_ = apu;
_ = bus_mod;
_ = cartridge_mod;
_ = controller_mod;
_ = mapper_mod;
_ = conformance;
}
comptime {
contract.validateSystem(Nes);
}
pub const conformance = @import("conformance/root.zig");