Compare commits
10
Commits
6f897a874c
...
460403d3e1
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
460403d3e1 | ||
|
|
71c747d962 | ||
|
|
754dbb9307 | ||
|
|
20c1106a9a | ||
|
|
9ba71b7b62 | ||
|
|
57f349fa17 | ||
|
|
f028213c11 | ||
|
|
425ebe0938 | ||
|
|
60a6364625 | ||
|
|
cc5cb881cc |
@@ -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,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/).
|
||||
|
||||
|
||||
@@ -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();
|
||||
|
||||
|
||||
@@ -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",
|
||||
|
||||
+128
-3
@@ -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
@@ -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 {
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
@@ -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());
|
||||
}
|
||||
@@ -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());
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
@@ -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
@@ -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));
|
||||
}
|
||||
|
||||
+92
-1086
File diff suppressed because it is too large
Load Diff
@@ -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());
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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());
|
||||
}
|
||||
|
||||
@@ -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());
|
||||
}
|
||||
@@ -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 {}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
@@ -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());
|
||||
}
|
||||
|
||||
@@ -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 {}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
@@ -1,73 +0,0 @@
|
||||
const types = @import("types.zig");
|
||||
const Mirroring = types.Mirroring;
|
||||
|
||||
const ColorDreams = @This();
|
||||
|
||||
prg_bank: u8 = 0,
|
||||
chr_bank: u8 = 0,
|
||||
|
||||
prg_bank_count: usize,
|
||||
chr_bank_count: usize,
|
||||
mirroring_mode: Mirroring,
|
||||
|
||||
pub fn init(
|
||||
prg_size: usize,
|
||||
chr_size: usize,
|
||||
_mirroring: Mirroring,
|
||||
) ColorDreams {
|
||||
return .{
|
||||
.prg_bank_count = prg_size / 0x8000,
|
||||
.chr_bank_count = if (chr_size > 0) chr_size / 0x2000 else 1,
|
||||
.mirroring_mode = _mirroring,
|
||||
};
|
||||
}
|
||||
|
||||
pub fn cpuMapRead(
|
||||
self: *const ColorDreams,
|
||||
address: u16,
|
||||
) ?usize {
|
||||
if (address < 0x8000) return null;
|
||||
|
||||
const offset = @as(usize, address - 0x8000);
|
||||
const bank = @as(usize, self.prg_bank) % self.prg_bank_count;
|
||||
return bank * 0x8000 + offset;
|
||||
}
|
||||
|
||||
pub fn cpuWrite(
|
||||
self: *ColorDreams,
|
||||
address: u16,
|
||||
value: u8,
|
||||
) void {
|
||||
if (address >= 0x8000) {
|
||||
self.prg_bank = value & 0x03;
|
||||
self.chr_bank = (value >> 4) & 0x0F;
|
||||
}
|
||||
}
|
||||
|
||||
pub fn ppuMapRead(
|
||||
self: *const ColorDreams,
|
||||
address: u16,
|
||||
) ?usize {
|
||||
if (address >= 0x2000) return null;
|
||||
|
||||
const bank = @as(usize, self.chr_bank) % self.chr_bank_count;
|
||||
return bank * 0x2000 + @as(usize, address);
|
||||
}
|
||||
|
||||
pub fn ppuMapWrite(
|
||||
_: *const ColorDreams,
|
||||
address: u16,
|
||||
) ?usize {
|
||||
if (address >= 0x2000) return null;
|
||||
return address;
|
||||
}
|
||||
|
||||
pub fn mirroring(
|
||||
self: *const ColorDreams,
|
||||
) Mirroring {
|
||||
return self.mirroring_mode;
|
||||
}
|
||||
|
||||
pub fn irqAsserted(_: *const ColorDreams) bool {
|
||||
return false;
|
||||
}
|
||||
@@ -1,73 +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);
|
||||
}
|
||||
|
||||
@@ -1,78 +0,0 @@
|
||||
const types = @import("types.zig");
|
||||
const Mirroring = types.Mirroring;
|
||||
|
||||
const Irem78 = @This();
|
||||
|
||||
prg_bank: u8 = 0,
|
||||
chr_bank: u8 = 0,
|
||||
mirroring_select: u1 = 0,
|
||||
|
||||
prg_bank_count: usize,
|
||||
chr_bank_count: usize,
|
||||
|
||||
pub fn init(
|
||||
prg_size: usize,
|
||||
chr_size: usize,
|
||||
) Irem78 {
|
||||
return .{
|
||||
.prg_bank_count = prg_size / 0x4000,
|
||||
.chr_bank_count = if (chr_size > 0) chr_size / 0x2000 else 1,
|
||||
};
|
||||
}
|
||||
|
||||
pub fn cpuMapRead(
|
||||
self: *const Irem78,
|
||||
address: u16,
|
||||
) ?usize {
|
||||
if (address < 0x8000) return null;
|
||||
|
||||
if (address < 0xC000) {
|
||||
const offset = @as(usize, address - 0x8000);
|
||||
const bank = @as(usize, self.prg_bank) % self.prg_bank_count;
|
||||
return bank * 0x4000 + offset;
|
||||
} else {
|
||||
const offset = @as(usize, address - 0xC000);
|
||||
const fixed_bank = if (self.prg_bank_count > 0) self.prg_bank_count - 1 else 0;
|
||||
return fixed_bank * 0x4000 + offset;
|
||||
}
|
||||
}
|
||||
|
||||
pub fn cpuWrite(
|
||||
self: *Irem78,
|
||||
address: u16,
|
||||
value: u8,
|
||||
) void {
|
||||
if (address < 0x8000) return;
|
||||
|
||||
self.prg_bank = value & 0x07;
|
||||
self.mirroring_select = @truncate((value >> 3) & 1);
|
||||
self.chr_bank = (value >> 4) & 0x0F;
|
||||
}
|
||||
|
||||
pub fn ppuMapRead(
|
||||
self: *const Irem78,
|
||||
address: u16,
|
||||
) ?usize {
|
||||
if (address >= 0x2000) return null;
|
||||
|
||||
const bank = @as(usize, self.chr_bank) % self.chr_bank_count;
|
||||
return bank * 0x2000 + @as(usize, address);
|
||||
}
|
||||
|
||||
pub fn ppuMapWrite(
|
||||
_: *const Irem78,
|
||||
address: u16,
|
||||
) ?usize {
|
||||
if (address >= 0x2000) return null;
|
||||
return address;
|
||||
}
|
||||
|
||||
pub fn mirroring(
|
||||
self: *const Irem78,
|
||||
) Mirroring {
|
||||
return if (self.mirroring_select == 0) .single_screen_lower else .single_screen_upper;
|
||||
}
|
||||
|
||||
pub fn irqAsserted(_: *const Irem78) bool {
|
||||
return false;
|
||||
}
|
||||
@@ -1,69 +0,0 @@
|
||||
const types = @import("types.zig");
|
||||
const Mirroring = types.Mirroring;
|
||||
const Mmc3 = @import("mmc3.zig");
|
||||
|
||||
const Mapper118 = @This();
|
||||
|
||||
mmc3: Mmc3,
|
||||
mirror_bit: u1 = 0,
|
||||
|
||||
pub fn init(
|
||||
prg_size: usize,
|
||||
chr_size: usize,
|
||||
) Mapper118 {
|
||||
return .{
|
||||
.mmc3 = Mmc3.init(prg_size, chr_size),
|
||||
};
|
||||
}
|
||||
|
||||
pub fn cpuMapRead(
|
||||
self: *const Mapper118,
|
||||
address: u16,
|
||||
) ?usize {
|
||||
return self.mmc3.cpuMapRead(address);
|
||||
}
|
||||
|
||||
pub fn cpuWrite(
|
||||
self: *Mapper118,
|
||||
address: u16,
|
||||
value: u8,
|
||||
) void {
|
||||
if (address == 0x8001) {
|
||||
if (self.mmc3.bank_select <= 5) {
|
||||
self.mirror_bit = @truncate((value >> 7) & 1);
|
||||
}
|
||||
}
|
||||
self.mmc3.cpuWrite(address, value);
|
||||
}
|
||||
|
||||
pub fn ppuMapRead(
|
||||
self: *Mapper118,
|
||||
address: u16,
|
||||
) ?usize {
|
||||
return self.mmc3.ppuMapRead(address);
|
||||
}
|
||||
|
||||
pub fn ppuMapWrite(
|
||||
self: *const Mapper118,
|
||||
address: u16,
|
||||
) ?usize {
|
||||
return self.mmc3.ppuMapWrite(address);
|
||||
}
|
||||
|
||||
pub fn mirroring(
|
||||
self: *const Mapper118,
|
||||
) Mirroring {
|
||||
return if (self.mirror_bit == 1) .single_screen_upper else .single_screen_lower;
|
||||
}
|
||||
|
||||
pub fn irqAsserted(
|
||||
self: *const Mapper118,
|
||||
) bool {
|
||||
return self.mmc3.irqAsserted();
|
||||
}
|
||||
|
||||
pub fn handleScanline(
|
||||
self: *Mapper118,
|
||||
) void {
|
||||
self.mmc3.handleScanline();
|
||||
}
|
||||
@@ -1,113 +0,0 @@
|
||||
const types = @import("types.zig");
|
||||
const Mirroring = types.Mirroring;
|
||||
const Mmc3 = @import("mmc3.zig");
|
||||
|
||||
const Mapper119 = @This();
|
||||
|
||||
mmc3: Mmc3,
|
||||
|
||||
pub fn init(
|
||||
prg_size: usize,
|
||||
chr_size: usize,
|
||||
) Mapper119 {
|
||||
return .{
|
||||
.mmc3 = Mmc3.init(prg_size, chr_size),
|
||||
};
|
||||
}
|
||||
|
||||
pub fn cpuMapRead(
|
||||
self: *const Mapper119,
|
||||
address: u16,
|
||||
) ?usize {
|
||||
return self.mmc3.cpuMapRead(address);
|
||||
}
|
||||
|
||||
pub fn cpuWrite(
|
||||
self: *Mapper119,
|
||||
address: u16,
|
||||
value: u8,
|
||||
) void {
|
||||
self.mmc3.cpuWrite(address, value);
|
||||
}
|
||||
|
||||
pub fn ppuMapRead(
|
||||
self: *Mapper119,
|
||||
address: u16,
|
||||
) ?usize {
|
||||
if (address >= 0x2000) return null;
|
||||
|
||||
const bank_val = self.getChrBankValue(address);
|
||||
const offset = (address % 0x0400);
|
||||
|
||||
if ((bank_val & 0x40) != 0) {
|
||||
// Bit 6 is set -> Route to CHR RAM
|
||||
const ram_bank = @as(usize, bank_val & 7);
|
||||
return 0x80000000 | (ram_bank * 0x0400 + offset);
|
||||
} else {
|
||||
// Route to CHR ROM
|
||||
return self.mmc3.ppuMapRead(address);
|
||||
}
|
||||
}
|
||||
|
||||
pub fn ppuMapWrite(
|
||||
self: *const Mapper119,
|
||||
address: u16,
|
||||
) ?usize {
|
||||
if (address >= 0x2000) return null;
|
||||
|
||||
const bank_val = self.getChrBankValue(address);
|
||||
const offset = (address % 0x0400);
|
||||
|
||||
if ((bank_val & 0x40) != 0) {
|
||||
// Bit 6 is set -> Route write to CHR RAM
|
||||
const ram_bank = @as(usize, bank_val & 7);
|
||||
return 0x80000000 | (ram_bank * 0x0400 + offset);
|
||||
} else {
|
||||
return self.mmc3.ppuMapWrite(address);
|
||||
}
|
||||
}
|
||||
|
||||
fn getChrBankValue(self: *const Mapper119, address: u16) u8 {
|
||||
const chr_mode = (self.mmc3.bank_select & 0x80) != 0;
|
||||
const addr_k = address / 0x0400;
|
||||
|
||||
if (!chr_mode) {
|
||||
return switch (addr_k) {
|
||||
0, 1 => (self.mmc3.bank_registers[0] & 0xfe) + @as(u8, @intCast(addr_k & 1)),
|
||||
2, 3 => (self.mmc3.bank_registers[1] & 0xfe) + @as(u8, @intCast(addr_k & 1)),
|
||||
4 => self.mmc3.bank_registers[2],
|
||||
5 => self.mmc3.bank_registers[3],
|
||||
6 => self.mmc3.bank_registers[4],
|
||||
7 => self.mmc3.bank_registers[5],
|
||||
else => unreachable,
|
||||
};
|
||||
} else {
|
||||
return switch (addr_k) {
|
||||
0 => self.mmc3.bank_registers[2],
|
||||
1 => self.mmc3.bank_registers[3],
|
||||
2 => self.mmc3.bank_registers[4],
|
||||
3 => self.mmc3.bank_registers[5],
|
||||
4, 5 => (self.mmc3.bank_registers[0] & 0xfe) + @as(u8, @intCast(addr_k & 1)),
|
||||
6, 7 => (self.mmc3.bank_registers[1] & 0xfe) + @as(u8, @intCast(addr_k & 1)),
|
||||
else => unreachable,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
pub fn mirroring(
|
||||
self: *const Mapper119,
|
||||
) Mirroring {
|
||||
return self.mmc3.mirroring();
|
||||
}
|
||||
|
||||
pub fn irqAsserted(
|
||||
self: *const Mapper119,
|
||||
) bool {
|
||||
return self.mmc3.irqAsserted();
|
||||
}
|
||||
|
||||
pub fn handleScanline(
|
||||
self: *Mapper119,
|
||||
) void {
|
||||
self.mmc3.handleScanline();
|
||||
}
|
||||
@@ -1,69 +0,0 @@
|
||||
const types = @import("types.zig");
|
||||
const Mirroring = types.Mirroring;
|
||||
|
||||
const Mapper13 = @This();
|
||||
|
||||
chr_bank: u8 = 0,
|
||||
fixed_mirroring: Mirroring,
|
||||
prg_mask: usize,
|
||||
|
||||
pub fn init(
|
||||
prg_size: usize,
|
||||
mirroring_mode: Mirroring,
|
||||
) Mapper13 {
|
||||
return .{
|
||||
.fixed_mirroring = mirroring_mode,
|
||||
.prg_mask = if (prg_size > 0) prg_size - 1 else 0x7fff,
|
||||
};
|
||||
}
|
||||
|
||||
pub fn cpuMapRead(
|
||||
self: *const Mapper13,
|
||||
address: u16,
|
||||
) ?usize {
|
||||
if (address < 0x8000) return null;
|
||||
return (@as(usize, address) - 0x8000) & self.prg_mask;
|
||||
}
|
||||
|
||||
pub fn cpuWrite(
|
||||
self: *Mapper13,
|
||||
address: u16,
|
||||
value: u8,
|
||||
) void {
|
||||
if (address < 0x8000) return;
|
||||
self.chr_bank = value & 0x03;
|
||||
}
|
||||
|
||||
pub fn ppuMapRead(
|
||||
self: *const Mapper13,
|
||||
address: u16,
|
||||
) ?usize {
|
||||
if (address >= 0x2000) return null;
|
||||
|
||||
if (address < 0x1000) {
|
||||
return 0x80000000 | @as(usize, address);
|
||||
} else {
|
||||
const bank = @as(usize, self.chr_bank & 3);
|
||||
const offset = @as(usize, address & 0x0fff);
|
||||
return 0x80000000 | (bank * 0x1000 + offset);
|
||||
}
|
||||
}
|
||||
|
||||
pub fn ppuMapWrite(
|
||||
self: *const Mapper13,
|
||||
address: u16,
|
||||
) ?usize {
|
||||
return self.ppuMapRead(address);
|
||||
}
|
||||
|
||||
pub fn mirroring(
|
||||
self: *const Mapper13,
|
||||
) Mirroring {
|
||||
return self.fixed_mirroring;
|
||||
}
|
||||
|
||||
pub fn irqAsserted(
|
||||
_: *const Mapper13,
|
||||
) bool {
|
||||
return false;
|
||||
}
|
||||
@@ -1,73 +0,0 @@
|
||||
const types = @import("types.zig");
|
||||
const Mirroring = types.Mirroring;
|
||||
|
||||
const Mapper140 = @This();
|
||||
|
||||
prg_bank: u8 = 0,
|
||||
chr_bank: u8 = 0,
|
||||
|
||||
prg_bank_count: usize,
|
||||
chr_bank_count: usize,
|
||||
mirroring_mode: Mirroring,
|
||||
|
||||
pub fn init(
|
||||
prg_size: usize,
|
||||
chr_size: usize,
|
||||
_mirroring: Mirroring,
|
||||
) Mapper140 {
|
||||
return .{
|
||||
.prg_bank_count = prg_size / 0x8000,
|
||||
.chr_bank_count = if (chr_size > 0) chr_size / 0x2000 else 1,
|
||||
.mirroring_mode = _mirroring,
|
||||
};
|
||||
}
|
||||
|
||||
pub fn cpuMapRead(
|
||||
self: *const Mapper140,
|
||||
address: u16,
|
||||
) ?usize {
|
||||
if (address < 0x8000) return null;
|
||||
|
||||
const offset = @as(usize, address - 0x8000);
|
||||
const bank = @as(usize, self.prg_bank) % self.prg_bank_count;
|
||||
return bank * 0x8000 + offset;
|
||||
}
|
||||
|
||||
pub fn cpuWrite(
|
||||
self: *Mapper140,
|
||||
address: u16,
|
||||
value: u8,
|
||||
) void {
|
||||
if (address >= 0x6000 and address <= 0x7FFF) {
|
||||
self.prg_bank = (value >> 4) & 0x03;
|
||||
self.chr_bank = value & 0x0F;
|
||||
}
|
||||
}
|
||||
|
||||
pub fn ppuMapRead(
|
||||
self: *const Mapper140,
|
||||
address: u16,
|
||||
) ?usize {
|
||||
if (address >= 0x2000) return null;
|
||||
|
||||
const bank = @as(usize, self.chr_bank) % self.chr_bank_count;
|
||||
return bank * 0x2000 + @as(usize, address);
|
||||
}
|
||||
|
||||
pub fn ppuMapWrite(
|
||||
_: *const Mapper140,
|
||||
address: u16,
|
||||
) ?usize {
|
||||
if (address >= 0x2000) return null;
|
||||
return address;
|
||||
}
|
||||
|
||||
pub fn mirroring(
|
||||
self: *const Mapper140,
|
||||
) Mirroring {
|
||||
return self.mirroring_mode;
|
||||
}
|
||||
|
||||
pub fn irqAsserted(_: *const Mapper140) bool {
|
||||
return false;
|
||||
}
|
||||
@@ -1,70 +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);
|
||||
}
|
||||
|
||||
@@ -1,108 +0,0 @@
|
||||
const types = @import("types.zig");
|
||||
const Mirroring = types.Mirroring;
|
||||
|
||||
const Mapper206 = @This();
|
||||
|
||||
bank_select: u3 = 0,
|
||||
|
||||
prg_bank0: u8 = 0,
|
||||
prg_bank1: u8 = 1,
|
||||
|
||||
chr_banks: [6]u8 = [_]u8{ 0, 2, 4, 5, 6, 7 },
|
||||
|
||||
prg_bank_count: usize,
|
||||
chr_bank_count: usize,
|
||||
mirroring_mode: Mirroring,
|
||||
|
||||
pub fn init(
|
||||
prg_size: usize,
|
||||
chr_size: usize,
|
||||
_mirroring: Mirroring,
|
||||
) Mapper206 {
|
||||
return .{
|
||||
.prg_bank_count = prg_size / 0x2000,
|
||||
.chr_bank_count = if (chr_size > 0) chr_size / 0x0400 else 1,
|
||||
.mirroring_mode = _mirroring,
|
||||
};
|
||||
}
|
||||
|
||||
pub fn cpuMapRead(
|
||||
self: *const Mapper206,
|
||||
address: u16,
|
||||
) ?usize {
|
||||
if (address < 0x8000) return null;
|
||||
|
||||
if (address < 0xA000) {
|
||||
const offset = @as(usize, address - 0x8000);
|
||||
const bank = @as(usize, self.prg_bank0) % self.prg_bank_count;
|
||||
return bank * 0x2000 + offset;
|
||||
} else if (address < 0xC000) {
|
||||
const offset = @as(usize, address - 0xA000);
|
||||
const bank = @as(usize, self.prg_bank1) % self.prg_bank_count;
|
||||
return bank * 0x2000 + offset;
|
||||
} else if (address < 0xE000) {
|
||||
const offset = @as(usize, address - 0xC000);
|
||||
const bank = if (self.prg_bank_count >= 2) self.prg_bank_count - 2 else 0;
|
||||
return bank * 0x2000 + offset;
|
||||
} else {
|
||||
const offset = @as(usize, address - 0xE000);
|
||||
const bank = if (self.prg_bank_count >= 1) self.prg_bank_count - 1 else 0;
|
||||
return bank * 0x2000 + offset;
|
||||
}
|
||||
}
|
||||
|
||||
pub fn cpuWrite(
|
||||
self: *Mapper206,
|
||||
address: u16,
|
||||
value: u8,
|
||||
) void {
|
||||
if (address >= 0x8000 and address <= 0x9FFF) {
|
||||
if ((address & 1) == 0) {
|
||||
self.bank_select = @truncate(value & 0x07);
|
||||
} else {
|
||||
switch (self.bank_select) {
|
||||
0...5 => |i| self.chr_banks[i] = value & 0x3F,
|
||||
6 => self.prg_bank0 = value & 0x0F,
|
||||
7 => self.prg_bank1 = value & 0x0F,
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn ppuMapRead(
|
||||
self: *const Mapper206,
|
||||
address: u16,
|
||||
) ?usize {
|
||||
if (address >= 0x2000) return null;
|
||||
|
||||
if (address < 0x0800) {
|
||||
const bank = (@as(usize, self.chr_banks[0] & 0xFE)) % self.chr_bank_count;
|
||||
return bank * 0x0400 + @as(usize, address);
|
||||
} else if (address < 0x1000) {
|
||||
const bank = (@as(usize, self.chr_banks[1] & 0xFE)) % self.chr_bank_count;
|
||||
return bank * 0x0400 + @as(usize, address - 0x0800);
|
||||
} else {
|
||||
const idx = 2 + (address - 0x1000) / 0x0400;
|
||||
const offset = (address - 0x1000) % 0x0400;
|
||||
const bank = @as(usize, self.chr_banks[idx]) % self.chr_bank_count;
|
||||
return bank * 0x0400 + @as(usize, offset);
|
||||
}
|
||||
}
|
||||
|
||||
pub fn ppuMapWrite(
|
||||
_: *const Mapper206,
|
||||
address: u16,
|
||||
) ?usize {
|
||||
if (address >= 0x2000) return null;
|
||||
return address;
|
||||
}
|
||||
|
||||
pub fn mirroring(
|
||||
self: *const Mapper206,
|
||||
) Mirroring {
|
||||
return self.mirroring_mode;
|
||||
}
|
||||
|
||||
pub fn irqAsserted(_: *const Mapper206) bool {
|
||||
return false;
|
||||
}
|
||||
@@ -1,105 +0,0 @@
|
||||
const types = @import("types.zig");
|
||||
const Mirroring = types.Mirroring;
|
||||
|
||||
const Mapper210 = @This();
|
||||
|
||||
prg_bank0: u8 = 0,
|
||||
prg_bank1: u8 = 0,
|
||||
prg_bank2: u8 = 0,
|
||||
|
||||
chr_banks: [8]u8 = [_]u8{ 0, 1, 2, 3, 4, 5, 6, 7 },
|
||||
mirroring_mode: Mirroring = .vertical,
|
||||
|
||||
prg_bank_count: usize,
|
||||
chr_bank_count: usize,
|
||||
|
||||
pub fn init(
|
||||
prg_size: usize,
|
||||
chr_size: usize,
|
||||
) Mapper210 {
|
||||
return .{
|
||||
.prg_bank_count = prg_size / 0x2000,
|
||||
.chr_bank_count = if (chr_size > 0) chr_size / 0x0400 else 1,
|
||||
};
|
||||
}
|
||||
|
||||
pub fn cpuMapRead(
|
||||
self: *const Mapper210,
|
||||
address: u16,
|
||||
) ?usize {
|
||||
if (address < 0x8000) return null;
|
||||
|
||||
if (address < 0xA000) {
|
||||
const offset = @as(usize, address - 0x8000);
|
||||
const bank = @as(usize, self.prg_bank0) % self.prg_bank_count;
|
||||
return bank * 0x2000 + offset;
|
||||
} else if (address < 0xC000) {
|
||||
const offset = @as(usize, address - 0xA000);
|
||||
const bank = @as(usize, self.prg_bank1) % self.prg_bank_count;
|
||||
return bank * 0x2000 + offset;
|
||||
} else if (address < 0xE000) {
|
||||
const offset = @as(usize, address - 0xC000);
|
||||
const bank = @as(usize, self.prg_bank2) % self.prg_bank_count;
|
||||
return bank * 0x2000 + offset;
|
||||
} else {
|
||||
const offset = @as(usize, address - 0xE000);
|
||||
const fixed_bank = if (self.prg_bank_count > 0) self.prg_bank_count - 1 else 0;
|
||||
return fixed_bank * 0x2000 + offset;
|
||||
}
|
||||
}
|
||||
|
||||
pub fn cpuWrite(
|
||||
self: *Mapper210,
|
||||
address: u16,
|
||||
value: u8,
|
||||
) void {
|
||||
if (address < 0x8000) return;
|
||||
|
||||
if (address >= 0x8000 and address <= 0xBFFF) {
|
||||
const index = (address - 0x8000) / 0x0800;
|
||||
self.chr_banks[index] = value;
|
||||
} else if (address >= 0xC000 and address <= 0xC7FF) {
|
||||
self.prg_bank0 = value & 0x3F;
|
||||
} else if (address >= 0xC800 and address <= 0xCFFF) {
|
||||
self.prg_bank1 = value & 0x3F;
|
||||
} else if (address >= 0xD000 and address <= 0xD7FF) {
|
||||
self.prg_bank2 = value & 0x3F;
|
||||
} else if (address >= 0xE000 and address <= 0xE7FF) {
|
||||
self.mirroring_mode = switch (@as(u2, @truncate(value >> 6))) {
|
||||
0 => .vertical,
|
||||
1 => .horizontal,
|
||||
2 => .single_screen_lower,
|
||||
3 => .single_screen_upper,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
pub fn ppuMapRead(
|
||||
self: *const Mapper210,
|
||||
address: u16,
|
||||
) ?usize {
|
||||
if (address >= 0x2000) return null;
|
||||
|
||||
const bank_idx = address / 0x0400;
|
||||
const offset = address % 0x0400;
|
||||
const bank = @as(usize, self.chr_banks[bank_idx]) % self.chr_bank_count;
|
||||
return bank * 0x0400 + offset;
|
||||
}
|
||||
|
||||
pub fn ppuMapWrite(
|
||||
_: *const Mapper210,
|
||||
address: u16,
|
||||
) ?usize {
|
||||
if (address >= 0x2000) return null;
|
||||
return address;
|
||||
}
|
||||
|
||||
pub fn mirroring(
|
||||
self: *const Mapper210,
|
||||
) Mirroring {
|
||||
return self.mirroring_mode;
|
||||
}
|
||||
|
||||
pub fn irqAsserted(_: *const Mapper210) bool {
|
||||
return false;
|
||||
}
|
||||
@@ -1,77 +0,0 @@
|
||||
const types = @import("types.zig");
|
||||
const Mirroring = types.Mirroring;
|
||||
|
||||
const Mapper232 = @This();
|
||||
|
||||
outer_block: u8 = 0,
|
||||
inner_bank: u8 = 0,
|
||||
|
||||
prg_bank_count: usize,
|
||||
mirroring_mode: Mirroring,
|
||||
|
||||
pub fn init(
|
||||
prg_size: usize,
|
||||
_mirroring: Mirroring,
|
||||
) Mapper232 {
|
||||
return .{
|
||||
.prg_bank_count = prg_size / 0x4000,
|
||||
.mirroring_mode = _mirroring,
|
||||
};
|
||||
}
|
||||
|
||||
pub fn cpuMapRead(
|
||||
self: *const Mapper232,
|
||||
address: u16,
|
||||
) ?usize {
|
||||
if (address < 0x8000) return null;
|
||||
|
||||
const block_start = (@as(usize, self.outer_block) * 4) % self.prg_bank_count;
|
||||
|
||||
if (address < 0xC000) {
|
||||
const offset = @as(usize, address - 0x8000);
|
||||
const bank = (block_start + (@as(usize, self.inner_bank) & 3)) % self.prg_bank_count;
|
||||
return bank * 0x4000 + offset;
|
||||
} else {
|
||||
const offset = @as(usize, address - 0xC000);
|
||||
const bank = (block_start + 3) % self.prg_bank_count;
|
||||
return bank * 0x4000 + offset;
|
||||
}
|
||||
}
|
||||
|
||||
pub fn cpuWrite(
|
||||
self: *Mapper232,
|
||||
address: u16,
|
||||
value: u8,
|
||||
) void {
|
||||
if (address >= 0x8000 and address <= 0x9FFF) {
|
||||
self.outer_block = (value >> 3) & 0x03;
|
||||
} else if (address >= 0xA000) {
|
||||
self.inner_bank = value & 0x03;
|
||||
}
|
||||
}
|
||||
|
||||
pub fn ppuMapRead(
|
||||
_: *const Mapper232,
|
||||
address: u16,
|
||||
) ?usize {
|
||||
if (address >= 0x2000) return null;
|
||||
return address;
|
||||
}
|
||||
|
||||
pub fn ppuMapWrite(
|
||||
_: *const Mapper232,
|
||||
address: u16,
|
||||
) ?usize {
|
||||
if (address >= 0x2000) return null;
|
||||
return address;
|
||||
}
|
||||
|
||||
pub fn mirroring(
|
||||
self: *const Mapper232,
|
||||
) Mirroring {
|
||||
return self.mirroring_mode;
|
||||
}
|
||||
|
||||
pub fn irqAsserted(_: *const Mapper232) bool {
|
||||
return false;
|
||||
}
|
||||
@@ -1,92 +0,0 @@
|
||||
const types = @import("types.zig");
|
||||
const Mirroring = types.Mirroring;
|
||||
|
||||
const Mapper34 = @This();
|
||||
|
||||
prg_bank: u8 = 0,
|
||||
chr_bank0: u8 = 0,
|
||||
chr_bank1: u8 = 1,
|
||||
|
||||
prg_bank_count: usize,
|
||||
chr_bank_count: usize,
|
||||
fixed_mirroring: Mirroring,
|
||||
is_nina01: bool,
|
||||
|
||||
pub fn init(
|
||||
prg_size: usize,
|
||||
chr_size: usize,
|
||||
mirroring_mode: Mirroring,
|
||||
) Mapper34 {
|
||||
return .{
|
||||
.prg_bank_count = if (prg_size >= 0x8000) prg_size / 0x8000 else 1,
|
||||
.chr_bank_count = if (chr_size > 0) chr_size / 0x1000 else 1,
|
||||
.fixed_mirroring = mirroring_mode,
|
||||
.is_nina01 = (chr_size > 0),
|
||||
};
|
||||
}
|
||||
|
||||
pub fn cpuMapRead(
|
||||
self: *const Mapper34,
|
||||
address: u16,
|
||||
) ?usize {
|
||||
if (address < 0x8000) return null;
|
||||
|
||||
const offset = @as(usize, address & 0x7fff);
|
||||
const bank = @as(usize, self.prg_bank) % self.prg_bank_count;
|
||||
|
||||
return bank * 0x8000 + offset;
|
||||
}
|
||||
|
||||
pub fn cpuWrite(
|
||||
self: *Mapper34,
|
||||
address: u16,
|
||||
value: u8,
|
||||
) void {
|
||||
if (self.is_nina01) {
|
||||
switch (address) {
|
||||
0x7ffd => self.prg_bank = value,
|
||||
0x7ffe => self.chr_bank0 = value,
|
||||
0x7fff => self.chr_bank1 = value,
|
||||
else => {},
|
||||
}
|
||||
} else {
|
||||
if (address >= 0x8000) {
|
||||
self.prg_bank = value;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn ppuMapRead(
|
||||
self: *const Mapper34,
|
||||
address: u16,
|
||||
) ?usize {
|
||||
if (address >= 0x2000) return null;
|
||||
|
||||
if (self.is_nina01) {
|
||||
const offset = @as(usize, address & 0x0fff);
|
||||
const bank_raw = if (address < 0x1000) self.chr_bank0 else self.chr_bank1;
|
||||
const bank = @as(usize, bank_raw) % self.chr_bank_count;
|
||||
return bank * 0x1000 + offset;
|
||||
} else {
|
||||
return @as(usize, address);
|
||||
}
|
||||
}
|
||||
|
||||
pub fn ppuMapWrite(
|
||||
self: *const Mapper34,
|
||||
address: u16,
|
||||
) ?usize {
|
||||
return self.ppuMapRead(address);
|
||||
}
|
||||
|
||||
pub fn mirroring(
|
||||
self: *const Mapper34,
|
||||
) Mirroring {
|
||||
return self.fixed_mirroring;
|
||||
}
|
||||
|
||||
pub fn irqAsserted(
|
||||
_: *const Mapper34,
|
||||
) bool {
|
||||
return false;
|
||||
}
|
||||
@@ -1,118 +0,0 @@
|
||||
const types = @import("types.zig");
|
||||
const Mirroring = types.Mirroring;
|
||||
|
||||
const Mapper65 = @This();
|
||||
|
||||
prg_banks: [3]u8 = [_]u8{ 0, 1, 2 },
|
||||
chr_banks: [8]u8 = [_]u8{ 0, 1, 2, 3, 4, 5, 6, 7 },
|
||||
|
||||
mirroring_mode: Mirroring = .vertical,
|
||||
|
||||
irq_enabled: bool = false,
|
||||
irq_counter: u16 = 0,
|
||||
irq_reload: u16 = 0,
|
||||
irq_assert: bool = false,
|
||||
|
||||
prg_bank_count: usize,
|
||||
chr_bank_count: usize,
|
||||
|
||||
pub fn init(
|
||||
prg_size: usize,
|
||||
chr_size: usize,
|
||||
mirroring_mode: Mirroring,
|
||||
) Mapper65 {
|
||||
return .{
|
||||
.prg_bank_count = prg_size / 0x2000,
|
||||
.chr_bank_count = if (chr_size > 0) chr_size / 0x0400 else 1,
|
||||
.mirroring_mode = mirroring_mode,
|
||||
};
|
||||
}
|
||||
|
||||
pub fn cpuMapRead(
|
||||
self: *const Mapper65,
|
||||
address: u16,
|
||||
) ?usize {
|
||||
if (address < 0x8000) return null;
|
||||
|
||||
const offset = @as(usize, address & 0x1fff);
|
||||
const last_bank = self.prg_bank_count - 1;
|
||||
|
||||
switch ((address - 0x8000) / 0x2000) {
|
||||
0 => return (@as(usize, self.prg_banks[0]) % self.prg_bank_count) * 0x2000 + offset,
|
||||
1 => return (@as(usize, self.prg_banks[1]) % self.prg_bank_count) * 0x2000 + offset,
|
||||
2 => return (@as(usize, self.prg_banks[2]) % self.prg_bank_count) * 0x2000 + offset,
|
||||
3 => return last_bank * 0x2000 + offset,
|
||||
else => unreachable,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn cpuWrite(
|
||||
self: *Mapper65,
|
||||
address: u16,
|
||||
value: u8,
|
||||
) void {
|
||||
if (address < 0x8000) return;
|
||||
|
||||
switch (address) {
|
||||
0x8000 => self.prg_banks[0] = value,
|
||||
0xa000 => self.prg_banks[1] = value,
|
||||
0xc000 => self.prg_banks[2] = value,
|
||||
|
||||
0x9001 => self.mirroring_mode = if ((value & 0x80) != 0) .horizontal else .vertical,
|
||||
|
||||
0x9003 => {
|
||||
self.irq_enabled = (value & 0x80) != 0;
|
||||
self.irq_assert = false;
|
||||
},
|
||||
0x9004 => {
|
||||
self.irq_reload = (self.irq_reload & 0xff00) | value;
|
||||
},
|
||||
0x9005 => {
|
||||
self.irq_reload = (self.irq_reload & 0x00ff) | (@as(u16, value) << 8);
|
||||
self.irq_counter = self.irq_reload;
|
||||
self.irq_assert = false;
|
||||
},
|
||||
|
||||
0xb000 => self.chr_banks[0] = value,
|
||||
0xb001 => self.chr_banks[1] = value,
|
||||
0xb002 => self.chr_banks[2] = value,
|
||||
0xb003 => self.chr_banks[3] = value,
|
||||
0xb004 => self.chr_banks[4] = value,
|
||||
0xb005 => self.chr_banks[5] = value,
|
||||
0xb006 => self.chr_banks[6] = value,
|
||||
0xb007 => self.chr_banks[7] = value,
|
||||
else => {},
|
||||
}
|
||||
}
|
||||
|
||||
pub fn ppuMapRead(
|
||||
self: *const Mapper65,
|
||||
address: u16,
|
||||
) ?usize {
|
||||
if (address >= 0x2000) return null;
|
||||
|
||||
const slot = address / 0x0400;
|
||||
const offset = @as(usize, address & 0x03ff);
|
||||
const bank = @as(usize, self.chr_banks[slot]) % self.chr_bank_count;
|
||||
|
||||
return bank * 0x0400 + offset;
|
||||
}
|
||||
|
||||
pub fn ppuMapWrite(
|
||||
self: *const Mapper65,
|
||||
address: u16,
|
||||
) ?usize {
|
||||
return self.ppuMapRead(address);
|
||||
}
|
||||
|
||||
pub fn mirroring(
|
||||
self: *const Mapper65,
|
||||
) Mirroring {
|
||||
return self.mirroring_mode;
|
||||
}
|
||||
|
||||
pub fn irqAsserted(
|
||||
self: *const Mapper65,
|
||||
) bool {
|
||||
return self.irq_assert;
|
||||
}
|
||||
@@ -1,103 +0,0 @@
|
||||
const types = @import("types.zig");
|
||||
const Mirroring = types.Mirroring;
|
||||
|
||||
const Mapper68 = @This();
|
||||
|
||||
chr_banks: [4]u8 = [_]u8{ 0, 0, 0, 0 },
|
||||
nt_banks: [2]u8 = [_]u8{ 0, 0 },
|
||||
prg_bank: u8 = 0,
|
||||
nametable_control: u8 = 0,
|
||||
|
||||
prg_bank_count: usize,
|
||||
chr_bank_count: usize,
|
||||
fixed_mirroring: Mirroring,
|
||||
|
||||
pub fn init(
|
||||
prg_size: usize,
|
||||
chr_size: usize,
|
||||
mirroring_mode: Mirroring,
|
||||
) Mapper68 {
|
||||
return .{
|
||||
.prg_bank_count = prg_size / 0x4000,
|
||||
.chr_bank_count = if (chr_size > 0) chr_size / 0x0800 else 1,
|
||||
.fixed_mirroring = mirroring_mode,
|
||||
};
|
||||
}
|
||||
|
||||
pub fn cpuMapRead(
|
||||
self: *const Mapper68,
|
||||
address: u16,
|
||||
) ?usize {
|
||||
if (address < 0x8000) return null;
|
||||
|
||||
const offset = @as(usize, address & 0x3fff);
|
||||
const last_bank = self.prg_bank_count - 1;
|
||||
|
||||
if (address < 0xc000) {
|
||||
const bank = @as(usize, self.prg_bank) % self.prg_bank_count;
|
||||
return bank * 0x4000 + offset;
|
||||
} else {
|
||||
return last_bank * 0x4000 + offset;
|
||||
}
|
||||
}
|
||||
|
||||
pub fn cpuWrite(
|
||||
self: *Mapper68,
|
||||
address: u16,
|
||||
value: u8,
|
||||
) void {
|
||||
if (address < 0x8000) return;
|
||||
|
||||
switch (address & 0xf000) {
|
||||
0x8000 => self.chr_banks[0] = value,
|
||||
0x9000 => self.chr_banks[1] = value,
|
||||
0xa000 => self.chr_banks[2] = value,
|
||||
0xb000 => self.chr_banks[3] = value,
|
||||
0xc000 => self.nt_banks[0] = value,
|
||||
0xd000 => self.nt_banks[1] = value,
|
||||
0xe000 => self.nametable_control = value,
|
||||
0xf000 => self.prg_bank = value & 0x0f,
|
||||
else => {},
|
||||
}
|
||||
}
|
||||
|
||||
pub fn ppuMapRead(
|
||||
self: *const Mapper68,
|
||||
address: u16,
|
||||
) ?usize {
|
||||
if (address >= 0x2000) return null;
|
||||
|
||||
const slot = address / 0x0800;
|
||||
const offset = @as(usize, address & 0x07ff);
|
||||
const bank = @as(usize, self.chr_banks[slot]) % self.chr_bank_count;
|
||||
|
||||
return bank * 0x0800 + offset;
|
||||
}
|
||||
|
||||
pub fn ppuMapWrite(
|
||||
self: *const Mapper68,
|
||||
address: u16,
|
||||
) ?usize {
|
||||
return self.ppuMapRead(address);
|
||||
}
|
||||
|
||||
pub fn mirroring(
|
||||
self: *const Mapper68,
|
||||
) Mirroring {
|
||||
if ((self.nametable_control & 0x10) == 0) {
|
||||
switch (self.nametable_control & 0x03) {
|
||||
0 => return .vertical,
|
||||
1 => return .horizontal,
|
||||
2 => return .single_screen_lower,
|
||||
3 => return .single_screen_upper,
|
||||
else => unreachable,
|
||||
}
|
||||
}
|
||||
return self.fixed_mirroring;
|
||||
}
|
||||
|
||||
pub fn irqAsserted(
|
||||
_: *const Mapper68,
|
||||
) bool {
|
||||
return false;
|
||||
}
|
||||
@@ -1,123 +0,0 @@
|
||||
const types = @import("types.zig");
|
||||
const Mirroring = types.Mirroring;
|
||||
|
||||
const Mapper69 = @This();
|
||||
|
||||
command: u8 = 0,
|
||||
chr_banks: [8]u8 = [_]u8{ 0, 1, 2, 3, 4, 5, 6, 7 },
|
||||
prg_banks: [4]u8 = [_]u8{ 0, 1, 2, 3 }, // $6000, $8000, $A000, $C000
|
||||
mirroring_mode: Mirroring = .vertical,
|
||||
|
||||
irq_enabled: bool = false,
|
||||
irq_counter: u16 = 0,
|
||||
irq_assert: bool = false,
|
||||
|
||||
prg_bank_count: usize,
|
||||
chr_bank_count: usize,
|
||||
|
||||
pub fn init(
|
||||
prg_size: usize,
|
||||
chr_size: usize,
|
||||
mirroring_mode: Mirroring,
|
||||
) Mapper69 {
|
||||
return .{
|
||||
.prg_bank_count = prg_size / 0x2000,
|
||||
.chr_bank_count = if (chr_size > 0) chr_size / 0x0400 else 1,
|
||||
.mirroring_mode = mirroring_mode,
|
||||
};
|
||||
}
|
||||
|
||||
pub fn cpuMapRead(
|
||||
self: *const Mapper69,
|
||||
address: u16,
|
||||
) ?usize {
|
||||
if (address < 0x6000) return null;
|
||||
|
||||
const offset = @as(usize, address & 0x1fff);
|
||||
const last_bank = self.prg_bank_count - 1;
|
||||
|
||||
if (address < 0x8000) {
|
||||
const bank = @as(usize, self.prg_banks[0] & 0x3f) % self.prg_bank_count;
|
||||
return bank * 0x2000 + offset;
|
||||
}
|
||||
|
||||
switch ((address - 0x8000) / 0x2000) {
|
||||
0 => return (@as(usize, self.prg_banks[1] & 0x3f) % self.prg_bank_count) * 0x2000 + offset,
|
||||
1 => return (@as(usize, self.prg_banks[2] & 0x3f) % self.prg_bank_count) * 0x2000 + offset,
|
||||
2 => return (@as(usize, self.prg_banks[3] & 0x3f) % self.prg_bank_count) * 0x2000 + offset,
|
||||
3 => return last_bank * 0x2000 + offset,
|
||||
else => unreachable,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn cpuWrite(
|
||||
self: *Mapper69,
|
||||
address: u16,
|
||||
value: u8,
|
||||
) void {
|
||||
if (address < 0x8000) return;
|
||||
|
||||
if (address < 0xa000) {
|
||||
self.command = value & 0x0f;
|
||||
} else if (address < 0xc000) {
|
||||
switch (self.command) {
|
||||
0...7 => |i| self.chr_banks[i] = value,
|
||||
8 => self.prg_banks[0] = value,
|
||||
9 => self.prg_banks[1] = value,
|
||||
0xa => self.prg_banks[2] = value,
|
||||
0xb => self.prg_banks[3] = value,
|
||||
0xc => {
|
||||
switch (value & 0x03) {
|
||||
0 => self.mirroring_mode = .vertical,
|
||||
1 => self.mirroring_mode = .horizontal,
|
||||
2 => self.mirroring_mode = .single_screen_lower,
|
||||
3 => self.mirroring_mode = .single_screen_upper,
|
||||
else => unreachable,
|
||||
}
|
||||
},
|
||||
0xd => {
|
||||
self.irq_enabled = (value & 0x01) != 0;
|
||||
self.irq_assert = false;
|
||||
},
|
||||
0xe => {
|
||||
self.irq_counter = (self.irq_counter & 0xff00) | value;
|
||||
},
|
||||
0xf => {
|
||||
self.irq_counter = (self.irq_counter & 0x00ff) | (@as(u16, value) << 8);
|
||||
},
|
||||
else => {},
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn ppuMapRead(
|
||||
self: *const Mapper69,
|
||||
address: u16,
|
||||
) ?usize {
|
||||
if (address >= 0x2000) return null;
|
||||
|
||||
const slot = address / 0x0400;
|
||||
const offset = @as(usize, address & 0x03ff);
|
||||
const bank = @as(usize, self.chr_banks[slot]) % self.chr_bank_count;
|
||||
|
||||
return bank * 0x0400 + offset;
|
||||
}
|
||||
|
||||
pub fn ppuMapWrite(
|
||||
self: *const Mapper69,
|
||||
address: u16,
|
||||
) ?usize {
|
||||
return self.ppuMapRead(address);
|
||||
}
|
||||
|
||||
pub fn mirroring(
|
||||
self: *const Mapper69,
|
||||
) Mirroring {
|
||||
return self.mirroring_mode;
|
||||
}
|
||||
|
||||
pub fn irqAsserted(
|
||||
self: *const Mapper69,
|
||||
) bool {
|
||||
return self.irq_assert;
|
||||
}
|
||||
@@ -1,91 +0,0 @@
|
||||
const types = @import("types.zig");
|
||||
const Mirroring = types.Mirroring;
|
||||
|
||||
const Mapper76 = @This();
|
||||
|
||||
bank_select: u8 = 0,
|
||||
bank_registers: [8]u8 = [_]u8{0} ** 8,
|
||||
prg_bank_count: usize,
|
||||
chr_bank_count: usize,
|
||||
fixed_mirroring: Mirroring,
|
||||
|
||||
pub fn init(
|
||||
prg_size: usize,
|
||||
chr_size: usize,
|
||||
mirroring_mode: Mirroring,
|
||||
) Mapper76 {
|
||||
return .{
|
||||
.prg_bank_count = prg_size / 0x2000,
|
||||
.chr_bank_count = if (chr_size > 0) chr_size / 0x0800 else 1,
|
||||
.fixed_mirroring = mirroring_mode,
|
||||
};
|
||||
}
|
||||
|
||||
pub fn cpuMapRead(
|
||||
self: *const Mapper76,
|
||||
address: u16,
|
||||
) ?usize {
|
||||
if (address < 0x8000) return null;
|
||||
|
||||
const offset = @as(usize, address & 0x1fff);
|
||||
const last_bank = self.prg_bank_count - 1;
|
||||
const second_last_bank = self.prg_bank_count - 2;
|
||||
|
||||
const r6 = @as(usize, self.bank_registers[6]) % self.prg_bank_count;
|
||||
const r7 = @as(usize, self.bank_registers[7]) % self.prg_bank_count;
|
||||
|
||||
switch ((address - 0x8000) / 0x2000) {
|
||||
0 => return r6 * 0x2000 + offset,
|
||||
1 => return r7 * 0x2000 + offset,
|
||||
2 => return second_last_bank * 0x2000 + offset,
|
||||
3 => return last_bank * 0x2000 + offset,
|
||||
else => unreachable,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn cpuWrite(
|
||||
self: *Mapper76,
|
||||
address: u16,
|
||||
value: u8,
|
||||
) void {
|
||||
if (address < 0x8000) return;
|
||||
|
||||
if ((address & 1) == 0) {
|
||||
self.bank_select = value & 0x07;
|
||||
} else {
|
||||
self.bank_registers[self.bank_select] = value;
|
||||
}
|
||||
}
|
||||
|
||||
pub fn ppuMapRead(
|
||||
self: *const Mapper76,
|
||||
address: u16,
|
||||
) ?usize {
|
||||
if (address >= 0x2000) return null;
|
||||
|
||||
const offset = @as(usize, address & 0x07ff);
|
||||
const slot = address / 0x0800;
|
||||
const reg_idx = @as(usize, 2 + slot);
|
||||
const bank = (@as(usize, self.bank_registers[reg_idx])) % self.chr_bank_count;
|
||||
|
||||
return bank * 0x0800 + offset;
|
||||
}
|
||||
|
||||
pub fn ppuMapWrite(
|
||||
self: *const Mapper76,
|
||||
address: u16,
|
||||
) ?usize {
|
||||
return self.ppuMapRead(address);
|
||||
}
|
||||
|
||||
pub fn mirroring(
|
||||
self: *const Mapper76,
|
||||
) Mirroring {
|
||||
return self.fixed_mirroring;
|
||||
}
|
||||
|
||||
pub fn irqAsserted(
|
||||
_: *const Mapper76,
|
||||
) bool {
|
||||
return false;
|
||||
}
|
||||
@@ -1,70 +0,0 @@
|
||||
const types = @import("types.zig");
|
||||
const Mirroring = types.Mirroring;
|
||||
|
||||
const Mapper87 = @This();
|
||||
|
||||
chr_bank: u8 = 0,
|
||||
prg_bank_count: usize,
|
||||
chr_bank_count: usize,
|
||||
mirroring_mode: Mirroring,
|
||||
|
||||
pub fn init(
|
||||
prg_size: usize,
|
||||
chr_size: usize,
|
||||
_mirroring: Mirroring,
|
||||
) Mapper87 {
|
||||
return .{
|
||||
.prg_bank_count = prg_size / 0x8000,
|
||||
.chr_bank_count = if (chr_size > 0) chr_size / 0x2000 else 1,
|
||||
.mirroring_mode = _mirroring,
|
||||
};
|
||||
}
|
||||
|
||||
pub fn cpuMapRead(
|
||||
self: *const Mapper87,
|
||||
address: u16,
|
||||
) ?usize {
|
||||
if (address < 0x8000) return null;
|
||||
|
||||
const offset = @as(usize, address - 0x8000);
|
||||
const bank = if (self.prg_bank_count > 0) (self.prg_bank_count - 1) else 0;
|
||||
return bank * 0x8000 + offset;
|
||||
}
|
||||
|
||||
pub fn cpuWrite(
|
||||
self: *Mapper87,
|
||||
address: u16,
|
||||
value: u8,
|
||||
) void {
|
||||
if (address >= 0x6000 and address <= 0x7FFF) {
|
||||
self.chr_bank = ((value & 1) << 1) | ((value >> 1) & 1);
|
||||
}
|
||||
}
|
||||
|
||||
pub fn ppuMapRead(
|
||||
self: *const Mapper87,
|
||||
address: u16,
|
||||
) ?usize {
|
||||
if (address >= 0x2000) return null;
|
||||
|
||||
const bank = @as(usize, self.chr_bank) % self.chr_bank_count;
|
||||
return bank * 0x2000 + @as(usize, address);
|
||||
}
|
||||
|
||||
pub fn ppuMapWrite(
|
||||
_: *const Mapper87,
|
||||
address: u16,
|
||||
) ?usize {
|
||||
if (address >= 0x2000) return null;
|
||||
return address;
|
||||
}
|
||||
|
||||
pub fn mirroring(
|
||||
self: *const Mapper87,
|
||||
) Mirroring {
|
||||
return self.mirroring_mode;
|
||||
}
|
||||
|
||||
pub fn irqAsserted(_: *const Mapper87) bool {
|
||||
return false;
|
||||
}
|
||||
@@ -1,103 +0,0 @@
|
||||
const types = @import("types.zig");
|
||||
const Mirroring = types.Mirroring;
|
||||
|
||||
const Mapper88 = @This();
|
||||
|
||||
bank_select: u8 = 0,
|
||||
bank_registers: [8]u8 = [_]u8{0} ** 8,
|
||||
prg_bank_count: usize,
|
||||
chr_bank_count: usize,
|
||||
fixed_mirroring: Mirroring,
|
||||
|
||||
pub fn init(
|
||||
prg_size: usize,
|
||||
chr_size: usize,
|
||||
mirroring_mode: Mirroring,
|
||||
) Mapper88 {
|
||||
return .{
|
||||
.prg_bank_count = prg_size / 0x2000,
|
||||
.chr_bank_count = if (chr_size > 0) chr_size / 0x0400 else 1,
|
||||
.fixed_mirroring = mirroring_mode,
|
||||
};
|
||||
}
|
||||
|
||||
pub fn cpuMapRead(
|
||||
self: *const Mapper88,
|
||||
address: u16,
|
||||
) ?usize {
|
||||
if (address < 0x8000) return null;
|
||||
|
||||
const offset = @as(usize, address & 0x1fff);
|
||||
const last_bank = self.prg_bank_count - 1;
|
||||
const second_last_bank = self.prg_bank_count - 2;
|
||||
|
||||
const r6 = @as(usize, self.bank_registers[6] & 0x3f) % self.prg_bank_count;
|
||||
const r7 = @as(usize, self.bank_registers[7] & 0x3f) % self.prg_bank_count;
|
||||
|
||||
switch ((address - 0x8000) / 0x2000) {
|
||||
0 => return r6 * 0x2000 + offset,
|
||||
1 => return r7 * 0x2000 + offset,
|
||||
2 => return second_last_bank * 0x2000 + offset,
|
||||
3 => return last_bank * 0x2000 + offset,
|
||||
else => unreachable,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn cpuWrite(
|
||||
self: *Mapper88,
|
||||
address: u16,
|
||||
value: u8,
|
||||
) void {
|
||||
if (address < 0x8000) return;
|
||||
|
||||
if ((address & 1) == 0) {
|
||||
self.bank_select = value & 0x07;
|
||||
} else {
|
||||
self.bank_registers[self.bank_select] = value;
|
||||
}
|
||||
}
|
||||
|
||||
pub fn ppuMapRead(
|
||||
self: *const Mapper88,
|
||||
address: u16,
|
||||
) ?usize {
|
||||
if (address >= 0x2000) return null;
|
||||
|
||||
const offset = @as(usize, address & 0x03ff);
|
||||
const page = address / 0x0400;
|
||||
|
||||
var bank: usize = 0;
|
||||
switch (page) {
|
||||
0, 1 => bank = (@as(usize, self.bank_registers[0] & 0xfe) + (page & 1)),
|
||||
2, 3 => bank = (@as(usize, self.bank_registers[1] & 0xfe) + (page & 1)),
|
||||
4, 5, 6, 7 => {
|
||||
const reg_idx = page - 2; // registers 2, 3, 4, 5
|
||||
const val = self.bank_registers[reg_idx];
|
||||
// Bit 6 maps to 64KB CHR block selection (bit 6 -> 0x40 added to bank number)
|
||||
bank = (@as(usize, val & 0x3f) | (@as(usize, val & 0x40)));
|
||||
},
|
||||
else => unreachable,
|
||||
}
|
||||
|
||||
bank = bank % self.chr_bank_count;
|
||||
return bank * 0x0400 + offset;
|
||||
}
|
||||
|
||||
pub fn ppuMapWrite(
|
||||
self: *const Mapper88,
|
||||
address: u16,
|
||||
) ?usize {
|
||||
return self.ppuMapRead(address);
|
||||
}
|
||||
|
||||
pub fn mirroring(
|
||||
self: *const Mapper88,
|
||||
) Mirroring {
|
||||
return self.fixed_mirroring;
|
||||
}
|
||||
|
||||
pub fn irqAsserted(
|
||||
_: *const Mapper88,
|
||||
) bool {
|
||||
return false;
|
||||
}
|
||||
@@ -1,78 +0,0 @@
|
||||
const types = @import("types.zig");
|
||||
const Mirroring = types.Mirroring;
|
||||
|
||||
const Mapper89 = @This();
|
||||
|
||||
prg_bank: u8 = 0,
|
||||
chr_bank: u8 = 0,
|
||||
mirroring_mode: Mirroring = .single_screen_lower,
|
||||
|
||||
prg_bank_count: usize,
|
||||
chr_bank_count: usize,
|
||||
|
||||
pub fn init(
|
||||
prg_size: usize,
|
||||
chr_size: usize,
|
||||
) Mapper89 {
|
||||
return .{
|
||||
.prg_bank_count = prg_size / 0x4000,
|
||||
.chr_bank_count = if (chr_size > 0) chr_size / 0x2000 else 1,
|
||||
};
|
||||
}
|
||||
|
||||
pub fn cpuMapRead(
|
||||
self: *const Mapper89,
|
||||
address: u16,
|
||||
) ?usize {
|
||||
if (address < 0x8000) return null;
|
||||
|
||||
if (address < 0xC000) {
|
||||
const offset = @as(usize, address - 0x8000);
|
||||
const bank = @as(usize, self.prg_bank) % self.prg_bank_count;
|
||||
return bank * 0x4000 + offset;
|
||||
} else {
|
||||
const offset = @as(usize, address - 0xC000);
|
||||
const fixed_bank = if (self.prg_bank_count > 0) self.prg_bank_count - 1 else 0;
|
||||
return fixed_bank * 0x4000 + offset;
|
||||
}
|
||||
}
|
||||
|
||||
pub fn cpuWrite(
|
||||
self: *Mapper89,
|
||||
address: u16,
|
||||
value: u8,
|
||||
) void {
|
||||
if (address >= 0x8000) {
|
||||
self.prg_bank = (value >> 4) & 0x07;
|
||||
self.chr_bank = (value & 0x07) | ((value & 0x80) >> 4);
|
||||
self.mirroring_mode = if ((value & 0x08) != 0) .single_screen_upper else .single_screen_lower;
|
||||
}
|
||||
}
|
||||
|
||||
pub fn ppuMapRead(
|
||||
self: *const Mapper89,
|
||||
address: u16,
|
||||
) ?usize {
|
||||
if (address >= 0x2000) return null;
|
||||
|
||||
const bank = @as(usize, self.chr_bank) % self.chr_bank_count;
|
||||
return bank * 0x2000 + @as(usize, address);
|
||||
}
|
||||
|
||||
pub fn ppuMapWrite(
|
||||
_: *const Mapper89,
|
||||
address: u16,
|
||||
) ?usize {
|
||||
if (address >= 0x2000) return null;
|
||||
return address;
|
||||
}
|
||||
|
||||
pub fn mirroring(
|
||||
self: *const Mapper89,
|
||||
) Mirroring {
|
||||
return self.mirroring_mode;
|
||||
}
|
||||
|
||||
pub fn irqAsserted(_: *const Mapper89) bool {
|
||||
return false;
|
||||
}
|
||||
@@ -1,73 +0,0 @@
|
||||
const types = @import("types.zig");
|
||||
const Mirroring = types.Mirroring;
|
||||
|
||||
const Mapper93 = @This();
|
||||
|
||||
prg_bank: u8 = 0,
|
||||
mirroring_mode: Mirroring,
|
||||
|
||||
prg_bank_count: usize,
|
||||
|
||||
pub fn init(
|
||||
prg_size: usize,
|
||||
_mirroring: Mirroring,
|
||||
) Mapper93 {
|
||||
return .{
|
||||
.prg_bank_count = prg_size / 0x4000,
|
||||
.mirroring_mode = _mirroring,
|
||||
};
|
||||
}
|
||||
|
||||
pub fn cpuMapRead(
|
||||
self: *const Mapper93,
|
||||
address: u16,
|
||||
) ?usize {
|
||||
if (address < 0x8000) return null;
|
||||
|
||||
if (address < 0xC000) {
|
||||
const offset = @as(usize, address - 0x8000);
|
||||
const bank = @as(usize, self.prg_bank) % self.prg_bank_count;
|
||||
return bank * 0x4000 + offset;
|
||||
} else {
|
||||
const offset = @as(usize, address - 0xC000);
|
||||
const fixed_bank = if (self.prg_bank_count > 0) self.prg_bank_count - 1 else 0;
|
||||
return fixed_bank * 0x4000 + offset;
|
||||
}
|
||||
}
|
||||
|
||||
pub fn cpuWrite(
|
||||
self: *Mapper93,
|
||||
address: u16,
|
||||
value: u8,
|
||||
) void {
|
||||
if (address >= 0x8000) {
|
||||
self.prg_bank = (value >> 4) & 0x07;
|
||||
self.mirroring_mode = if ((value & 1) != 0) .horizontal else .vertical;
|
||||
}
|
||||
}
|
||||
|
||||
pub fn ppuMapRead(
|
||||
_: *const Mapper93,
|
||||
address: u16,
|
||||
) ?usize {
|
||||
if (address >= 0x2000) return null;
|
||||
return address;
|
||||
}
|
||||
|
||||
pub fn ppuMapWrite(
|
||||
_: *const Mapper93,
|
||||
address: u16,
|
||||
) ?usize {
|
||||
if (address >= 0x2000) return null;
|
||||
return address;
|
||||
}
|
||||
|
||||
pub fn mirroring(
|
||||
self: *const Mapper93,
|
||||
) Mirroring {
|
||||
return self.mirroring_mode;
|
||||
}
|
||||
|
||||
pub fn irqAsserted(_: *const Mapper93) bool {
|
||||
return false;
|
||||
}
|
||||
@@ -1,71 +0,0 @@
|
||||
const types = @import("types.zig");
|
||||
const Mirroring = types.Mirroring;
|
||||
|
||||
const Mapper94 = @This();
|
||||
|
||||
prg_bank: u8 = 0,
|
||||
prg_bank_count: usize,
|
||||
mirroring_mode: Mirroring,
|
||||
|
||||
pub fn init(
|
||||
prg_size: usize,
|
||||
_mirroring: Mirroring,
|
||||
) Mapper94 {
|
||||
return .{
|
||||
.prg_bank_count = prg_size / 0x4000,
|
||||
.mirroring_mode = _mirroring,
|
||||
};
|
||||
}
|
||||
|
||||
pub fn cpuMapRead(
|
||||
self: *const Mapper94,
|
||||
address: u16,
|
||||
) ?usize {
|
||||
if (address < 0x8000) return null;
|
||||
|
||||
if (address < 0xC000) {
|
||||
const offset = @as(usize, address - 0x8000);
|
||||
const bank = @as(usize, self.prg_bank) % self.prg_bank_count;
|
||||
return bank * 0x4000 + offset;
|
||||
} else {
|
||||
const offset = @as(usize, address - 0xC000);
|
||||
const fixed_bank = if (self.prg_bank_count > 0) self.prg_bank_count - 1 else 0;
|
||||
return fixed_bank * 0x4000 + offset;
|
||||
}
|
||||
}
|
||||
|
||||
pub fn cpuWrite(
|
||||
self: *Mapper94,
|
||||
address: u16,
|
||||
value: u8,
|
||||
) void {
|
||||
if (address >= 0x8000) {
|
||||
self.prg_bank = (value >> 2) & 0x07;
|
||||
}
|
||||
}
|
||||
|
||||
pub fn ppuMapRead(
|
||||
_: *const Mapper94,
|
||||
address: u16,
|
||||
) ?usize {
|
||||
if (address >= 0x2000) return null;
|
||||
return address;
|
||||
}
|
||||
|
||||
pub fn ppuMapWrite(
|
||||
_: *const Mapper94,
|
||||
address: u16,
|
||||
) ?usize {
|
||||
if (address >= 0x2000) return null;
|
||||
return address;
|
||||
}
|
||||
|
||||
pub fn mirroring(
|
||||
self: *const Mapper94,
|
||||
) Mirroring {
|
||||
return self.mirroring_mode;
|
||||
}
|
||||
|
||||
pub fn irqAsserted(_: *const Mapper94) bool {
|
||||
return false;
|
||||
}
|
||||
+150
-111
@@ -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
@@ -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
@@ -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
@@ -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);
|
||||
}
|
||||
|
||||
@@ -1,171 +0,0 @@
|
||||
const types = @import("types.zig");
|
||||
const Mirroring = types.Mirroring;
|
||||
|
||||
const Mmc5 = @This();
|
||||
|
||||
prg_mode: u2 = 3,
|
||||
chr_mode: u2 = 0,
|
||||
|
||||
prg_banks: [4]u8 = [_]u8{ 0, 0, 1, 2 }, // $6000, $8000, $A000, $C000
|
||||
chr_banks: [12]u16 = [_]u16{0} ** 12,
|
||||
|
||||
mult_a: u8 = 0,
|
||||
mult_b: u8 = 0,
|
||||
|
||||
irq_scanline: u8 = 0,
|
||||
irq_enabled: bool = false,
|
||||
irq_assert: bool = false,
|
||||
irq_in_frame: bool = false,
|
||||
current_scanline: u8 = 0,
|
||||
|
||||
mirroring_mode: Mirroring = .vertical,
|
||||
ex_ram: [1024]u8 = [_]u8{0} ** 1024,
|
||||
|
||||
prg_bank_count: usize,
|
||||
chr_bank_count: usize,
|
||||
|
||||
pub fn init(
|
||||
prg_size: usize,
|
||||
chr_size: usize,
|
||||
) Mmc5 {
|
||||
return .{
|
||||
.prg_bank_count = prg_size / 0x2000,
|
||||
.chr_bank_count = if (chr_size > 0) chr_size / 0x0400 else 1,
|
||||
};
|
||||
}
|
||||
|
||||
pub fn cpuMapRead(
|
||||
self: *const Mmc5,
|
||||
address: u16,
|
||||
) ?usize {
|
||||
if (address < 0x5c00) return null;
|
||||
|
||||
if (address >= 0x5c00 and address <= 0x5fff) {
|
||||
return 0x80000000 | @as(usize, address - 0x5c00);
|
||||
}
|
||||
|
||||
if (address < 0x8000) {
|
||||
const bank = @as(usize, self.prg_banks[0] & 0x7f) % self.prg_bank_count;
|
||||
return bank * 0x2000 + (address & 0x1fff);
|
||||
}
|
||||
|
||||
const last_bank = self.prg_bank_count - 1;
|
||||
const offset = @as(usize, address & 0x1fff);
|
||||
|
||||
switch ((address - 0x8000) / 0x2000) {
|
||||
0 => return (@as(usize, self.prg_banks[1] & 0x7f) % self.prg_bank_count) * 0x2000 + offset,
|
||||
1 => return (@as(usize, self.prg_banks[2] & 0x7f) % self.prg_bank_count) * 0x2000 + offset,
|
||||
2 => return (@as(usize, self.prg_banks[3] & 0x7f) % self.prg_bank_count) * 0x2000 + offset,
|
||||
3 => return last_bank * 0x2000 + offset,
|
||||
else => unreachable,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn cpuWrite(
|
||||
self: *Mmc5,
|
||||
address: u16,
|
||||
value: u8,
|
||||
) void {
|
||||
if (address >= 0x5c00 and address <= 0x5fff) {
|
||||
self.ex_ram[address - 0x5c00] = value;
|
||||
return;
|
||||
}
|
||||
|
||||
switch (address) {
|
||||
0x5100 => self.prg_mode = @truncate(value & 3),
|
||||
0x5101 => self.chr_mode = @truncate(value & 3),
|
||||
0x5105 => {
|
||||
switch (@as(u2, @truncate(value & 3))) {
|
||||
0 => self.mirroring_mode = .vertical,
|
||||
1 => self.mirroring_mode = .horizontal,
|
||||
2 => self.mirroring_mode = .single_screen_lower,
|
||||
3 => self.mirroring_mode = .single_screen_upper,
|
||||
}
|
||||
},
|
||||
0x5114 => self.prg_banks[0] = value,
|
||||
0x5115 => self.prg_banks[1] = value,
|
||||
0x5116 => self.prg_banks[2] = value,
|
||||
0x5117 => self.prg_banks[3] = value,
|
||||
0x5120...0x512b => |addr| {
|
||||
const idx = addr - 0x5120;
|
||||
self.chr_banks[idx] = value;
|
||||
},
|
||||
0x5203 => self.irq_scanline = value,
|
||||
0x5204 => {
|
||||
self.irq_enabled = (value & 0x80) != 0;
|
||||
self.irq_assert = false;
|
||||
},
|
||||
0x5205 => self.mult_a = value,
|
||||
0x5206 => self.mult_b = value,
|
||||
else => {},
|
||||
}
|
||||
}
|
||||
|
||||
pub fn readRegister(
|
||||
self: *Mmc5,
|
||||
address: u16,
|
||||
) ?u8 {
|
||||
switch (address) {
|
||||
0x5204 => {
|
||||
const res: u8 = (if (self.irq_assert) @as(u8, 0x80) else 0) | (if (self.irq_in_frame) @as(u8, 0x40) else 0);
|
||||
self.irq_assert = false;
|
||||
return res;
|
||||
},
|
||||
0x5205 => {
|
||||
const prod = @as(u16, self.mult_a) * @as(u16, self.mult_b);
|
||||
return @truncate(prod);
|
||||
},
|
||||
0x5206 => {
|
||||
const prod = @as(u16, self.mult_a) * @as(u16, self.mult_b);
|
||||
return @truncate(prod >> 8);
|
||||
},
|
||||
else => return null,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn ppuMapRead(
|
||||
self: *const Mmc5,
|
||||
address: u16,
|
||||
) ?usize {
|
||||
if (address >= 0x2000) return null;
|
||||
|
||||
const slot = address / 0x0400;
|
||||
const offset = @as(usize, address & 0x03ff);
|
||||
const bank = @as(usize, self.chr_banks[slot]) % self.chr_bank_count;
|
||||
|
||||
return bank * 0x0400 + offset;
|
||||
}
|
||||
|
||||
pub fn ppuMapWrite(
|
||||
self: *const Mmc5,
|
||||
address: u16,
|
||||
) ?usize {
|
||||
return self.ppuMapRead(address);
|
||||
}
|
||||
|
||||
pub fn handleScanline(
|
||||
self: *Mmc5,
|
||||
) void {
|
||||
self.current_scanline += 1;
|
||||
if (self.current_scanline == self.irq_scanline) {
|
||||
if (self.irq_enabled) {
|
||||
self.irq_assert = true;
|
||||
}
|
||||
}
|
||||
if (self.current_scanline >= 240) {
|
||||
self.current_scanline = 0;
|
||||
self.irq_in_frame = false;
|
||||
}
|
||||
}
|
||||
|
||||
pub fn mirroring(
|
||||
self: *const Mmc5,
|
||||
) Mirroring {
|
||||
return self.mirroring_mode;
|
||||
}
|
||||
|
||||
pub fn irqAsserted(
|
||||
self: *const Mmc5,
|
||||
) bool {
|
||||
return self.irq_assert;
|
||||
}
|
||||
@@ -1,73 +0,0 @@
|
||||
const types = @import("types.zig");
|
||||
const Mirroring = types.Mirroring;
|
||||
|
||||
const Nina03 = @This();
|
||||
|
||||
prg_bank: u8 = 0,
|
||||
chr_bank: u8 = 0,
|
||||
|
||||
prg_bank_count: usize,
|
||||
chr_bank_count: usize,
|
||||
mirroring_mode: Mirroring,
|
||||
|
||||
pub fn init(
|
||||
prg_size: usize,
|
||||
chr_size: usize,
|
||||
_mirroring: Mirroring,
|
||||
) Nina03 {
|
||||
return .{
|
||||
.prg_bank_count = prg_size / 0x8000,
|
||||
.chr_bank_count = if (chr_size > 0) chr_size / 0x2000 else 1,
|
||||
.mirroring_mode = _mirroring,
|
||||
};
|
||||
}
|
||||
|
||||
pub fn cpuMapRead(
|
||||
self: *const Nina03,
|
||||
address: u16,
|
||||
) ?usize {
|
||||
if (address < 0x8000) return null;
|
||||
|
||||
const offset = @as(usize, address - 0x8000);
|
||||
const bank = @as(usize, self.prg_bank) % self.prg_bank_count;
|
||||
return bank * 0x8000 + offset;
|
||||
}
|
||||
|
||||
pub fn cpuWrite(
|
||||
self: *Nina03,
|
||||
address: u16,
|
||||
value: u8,
|
||||
) void {
|
||||
if (address >= 0x4100 and address <= 0x5FFF) {
|
||||
self.prg_bank = @truncate((value >> 3) & 1);
|
||||
self.chr_bank = value & 0x07;
|
||||
}
|
||||
}
|
||||
|
||||
pub fn ppuMapRead(
|
||||
self: *const Nina03,
|
||||
address: u16,
|
||||
) ?usize {
|
||||
if (address >= 0x2000) return null;
|
||||
|
||||
const bank = @as(usize, self.chr_bank) % self.chr_bank_count;
|
||||
return bank * 0x2000 + @as(usize, address);
|
||||
}
|
||||
|
||||
pub fn ppuMapWrite(
|
||||
_: *const Nina03,
|
||||
address: u16,
|
||||
) ?usize {
|
||||
if (address >= 0x2000) return null;
|
||||
return address;
|
||||
}
|
||||
|
||||
pub fn mirroring(
|
||||
self: *const Nina03,
|
||||
) Mirroring {
|
||||
return self.mirroring_mode;
|
||||
}
|
||||
|
||||
pub fn irqAsserted(_: *const Nina03) bool {
|
||||
return false;
|
||||
}
|
||||
@@ -1,71 +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
@@ -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;
|
||||
}
|
||||
|
||||
@@ -1,9 +0,0 @@
|
||||
pub const Mirroring = enum {
|
||||
horizontal,
|
||||
vertical,
|
||||
four_screen,
|
||||
|
||||
// Needed by later mappers.
|
||||
single_screen_lower,
|
||||
single_screen_upper,
|
||||
};
|
||||
@@ -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());
|
||||
}
|
||||
@@ -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 {}
|
||||
|
||||
@@ -1,106 +0,0 @@
|
||||
const types = @import("types.zig");
|
||||
const Mirroring = types.Mirroring;
|
||||
|
||||
const Vrc1 = @This();
|
||||
|
||||
prg_bank0: u8 = 0,
|
||||
prg_bank1: u8 = 0,
|
||||
prg_bank2: u8 = 0,
|
||||
|
||||
chr_bank0: u8 = 0,
|
||||
chr_bank1: u8 = 0,
|
||||
|
||||
mirroring_mode: Mirroring = .vertical,
|
||||
|
||||
prg_bank_count: usize,
|
||||
chr_bank_count: usize,
|
||||
|
||||
pub fn init(
|
||||
prg_size: usize,
|
||||
chr_size: usize,
|
||||
) Vrc1 {
|
||||
return .{
|
||||
.prg_bank_count = prg_size / 0x2000,
|
||||
.chr_bank_count = if (chr_size > 0) chr_size / 0x1000 else 1,
|
||||
};
|
||||
}
|
||||
|
||||
pub fn cpuMapRead(
|
||||
self: *const Vrc1,
|
||||
address: u16,
|
||||
) ?usize {
|
||||
if (address < 0x8000) return null;
|
||||
|
||||
if (address < 0xA000) {
|
||||
const offset = @as(usize, address - 0x8000);
|
||||
const bank = @as(usize, self.prg_bank0) % self.prg_bank_count;
|
||||
return bank * 0x2000 + offset;
|
||||
} else if (address < 0xC000) {
|
||||
const offset = @as(usize, address - 0xA000);
|
||||
const bank = @as(usize, self.prg_bank1) % self.prg_bank_count;
|
||||
return bank * 0x2000 + offset;
|
||||
} else if (address < 0xE000) {
|
||||
const offset = @as(usize, address - 0xC000);
|
||||
const bank = @as(usize, self.prg_bank2) % self.prg_bank_count;
|
||||
return bank * 0x2000 + offset;
|
||||
} else {
|
||||
const offset = @as(usize, address - 0xE000);
|
||||
const fixed_bank = if (self.prg_bank_count > 0) self.prg_bank_count - 1 else 0;
|
||||
return fixed_bank * 0x2000 + offset;
|
||||
}
|
||||
}
|
||||
|
||||
pub fn cpuWrite(
|
||||
self: *Vrc1,
|
||||
address: u16,
|
||||
value: u8,
|
||||
) void {
|
||||
if (address < 0x8000) return;
|
||||
|
||||
switch (address & 0xF000) {
|
||||
0x8000 => self.prg_bank0 = value & 0x0F,
|
||||
0x9000 => {
|
||||
self.mirroring_mode = if ((value & 1) != 0) .horizontal else .vertical;
|
||||
self.chr_bank0 = (self.chr_bank0 & 0x0F) | ((value & 0x02) << 3);
|
||||
self.chr_bank1 = (self.chr_bank1 & 0x0F) | ((value & 0x04) << 2);
|
||||
},
|
||||
0xA000 => self.prg_bank1 = value & 0x0F,
|
||||
0xC000 => self.prg_bank2 = value & 0x0F,
|
||||
0xE000 => self.chr_bank0 = (self.chr_bank0 & 0x10) | (value & 0x0F),
|
||||
0xF000 => self.chr_bank1 = (self.chr_bank1 & 0x10) | (value & 0x0F),
|
||||
else => {},
|
||||
}
|
||||
}
|
||||
|
||||
pub fn ppuMapRead(
|
||||
self: *const Vrc1,
|
||||
address: u16,
|
||||
) ?usize {
|
||||
if (address >= 0x2000) return null;
|
||||
|
||||
if (address < 0x1000) {
|
||||
const bank = @as(usize, self.chr_bank0) % self.chr_bank_count;
|
||||
return bank * 0x1000 + @as(usize, address);
|
||||
} else {
|
||||
const bank = @as(usize, self.chr_bank1) % self.chr_bank_count;
|
||||
return bank * 0x1000 + @as(usize, address - 0x1000);
|
||||
}
|
||||
}
|
||||
|
||||
pub fn ppuMapWrite(
|
||||
_: *const Vrc1,
|
||||
address: u16,
|
||||
) ?usize {
|
||||
if (address >= 0x2000) return null;
|
||||
return address;
|
||||
}
|
||||
|
||||
pub fn mirroring(
|
||||
self: *const Vrc1,
|
||||
) Mirroring {
|
||||
return self.mirroring_mode;
|
||||
}
|
||||
|
||||
pub fn irqAsserted(_: *const Vrc1) bool {
|
||||
return false;
|
||||
}
|
||||
@@ -1,127 +0,0 @@
|
||||
const types = @import("types.zig");
|
||||
const Mirroring = types.Mirroring;
|
||||
|
||||
const Vrc2 = @This();
|
||||
|
||||
prg_banks: [2]u8 = [_]u8{ 0, 1 },
|
||||
chr_banks_lo: [8]u4 = [_]u4{0} ** 8,
|
||||
chr_banks_hi: [8]u5 = [_]u5{0} ** 8,
|
||||
|
||||
mirroring_mode: Mirroring = .vertical,
|
||||
|
||||
mapper_id: u8,
|
||||
shift_a: u3,
|
||||
shift_b: u3,
|
||||
|
||||
prg_bank_count: usize,
|
||||
chr_bank_count: usize,
|
||||
|
||||
pub fn init(
|
||||
prg_size: usize,
|
||||
chr_size: usize,
|
||||
mapper_id: u8,
|
||||
) Vrc2 {
|
||||
const shift_a: u3 = if (mapper_id == 22) 1 else 0;
|
||||
const shift_b: u3 = if (mapper_id == 22) 0 else 1;
|
||||
|
||||
return .{
|
||||
.mapper_id = mapper_id,
|
||||
.prg_bank_count = prg_size / 0x2000,
|
||||
.chr_bank_count = if (chr_size > 0) chr_size / 0x0400 else 1,
|
||||
.shift_a = shift_a,
|
||||
.shift_b = shift_b,
|
||||
};
|
||||
}
|
||||
|
||||
pub fn cpuMapRead(
|
||||
self: *const Vrc2,
|
||||
address: u16,
|
||||
) ?usize {
|
||||
if (address < 0x8000) return null;
|
||||
|
||||
const offset = @as(usize, address & 0x1fff);
|
||||
const last_bank = self.prg_bank_count - 1;
|
||||
const second_last_bank = self.prg_bank_count - 2;
|
||||
|
||||
const r0 = @as(usize, self.prg_banks[0] & 0x1f) % self.prg_bank_count;
|
||||
const r1 = @as(usize, self.prg_banks[1] & 0x1f) % self.prg_bank_count;
|
||||
|
||||
switch ((address - 0x8000) / 0x2000) {
|
||||
0 => return r0 * 0x2000 + offset,
|
||||
1 => return r1 * 0x2000 + offset,
|
||||
2 => return second_last_bank * 0x2000 + offset,
|
||||
3 => return last_bank * 0x2000 + offset,
|
||||
else => unreachable,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn cpuWrite(
|
||||
self: *Vrc2,
|
||||
address: u16,
|
||||
value: u8,
|
||||
) void {
|
||||
if (address < 0x8000) return;
|
||||
|
||||
const reg_addr = address & 0xf000;
|
||||
const a = @as(u1, @truncate((address >> self.shift_a) & 1));
|
||||
const b = @as(u1, @truncate((address >> self.shift_b) & 1));
|
||||
const line = (@as(u2, b) << 1) | a;
|
||||
|
||||
switch (reg_addr) {
|
||||
0x8000 => self.prg_banks[0] = value & 0x1f,
|
||||
0x9000 => {
|
||||
self.mirroring_mode = if ((value & 0x01) != 0) .horizontal else .vertical;
|
||||
},
|
||||
0xa000 => self.prg_banks[1] = value & 0x1f,
|
||||
0xb000, 0xc000, 0xd000, 0xe000 => {
|
||||
const slot_base = ((reg_addr - 0xb000) / 0x1000) * 2;
|
||||
const is_hi = (line & 2) != 0;
|
||||
const is_odd = (line & 1) != 0;
|
||||
const slot = slot_base + (if (is_odd) @as(usize, 1) else 0);
|
||||
|
||||
if (!is_hi) {
|
||||
self.chr_banks_lo[slot] = @truncate(value & 0x0f);
|
||||
} else {
|
||||
self.chr_banks_hi[slot] = @truncate(value & 0x1f);
|
||||
}
|
||||
},
|
||||
else => {},
|
||||
}
|
||||
}
|
||||
|
||||
pub fn ppuMapRead(
|
||||
self: *const Vrc2,
|
||||
address: u16,
|
||||
) ?usize {
|
||||
if (address >= 0x2000) return null;
|
||||
|
||||
const slot = address / 0x0400;
|
||||
const offset = @as(usize, address & 0x03ff);
|
||||
|
||||
var bank_val = (@as(usize, self.chr_banks_hi[slot]) << 4) | @as(usize, self.chr_banks_lo[slot]);
|
||||
if (self.mapper_id == 22) {
|
||||
bank_val >>= 1;
|
||||
}
|
||||
const bank = bank_val % self.chr_bank_count;
|
||||
|
||||
return bank * 0x0400 + offset;
|
||||
}
|
||||
|
||||
pub fn ppuMapWrite(
|
||||
self: *const Vrc2,
|
||||
address: u16,
|
||||
) ?usize {
|
||||
return self.ppuMapRead(address);
|
||||
}
|
||||
|
||||
pub fn mirroring(
|
||||
self: *const Vrc2,
|
||||
) Mirroring {
|
||||
return self.mirroring_mode;
|
||||
}
|
||||
|
||||
pub fn irqAsserted(
|
||||
_: *const Vrc2,
|
||||
) bool {
|
||||
return false;
|
||||
}
|
||||
@@ -1,118 +0,0 @@
|
||||
const types = @import("types.zig");
|
||||
const Mirroring = types.Mirroring;
|
||||
|
||||
const Vrc3 = @This();
|
||||
|
||||
prg_bank: u8 = 0,
|
||||
prg_bank_count: usize,
|
||||
mirroring_mode: Mirroring,
|
||||
|
||||
irq_latch: u16 = 0,
|
||||
irq_counter: u16 = 0,
|
||||
irq_enabled: bool = false,
|
||||
irq_mode_8bit: bool = false,
|
||||
irq_asserted_flag: bool = false,
|
||||
|
||||
pub fn init(
|
||||
prg_size: usize,
|
||||
_mirroring: Mirroring,
|
||||
) Vrc3 {
|
||||
return .{
|
||||
.prg_bank_count = prg_size / 0x4000,
|
||||
.mirroring_mode = _mirroring,
|
||||
};
|
||||
}
|
||||
|
||||
pub fn cpuMapRead(
|
||||
self: *const Vrc3,
|
||||
address: u16,
|
||||
) ?usize {
|
||||
if (address < 0x8000) return null;
|
||||
|
||||
if (address < 0xC000) {
|
||||
const offset = @as(usize, address - 0x8000);
|
||||
const bank = @as(usize, self.prg_bank) % self.prg_bank_count;
|
||||
return bank * 0x4000 + offset;
|
||||
} else {
|
||||
const offset = @as(usize, address - 0xC000);
|
||||
const fixed_bank = if (self.prg_bank_count > 0) self.prg_bank_count - 1 else 0;
|
||||
return fixed_bank * 0x4000 + offset;
|
||||
}
|
||||
}
|
||||
|
||||
pub fn cpuWrite(
|
||||
self: *Vrc3,
|
||||
address: u16,
|
||||
value: u8,
|
||||
) void {
|
||||
if (address < 0x8000) return;
|
||||
|
||||
switch (address & 0xF000) {
|
||||
0x8000 => self.irq_latch = (self.irq_latch & 0xFFF0) | (value & 0x0F),
|
||||
0x9000 => self.irq_latch = (self.irq_latch & 0xFF0F) | ((@as(u16, value) & 0x0F) << 4),
|
||||
0xA000 => self.irq_latch = (self.irq_latch & 0xF0FF) | ((@as(u16, value) & 0x0F) << 8),
|
||||
0xB000 => self.irq_latch = (self.irq_latch & 0x0FFF) | ((@as(u16, value) & 0x0F) << 12),
|
||||
0xC000 => {
|
||||
self.irq_enabled = (value & 0x02) != 0;
|
||||
self.irq_mode_8bit = (value & 0x04) != 0;
|
||||
self.irq_asserted_flag = false;
|
||||
if (self.irq_enabled) {
|
||||
self.irq_counter = self.irq_latch;
|
||||
}
|
||||
},
|
||||
0xD000 => {
|
||||
self.irq_asserted_flag = false;
|
||||
},
|
||||
0xF000 => {
|
||||
self.prg_bank = value & 0x07;
|
||||
},
|
||||
else => {},
|
||||
}
|
||||
}
|
||||
|
||||
pub fn tickCycle(self: *Vrc3) void {
|
||||
if (self.irq_enabled) {
|
||||
if (self.irq_mode_8bit) {
|
||||
const low = @as(u8, @truncate(self.irq_counter));
|
||||
if (low == 0xFF) {
|
||||
self.irq_counter = (self.irq_counter & 0xFF00) | @as(u16, @as(u8, @truncate(self.irq_latch)));
|
||||
self.irq_asserted_flag = true;
|
||||
} else {
|
||||
self.irq_counter += 1;
|
||||
}
|
||||
} else {
|
||||
if (self.irq_counter == 0xFFFF) {
|
||||
self.irq_counter = self.irq_latch;
|
||||
self.irq_asserted_flag = true;
|
||||
} else {
|
||||
self.irq_counter += 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn ppuMapRead(
|
||||
_: *const Vrc3,
|
||||
address: u16,
|
||||
) ?usize {
|
||||
if (address >= 0x2000) return null;
|
||||
return address;
|
||||
}
|
||||
|
||||
pub fn ppuMapWrite(
|
||||
_: *const Vrc3,
|
||||
address: u16,
|
||||
) ?usize {
|
||||
if (address >= 0x2000) return null;
|
||||
return address;
|
||||
}
|
||||
|
||||
pub fn mirroring(
|
||||
self: *const Vrc3,
|
||||
) Mirroring {
|
||||
return self.mirroring_mode;
|
||||
}
|
||||
|
||||
pub fn irqAsserted(self: *const Vrc3) bool {
|
||||
return self.irq_asserted_flag;
|
||||
}
|
||||
@@ -1,179 +0,0 @@
|
||||
const types = @import("types.zig");
|
||||
const Mirroring = types.Mirroring;
|
||||
|
||||
const Vrc4 = @This();
|
||||
|
||||
prg_banks: [2]u8 = [_]u8{ 0, 1 },
|
||||
chr_banks_lo: [8]u4 = [_]u4{0} ** 8,
|
||||
chr_banks_hi: [8]u5 = [_]u5{0} ** 8,
|
||||
|
||||
prg_mode: u1 = 0,
|
||||
mirroring_mode: Mirroring = .vertical,
|
||||
|
||||
irq_reload: u8 = 0,
|
||||
irq_counter: u8 = 0,
|
||||
irq_enabled: bool = false,
|
||||
irq_enable_on_ack: bool = false,
|
||||
irq_mode: u1 = 0, // 0 = cycle, 1 = scanline
|
||||
irq_prescaler: i16 = 341,
|
||||
irq_assert: bool = false,
|
||||
|
||||
shift_a: u3,
|
||||
shift_b: u3,
|
||||
|
||||
prg_bank_count: usize,
|
||||
chr_bank_count: usize,
|
||||
|
||||
pub fn init(
|
||||
prg_size: usize,
|
||||
chr_size: usize,
|
||||
mapper_id: u8,
|
||||
) Vrc4 {
|
||||
const shift_a: u3 = if (mapper_id == 21) 1 else 0;
|
||||
const shift_b: u3 = if (mapper_id == 21) 2 else 2;
|
||||
|
||||
return .{
|
||||
.prg_bank_count = prg_size / 0x2000,
|
||||
.chr_bank_count = if (chr_size > 0) chr_size / 0x0400 else 1,
|
||||
.shift_a = shift_a,
|
||||
.shift_b = shift_b,
|
||||
};
|
||||
}
|
||||
|
||||
pub fn cpuMapRead(
|
||||
self: *const Vrc4,
|
||||
address: u16,
|
||||
) ?usize {
|
||||
if (address < 0x8000) return null;
|
||||
|
||||
const offset = @as(usize, address & 0x1fff);
|
||||
const last_bank = self.prg_bank_count - 1;
|
||||
const second_last_bank = self.prg_bank_count - 2;
|
||||
|
||||
const r0 = @as(usize, self.prg_banks[0] & 0x1f) % self.prg_bank_count;
|
||||
const r1 = @as(usize, self.prg_banks[1] & 0x1f) % self.prg_bank_count;
|
||||
|
||||
const bank_idx = (address - 0x8000) / 0x2000;
|
||||
var bank: usize = 0;
|
||||
|
||||
if (self.prg_mode == 0) {
|
||||
switch (bank_idx) {
|
||||
0 => bank = r0,
|
||||
1 => bank = r1,
|
||||
2 => bank = second_last_bank,
|
||||
3 => bank = last_bank,
|
||||
else => unreachable,
|
||||
}
|
||||
} else {
|
||||
switch (bank_idx) {
|
||||
0 => bank = second_last_bank,
|
||||
1 => bank = r1,
|
||||
2 => bank = r0,
|
||||
3 => bank = last_bank,
|
||||
else => unreachable,
|
||||
}
|
||||
}
|
||||
|
||||
return bank * 0x2000 + offset;
|
||||
}
|
||||
|
||||
pub fn cpuWrite(
|
||||
self: *Vrc4,
|
||||
address: u16,
|
||||
value: u8,
|
||||
) void {
|
||||
if (address < 0x8000) return;
|
||||
|
||||
const reg_addr = address & 0xf000;
|
||||
const a = @as(u1, @truncate((address >> self.shift_a) & 1));
|
||||
const b = @as(u1, @truncate((address >> self.shift_b) & 1));
|
||||
const line = (@as(u2, b) << 1) | a;
|
||||
|
||||
switch (reg_addr) {
|
||||
0x8000 => {
|
||||
self.prg_banks[0] = value & 0x1f;
|
||||
},
|
||||
0x9000 => {
|
||||
if (line == 0) {
|
||||
switch (@as(u2, @truncate(value & 3))) {
|
||||
0 => self.mirroring_mode = .vertical,
|
||||
1 => self.mirroring_mode = .horizontal,
|
||||
2 => self.mirroring_mode = .single_screen_lower,
|
||||
3 => self.mirroring_mode = .single_screen_upper,
|
||||
}
|
||||
} else if (line == 1 or line == 2) {
|
||||
self.prg_mode = @truncate((value >> 1) & 1);
|
||||
}
|
||||
},
|
||||
0xa000 => {
|
||||
self.prg_banks[1] = value & 0x1f;
|
||||
},
|
||||
0xb000, 0xc000, 0xd000, 0xe000 => {
|
||||
const slot_base = ((reg_addr - 0xb000) / 0x1000) * 2;
|
||||
const is_hi = (line & 2) != 0;
|
||||
const is_odd = (line & 1) != 0;
|
||||
const slot = slot_base + (if (is_odd) @as(usize, 1) else 0);
|
||||
|
||||
if (!is_hi) {
|
||||
self.chr_banks_lo[slot] = @truncate(value & 0x0f);
|
||||
} else {
|
||||
self.chr_banks_hi[slot] = @truncate(value & 0x1f);
|
||||
}
|
||||
},
|
||||
0xf000 => {
|
||||
switch (line) {
|
||||
0 => self.irq_reload = (self.irq_reload & 0xf0) | (value & 0x0f),
|
||||
1 => self.irq_reload = (self.irq_reload & 0x0f) | ((value & 0x0f) << 4),
|
||||
2 => {
|
||||
self.irq_mode = @truncate((value >> 2) & 1);
|
||||
self.irq_enabled = (value & 0x02) != 0;
|
||||
self.irq_enable_on_ack = (value & 0x01) != 0;
|
||||
if (self.irq_enabled) {
|
||||
self.irq_counter = self.irq_reload;
|
||||
self.irq_prescaler = 341;
|
||||
}
|
||||
self.irq_assert = false;
|
||||
},
|
||||
3 => {
|
||||
self.irq_enabled = self.irq_enable_on_ack;
|
||||
self.irq_assert = false;
|
||||
},
|
||||
}
|
||||
},
|
||||
else => {},
|
||||
}
|
||||
}
|
||||
|
||||
pub fn ppuMapRead(
|
||||
self: *const Vrc4,
|
||||
address: u16,
|
||||
) ?usize {
|
||||
if (address >= 0x2000) return null;
|
||||
|
||||
const slot = address / 0x0400;
|
||||
const offset = @as(usize, address & 0x03ff);
|
||||
|
||||
const bank_val = (@as(usize, self.chr_banks_hi[slot]) << 4) | @as(usize, self.chr_banks_lo[slot]);
|
||||
const bank = bank_val % self.chr_bank_count;
|
||||
|
||||
return bank * 0x0400 + offset;
|
||||
}
|
||||
|
||||
pub fn ppuMapWrite(
|
||||
self: *const Vrc4,
|
||||
address: u16,
|
||||
) ?usize {
|
||||
return self.ppuMapRead(address);
|
||||
}
|
||||
|
||||
pub fn mirroring(
|
||||
self: *const Vrc4,
|
||||
) Mirroring {
|
||||
return self.mirroring_mode;
|
||||
}
|
||||
|
||||
pub fn irqAsserted(
|
||||
self: *const Vrc4,
|
||||
) bool {
|
||||
return self.irq_assert;
|
||||
}
|
||||
@@ -1,152 +0,0 @@
|
||||
const types = @import("types.zig");
|
||||
const Mirroring = types.Mirroring;
|
||||
|
||||
const Vrc6 = @This();
|
||||
|
||||
prg_banks: [2]u8 = [_]u8{ 0, 1 },
|
||||
chr_banks: [8]u8 = [_]u8{ 0, 1, 2, 3, 4, 5, 6, 7 },
|
||||
|
||||
mirroring_mode: Mirroring = .vertical,
|
||||
|
||||
irq_reload: u8 = 0,
|
||||
irq_counter: u8 = 0,
|
||||
irq_enabled: bool = false,
|
||||
irq_enable_on_ack: bool = false,
|
||||
irq_mode: u1 = 0,
|
||||
irq_assert: bool = false,
|
||||
|
||||
shift_a: u3,
|
||||
shift_b: u3,
|
||||
|
||||
prg_bank_count: usize,
|
||||
chr_bank_count: usize,
|
||||
|
||||
pub fn init(
|
||||
prg_size: usize,
|
||||
chr_size: usize,
|
||||
mapper_id: u8,
|
||||
) Vrc6 {
|
||||
const shift_a: u3 = if (mapper_id == 26) 1 else 0;
|
||||
const shift_b: u3 = if (mapper_id == 26) 0 else 1;
|
||||
|
||||
return .{
|
||||
.prg_bank_count = prg_size / 0x2000,
|
||||
.chr_bank_count = if (chr_size > 0) chr_size / 0x0400 else 1,
|
||||
.shift_a = shift_a,
|
||||
.shift_b = shift_b,
|
||||
};
|
||||
}
|
||||
|
||||
pub fn cpuMapRead(
|
||||
self: *const Vrc6,
|
||||
address: u16,
|
||||
) ?usize {
|
||||
if (address < 0x8000) return null;
|
||||
|
||||
const last_bank = self.prg_bank_count - 1;
|
||||
|
||||
if (address < 0xc000) {
|
||||
const offset = @as(usize, address & 0x3fff);
|
||||
const bank16 = @as(usize, self.prg_banks[0] & 0x0f) % (self.prg_bank_count / 2);
|
||||
return bank16 * 0x4000 + offset;
|
||||
} else if (address < 0xe000) {
|
||||
const offset = @as(usize, address & 0x1fff);
|
||||
const bank8 = @as(usize, self.prg_banks[1] & 0x1f) % self.prg_bank_count;
|
||||
return bank8 * 0x2000 + offset;
|
||||
} else {
|
||||
const offset = @as(usize, address & 0x1fff);
|
||||
return last_bank * 0x2000 + offset;
|
||||
}
|
||||
}
|
||||
|
||||
pub fn cpuWrite(
|
||||
self: *Vrc6,
|
||||
address: u16,
|
||||
value: u8,
|
||||
) void {
|
||||
if (address < 0x8000) return;
|
||||
|
||||
const reg_addr = address & 0xf000;
|
||||
const a = @as(u1, @truncate((address >> self.shift_a) & 1));
|
||||
const b = @as(u1, @truncate((address >> self.shift_b) & 1));
|
||||
const line = (@as(u2, b) << 1) | a;
|
||||
|
||||
switch (reg_addr) {
|
||||
0x8000 => {
|
||||
if (line == 0) self.prg_banks[0] = value & 0x0f;
|
||||
},
|
||||
0x9000...0xb000 => {
|
||||
if (reg_addr == 0xb000 and line == 3) {
|
||||
switch (@as(u2, @truncate((value >> 2) & 3))) {
|
||||
0 => self.mirroring_mode = .vertical,
|
||||
1 => self.mirroring_mode = .horizontal,
|
||||
2 => self.mirroring_mode = .single_screen_lower,
|
||||
3 => self.mirroring_mode = .single_screen_upper,
|
||||
}
|
||||
}
|
||||
},
|
||||
0xc000 => {
|
||||
if (line == 0) self.prg_banks[1] = value & 0x1f;
|
||||
},
|
||||
0xd000 => {
|
||||
const slot = @as(usize, line);
|
||||
self.chr_banks[slot] = value;
|
||||
},
|
||||
0xe000 => {
|
||||
const slot = 4 + @as(usize, line);
|
||||
self.chr_banks[slot] = value;
|
||||
},
|
||||
0xf000 => {
|
||||
switch (line) {
|
||||
0 => self.irq_reload = value,
|
||||
1 => {
|
||||
self.irq_mode = @truncate((value >> 2) & 1);
|
||||
self.irq_enabled = (value & 0x02) != 0;
|
||||
self.irq_enable_on_ack = (value & 0x01) != 0;
|
||||
if (self.irq_enabled) {
|
||||
self.irq_counter = self.irq_reload;
|
||||
}
|
||||
self.irq_assert = false;
|
||||
},
|
||||
2 => {
|
||||
self.irq_enabled = self.irq_enable_on_ack;
|
||||
self.irq_assert = false;
|
||||
},
|
||||
else => {},
|
||||
}
|
||||
},
|
||||
else => {},
|
||||
}
|
||||
}
|
||||
|
||||
pub fn ppuMapRead(
|
||||
self: *const Vrc6,
|
||||
address: u16,
|
||||
) ?usize {
|
||||
if (address >= 0x2000) return null;
|
||||
|
||||
const slot = address / 0x0400;
|
||||
const offset = @as(usize, address & 0x03ff);
|
||||
const bank = @as(usize, self.chr_banks[slot]) % self.chr_bank_count;
|
||||
|
||||
return bank * 0x0400 + offset;
|
||||
}
|
||||
|
||||
pub fn ppuMapWrite(
|
||||
self: *const Vrc6,
|
||||
address: u16,
|
||||
) ?usize {
|
||||
return self.ppuMapRead(address);
|
||||
}
|
||||
|
||||
pub fn mirroring(
|
||||
self: *const Vrc6,
|
||||
) Mirroring {
|
||||
return self.mirroring_mode;
|
||||
}
|
||||
|
||||
pub fn irqAsserted(
|
||||
self: *const Vrc6,
|
||||
) bool {
|
||||
return self.irq_assert;
|
||||
}
|
||||
@@ -1,145 +0,0 @@
|
||||
const types = @import("types.zig");
|
||||
const Mirroring = types.Mirroring;
|
||||
|
||||
const Vrc7 = @This();
|
||||
|
||||
prg_banks: [3]u8 = [_]u8{ 0, 1, 2 },
|
||||
chr_banks: [8]u8 = [_]u8{ 0, 1, 2, 3, 4, 5, 6, 7 },
|
||||
|
||||
mirroring_mode: Mirroring = .vertical,
|
||||
|
||||
irq_reload: u8 = 0,
|
||||
irq_counter: u8 = 0,
|
||||
irq_enabled: bool = false,
|
||||
irq_enable_on_ack: bool = false,
|
||||
irq_mode: u1 = 0,
|
||||
irq_assert: bool = false,
|
||||
|
||||
prg_bank_count: usize,
|
||||
chr_bank_count: usize,
|
||||
|
||||
pub fn init(
|
||||
prg_size: usize,
|
||||
chr_size: usize,
|
||||
) Vrc7 {
|
||||
return .{
|
||||
.prg_bank_count = prg_size / 0x2000,
|
||||
.chr_bank_count = if (chr_size > 0) chr_size / 0x0400 else 1,
|
||||
};
|
||||
}
|
||||
|
||||
pub fn cpuMapRead(
|
||||
self: *const Vrc7,
|
||||
address: u16,
|
||||
) ?usize {
|
||||
if (address < 0x8000) return null;
|
||||
|
||||
const offset = @as(usize, address & 0x1fff);
|
||||
const last_bank = self.prg_bank_count - 1;
|
||||
|
||||
switch ((address - 0x8000) / 0x2000) {
|
||||
0 => return (@as(usize, self.prg_banks[0] & 0x3f) % self.prg_bank_count) * 0x2000 + offset,
|
||||
1 => return (@as(usize, self.prg_banks[1] & 0x3f) % self.prg_bank_count) * 0x2000 + offset,
|
||||
2 => return (@as(usize, self.prg_banks[2] & 0x3f) % self.prg_bank_count) * 0x2000 + offset,
|
||||
3 => return last_bank * 0x2000 + offset,
|
||||
else => unreachable,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn cpuWrite(
|
||||
self: *Vrc7,
|
||||
address: u16,
|
||||
value: u8,
|
||||
) void {
|
||||
if (address < 0x8000) return;
|
||||
|
||||
const reg_addr = address & 0xf000;
|
||||
const a0 = if ((address & 0x10) != 0 or (address & 0x08) != 0) @as(u2, 1) else 0;
|
||||
const a1 = if ((address & 0x20) != 0) @as(u2, 2) else 0;
|
||||
const line = a0 | a1;
|
||||
|
||||
switch (reg_addr) {
|
||||
0x8000 => {
|
||||
if (line == 0) self.prg_banks[0] = value & 0x3f;
|
||||
if (line == 1 or line == 2) self.prg_banks[1] = value & 0x3f;
|
||||
},
|
||||
0x9000 => {
|
||||
if (line == 0) self.prg_banks[2] = value & 0x3f;
|
||||
if (line == 2) {
|
||||
switch (@as(u2, @truncate(value & 3))) {
|
||||
0 => self.mirroring_mode = .vertical,
|
||||
1 => self.mirroring_mode = .horizontal,
|
||||
2 => self.mirroring_mode = .single_screen_lower,
|
||||
3 => self.mirroring_mode = .single_screen_upper,
|
||||
}
|
||||
}
|
||||
},
|
||||
0xa000 => {
|
||||
if (line == 0) self.chr_banks[0] = value;
|
||||
if (line == 1 or line == 2) self.chr_banks[1] = value;
|
||||
},
|
||||
0xb000 => {
|
||||
if (line == 0) self.chr_banks[2] = value;
|
||||
if (line == 1 or line == 2) self.chr_banks[3] = value;
|
||||
},
|
||||
0xc000 => {
|
||||
if (line == 0) self.chr_banks[4] = value;
|
||||
if (line == 1 or line == 2) self.chr_banks[5] = value;
|
||||
},
|
||||
0xd000 => {
|
||||
if (line == 0) self.chr_banks[6] = value;
|
||||
if (line == 1 or line == 2) self.chr_banks[7] = value;
|
||||
},
|
||||
0xe000 => {
|
||||
if (line == 0) self.irq_reload = value;
|
||||
if (line == 1 or line == 2) {
|
||||
self.irq_mode = @truncate((value >> 2) & 1);
|
||||
self.irq_enabled = (value & 0x02) != 0;
|
||||
self.irq_enable_on_ack = (value & 0x01) != 0;
|
||||
if (self.irq_enabled) {
|
||||
self.irq_counter = self.irq_reload;
|
||||
}
|
||||
self.irq_assert = false;
|
||||
}
|
||||
},
|
||||
0xf000 => {
|
||||
if (line == 0) {
|
||||
self.irq_enabled = self.irq_enable_on_ack;
|
||||
self.irq_assert = false;
|
||||
}
|
||||
},
|
||||
else => {},
|
||||
}
|
||||
}
|
||||
|
||||
pub fn ppuMapRead(
|
||||
self: *const Vrc7,
|
||||
address: u16,
|
||||
) ?usize {
|
||||
if (address >= 0x2000) return null;
|
||||
|
||||
const slot = address / 0x0400;
|
||||
const offset = @as(usize, address & 0x03ff);
|
||||
const bank = @as(usize, self.chr_banks[slot]) % self.chr_bank_count;
|
||||
|
||||
return bank * 0x0400 + offset;
|
||||
}
|
||||
|
||||
pub fn ppuMapWrite(
|
||||
self: *const Vrc7,
|
||||
address: u16,
|
||||
) ?usize {
|
||||
return self.ppuMapRead(address);
|
||||
}
|
||||
|
||||
pub fn mirroring(
|
||||
self: *const Vrc7,
|
||||
) Mirroring {
|
||||
return self.mirroring_mode;
|
||||
}
|
||||
|
||||
pub fn irqAsserted(
|
||||
self: *const Vrc7,
|
||||
) bool {
|
||||
return self.irq_assert;
|
||||
}
|
||||
@@ -1,779 +0,0 @@
|
||||
const std = @import("std");
|
||||
const Cartridge = @import("cartridge.zig");
|
||||
|
||||
const mapper = @import("mapper/root.zig");
|
||||
const Mirroring = mapper.Mirroring;
|
||||
|
||||
const Ppu = @This();
|
||||
|
||||
pub const width = 256;
|
||||
pub const height = 240;
|
||||
|
||||
// NTSC 2C02 64-color system palette mapped directly to RGB565 format
|
||||
pub const nes_ntsc_palette_rgb565: [64]u16 = .{
|
||||
0x52AA, 0x00EE, 0x0892, 0x3011, 0x400C, 0x5806, 0x5020, 0x38C0,
|
||||
0x2140, 0x09C0, 0x0200, 0x01E0, 0x0187, 0x0000, 0x0000, 0x0000,
|
||||
0x9CB3, 0x0A78, 0x319D, 0x58FC, 0x88B6, 0xA08C, 0x9904, 0x79E0,
|
||||
0x52C0, 0x2B80, 0x0BE0, 0x03A5, 0x032F, 0x0000, 0x0000, 0x0000,
|
||||
0xEF7D, 0x4CDD, 0x7BFD, 0xB31D, 0xE2BD, 0xEACE, 0xEB4C, 0xD444,
|
||||
0xA540, 0x7620, 0x4E84, 0x3E6D, 0x3DA9, 0x3DF7, 0x0000, 0x0000,
|
||||
0xEF7D, 0xAE6D, 0xBD7D, 0xD59D, 0xED7D, 0xED7A, 0xEDB6, 0xE632,
|
||||
0xCE8F, 0xB6EF, 0xAF12, 0x9F16, 0xA6BD, 0xA514, 0x0000, 0x0000,
|
||||
};
|
||||
|
||||
framebuffer: [width * height]u16 =
|
||||
[_]u16{0} ** (width * height),
|
||||
|
||||
/// Four-screen sized so the same structure
|
||||
/// can handle that mode without allocation.
|
||||
nametable: [0x1000]u8 =
|
||||
[_]u8{0} ** 0x1000,
|
||||
|
||||
palette: [32]u8 =
|
||||
[_]u8{0} ** 32,
|
||||
|
||||
oam: [256]u8 =
|
||||
[_]u8{0} ** 256,
|
||||
|
||||
ctrl: u8 = 0,
|
||||
mask: u8 = 0,
|
||||
status: u8 = 0,
|
||||
|
||||
oam_addr: u8 = 0,
|
||||
|
||||
vram_addr: u16 = 0,
|
||||
temp_addr: u16 = 0,
|
||||
|
||||
fine_x: u3 = 0,
|
||||
write_latch: bool = false,
|
||||
|
||||
read_buffer: u8 = 0,
|
||||
io_latch: u8 = 0,
|
||||
|
||||
dot: u16 = 0,
|
||||
scanline: u16 = 261,
|
||||
|
||||
frame_number: u64 = 0,
|
||||
frame_ready: bool = false,
|
||||
|
||||
// internal shift registers & tile latches for background rendering
|
||||
bg_shift_pattern_lo: u16 = 0,
|
||||
bg_shift_pattern_hi: u16 = 0,
|
||||
bg_shift_attrib_lo: u16 = 0,
|
||||
bg_shift_attrib_hi: u16 = 0,
|
||||
|
||||
next_tile_id: u8 = 0,
|
||||
next_tile_attrib: u8 = 0,
|
||||
next_tile_lsb: u8 = 0,
|
||||
next_tile_msb: u8 = 0,
|
||||
|
||||
// secondary OAM scanline sprite evaluation buffer
|
||||
sec_oam_pattern_lo: [8]u8 = [_]u8{0} ** 8,
|
||||
sec_oam_pattern_hi: [8]u8 = [_]u8{0} ** 8,
|
||||
sec_oam_attribute: [8]u8 = [_]u8{0} ** 8,
|
||||
sec_oam_x: [8]u8 = [_]u8{0} ** 8,
|
||||
sec_oam_is_zero: [8]bool = [_]bool{false} ** 8,
|
||||
sec_oam_count: u8 = 0,
|
||||
|
||||
pub fn reset(
|
||||
self: *Ppu,
|
||||
) void {
|
||||
self.ctrl = 0;
|
||||
self.mask = 0;
|
||||
self.status = 0;
|
||||
|
||||
self.oam_addr = 0;
|
||||
|
||||
self.vram_addr = 0;
|
||||
self.temp_addr = 0;
|
||||
|
||||
self.fine_x = 0;
|
||||
self.write_latch = false;
|
||||
|
||||
self.read_buffer = 0;
|
||||
self.io_latch = 0;
|
||||
|
||||
self.dot = 0;
|
||||
self.scanline = 261;
|
||||
|
||||
self.frame_ready = false;
|
||||
|
||||
self.bg_shift_pattern_lo = 0;
|
||||
self.bg_shift_pattern_hi = 0;
|
||||
self.bg_shift_attrib_lo = 0;
|
||||
self.bg_shift_attrib_hi = 0;
|
||||
self.next_tile_id = 0;
|
||||
self.next_tile_attrib = 0;
|
||||
self.next_tile_lsb = 0;
|
||||
self.next_tile_msb = 0;
|
||||
self.sec_oam_count = 0;
|
||||
}
|
||||
|
||||
pub fn cpuRead(
|
||||
self: *Ppu,
|
||||
cartridge: *Cartridge,
|
||||
register: u16,
|
||||
) u8 {
|
||||
const reg = register & 7;
|
||||
|
||||
const value: u8 = switch (reg) {
|
||||
// PPUSTATUS
|
||||
2 => blk: {
|
||||
const result =
|
||||
(self.status & 0xe0) |
|
||||
(self.io_latch & 0x1f);
|
||||
|
||||
self.status &= ~@as(u8, 0x80);
|
||||
self.write_latch = false;
|
||||
|
||||
break :blk result;
|
||||
},
|
||||
|
||||
// OAMDATA
|
||||
4 => self.oam[self.oam_addr],
|
||||
|
||||
// PPUDATA
|
||||
7 => self.readData(cartridge),
|
||||
|
||||
else => self.io_latch,
|
||||
};
|
||||
|
||||
self.io_latch = value;
|
||||
|
||||
return value;
|
||||
}
|
||||
|
||||
pub fn cpuWrite(
|
||||
self: *Ppu,
|
||||
cartridge: *Cartridge,
|
||||
register: u16,
|
||||
value: u8,
|
||||
) void {
|
||||
const reg = register & 7;
|
||||
|
||||
self.io_latch = value;
|
||||
|
||||
switch (reg) {
|
||||
// PPUCTRL
|
||||
0 => {
|
||||
self.ctrl = value;
|
||||
|
||||
self.temp_addr =
|
||||
(self.temp_addr & 0xf3ff) |
|
||||
(@as(u16, value & 0x03) << 10);
|
||||
},
|
||||
|
||||
// PPUMASK
|
||||
1 => {
|
||||
self.mask = value;
|
||||
},
|
||||
|
||||
// OAMADDR
|
||||
3 => {
|
||||
self.oam_addr = value;
|
||||
},
|
||||
|
||||
// OAMDATA
|
||||
4 => {
|
||||
self.writeOamDma(value);
|
||||
},
|
||||
|
||||
// PPUSCROLL
|
||||
5 => {
|
||||
if (!self.write_latch) {
|
||||
self.fine_x =
|
||||
@truncate(value & 7);
|
||||
|
||||
self.temp_addr =
|
||||
(self.temp_addr & 0xffe0) |
|
||||
@as(u16, value >> 3);
|
||||
|
||||
self.write_latch = true;
|
||||
} else {
|
||||
self.temp_addr =
|
||||
(self.temp_addr & 0x8c1f) |
|
||||
(@as(u16, value & 0x07) << 12) |
|
||||
(@as(u16, value & 0xf8) << 2);
|
||||
|
||||
self.write_latch = false;
|
||||
}
|
||||
},
|
||||
|
||||
// PPUADDR
|
||||
6 => {
|
||||
if (!self.write_latch) {
|
||||
self.temp_addr =
|
||||
(self.temp_addr & 0x00ff) |
|
||||
(@as(u16, value & 0x3f) << 8);
|
||||
|
||||
self.write_latch = true;
|
||||
} else {
|
||||
self.temp_addr =
|
||||
(self.temp_addr & 0x7f00) |
|
||||
@as(u16, value);
|
||||
|
||||
self.vram_addr =
|
||||
self.temp_addr;
|
||||
|
||||
self.write_latch = false;
|
||||
}
|
||||
},
|
||||
|
||||
// PPUDATA
|
||||
7 => {
|
||||
self.writeMemory(
|
||||
cartridge,
|
||||
self.vram_addr,
|
||||
value,
|
||||
);
|
||||
|
||||
self.incrementVramAddress();
|
||||
},
|
||||
|
||||
else => {},
|
||||
}
|
||||
}
|
||||
|
||||
pub fn writeOamDma(
|
||||
self: *Ppu,
|
||||
value: u8,
|
||||
) void {
|
||||
self.oam[self.oam_addr] = value;
|
||||
self.oam_addr +%= 1;
|
||||
}
|
||||
|
||||
pub fn tick(
|
||||
self: *Ppu,
|
||||
cartridge: *Cartridge,
|
||||
) void {
|
||||
const rendering_enabled = (self.mask & 0x18) != 0;
|
||||
const is_visible_scanline = (self.scanline < 240);
|
||||
const is_prerender_scanline = (self.scanline == 261);
|
||||
|
||||
// Start of VBlank.
|
||||
if (self.scanline == 241 and
|
||||
self.dot == 1)
|
||||
{
|
||||
self.status |= 0x80;
|
||||
}
|
||||
|
||||
// Pre-render line.
|
||||
if (is_prerender_scanline and
|
||||
self.dot == 1)
|
||||
{
|
||||
// Clear: VBlank, sprite zero hit, sprite overflow
|
||||
self.status &= ~@as(u8, 0xe0);
|
||||
}
|
||||
|
||||
// --- Rendering Pipeline ---
|
||||
if (rendering_enabled and (is_visible_scanline or is_prerender_scanline)) {
|
||||
if ((self.dot >= 1 and self.dot <= 256) or (self.dot >= 321 and self.dot <= 336)) {
|
||||
self.shiftBackgroundRegisters();
|
||||
|
||||
const cycle_in_tile = (self.dot - 1) % 8;
|
||||
switch (cycle_in_tile) {
|
||||
0 => {
|
||||
self.loadShiftRegisters();
|
||||
const nt_addr = 0x2000 | (self.vram_addr & 0x0fff);
|
||||
self.next_tile_id = self.readMemory(cartridge, nt_addr);
|
||||
},
|
||||
2 => {
|
||||
const attr_addr = 0x23c0 |
|
||||
(self.vram_addr & 0x0c00) |
|
||||
((self.vram_addr >> 4) & 0x38) |
|
||||
((self.vram_addr >> 2) & 0x07);
|
||||
const attr_byte = self.readMemory(cartridge, attr_addr);
|
||||
const shift: u3 = @intCast(((self.vram_addr >> 4) & 4) | (self.vram_addr & 2));
|
||||
self.next_tile_attrib = (attr_byte >> shift) & 0x03;
|
||||
},
|
||||
4 => {
|
||||
const bg_table: u16 = if ((self.ctrl & 0x10) != 0) 0x1000 else 0x0000;
|
||||
const fine_y = (self.vram_addr >> 12) & 7;
|
||||
const pattern_addr = bg_table + (@as(u16, self.next_tile_id) << 4) + fine_y;
|
||||
self.next_tile_lsb = self.readMemory(cartridge, pattern_addr);
|
||||
},
|
||||
6 => {
|
||||
const bg_table: u16 = if ((self.ctrl & 0x10) != 0) 0x1000 else 0x0000;
|
||||
const fine_y = (self.vram_addr >> 12) & 7;
|
||||
const pattern_addr = bg_table + (@as(u16, self.next_tile_id) << 4) + fine_y + 8;
|
||||
self.next_tile_msb = self.readMemory(cartridge, pattern_addr);
|
||||
},
|
||||
7 => {
|
||||
self.incrementCoarseX();
|
||||
},
|
||||
else => {},
|
||||
}
|
||||
}
|
||||
|
||||
if (self.dot == 256) {
|
||||
self.incrementY();
|
||||
}
|
||||
|
||||
if (self.dot == 257) {
|
||||
self.loadShiftRegisters();
|
||||
self.copyHorizontal();
|
||||
}
|
||||
|
||||
if (is_prerender_scanline and self.dot >= 280 and self.dot <= 304) {
|
||||
self.copyVertical();
|
||||
}
|
||||
|
||||
if (self.dot == 257 and is_visible_scanline) {
|
||||
self.evaluateSprites(cartridge);
|
||||
}
|
||||
|
||||
if (self.dot == 260 and is_visible_scanline) {
|
||||
cartridge.handleScanline();
|
||||
}
|
||||
}
|
||||
|
||||
// --- Pixel Output ---
|
||||
if (is_visible_scanline and self.dot >= 1 and self.dot <= 256) {
|
||||
self.renderPixel(cartridge);
|
||||
}
|
||||
|
||||
// --- Dot & Scanline Counters ---
|
||||
self.dot += 1;
|
||||
|
||||
if (self.dot == 341) {
|
||||
self.dot = 0;
|
||||
self.scanline += 1;
|
||||
|
||||
if (self.scanline == 262) {
|
||||
self.scanline = 0;
|
||||
|
||||
self.frame_number +%= 1;
|
||||
self.frame_ready = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn nmiAsserted(
|
||||
self: *const Ppu,
|
||||
) bool {
|
||||
const nmi_enabled =
|
||||
(self.ctrl & 0x80) != 0;
|
||||
|
||||
const in_vblank =
|
||||
(self.status & 0x80) != 0;
|
||||
|
||||
return nmi_enabled and in_vblank;
|
||||
}
|
||||
|
||||
fn shiftBackgroundRegisters(self: *Ppu) void {
|
||||
if ((self.mask & 0x08) != 0) {
|
||||
self.bg_shift_pattern_lo <<= 1;
|
||||
self.bg_shift_pattern_hi <<= 1;
|
||||
self.bg_shift_attrib_lo <<= 1;
|
||||
self.bg_shift_attrib_hi <<= 1;
|
||||
}
|
||||
}
|
||||
|
||||
fn loadShiftRegisters(self: *Ppu) void {
|
||||
self.bg_shift_pattern_lo = (self.bg_shift_pattern_lo & 0xff00) | self.next_tile_lsb;
|
||||
self.bg_shift_pattern_hi = (self.bg_shift_pattern_hi & 0xff00) | self.next_tile_msb;
|
||||
|
||||
const attr_lo: u16 = if ((self.next_tile_attrib & 1) != 0) 0x00ff else 0x0000;
|
||||
const attr_hi: u16 = if ((self.next_tile_attrib & 2) != 0) 0x00ff else 0x0000;
|
||||
self.bg_shift_attrib_lo = (self.bg_shift_attrib_lo & 0xff00) | attr_lo;
|
||||
self.bg_shift_attrib_hi = (self.bg_shift_attrib_hi & 0xff00) | attr_hi;
|
||||
}
|
||||
|
||||
inline fn incrementCoarseX(self: *Ppu) void {
|
||||
if ((self.vram_addr & 0x001f) == 31) {
|
||||
self.vram_addr &= ~@as(u16, 0x001f);
|
||||
self.vram_addr ^= 0x0400;
|
||||
} else {
|
||||
self.vram_addr += 1;
|
||||
}
|
||||
}
|
||||
|
||||
inline fn incrementY(self: *Ppu) void {
|
||||
if ((self.vram_addr & 0x7000) != 0x7000) {
|
||||
self.vram_addr += 0x1000;
|
||||
} else {
|
||||
self.vram_addr &= ~@as(u16, 0x7000);
|
||||
var y = (self.vram_addr & 0x03e0) >> 5;
|
||||
if (y == 29) {
|
||||
y = 0;
|
||||
self.vram_addr ^= 0x0800;
|
||||
} else if (y == 31) {
|
||||
y = 0;
|
||||
} else {
|
||||
y += 1;
|
||||
}
|
||||
self.vram_addr = (self.vram_addr & ~@as(u16, 0x03e0)) | (y << 5);
|
||||
}
|
||||
}
|
||||
|
||||
inline fn copyHorizontal(self: *Ppu) void {
|
||||
self.vram_addr = (self.vram_addr & ~@as(u16, 0x041f)) | (self.temp_addr & 0x041f);
|
||||
}
|
||||
|
||||
inline fn copyVertical(self: *Ppu) void {
|
||||
self.vram_addr = (self.vram_addr & ~@as(u16, 0x7be0)) | (self.temp_addr & 0x7be0);
|
||||
}
|
||||
|
||||
inline fn evaluateSprites(
|
||||
self: *Ppu,
|
||||
cartridge: *Cartridge,
|
||||
) void {
|
||||
self.sec_oam_count = 0;
|
||||
const sprite_height: u16 = if ((self.ctrl & 0x20) != 0) 16 else 8;
|
||||
|
||||
var oam_idx: usize = 0;
|
||||
var i: usize = 0;
|
||||
while (i < 64) : ({
|
||||
i += 1;
|
||||
oam_idx += 4;
|
||||
}) {
|
||||
const sprite_y = @as(u16, self.oam[oam_idx]);
|
||||
const diff = @as(i16, @intCast(self.scanline)) - @as(i16, @intCast(sprite_y));
|
||||
|
||||
if (diff >= 0 and diff < sprite_height) {
|
||||
if (self.sec_oam_count < 8) {
|
||||
const idx = self.sec_oam_count;
|
||||
const tile = self.oam[oam_idx + 1];
|
||||
const attr = self.oam[oam_idx + 2];
|
||||
const x = self.oam[oam_idx + 3];
|
||||
|
||||
var row: u16 = @intCast(diff);
|
||||
// Vertical flip
|
||||
if ((attr & 0x80) != 0) {
|
||||
row = (sprite_height - 1) - row;
|
||||
}
|
||||
|
||||
var pattern_addr: u16 = 0;
|
||||
if (sprite_height == 8) {
|
||||
const pattern_table: u16 = if ((self.ctrl & 0x08) != 0) 0x1000 else 0x0000;
|
||||
pattern_addr = pattern_table + (@as(u16, tile) << 4) + row;
|
||||
} else {
|
||||
// 8x16 mode
|
||||
const pattern_table: u16 = if ((tile & 1) != 0) 0x1000 else 0x0000;
|
||||
const tile_index: u16 = tile & 0xfe;
|
||||
if (row < 8) {
|
||||
pattern_addr = pattern_table + (tile_index << 4) + row;
|
||||
} else {
|
||||
pattern_addr = pattern_table + ((tile_index + 1) << 4) + (row - 8);
|
||||
}
|
||||
}
|
||||
|
||||
var pat_lo = self.readMemory(cartridge, pattern_addr);
|
||||
var pat_hi = self.readMemory(cartridge, pattern_addr + 8);
|
||||
|
||||
// Horizontal flip
|
||||
if ((attr & 0x40) != 0) {
|
||||
pat_lo = @bitReverse(pat_lo);
|
||||
pat_hi = @bitReverse(pat_hi);
|
||||
}
|
||||
|
||||
self.sec_oam_pattern_lo[idx] = pat_lo;
|
||||
self.sec_oam_pattern_hi[idx] = pat_hi;
|
||||
self.sec_oam_attribute[idx] = attr;
|
||||
self.sec_oam_x[idx] = x;
|
||||
self.sec_oam_is_zero[idx] = (i == 0);
|
||||
|
||||
self.sec_oam_count += 1;
|
||||
} else {
|
||||
// Sprite overflow
|
||||
self.status |= 0x20;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn renderPixel(
|
||||
self: *Ppu,
|
||||
_: *Cartridge,
|
||||
) void {
|
||||
const x = self.dot - 1;
|
||||
|
||||
var bg_pixel: u8 = 0;
|
||||
var bg_palette: u8 = 0;
|
||||
|
||||
if ((self.mask & 0x08) != 0 and (x >= 8 or (self.mask & 0x02) != 0)) {
|
||||
const bit_shift: u4 = 15 - @as(u4, self.fine_x);
|
||||
const p1 = @as(u8, @truncate((self.bg_shift_pattern_lo >> bit_shift) & 1));
|
||||
const p2 = @as(u8, @truncate((self.bg_shift_pattern_hi >> bit_shift) & 1));
|
||||
bg_pixel = (p2 << 1) | p1;
|
||||
|
||||
const a1 = @as(u8, @truncate((self.bg_shift_attrib_lo >> bit_shift) & 1));
|
||||
const a2 = @as(u8, @truncate((self.bg_shift_attrib_hi >> bit_shift) & 1));
|
||||
bg_palette = (a2 << 1) | a1;
|
||||
}
|
||||
|
||||
var fg_pixel: u8 = 0;
|
||||
var fg_palette: u8 = 0;
|
||||
var fg_priority: bool = false;
|
||||
var is_sprite_zero: bool = false;
|
||||
|
||||
if (self.sec_oam_count > 0 and (self.mask & 0x10) != 0 and (x >= 8 or (self.mask & 0x04) != 0)) {
|
||||
for (0..self.sec_oam_count) |i| {
|
||||
const spr_x = self.sec_oam_x[i];
|
||||
if (x >= spr_x and x < spr_x + 8) {
|
||||
const offset: u3 = @intCast(x - spr_x);
|
||||
const shift: u3 = 7 - offset;
|
||||
const p1 = (self.sec_oam_pattern_lo[i] >> shift) & 1;
|
||||
const p2 = (self.sec_oam_pattern_hi[i] >> shift) & 1;
|
||||
const pixel = (p2 << 1) | p1;
|
||||
|
||||
if (pixel != 0) {
|
||||
fg_pixel = pixel;
|
||||
fg_palette = 4 + (self.sec_oam_attribute[i] & 0x03);
|
||||
fg_priority = (self.sec_oam_attribute[i] & 0x20) != 0;
|
||||
is_sprite_zero = self.sec_oam_is_zero[i];
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
var final_palette_entry: u16 = 0;
|
||||
|
||||
if (bg_pixel == 0 and fg_pixel == 0) {
|
||||
final_palette_entry = 0x00;
|
||||
} else if (bg_pixel == 0 and fg_pixel != 0) {
|
||||
final_palette_entry = @as(u16, fg_palette) * 4 + fg_pixel;
|
||||
} else if (bg_pixel != 0 and fg_pixel == 0) {
|
||||
final_palette_entry = @as(u16, bg_palette) * 4 + bg_pixel;
|
||||
} else {
|
||||
// Both bg and fg pixels present
|
||||
if (is_sprite_zero and x < 255 and (self.mask & 0x18) == 0x18) {
|
||||
self.status |= 0x40; // Sprite 0 hit
|
||||
}
|
||||
|
||||
if (fg_priority) {
|
||||
final_palette_entry = @as(u16, bg_palette) * 4 + bg_pixel;
|
||||
} else {
|
||||
final_palette_entry = @as(u16, fg_palette) * 4 + fg_pixel;
|
||||
}
|
||||
}
|
||||
|
||||
var color_idx = self.palette[mapPalette(0x3f00 + final_palette_entry)] & 0x3f;
|
||||
if ((self.mask & 0x01) != 0) {
|
||||
color_idx &= 0x30;
|
||||
}
|
||||
var pixel_rgb = nes_ntsc_palette_rgb565[color_idx];
|
||||
|
||||
const emphasis = (self.mask >> 5) & 7;
|
||||
if (emphasis != 0) {
|
||||
var r = (pixel_rgb >> 11) & 0x1F;
|
||||
var g = (pixel_rgb >> 5) & 0x3F;
|
||||
var b = pixel_rgb & 0x1F;
|
||||
|
||||
// Red emphasis (bit 5)
|
||||
if ((emphasis & 1) != 0) {
|
||||
g = (g * 3) / 4;
|
||||
b = (b * 3) / 4;
|
||||
}
|
||||
// Green emphasis (bit 6)
|
||||
if ((emphasis & 2) != 0) {
|
||||
r = (r * 3) / 4;
|
||||
b = (b * 3) / 4;
|
||||
}
|
||||
// Blue emphasis (bit 7)
|
||||
if ((emphasis & 4) != 0) {
|
||||
r = (r * 3) / 4;
|
||||
g = (g * 3) / 4;
|
||||
}
|
||||
|
||||
pixel_rgb = (@as(u16, r) << 11) | (@as(u16, g) << 5) | @as(u16, b);
|
||||
}
|
||||
|
||||
self.framebuffer[self.scanline * 256 + x] = pixel_rgb;
|
||||
}
|
||||
|
||||
fn incrementVramAddress(
|
||||
self: *Ppu,
|
||||
) void {
|
||||
const increment: u16 =
|
||||
if ((self.ctrl & 0x04) != 0)
|
||||
32
|
||||
else
|
||||
1;
|
||||
|
||||
self.vram_addr =
|
||||
(self.vram_addr +% increment) &
|
||||
0x3fff;
|
||||
}
|
||||
|
||||
fn readData(
|
||||
self: *Ppu,
|
||||
cartridge: *Cartridge,
|
||||
) u8 {
|
||||
const address =
|
||||
self.vram_addr & 0x3fff;
|
||||
|
||||
const value: u8 =
|
||||
if (address < 0x3f00) blk: {
|
||||
const previous =
|
||||
self.read_buffer;
|
||||
|
||||
self.read_buffer =
|
||||
self.readMemory(
|
||||
cartridge,
|
||||
address,
|
||||
);
|
||||
|
||||
break :blk previous;
|
||||
} else blk: {
|
||||
const result =
|
||||
self.readMemory(
|
||||
cartridge,
|
||||
address,
|
||||
);
|
||||
|
||||
// Palette reads aren't delayed, but
|
||||
// still update the internal buffer.
|
||||
self.read_buffer =
|
||||
self.readMemory(
|
||||
cartridge,
|
||||
address - 0x1000,
|
||||
);
|
||||
|
||||
break :blk result;
|
||||
};
|
||||
|
||||
self.incrementVramAddress();
|
||||
|
||||
return value;
|
||||
}
|
||||
|
||||
inline fn readMemory(
|
||||
self: *Ppu,
|
||||
cartridge: *Cartridge,
|
||||
address_: u16,
|
||||
) u8 {
|
||||
const address =
|
||||
address_ & 0x3fff;
|
||||
|
||||
return switch (address) {
|
||||
0x0000...0x1fff => cartridge.ppuRead(address),
|
||||
|
||||
0x2000...0x3eff => self.nametable[
|
||||
self.mapNametable(
|
||||
cartridge.mirroring(),
|
||||
address,
|
||||
)
|
||||
],
|
||||
|
||||
0x3f00...0x3fff => self.palette[
|
||||
mapPalette(address)
|
||||
],
|
||||
|
||||
else => unreachable,
|
||||
};
|
||||
}
|
||||
|
||||
inline fn writeMemory(
|
||||
self: *Ppu,
|
||||
cartridge: *Cartridge,
|
||||
address_: u16,
|
||||
value: u8,
|
||||
) void {
|
||||
const address =
|
||||
address_ & 0x3fff;
|
||||
|
||||
switch (address) {
|
||||
0x0000...0x1fff => cartridge.ppuWrite(
|
||||
address,
|
||||
value,
|
||||
),
|
||||
|
||||
0x2000...0x3eff => self.nametable[
|
||||
self.mapNametable(
|
||||
cartridge.mirroring(),
|
||||
address,
|
||||
)
|
||||
] = value,
|
||||
|
||||
0x3f00...0x3fff => self.palette[
|
||||
mapPalette(address)
|
||||
] = value,
|
||||
|
||||
else => unreachable,
|
||||
}
|
||||
}
|
||||
|
||||
inline fn mapNametable(
|
||||
_: *const Ppu,
|
||||
mirroring: Mirroring,
|
||||
address: u16,
|
||||
) usize {
|
||||
const relative =
|
||||
(@as(usize, address) - 0x2000) &
|
||||
0x0fff;
|
||||
|
||||
const table =
|
||||
relative >> 10;
|
||||
|
||||
const offset =
|
||||
relative & 0x3ff;
|
||||
|
||||
const mapped_table: usize =
|
||||
switch (mirroring) {
|
||||
.vertical => switch (table) {
|
||||
0, 2 => 0,
|
||||
1, 3 => 1,
|
||||
else => unreachable,
|
||||
},
|
||||
|
||||
.horizontal => switch (table) {
|
||||
0, 1 => 0,
|
||||
2, 3 => 1,
|
||||
else => unreachable,
|
||||
},
|
||||
|
||||
.single_screen_lower => 0,
|
||||
.single_screen_upper => 1,
|
||||
|
||||
.four_screen => table,
|
||||
};
|
||||
|
||||
return mapped_table * 0x400 +
|
||||
offset;
|
||||
}
|
||||
|
||||
inline fn mapPalette(
|
||||
address: u16,
|
||||
) usize {
|
||||
var index: usize =
|
||||
(@as(usize, address) - 0x3f00) &
|
||||
0x1f;
|
||||
|
||||
// Universal background color mirrors.
|
||||
switch (index) {
|
||||
0x10 => index = 0x00,
|
||||
0x14 => index = 0x04,
|
||||
0x18 => index = 0x08,
|
||||
0x1c => index = 0x0c,
|
||||
else => {},
|
||||
}
|
||||
|
||||
return index;
|
||||
}
|
||||
|
||||
test "NES PPU - Palette Mirroring & NTSC Palette LUT" {
|
||||
try std.testing.expectEqual(@as(usize, 0), mapPalette(0x3F00));
|
||||
try std.testing.expectEqual(@as(usize, 0), mapPalette(0x3F10));
|
||||
try std.testing.expectEqual(@as(usize, 4), mapPalette(0x3F04));
|
||||
try std.testing.expectEqual(@as(usize, 4), mapPalette(0x3F14));
|
||||
|
||||
// Verify system palette RGB565 LUT contains non-zero color constants
|
||||
try std.testing.expectEqual(@as(u16, 0x52AA), nes_ntsc_palette_rgb565[0]);
|
||||
try std.testing.expectEqual(@as(u16, 0xEF7D), nes_ntsc_palette_rgb565[0x20]);
|
||||
}
|
||||
|
||||
test "NES PPU - Scroll Address Increments" {
|
||||
var ppu = Ppu{};
|
||||
ppu.reset();
|
||||
|
||||
ppu.vram_addr = 0x001F; // Coarse X = 31
|
||||
ppu.incrementCoarseX();
|
||||
try std.testing.expectEqual(@as(u16, 0x0400), ppu.vram_addr); // Coarse X = 0, switched nametable X
|
||||
|
||||
ppu.vram_addr = 0x73A0; // Coarse Y = 29, Fine Y = 7
|
||||
ppu.incrementY();
|
||||
try std.testing.expectEqual(@as(u16, 0x0800), ppu.vram_addr); // Coarse Y = 0, Fine Y = 0, switched nametable Y
|
||||
}
|
||||
@@ -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));
|
||||
}
|
||||
@@ -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));
|
||||
}
|
||||
@@ -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));
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
@@ -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
@@ -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");
|
||||
|
||||
|
||||
Reference in New Issue
Block a user