Compare commits
12
Commits
6f897a874c
..
master
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
1240498674 | ||
|
|
296a6c40fa | ||
|
|
460403d3e1 | ||
|
|
71c747d962 | ||
|
|
754dbb9307 | ||
|
|
20c1106a9a | ||
|
|
9ba71b7b62 | ||
|
|
57f349fa17 | ||
|
|
f028213c11 | ||
|
|
425ebe0938 | ||
|
|
60a6364625 | ||
|
|
cc5cb881cc |
@@ -0,0 +1,30 @@
|
|||||||
|
name: CI
|
||||||
|
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
pull_request:
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
test:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- name: Checkout
|
||||||
|
uses: actions/checkout@v4
|
||||||
|
|
||||||
|
- name: Setup Zig
|
||||||
|
uses: https://codeberg.org/mlugg/setup-zig@v2
|
||||||
|
with:
|
||||||
|
version: master
|
||||||
|
|
||||||
|
- name: Zig version
|
||||||
|
run: zig version
|
||||||
|
|
||||||
|
- name: Test
|
||||||
|
run: zig build test --summary all
|
||||||
|
|
||||||
|
- name: Headless release build
|
||||||
|
run: zig build -Dheadless=true -Doptimize=ReleaseSafe
|
||||||
|
|
||||||
|
- name: Release build
|
||||||
|
run: zig build -Doptimize=ReleaseSafe
|
||||||
@@ -2,9 +2,10 @@ shell: |
|
|||||||
set -euxo pipefail
|
set -euxo pipefail
|
||||||
|
|
||||||
echo "=== Radicle CI ==="
|
echo "=== Radicle CI ==="
|
||||||
echo "Repository:"
|
echo "Repository: $(pwd)"
|
||||||
pwd
|
|
||||||
|
|
||||||
zig version
|
zig version
|
||||||
|
|
||||||
|
# test suite & release builds
|
||||||
|
zig build test --summary all
|
||||||
|
zig build -Dheadless=true -Doptimize=ReleaseSafe
|
||||||
zig build -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/).
|
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_step = b.step("test", "Run 6soz tests");
|
||||||
|
|
||||||
const test_options = b.addOptions();
|
const test_options = b.addOptions();
|
||||||
if (b.lazyDependency("nes_test_roms", .{})) |roms_dep| {
|
const roms_dep = b.dependency("nes_test_roms", .{});
|
||||||
test_options.addOption(?[]const u8, "nes_test_roms_dir", roms_dep.path("").getPath(b));
|
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);
|
const func_dep = b.dependency("m6502_functional_tests", .{});
|
||||||
}
|
|
||||||
if (b.lazyDependency("m6502_functional_tests", .{})) |func_dep| {
|
|
||||||
test_options.addOption(?[]const u8, "m6502_functional_tests_dir", func_dep.path("").getPath(b));
|
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 options_mod = test_options.createModule();
|
const options_mod = test_options.createModule();
|
||||||
|
|
||||||
|
|||||||
@@ -6,12 +6,10 @@
|
|||||||
.nes_test_roms = .{
|
.nes_test_roms = .{
|
||||||
.url = "https://github.com/christopherpow/nes-test-roms/archive/refs/heads/master.tar.gz",
|
.url = "https://github.com/christopherpow/nes-test-roms/archive/refs/heads/master.tar.gz",
|
||||||
.hash = "N-V-__8AANPxaAENPnXYuDVuqgUcOdrux1zKqz8bZZYf0RA2",
|
.hash = "N-V-__8AANPxaAENPnXYuDVuqgUcOdrux1zKqz8bZZYf0RA2",
|
||||||
.lazy = true,
|
|
||||||
},
|
},
|
||||||
.m6502_functional_tests = .{
|
.m6502_functional_tests = .{
|
||||||
.url = "https://github.com/Klaus2m5/6502_65C02_functional_tests/archive/refs/heads/master.tar.gz",
|
.url = "https://github.com/Klaus2m5/6502_65C02_functional_tests/archive/refs/heads/master.tar.gz",
|
||||||
.hash = "N-V-__8AAKmLHACkwIMCMRdqnsyLtL5hbQdi8cm3nGs8n-O7",
|
.hash = "N-V-__8AAKmLHACkwIMCMRdqnsyLtL5hbQdi8cm3nGs8n-O7",
|
||||||
.lazy = true,
|
|
||||||
},
|
},
|
||||||
.raylib = .{
|
.raylib = .{
|
||||||
.url = "https://github.com/raysan5/raylib/archive/refs/heads/master.tar.gz",
|
.url = "https://github.com/raysan5/raylib/archive/refs/heads/master.tar.gz",
|
||||||
|
|||||||
Binary file not shown.
+128
-3
@@ -1,5 +1,12 @@
|
|||||||
const std = @import("std");
|
const std = @import("std");
|
||||||
const Nes = @import("system").nes;
|
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 {
|
pub fn main(init: std.process.Init) !void {
|
||||||
const allocator = init.gpa;
|
const allocator = init.gpa;
|
||||||
@@ -44,10 +51,13 @@ pub fn main(init: std.process.Init) !void {
|
|||||||
|
|
||||||
const rom_path = args[2];
|
const rom_path = args[2];
|
||||||
var target_frames: u64 = 60;
|
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 save_state_path: ?[]const u8 = null;
|
||||||
var load_state_path: ?[]const u8 = null;
|
var load_state_path: ?[]const u8 = null;
|
||||||
var save_sram_path: ?[]const u8 = null;
|
var save_sram_path: ?[]const u8 = null;
|
||||||
var load_sram_path: ?[]const u8 = null;
|
var load_sram_path: ?[]const u8 = null;
|
||||||
|
var press_spec: ?PressSpec = null;
|
||||||
|
|
||||||
var i: usize = 3;
|
var i: usize = 3;
|
||||||
while (i < args_count) : (i += 1) {
|
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) {
|
if (std.mem.eql(u8, arg, "--frames") and i + 1 < args_count) {
|
||||||
i += 1;
|
i += 1;
|
||||||
target_frames = try std.fmt.parseInt(u64, args[i], 10);
|
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) {
|
} else if (std.mem.eql(u8, arg, "--save-state") and i + 1 < args_count) {
|
||||||
i += 1;
|
i += 1;
|
||||||
save_state_path = args[i];
|
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{
|
const nes_bindings = frontend.Bindings{
|
||||||
.keys = &.{
|
.keys = &.{
|
||||||
|
// Primary A / B buttons
|
||||||
.{ .key = .z, .target = .{ .button = .{ .device = 0, .button = 0 } } },
|
.{ .key = .z, .target = .{ .button = .{ .device = 0, .button = 0 } } },
|
||||||
.{ .key = .x, .target = .{ .button = .{ .device = 0, .button = 1 } } },
|
.{ .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 = .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 = .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 = .up, .target = .{ .button = .{ .device = 0, .button = 4 } } },
|
||||||
.{ .key = .down, .target = .{ .button = .{ .device = 0, .button = 5 } } },
|
.{ .key = .down, .target = .{ .button = .{ .device = 0, .button = 5 } } },
|
||||||
.{ .key = .left, .target = .{ .button = .{ .device = 0, .button = 6 } } },
|
.{ .key = .left, .target = .{ .button = .{ .device = 0, .button = 6 } } },
|
||||||
.{ .key = .right, .target = .{ .button = .{ .device = 0, .button = 7 } } },
|
.{ .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 = .f5, .target = .{ .action = .quick_save } },
|
||||||
.{ .key = .f8, .target = .{ .action = .quick_load } },
|
.{ .key = .f8, .target = .{ .action = .quick_load } },
|
||||||
.{ .key = .p, .target = .{ .action = .toggle_pause } },
|
.{ .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});
|
std.debug.print("[6soz] Running NES emulator for {d} frames...\n", .{target_frames});
|
||||||
const start_time = std.Io.Clock.Timestamp.now(init.io, .awake);
|
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;
|
var frame: u64 = 0;
|
||||||
while (frame < target_frames and app.state == .running) : (frame += 1) {
|
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);
|
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);
|
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(" - Frames: {d}\n", .{target_frames});
|
||||||
std.debug.print(" - Elapsed Time: {d:.3}s\n", .{elapsed_sec});
|
std.debug.print(" - Elapsed Time: {d:.3}s\n", .{elapsed_sec});
|
||||||
std.debug.print(" - Performance: {d:.1} FPS\n", .{fps});
|
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| {
|
if (save_state_path) |path| {
|
||||||
@@ -189,6 +311,9 @@ fn printUsage() void {
|
|||||||
\\
|
\\
|
||||||
\\Options:
|
\\Options:
|
||||||
\\ --frames <N> Number of frames to execute (default: 60)
|
\\ --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
|
\\ --save-state <file> Save state binary file output
|
||||||
\\ --load-state <file> Load state binary file input
|
\\ --load-state <file> Load state binary file input
|
||||||
\\ --save-sram <file> Save battery PRG RAM output
|
\\ --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_head: usize = 0,
|
||||||
audio_ring_tail: usize = 0,
|
audio_ring_tail: usize = 0,
|
||||||
|
|
||||||
event_queue: [16]platform.Event = undefined,
|
event_queue: [64]platform.Event = undefined,
|
||||||
event_head: usize = 0,
|
event_head: usize = 0,
|
||||||
event_tail: 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 {
|
pub fn init(title: [*:0]const u8, width: i32, height: i32, scale: i32) Driver {
|
||||||
c.InitWindow(width * scale, height * scale, title);
|
c.InitWindow(width * scale, height * scale, title);
|
||||||
@@ -63,10 +64,22 @@ pub const Driver = struct {
|
|||||||
return ev;
|
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
|
// Poll Raylib Key Events
|
||||||
const key_mappings = [_]struct { c_key: c_int, key: platform.Key }{
|
const key_mappings = [_]struct { c_key: c_int, key: platform.Key }{
|
||||||
.{ .c_key = c.KEY_Z, .key = .z },
|
.{ .c_key = c.KEY_Z, .key = .z },
|
||||||
.{ .c_key = c.KEY_X, .key = .x },
|
.{ .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_RIGHT_SHIFT, .key = .right_shift },
|
||||||
.{ .c_key = c.KEY_LEFT_SHIFT, .key = .left_shift },
|
.{ .c_key = c.KEY_LEFT_SHIFT, .key = .left_shift },
|
||||||
.{ .c_key = c.KEY_ENTER, .key = .enter },
|
.{ .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_DOWN, .key = .down },
|
||||||
.{ .c_key = c.KEY_LEFT, .key = .left },
|
.{ .c_key = c.KEY_LEFT, .key = .left },
|
||||||
.{ .c_key = c.KEY_RIGHT, .key = .right },
|
.{ .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_F5, .key = .f5 },
|
||||||
.{ .c_key = c.KEY_F8, .key = .f8 },
|
.{ .c_key = c.KEY_F8, .key = .f8 },
|
||||||
.{ .c_key = c.KEY_P, .key = .p },
|
.{ .c_key = c.KEY_P, .key = .p },
|
||||||
@@ -81,12 +98,42 @@ pub const Driver = struct {
|
|||||||
|
|
||||||
for (key_mappings) |mapping| {
|
for (key_mappings) |mapping| {
|
||||||
if (c.IsKeyPressed(mapping.c_key)) {
|
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)) {
|
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;
|
return null;
|
||||||
}
|
}
|
||||||
@@ -116,6 +163,8 @@ pub const Driver = struct {
|
|||||||
.height = @floatFromInt(self.height * self.scale),
|
.height = @floatFromInt(self.height * self.scale),
|
||||||
};
|
};
|
||||||
c.DrawTexturePro(self.texture, src, dest, c.Vector2{ .x = 0, .y = 0 }, 0.0, c.WHITE);
|
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 {
|
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");
|
ppu: Ppu,
|
||||||
|
apu: Apu,
|
||||||
const Bus = @This();
|
controllers: [2]Controller = .{ Controller.init(), Controller.init() },
|
||||||
|
cartridge: ?Cartridge = null,
|
||||||
ram: [0x800]u8 =
|
|
||||||
[_]u8{0} ** 0x800,
|
|
||||||
|
|
||||||
cartridge: Cartridge,
|
|
||||||
|
|
||||||
ppu: Ppu = .{},
|
|
||||||
apu: Apu = .{},
|
|
||||||
|
|
||||||
controllers: [2]Controller = .{
|
|
||||||
.{},
|
|
||||||
.{},
|
|
||||||
},
|
|
||||||
|
|
||||||
open_bus: u8 = 0,
|
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(
|
pub fn init(region: Region, cart: ?Cartridge) Bus {
|
||||||
rom: []const u8,
|
|
||||||
) Cartridge.Error!Bus {
|
|
||||||
return .{
|
return .{
|
||||||
.cartridge = try Cartridge.init(rom),
|
.ppu = Ppu.init(region),
|
||||||
|
.apu = Apu.init(region),
|
||||||
|
.cartridge = cart,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn reset(
|
pub fn reset(self: *Bus) void {
|
||||||
self: *Bus,
|
|
||||||
) void {
|
|
||||||
self.ppu.reset();
|
self.ppu.reset();
|
||||||
self.apu.reset();
|
self.apu.reset();
|
||||||
|
self.controllers[0].reset();
|
||||||
for (&self.controllers) |*controller|
|
self.controllers[1].reset();
|
||||||
controller.reset();
|
self.dma_active = false;
|
||||||
|
self.dma_dummy = true;
|
||||||
self.oam_dma_page = null;
|
self.dma_addr = 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
pub inline fn read(
|
pub fn mapNametableAddress(mirroring_mode: Mirroring, address: u16) u11 {
|
||||||
self: *Bus,
|
const v = (address - 0x2000) & 0x0fff;
|
||||||
address: u16,
|
return switch (mirroring_mode) {
|
||||||
) u8 {
|
.horizontal => @truncate(((v >> 1) & 0x0400) | (v & 0x03ff)),
|
||||||
const value: u8 = switch (address) {
|
.vertical => @truncate(v & 0x07ff),
|
||||||
// 2 KiB internal RAM + mirrors.
|
.single_screen_lower => @truncate(v & 0x03ff),
|
||||||
0x0000...0x1fff => self.ram[address & 0x07ff],
|
.single_screen_upper => @truncate(0x0400 | (v & 0x03ff)),
|
||||||
|
.four_screen => @truncate(v & 0x07ff),
|
||||||
// PPU registers + mirrors.
|
|
||||||
0x2000...0x3fff => self.ppu.cpuRead(
|
|
||||||
&self.cartridge,
|
|
||||||
address & 7,
|
|
||||||
),
|
|
||||||
|
|
||||||
// APU status.
|
|
||||||
0x4015 => self.apu.readStatus(),
|
|
||||||
|
|
||||||
// Controller 1.
|
|
||||||
0x4016 => self.controllers[0].read(),
|
|
||||||
|
|
||||||
// Controller 2.
|
|
||||||
0x4017 => self.controllers[1].read(),
|
|
||||||
|
|
||||||
// Cartridge space.
|
|
||||||
0x4020...0xffff => self.cartridge.cpuRead(address),
|
|
||||||
|
|
||||||
else => self.open_bus,
|
|
||||||
};
|
};
|
||||||
|
|
||||||
self.open_bus = value;
|
|
||||||
|
|
||||||
return value;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
pub inline fn write(
|
pub const PpuBus = struct {
|
||||||
self: *Bus,
|
bus: *Bus,
|
||||||
address: u16,
|
|
||||||
value: u8,
|
pub fn read(self: *PpuBus, address: u16) u8 {
|
||||||
) void {
|
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;
|
self.open_bus = value;
|
||||||
|
|
||||||
switch (address) {
|
switch (address) {
|
||||||
0x0000...0x1fff => self.ram[address & 0x07ff] =
|
0x0000...0x1fff => self.ram[address & 0x07ff] = value,
|
||||||
value,
|
0x2000...0x3fff => {
|
||||||
|
var ppu_bus = self.getPpuBus();
|
||||||
0x2000...0x3fff => self.ppu.cpuWrite(
|
_ = self.ppu.cpuWrite(&ppu_bus, address, value);
|
||||||
&self.cartridge,
|
},
|
||||||
address & 7,
|
0x4000...0x4013, 0x4015, 0x4017 => self.apu.write(address, value),
|
||||||
value,
|
0x4014 => {
|
||||||
),
|
self.dma_page = value;
|
||||||
|
self.dma_addr = 0;
|
||||||
0x4000...0x4013 => self.apu.write(
|
self.dma_active = true;
|
||||||
address,
|
self.dma_dummy = true;
|
||||||
value,
|
},
|
||||||
),
|
|
||||||
|
|
||||||
// OAMDMA
|
|
||||||
0x4014 => self.oam_dma_page = value,
|
|
||||||
|
|
||||||
0x4015 => self.apu.writeStatus(value),
|
|
||||||
|
|
||||||
0x4016 => {
|
0x4016 => {
|
||||||
self.controllers[0].write(value);
|
self.controllers[0].write(value);
|
||||||
self.controllers[1].write(value);
|
self.controllers[1].write(value);
|
||||||
},
|
},
|
||||||
|
0x4018...0x401f => {},
|
||||||
0x4017 => self.apu.writeFrameCounter(value),
|
0x4020...0xffff => {
|
||||||
|
if (self.cartridge) |*c| {
|
||||||
else => self.cartridge.cpuWrite(
|
_ = c.cpuWrite(address, value);
|
||||||
address,
|
}
|
||||||
value,
|
},
|
||||||
),
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Called exactly once for every CPU cycle.
|
/// Clocks one CPU cycle: steps APU and 3 PPU dots.
|
||||||
pub inline fn tick(
|
pub fn tick(self: *Bus) void {
|
||||||
self: *Bus,
|
self.apu.clockCpu();
|
||||||
) void {
|
self.ppu.registers.tickCpuCycle();
|
||||||
self.apu.tick();
|
|
||||||
|
|
||||||
// NTSC NES: PPU runs at 3× CPU clock.
|
var ppu_bus = self.getPpuBus();
|
||||||
self.ppu.tick(&self.cartridge);
|
self.ppu.clock(&ppu_bus);
|
||||||
self.ppu.tick(&self.cartridge);
|
self.ppu.clock(&ppu_bus);
|
||||||
self.ppu.tick(&self.cartridge);
|
self.ppu.clock(&ppu_bus);
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn nmiLine(
|
/// Executes one DMA CPU cycle step.
|
||||||
self: *Bus,
|
pub fn stepDma(self: *Bus) void {
|
||||||
) bool {
|
if (self.dma_dummy) {
|
||||||
return self.ppu.nmiAsserted();
|
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(
|
pub fn nmiLine(self: *const Bus) bool {
|
||||||
self: *Bus,
|
return self.ppu.nmiLine();
|
||||||
) bool {
|
|
||||||
return self.apu.irqAsserted() or
|
|
||||||
self.cartridge.irqAsserted();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn takeOamDmaPage(
|
pub fn irqLine(self: *const Bus) bool {
|
||||||
self: *Bus,
|
const apu_irq = self.apu.irqAsserted();
|
||||||
) ?u8 {
|
const cart_irq = if (self.cartridge) |*c| c.irqLine() else false;
|
||||||
const page =
|
return apu_irq or cart_irq;
|
||||||
self.oam_dma_page;
|
}
|
||||||
|
|
||||||
self.oam_dma_page = null;
|
// consolidated bus memory mirroring test suite
|
||||||
|
test "bus internal RAM and CIRAM nametable mirroring" {
|
||||||
return page;
|
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) {
|
const std = @import("std");
|
||||||
a = 0,
|
const common = @import("common.zig");
|
||||||
b = 1,
|
|
||||||
select = 2,
|
|
||||||
start = 3,
|
|
||||||
up = 4,
|
|
||||||
down = 5,
|
|
||||||
left = 6,
|
|
||||||
right = 7,
|
|
||||||
};
|
|
||||||
|
|
||||||
const Controller = @This();
|
pub const Controller = @This();
|
||||||
|
|
||||||
buttons: u8 = 0,
|
buttons: u8 = 0,
|
||||||
|
|
||||||
shift_register: u8 = 0,
|
shift_register: u8 = 0,
|
||||||
strobe: bool = false,
|
strobe: bool = false,
|
||||||
|
|
||||||
pub fn reset(
|
pub fn init() Controller {
|
||||||
self: *Controller,
|
return .{};
|
||||||
) void {
|
}
|
||||||
|
|
||||||
|
pub fn reset(self: *Controller) void {
|
||||||
self.shift_register = 0;
|
self.shift_register = 0;
|
||||||
self.strobe = false;
|
self.strobe = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn setButtons(
|
pub fn setButtons(self: *Controller, buttons: u8) void {
|
||||||
self: *Controller,
|
|
||||||
buttons: u8,
|
|
||||||
) void {
|
|
||||||
self.buttons = buttons;
|
self.buttons = buttons;
|
||||||
|
if (self.strobe) {
|
||||||
if (self.strobe)
|
|
||||||
self.shift_register = buttons;
|
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;
|
self.strobe = new_strobe;
|
||||||
|
if (new_strobe) {
|
||||||
|
self.shift_register = self.buttons;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn read(
|
pub fn read(self: *Controller) u8 {
|
||||||
self: *Controller,
|
if (self.strobe) {
|
||||||
) u8 {
|
return (self.buttons & 1) | 0x40;
|
||||||
if (self.strobe)
|
}
|
||||||
return self.buttons & 1;
|
const val = self.shift_register & 1;
|
||||||
|
// Real controllers shift in 1s once all 8 bits are read
|
||||||
const value =
|
self.shift_register = (self.shift_register >> 1) | 0x80;
|
||||||
self.shift_register & 1;
|
return val | 0x40;
|
||||||
|
}
|
||||||
// Real controllers return 1 after all
|
|
||||||
// eight buttons have shifted out.
|
// consolidated controller test suite
|
||||||
self.shift_register =
|
test "controller strobe and serial shift read" {
|
||||||
(self.shift_register >> 1) | 0x80;
|
var c = Controller.init();
|
||||||
|
c.setButtons(0x09); // A and Start pressed
|
||||||
return value;
|
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 std = @import("std");
|
||||||
const Mirroring = types.Mirroring;
|
const common = @import("../common.zig");
|
||||||
|
const Mirroring = common.Mirroring;
|
||||||
|
|
||||||
const Axrom = @This();
|
pub const Axrom = @This();
|
||||||
|
|
||||||
prg_bank_select: u8 = 0,
|
prg_bank: u4 = 0,
|
||||||
mirroring_select: u1 = 0,
|
mirroring_mode: Mirroring = .single_screen_lower,
|
||||||
prg_bank_count: usize,
|
|
||||||
|
|
||||||
pub fn init(
|
pub fn init() Axrom {
|
||||||
prg_size: usize,
|
return .{};
|
||||||
) Axrom {
|
|
||||||
return .{
|
|
||||||
.prg_bank_count = prg_size / 0x8000,
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn cpuMapRead(
|
pub fn cpuRead(self: *const Axrom, address: u16, prg_rom: []const u8, prg_ram: []const u8) ?u8 {
|
||||||
self: *const Axrom,
|
if (address >= 0x6000 and address <= 0x7fff) {
|
||||||
address: u16,
|
const offset = address - 0x6000;
|
||||||
) ?usize {
|
if (offset < prg_ram.len) return prg_ram[offset];
|
||||||
if (address < 0x8000) return null;
|
return 0;
|
||||||
|
|
||||||
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);
|
|
||||||
}
|
}
|
||||||
|
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(
|
pub fn cpuWrite(self: *Axrom, address: u16, value: u8, prg_ram: []u8) bool {
|
||||||
_: *const Axrom,
|
if (address >= 0x6000 and address <= 0x7fff) {
|
||||||
address: u16,
|
const offset = address - 0x6000;
|
||||||
) ?usize {
|
if (offset < prg_ram.len) {
|
||||||
if (address >= 0x2000) return null;
|
prg_ram[offset] = value;
|
||||||
return address;
|
return true;
|
||||||
}
|
}
|
||||||
|
}
|
||||||
pub fn ppuMapWrite(
|
if (address >= 0x8000 and address <= 0xffff) {
|
||||||
_: *const Axrom,
|
self.prg_bank = @truncate(value & 0x0f);
|
||||||
address: u16,
|
self.mirroring_mode = if ((value & 0x10) != 0) .single_screen_upper else .single_screen_lower;
|
||||||
) ?usize {
|
return true;
|
||||||
if (address >= 0x2000) return null;
|
}
|
||||||
return address;
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn mirroring(
|
|
||||||
self: *const Axrom,
|
|
||||||
) Mirroring {
|
|
||||||
return if (self.mirroring_select == 0) .single_screen_lower else .single_screen_upper;
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn irqAsserted(_: *const Axrom) bool {
|
|
||||||
return false;
|
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 std = @import("std");
|
||||||
const Mirroring = types.Mirroring;
|
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,
|
prg_bank: u8 = 0,
|
||||||
mirroring_select: u1 = 0,
|
|
||||||
mirroring_mode: Mirroring,
|
mirroring_mode: Mirroring,
|
||||||
has_single_screen_mirroring: bool = false,
|
has_mirroring_control: bool,
|
||||||
|
|
||||||
prg_bank_count: usize,
|
pub fn init(initial_mirroring: Mirroring) Camerica {
|
||||||
|
|
||||||
pub fn init(
|
|
||||||
prg_size: usize,
|
|
||||||
_mirroring: Mirroring,
|
|
||||||
) Camerica {
|
|
||||||
return .{
|
return .{
|
||||||
.prg_bank_count = prg_size / 0x4000,
|
.mirroring_mode = initial_mirroring,
|
||||||
.mirroring_mode = _mirroring,
|
.has_mirroring_control = false,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn cpuMapRead(
|
pub fn cpuRead(self: *const Camerica, address: u16, prg_rom: []const u8, prg_ram: []const u8) ?u8 {
|
||||||
self: *const Camerica,
|
if (address >= 0x6000 and address <= 0x7fff) {
|
||||||
address: u16,
|
const offset = address - 0x6000;
|
||||||
) ?usize {
|
if (offset < prg_ram.len) return prg_ram[offset];
|
||||||
if (address < 0x8000) return null;
|
return 0;
|
||||||
|
}
|
||||||
if (address < 0xC000) {
|
if (address >= 0x8000 and address <= 0xffff) {
|
||||||
const offset = @as(usize, address - 0x8000);
|
const total_16k_banks = @max(1, prg_rom.len / 0x4000);
|
||||||
const bank = @as(usize, self.prg_bank) % self.prg_bank_count;
|
var bank: usize = 0;
|
||||||
return bank * 0x4000 + offset;
|
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 {
|
} else {
|
||||||
const offset = @as(usize, address - 0xC000);
|
bank = total_16k_banks - 1;
|
||||||
const fixed_bank = if (self.prg_bank_count > 0) self.prg_bank_count - 1 else 0;
|
const offset = (bank * 0x4000) + (address - 0xc000);
|
||||||
return fixed_bank * 0x4000 + offset;
|
if (offset < prg_rom.len) return prg_rom[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;
|
|
||||||
}
|
}
|
||||||
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn ppuMapRead(
|
pub fn cpuWrite(self: *Camerica, address: u16, value: u8, prg_ram: []u8) bool {
|
||||||
_: *const Camerica,
|
if (address >= 0x6000 and address <= 0x7fff) {
|
||||||
address: u16,
|
const offset = address - 0x6000;
|
||||||
) ?usize {
|
if (offset < prg_ram.len) {
|
||||||
if (address >= 0x2000) return null;
|
prg_ram[offset] = value;
|
||||||
return address;
|
return true;
|
||||||
}
|
|
||||||
|
|
||||||
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;
|
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
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;
|
return self.mirroring_mode;
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn irqAsserted(_: *const Camerica) bool {
|
pub fn irqLine(_: *const Camerica) bool {
|
||||||
return false;
|
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 std = @import("std");
|
||||||
const Mirroring = types.Mirroring;
|
const common = @import("../common.zig");
|
||||||
|
const Mirroring = common.Mirroring;
|
||||||
|
|
||||||
const Cnrom = @This();
|
pub const Cnrom = @This();
|
||||||
|
|
||||||
prg_16k: bool,
|
chr_bank: u8 = 0,
|
||||||
chr_bank_select: u8 = 0,
|
|
||||||
chr_bank_count: usize,
|
|
||||||
mirroring_mode: Mirroring,
|
mirroring_mode: Mirroring,
|
||||||
|
|
||||||
pub fn init(
|
pub fn init(initial_mirroring: Mirroring) Cnrom {
|
||||||
prg_size: usize,
|
return .{ .mirroring_mode = initial_mirroring };
|
||||||
chr_size: usize,
|
|
||||||
_mirroring: Mirroring,
|
|
||||||
) Cnrom {
|
|
||||||
return .{
|
|
||||||
.prg_16k = prg_size == 0x4000,
|
|
||||||
.chr_bank_count = if (chr_size > 0) chr_size / 0x2000 else 1,
|
|
||||||
.mirroring_mode = _mirroring,
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn cpuMapRead(
|
pub fn cpuRead(_: *const Cnrom, address: u16, prg_rom: []const u8, prg_ram: []const u8) ?u8 {
|
||||||
self: *const Cnrom,
|
if (address >= 0x6000 and address <= 0x7fff) {
|
||||||
address: u16,
|
const offset = address - 0x6000;
|
||||||
) ?usize {
|
if (offset < prg_ram.len) return prg_ram[offset];
|
||||||
if (address < 0x8000) return null;
|
return 0;
|
||||||
|
|
||||||
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;
|
|
||||||
}
|
}
|
||||||
|
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(
|
pub fn cpuWrite(self: *Cnrom, address: u16, value: u8, prg_ram: []u8) bool {
|
||||||
self: *const Cnrom,
|
if (address >= 0x6000 and address <= 0x7fff) {
|
||||||
address: u16,
|
const offset = address - 0x6000;
|
||||||
) ?usize {
|
if (offset < prg_ram.len) {
|
||||||
if (address >= 0x2000) return null;
|
prg_ram[offset] = value;
|
||||||
|
return true;
|
||||||
const bank = @as(usize, self.chr_bank_select) % self.chr_bank_count;
|
}
|
||||||
return bank * 0x2000 + @as(usize, address);
|
}
|
||||||
|
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(
|
pub fn ppuRead(self: *const Cnrom, address: u16, chr_rom: []const u8, _: []const u8, _: bool) ?u8 {
|
||||||
_: *const Cnrom,
|
if (address < 0x2000) {
|
||||||
address: u16,
|
if (chr_rom.len > 0) {
|
||||||
) ?usize {
|
const total_8k_banks = @max(1, chr_rom.len / 0x2000);
|
||||||
if (address >= 0x2000) return null;
|
const bank = @as(usize, self.chr_bank) % total_8k_banks;
|
||||||
return address;
|
const offset = (bank * 0x2000) + address;
|
||||||
|
if (offset < chr_rom.len) return chr_rom[offset];
|
||||||
|
}
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn mirroring(
|
pub fn ppuWrite(_: *Cnrom, _: u16, _: u8, _: []u8, _: bool) bool {
|
||||||
self: *const Cnrom,
|
return false;
|
||||||
) Mirroring {
|
}
|
||||||
|
|
||||||
|
pub fn mirroring(self: *const Cnrom) Mirroring {
|
||||||
return self.mirroring_mode;
|
return self.mirroring_mode;
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn irqAsserted(_: *const Cnrom) bool {
|
pub fn irqLine(_: *const Cnrom) bool {
|
||||||
return false;
|
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 std = @import("std");
|
||||||
const Mirroring = types.Mirroring;
|
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,
|
prg_bank: u8 = 0,
|
||||||
chr_bank: u8 = 0,
|
chr_bank: u8 = 0,
|
||||||
|
|
||||||
prg_bank_count: usize,
|
|
||||||
chr_bank_count: usize,
|
|
||||||
mirroring_mode: Mirroring,
|
mirroring_mode: Mirroring,
|
||||||
|
|
||||||
pub fn init(
|
pub fn init(initial_mirroring: Mirroring) Gxrom {
|
||||||
prg_size: usize,
|
return .{ .mirroring_mode = initial_mirroring };
|
||||||
chr_size: usize,
|
|
||||||
_mirroring: Mirroring,
|
|
||||||
) Gxrom {
|
|
||||||
return .{
|
|
||||||
.prg_bank_count = prg_size / 0x8000,
|
|
||||||
.chr_bank_count = if (chr_size > 0) chr_size / 0x2000 else 1,
|
|
||||||
.mirroring_mode = _mirroring,
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn cpuMapRead(
|
pub fn cpuRead(self: *const Gxrom, address: u16, prg_rom: []const u8, prg_ram: []const u8) ?u8 {
|
||||||
self: *const Gxrom,
|
if (address >= 0x6000 and address <= 0x7fff) {
|
||||||
address: u16,
|
const offset = address - 0x6000;
|
||||||
) ?usize {
|
if (offset < prg_ram.len) return prg_ram[offset];
|
||||||
if (address < 0x8000) return null;
|
return 0;
|
||||||
|
|
||||||
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;
|
|
||||||
}
|
}
|
||||||
|
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(
|
pub fn cpuWrite(self: *Gxrom, address: u16, value: u8, prg_ram: []u8) bool {
|
||||||
self: *const Gxrom,
|
if (address >= 0x6000 and address <= 0x7fff) {
|
||||||
address: u16,
|
const offset = address - 0x6000;
|
||||||
) ?usize {
|
if (offset < prg_ram.len) {
|
||||||
if (address >= 0x2000) return null;
|
prg_ram[offset] = value;
|
||||||
|
return true;
|
||||||
const bank = @as(usize, self.chr_bank) % self.chr_bank_count;
|
}
|
||||||
return bank * 0x2000 + @as(usize, address);
|
}
|
||||||
|
if (address >= 0x8000 and address <= 0xffff) {
|
||||||
|
self.prg_bank = (value >> 4) & 0x03;
|
||||||
|
self.chr_bank = value & 0x03;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn ppuMapWrite(
|
pub fn ppuRead(self: *const Gxrom, address: u16, chr_rom: []const u8, chr_ram: []const u8, chr_is_ram: bool) ?u8 {
|
||||||
_: *const Gxrom,
|
if (address < 0x2000) {
|
||||||
address: u16,
|
if (chr_is_ram) {
|
||||||
) ?usize {
|
if (address < chr_ram.len) return chr_ram[address];
|
||||||
if (address >= 0x2000) return null;
|
} else if (chr_rom.len > 0) {
|
||||||
return address;
|
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(
|
pub fn ppuWrite(_: *Gxrom, address: u16, value: u8, chr_ram: []u8, chr_is_ram: bool) bool {
|
||||||
self: *const Gxrom,
|
if (address < 0x2000 and chr_is_ram) {
|
||||||
) Mirroring {
|
if (address < chr_ram.len) {
|
||||||
|
chr_ram[address] = value;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn mirroring(self: *const Gxrom) Mirroring {
|
||||||
return self.mirroring_mode;
|
return self.mirroring_mode;
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn irqAsserted(_: *const Gxrom) bool {
|
pub fn irqLine(_: *const Gxrom) bool {
|
||||||
return false;
|
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 std = @import("std");
|
||||||
const Mirroring = types.Mirroring;
|
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: u8 = 0,
|
||||||
prg_bank_count: usize,
|
|
||||||
mirroring_mode: Mirroring,
|
mirroring_mode: Mirroring,
|
||||||
|
|
||||||
pub fn init(
|
pub fn init(initial_mirroring: Mirroring) Mapper180 {
|
||||||
prg_size: usize,
|
return .{ .mirroring_mode = initial_mirroring };
|
||||||
_mirroring: Mirroring,
|
|
||||||
) Mapper180 {
|
|
||||||
return .{
|
|
||||||
.prg_bank_count = prg_size / 0x4000,
|
|
||||||
.mirroring_mode = _mirroring,
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn cpuMapRead(
|
pub fn cpuRead(self: *const Mapper180, address: u16, prg_rom: []const u8, prg_ram: []const u8) ?u8 {
|
||||||
self: *const Mapper180,
|
if (address >= 0x6000 and address <= 0x7fff) {
|
||||||
address: u16,
|
const offset = address - 0x6000;
|
||||||
) ?usize {
|
if (offset < prg_ram.len) return prg_ram[offset];
|
||||||
if (address < 0x8000) return null;
|
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) {
|
if (address < 0xc000) {
|
||||||
const offset = @as(usize, address - 0x8000);
|
bank = 0;
|
||||||
return offset; // Fixed bank 0
|
const offset = address - 0x8000;
|
||||||
|
if (offset < prg_rom.len) return prg_rom[offset];
|
||||||
} else {
|
} else {
|
||||||
const offset = @as(usize, address - 0xC000);
|
bank = @as(usize, self.prg_bank) % total_16k_banks;
|
||||||
const bank = @as(usize, self.prg_bank) % self.prg_bank_count;
|
const offset = (bank * 0x4000) + (address - 0xc000);
|
||||||
return bank * 0x4000 + offset;
|
if (offset < prg_rom.len) return prg_rom[offset];
|
||||||
}
|
}
|
||||||
}
|
return 0;
|
||||||
|
|
||||||
pub fn cpuWrite(
|
|
||||||
self: *Mapper180,
|
|
||||||
address: u16,
|
|
||||||
value: u8,
|
|
||||||
) void {
|
|
||||||
if (address >= 0x8000) {
|
|
||||||
self.prg_bank = value & 0x07;
|
|
||||||
}
|
}
|
||||||
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn ppuMapRead(
|
pub fn cpuWrite(self: *Mapper180, address: u16, value: u8, prg_ram: []u8) bool {
|
||||||
_: *const Mapper180,
|
if (address >= 0x6000 and address <= 0x7fff) {
|
||||||
address: u16,
|
const offset = address - 0x6000;
|
||||||
) ?usize {
|
if (offset < prg_ram.len) {
|
||||||
if (address >= 0x2000) return null;
|
prg_ram[offset] = value;
|
||||||
return address;
|
return true;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if (address >= 0x8000 and address <= 0xffff) {
|
||||||
|
self.prg_bank = value & 0x0f;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn ppuMapWrite(
|
pub fn ppuRead(_: *const Mapper180, address: u16, chr_rom: []const u8, chr_ram: []const u8, chr_is_ram: bool) ?u8 {
|
||||||
_: *const Mapper180,
|
if (address < 0x2000) {
|
||||||
address: u16,
|
if (chr_is_ram) {
|
||||||
) ?usize {
|
if (address < chr_ram.len) return chr_ram[address];
|
||||||
if (address >= 0x2000) return null;
|
} else if (chr_rom.len > 0) {
|
||||||
return address;
|
if (address < chr_rom.len) return chr_rom[address];
|
||||||
|
}
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn mirroring(
|
pub fn ppuWrite(_: *Mapper180, address: u16, value: u8, chr_ram: []u8, chr_is_ram: bool) bool {
|
||||||
self: *const Mapper180,
|
if (address < 0x2000 and chr_is_ram) {
|
||||||
) Mirroring {
|
if (address < chr_ram.len) {
|
||||||
|
chr_ram[address] = value;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn mirroring(self: *const Mapper180) Mirroring {
|
||||||
return self.mirroring_mode;
|
return self.mirroring_mode;
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn irqAsserted(_: *const Mapper180) bool {
|
pub fn irqLine(_: *const Mapper180) bool {
|
||||||
return false;
|
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;
|
|
||||||
}
|
|
||||||
+123
-84
@@ -1,130 +1,152 @@
|
|||||||
const types = @import("types.zig");
|
const std = @import("std");
|
||||||
const Mirroring = types.Mirroring;
|
const common = @import("../common.zig");
|
||||||
|
const Mirroring = common.Mirroring;
|
||||||
|
|
||||||
const Mmc1 = @This();
|
pub const Mmc1 = @This();
|
||||||
|
|
||||||
shift_register: u8 = 0x10,
|
shift_register: u5 = 0x10,
|
||||||
control: u8 = 0x0c, // Default: PRG mode 3 (fix $C000)
|
control: u5 = 0x0c, // PRG mode 3 (fixed $C000)
|
||||||
chr_bank_0: u8 = 0,
|
chr_bank_0: u5 = 0,
|
||||||
chr_bank_1: u8 = 0,
|
chr_bank_1: u5 = 0,
|
||||||
prg_bank: u8 = 0,
|
prg_bank: u5 = 0,
|
||||||
|
|
||||||
prg_bank_count: usize,
|
pub fn init() Mmc1 {
|
||||||
chr_bank_count: usize,
|
return .{};
|
||||||
|
|
||||||
pub fn init(
|
|
||||||
prg_size: usize,
|
|
||||||
chr_size: usize,
|
|
||||||
) Mmc1 {
|
|
||||||
return .{
|
|
||||||
.prg_bank_count = prg_size / 0x4000,
|
|
||||||
.chr_bank_count = if (chr_size > 0) chr_size / 0x1000 else 2,
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn cpuMapRead(
|
pub fn cpuRead(self: *const Mmc1, address: u16, prg_rom: []const u8, prg_ram: []const u8) ?u8 {
|
||||||
self: *const Mmc1,
|
if (address >= 0x6000 and address <= 0x7fff) {
|
||||||
address: u16,
|
// Bit 4 of prg_bank is PRG RAM disable (0: enabled, 1: disabled)
|
||||||
) ?usize {
|
if ((self.prg_bank & 0x10) != 0) return 0;
|
||||||
if (address < 0x8000) return null;
|
const offset = address - 0x6000;
|
||||||
|
if (offset < prg_ram.len) return prg_ram[offset];
|
||||||
const prg_mode = (self.control >> 2) & 0x03;
|
return 0;
|
||||||
const offset = @as(usize, address & 0x3fff);
|
}
|
||||||
|
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;
|
||||||
|
|
||||||
switch (prg_mode) {
|
switch (prg_mode) {
|
||||||
0, 1 => {
|
0, 1 => {
|
||||||
// 32 KB mode
|
// 32 KB mode: ignore low bit of prg_bank
|
||||||
const bank = @as(usize, self.prg_bank & 0x0e) % self.prg_bank_count;
|
bank = ((self.prg_bank & 0x0e) | surom_base) % total_16k_banks;
|
||||||
return bank * 0x4000 + @as(usize, address - 0x8000);
|
const offset = (bank * 0x4000) + (address - 0x8000);
|
||||||
|
if (offset < prg_rom.len) return prg_rom[offset];
|
||||||
},
|
},
|
||||||
2 => {
|
2 => {
|
||||||
// Fix first bank at $8000, switch 16 KB bank at $C000
|
// Fix first bank at $8000, switch 16 KB bank at $C000
|
||||||
if (address < 0xc000) {
|
if (address < 0xc000) {
|
||||||
return offset;
|
bank = surom_base % total_16k_banks;
|
||||||
|
const offset = (bank * 0x4000) + (address - 0x8000);
|
||||||
|
if (offset < prg_rom.len) return prg_rom[offset];
|
||||||
} else {
|
} else {
|
||||||
const bank = @as(usize, self.prg_bank & 0x0f) % self.prg_bank_count;
|
bank = ((self.prg_bank & 0x0f) | surom_base) % total_16k_banks;
|
||||||
return bank * 0x4000 + offset;
|
const offset = (bank * 0x4000) + (address - 0xc000);
|
||||||
|
if (offset < prg_rom.len) return prg_rom[offset];
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
3 => {
|
3 => {
|
||||||
// Fix last bank at $C000, switch 16 KB bank at $8000
|
// Switch 16 KB bank at $8000, fix last bank at $C000
|
||||||
if (address < 0xc000) {
|
if (address < 0xc000) {
|
||||||
const bank = @as(usize, self.prg_bank & 0x0f) % self.prg_bank_count;
|
bank = ((self.prg_bank & 0x0f) | surom_base) % total_16k_banks;
|
||||||
return bank * 0x4000 + offset;
|
const offset = (bank * 0x4000) + (address - 0x8000);
|
||||||
|
if (offset < prg_rom.len) return prg_rom[offset];
|
||||||
} else {
|
} else {
|
||||||
const last_bank = self.prg_bank_count - 1;
|
bank = (surom_base + 15) % total_16k_banks;
|
||||||
return last_bank * 0x4000 + offset;
|
const offset = (bank * 0x4000) + (address - 0xc000);
|
||||||
|
if (offset < prg_rom.len) return prg_rom[offset];
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
else => unreachable,
|
|
||||||
}
|
}
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn cpuWrite(
|
pub fn cpuWrite(self: *Mmc1, address: u16, value: u8, prg_ram: []u8) bool {
|
||||||
self: *Mmc1,
|
if (address >= 0x6000 and address <= 0x7fff) {
|
||||||
address: u16,
|
if ((self.prg_bank & 0x10) != 0) return false;
|
||||||
value: u8,
|
const offset = address - 0x6000;
|
||||||
) void {
|
if (offset < prg_ram.len) {
|
||||||
if (address < 0x8000) return;
|
prg_ram[offset] = value;
|
||||||
|
return true;
|
||||||
// Bit 7 reset flag
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if (address >= 0x8000 and address <= 0xffff) {
|
||||||
if ((value & 0x80) != 0) {
|
if ((value & 0x80) != 0) {
|
||||||
|
// Reset shift register
|
||||||
self.shift_register = 0x10;
|
self.shift_register = 0x10;
|
||||||
self.control |= 0x0c;
|
self.control |= 0x0c; // PRG mode 3
|
||||||
return;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
const complete = (self.shift_register & 1) != 0;
|
const is_full = (self.shift_register & 1) != 0;
|
||||||
self.shift_register = (self.shift_register >> 1) | ((value & 1) << 4);
|
self.shift_register = (self.shift_register >> 1) | (@as(u5, @truncate(value & 1)) << 4);
|
||||||
|
|
||||||
if (complete) {
|
if (is_full) {
|
||||||
const reg_data = self.shift_register;
|
const reg_val = self.shift_register;
|
||||||
self.shift_register = 0x10;
|
self.shift_register = 0x10;
|
||||||
|
|
||||||
switch ((address >> 13) & 0x03) {
|
const reg_select = (address >> 13) & 0x03;
|
||||||
0 => self.control = reg_data,
|
switch (reg_select) {
|
||||||
1 => self.chr_bank_0 = reg_data,
|
0 => self.control = reg_val,
|
||||||
2 => self.chr_bank_1 = reg_data,
|
1 => self.chr_bank_0 = reg_val,
|
||||||
3 => self.prg_bank = reg_data,
|
2 => self.chr_bank_1 = reg_val,
|
||||||
|
3 => self.prg_bank = reg_val,
|
||||||
else => unreachable,
|
else => unreachable,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn ppuMapRead(
|
pub fn ppuRead(self: *const Mmc1, address: u16, chr_rom: []const u8, chr_ram: []const u8, chr_is_ram: bool) ?u8 {
|
||||||
self: *const Mmc1,
|
if (address < 0x2000) {
|
||||||
address: u16,
|
if (chr_is_ram) {
|
||||||
) ?usize {
|
if (address < chr_ram.len) return chr_ram[address];
|
||||||
if (address >= 0x2000) return null;
|
return 0;
|
||||||
|
}
|
||||||
|
if (chr_rom.len == 0) return 0;
|
||||||
|
|
||||||
const chr_mode = (self.control & 0x10) != 0;
|
const chr_4k_mode = (self.control & 0x10) != 0;
|
||||||
if (!chr_mode) {
|
const total_4k_banks = @max(1, chr_rom.len / 0x1000);
|
||||||
// 8 KB CHR mode
|
var offset: usize = 0;
|
||||||
const bank = @as(usize, self.chr_bank_0 & 0x1e) % self.chr_bank_count;
|
|
||||||
return bank * 0x1000 + @as(usize, address);
|
if (chr_4k_mode) {
|
||||||
} else {
|
|
||||||
// 4 KB CHR mode
|
|
||||||
if (address < 0x1000) {
|
if (address < 0x1000) {
|
||||||
const bank = @as(usize, self.chr_bank_0) % self.chr_bank_count;
|
const bank = @as(usize, self.chr_bank_0) % total_4k_banks;
|
||||||
return bank * 0x1000 + @as(usize, address & 0x0fff);
|
offset = (bank * 0x1000) + address;
|
||||||
} else {
|
} else {
|
||||||
const bank = @as(usize, self.chr_bank_1) % self.chr_bank_count;
|
const bank = @as(usize, self.chr_bank_1) % total_4k_banks;
|
||||||
return bank * 0x1000 + @as(usize, address & 0x0fff);
|
offset = (bank * 0x1000) + (address - 0x1000);
|
||||||
}
|
}
|
||||||
|
} else {
|
||||||
|
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 ppuMapWrite(
|
pub fn ppuWrite(_: *Mmc1, address: u16, value: u8, chr_ram: []u8, chr_is_ram: bool) bool {
|
||||||
_: *const Mmc1,
|
if (address < 0x2000 and chr_is_ram) {
|
||||||
address: u16,
|
if (address < chr_ram.len) {
|
||||||
) ?usize {
|
chr_ram[address] = value;
|
||||||
if (address >= 0x2000) return null;
|
return true;
|
||||||
return address;
|
}
|
||||||
|
}
|
||||||
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn mirroring(
|
pub fn mirroring(self: *const Mmc1) Mirroring {
|
||||||
self: *const Mmc1,
|
|
||||||
) Mirroring {
|
|
||||||
return switch (self.control & 0x03) {
|
return switch (self.control & 0x03) {
|
||||||
0 => .single_screen_lower,
|
0 => .single_screen_lower,
|
||||||
1 => .single_screen_upper,
|
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;
|
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 std = @import("std");
|
||||||
const Mirroring = types.Mirroring;
|
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,
|
prg_bank: u8 = 0,
|
||||||
chr_0fd: u8 = 0,
|
chr_bank_0_fd: u8 = 0,
|
||||||
chr_0fe: u8 = 0,
|
chr_bank_0_fe: u8 = 0,
|
||||||
chr_1fd: u8 = 0,
|
chr_bank_1_fd: u8 = 0,
|
||||||
chr_1fe: u8 = 0,
|
chr_bank_1_fe: u8 = 0,
|
||||||
|
latch_0: u1 = 0,
|
||||||
latch0: u1 = 0, // 0 = 0FD, 1 = 0FE
|
latch_1: u1 = 0,
|
||||||
latch1: u1 = 0, // 0 = 1FD, 1 = 1FE
|
|
||||||
|
|
||||||
mirroring_mode: Mirroring = .vertical,
|
mirroring_mode: Mirroring = .vertical,
|
||||||
|
|
||||||
prg_bank_count: usize,
|
pub fn init() Mmc2 {
|
||||||
chr_bank_count: usize,
|
return .{};
|
||||||
|
|
||||||
pub fn init(
|
|
||||||
prg_size: usize,
|
|
||||||
chr_size: usize,
|
|
||||||
) Mmc2 {
|
|
||||||
return .{
|
|
||||||
.prg_bank_count = prg_size / 0x2000,
|
|
||||||
.chr_bank_count = if (chr_size > 0) chr_size / 0x1000 else 1,
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn cpuMapRead(
|
pub fn cpuRead(self: *const Mmc2, address: u16, prg_rom: []const u8, prg_ram: []const u8) ?u8 {
|
||||||
self: *const Mmc2,
|
if (address >= 0x6000 and address <= 0x7fff) {
|
||||||
address: u16,
|
const offset = address - 0x6000;
|
||||||
) ?usize {
|
if (offset < prg_ram.len) return prg_ram[offset];
|
||||||
if (address < 0x8000) return null;
|
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) {
|
switch (address) {
|
||||||
const bank = @as(usize, self.prg_bank) % self.prg_bank_count;
|
0x8000...0x9fff => bank = @as(usize, self.prg_bank) % total_8k_banks,
|
||||||
return bank * 0x2000 + @as(usize, address - 0x8000);
|
0xa000...0xbfff => bank = if (total_8k_banks >= 3) total_8k_banks - 3 else 0,
|
||||||
} else {
|
0xc000...0xdfff => bank = if (total_8k_banks >= 2) total_8k_banks - 2 else 0,
|
||||||
const offset = @as(usize, address - 0xA000);
|
0xe000...0xffff => bank = total_8k_banks - 1,
|
||||||
const fixed_start = if (self.prg_bank_count >= 3) (self.prg_bank_count - 3) * 0x2000 else 0;
|
else => return 0,
|
||||||
return fixed_start + offset;
|
}
|
||||||
|
|
||||||
|
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(
|
pub fn ppuRead(self: *const Mmc2, address: u16, chr_rom: []const u8, chr_ram: []const u8, chr_is_ram: bool) ?u8 {
|
||||||
self: *Mmc2,
|
if (address < 0x2000) {
|
||||||
address: u16,
|
if (chr_is_ram) {
|
||||||
value: u8,
|
if (address < chr_ram.len) return chr_ram[address];
|
||||||
) void {
|
return 0;
|
||||||
if (address < 0x8000) return;
|
}
|
||||||
|
if (chr_rom.len == 0) return 0;
|
||||||
|
|
||||||
switch (address & 0xF000) {
|
const total_4k_banks = @max(1, chr_rom.len / 0x1000);
|
||||||
0xA000 => self.prg_bank = value & 0x0F,
|
const bank = if (address < 0x1000)
|
||||||
0xB000 => self.chr_0fd = value & 0x1F,
|
(if (self.latch_0 == 0) self.chr_bank_0_fd else self.chr_bank_0_fe)
|
||||||
0xC000 => self.chr_0fe = value & 0x1F,
|
else
|
||||||
0xD000 => self.chr_1fd = value & 0x1F,
|
(if (self.latch_1 == 0) self.chr_bank_1_fd else self.chr_bank_1_fe);
|
||||||
0xE000 => self.chr_1fe = value & 0x1F,
|
|
||||||
0xF000 => self.mirroring_mode = if ((value & 1) == 0) .vertical else .horizontal,
|
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 => {},
|
else => {},
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn ppuMapRead(
|
pub fn mirroring(self: *const Mmc2) Mirroring {
|
||||||
self: *Mmc2,
|
|
||||||
address: u16,
|
|
||||||
) ?usize {
|
|
||||||
if (address >= 0x2000) return null;
|
|
||||||
|
|
||||||
var bank: usize = 0;
|
|
||||||
if (address < 0x1000) {
|
|
||||||
const selected_bank = if (self.latch0 == 0) self.chr_0fd else self.chr_0fe;
|
|
||||||
bank = @as(usize, selected_bank) % self.chr_bank_count;
|
|
||||||
} else {
|
|
||||||
const selected_bank = if (self.latch1 == 0) self.chr_1fd else self.chr_1fe;
|
|
||||||
bank = @as(usize, selected_bank) % self.chr_bank_count;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Check PPU tile latches
|
|
||||||
if (address == 0x0FD0) self.latch0 = 0;
|
|
||||||
if (address == 0x0FE0) self.latch0 = 1;
|
|
||||||
if (address == 0x1FD0) self.latch1 = 0;
|
|
||||||
if (address == 0x1FE0) self.latch1 = 1;
|
|
||||||
|
|
||||||
return bank * 0x1000 + @as(usize, address & 0x0FFF);
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn ppuMapWrite(
|
|
||||||
_: *const Mmc2,
|
|
||||||
address: u16,
|
|
||||||
) ?usize {
|
|
||||||
if (address >= 0x2000) return null;
|
|
||||||
return address;
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn mirroring(
|
|
||||||
self: *const Mmc2,
|
|
||||||
) Mirroring {
|
|
||||||
return self.mirroring_mode;
|
return self.mirroring_mode;
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn irqAsserted(_: *const Mmc2) bool {
|
pub fn irqLine(_: *const Mmc2) bool {
|
||||||
return false;
|
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);
|
||||||
|
}
|
||||||
|
|||||||
+179
-118
@@ -1,141 +1,172 @@
|
|||||||
const types = @import("types.zig");
|
const std = @import("std");
|
||||||
const Mirroring = types.Mirroring;
|
const common = @import("../common.zig");
|
||||||
|
const Mirroring = common.Mirroring;
|
||||||
|
|
||||||
const Mmc3 = @This();
|
pub const Mmc3 = @This();
|
||||||
|
|
||||||
bank_select: u8 = 0,
|
bank_select: u8 = 0,
|
||||||
bank_registers: [8]u8 = [_]u8{0} ** 8,
|
registers: [8]u8 = [_]u8{0} ** 8,
|
||||||
|
mirroring_mode: Mirroring = .vertical,
|
||||||
mirroring_mode: Mirroring = .horizontal,
|
prg_ram_protect: u8 = 0x80,
|
||||||
|
|
||||||
irq_latch: u8 = 0,
|
irq_latch: u8 = 0,
|
||||||
irq_counter: u8 = 0,
|
irq_counter: u8 = 0,
|
||||||
irq_reload: bool = false,
|
irq_reload: bool = false,
|
||||||
irq_enabled: bool = false,
|
irq_enabled: bool = false,
|
||||||
irq_pending: bool = false,
|
irq_asserted: bool = false,
|
||||||
|
|
||||||
prg_bank_count: usize,
|
last_a12: bool = false,
|
||||||
chr_bank_count: usize,
|
a12_low_cycles: u32 = 0,
|
||||||
|
|
||||||
pub fn init(
|
pub fn init(initial_mirroring: Mirroring) Mmc3 {
|
||||||
prg_size: usize,
|
|
||||||
chr_size: usize,
|
|
||||||
) Mmc3 {
|
|
||||||
return .{
|
return .{
|
||||||
.prg_bank_count = prg_size / 0x2000,
|
.mirroring_mode = initial_mirroring,
|
||||||
.chr_bank_count = if (chr_size > 0) chr_size / 0x0400 else 8,
|
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn cpuMapRead(
|
pub fn cpuRead(self: *const Mmc3, address: u16, prg_rom: []const u8, prg_ram: []const u8) ?u8 {
|
||||||
self: *const Mmc3,
|
if (address >= 0x6000 and address <= 0x7fff) {
|
||||||
address: u16,
|
if ((self.prg_ram_protect & 0x80) == 0) return 0;
|
||||||
) ?usize {
|
const offset = address - 0x6000;
|
||||||
if (address < 0x8000) return null;
|
if (offset < prg_ram.len) return prg_ram[offset];
|
||||||
|
return 0;
|
||||||
const offset = @as(usize, address & 0x1fff);
|
}
|
||||||
|
if (address >= 0x8000 and address <= 0xffff) {
|
||||||
|
const total_8k_banks = @max(1, prg_rom.len / 0x2000);
|
||||||
const prg_mode = (self.bank_select & 0x40) != 0;
|
const prg_mode = (self.bank_select & 0x40) != 0;
|
||||||
|
|
||||||
const last_bank = self.prg_bank_count - 1;
|
|
||||||
const second_last_bank = self.prg_bank_count - 2;
|
|
||||||
|
|
||||||
const r6 = @as(usize, self.bank_registers[6] & 0x3f) % self.prg_bank_count;
|
|
||||||
const r7 = @as(usize, self.bank_registers[7] & 0x3f) % self.prg_bank_count;
|
|
||||||
|
|
||||||
switch ((address - 0x8000) / 0x2000) {
|
|
||||||
0 => { // $8000-$9FFF
|
|
||||||
const bank = if (!prg_mode) r6 else second_last_bank;
|
|
||||||
return bank * 0x2000 + offset;
|
|
||||||
},
|
|
||||||
1 => { // $A000-$BFFF
|
|
||||||
return r7 * 0x2000 + offset;
|
|
||||||
},
|
|
||||||
2 => { // $C000-$DFFF
|
|
||||||
const bank = if (!prg_mode) second_last_bank else r6;
|
|
||||||
return bank * 0x2000 + offset;
|
|
||||||
},
|
|
||||||
3 => { // $E000-$FFFF
|
|
||||||
return last_bank * 0x2000 + offset;
|
|
||||||
},
|
|
||||||
else => unreachable,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn cpuWrite(
|
|
||||||
self: *Mmc3,
|
|
||||||
address: u16,
|
|
||||||
value: u8,
|
|
||||||
) void {
|
|
||||||
if (address < 0x8000) return;
|
|
||||||
|
|
||||||
switch (address & 0xe001) {
|
|
||||||
0x8000 => self.bank_select = value,
|
|
||||||
0x8001 => {
|
|
||||||
const reg = self.bank_select & 0x07;
|
|
||||||
self.bank_registers[reg] = value;
|
|
||||||
},
|
|
||||||
0xa000 => {
|
|
||||||
self.mirroring_mode = if ((value & 1) != 0) .horizontal else .vertical;
|
|
||||||
},
|
|
||||||
0xa001 => {}, // PRG RAM protect
|
|
||||||
0xc000 => self.irq_latch = value,
|
|
||||||
0xc001 => self.irq_reload = true,
|
|
||||||
0xe000 => {
|
|
||||||
self.irq_enabled = false;
|
|
||||||
self.irq_pending = false;
|
|
||||||
},
|
|
||||||
0xe001 => self.irq_enabled = true,
|
|
||||||
else => {},
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn ppuMapRead(
|
|
||||||
self: *const Mmc3,
|
|
||||||
address: u16,
|
|
||||||
) ?usize {
|
|
||||||
if (address >= 0x2000) return null;
|
|
||||||
|
|
||||||
const chr_mode = (self.bank_select & 0x80) != 0;
|
|
||||||
const offset = @as(usize, address & 0x03ff);
|
|
||||||
|
|
||||||
var bank: usize = 0;
|
var bank: usize = 0;
|
||||||
const addr_k = address / 0x0400;
|
|
||||||
|
|
||||||
if (!chr_mode) {
|
switch (address) {
|
||||||
switch (addr_k) {
|
0x8000...0x9fff => {
|
||||||
0, 1 => bank = (@as(usize, self.bank_registers[0] & 0xfe) + (addr_k & 1)),
|
bank = if (!prg_mode) self.registers[6] else (total_8k_banks - 2);
|
||||||
2, 3 => bank = (@as(usize, self.bank_registers[1] & 0xfe) + (addr_k & 1)),
|
},
|
||||||
4 => bank = self.bank_registers[2],
|
0xa000...0xbfff => {
|
||||||
5 => bank = self.bank_registers[3],
|
bank = self.registers[7];
|
||||||
6 => bank = self.bank_registers[4],
|
},
|
||||||
7 => bank = self.bank_registers[5],
|
0xc000...0xdfff => {
|
||||||
|
bank = if (!prg_mode) (total_8k_banks - 2) else self.registers[6];
|
||||||
|
},
|
||||||
|
0xe000...0xffff => {
|
||||||
|
bank = total_8k_banks - 1;
|
||||||
|
},
|
||||||
|
else => return 0,
|
||||||
|
}
|
||||||
|
|
||||||
|
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 => unreachable,
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
switch (addr_k) {
|
switch (address) {
|
||||||
0 => bank = self.bank_registers[2],
|
0x0000...0x03ff => bank = self.registers[2],
|
||||||
1 => bank = self.bank_registers[3],
|
0x0400...0x07ff => bank = self.registers[3],
|
||||||
2 => bank = self.bank_registers[4],
|
0x0800...0x0bff => bank = self.registers[4],
|
||||||
3 => bank = self.bank_registers[5],
|
0x0c00...0x0fff => bank = self.registers[5],
|
||||||
4, 5 => bank = (@as(usize, self.bank_registers[0] & 0xfe) + (addr_k & 1)),
|
0x1000...0x17ff => bank = (self.registers[0] & 0xfe) +% ((address >> 10) & 1),
|
||||||
6, 7 => bank = (@as(usize, self.bank_registers[1] & 0xfe) + (addr_k & 1)),
|
0x1800...0x1fff => bank = (self.registers[1] & 0xfe) +% ((address >> 10) & 1),
|
||||||
else => unreachable,
|
else => unreachable,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
bank = bank % self.chr_bank_count;
|
bank %= total_1k_banks;
|
||||||
return bank * 0x0400 + offset;
|
const offset = (bank * 0x0400) + (address & 0x03ff);
|
||||||
|
if (offset < chr_rom.len) return chr_rom[offset];
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn ppuMapWrite(
|
pub fn ppuWrite(_: *Mmc3, address: u16, value: u8, chr_ram: []u8, chr_is_ram: bool) bool {
|
||||||
_: *const Mmc3,
|
if (address < 0x2000 and chr_is_ram) {
|
||||||
address: u16,
|
if (address < chr_ram.len) {
|
||||||
) ?usize {
|
chr_ram[address] = value;
|
||||||
if (address >= 0x2000) return null;
|
return true;
|
||||||
return address;
|
}
|
||||||
|
}
|
||||||
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn handleScanline(self: *Mmc3) void {
|
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) {
|
if (self.irq_counter == 0 or self.irq_reload) {
|
||||||
self.irq_counter = self.irq_latch;
|
self.irq_counter = self.irq_latch;
|
||||||
self.irq_reload = false;
|
self.irq_reload = false;
|
||||||
@@ -144,18 +175,48 @@ pub fn handleScanline(self: *Mmc3) void {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (self.irq_counter == 0 and self.irq_enabled) {
|
if (self.irq_counter == 0 and self.irq_enabled) {
|
||||||
self.irq_pending = true;
|
self.irq_asserted = true;
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
self.a12_low_cycles = 0;
|
||||||
|
} else {
|
||||||
|
self.a12_low_cycles +|= 1;
|
||||||
|
}
|
||||||
|
self.last_a12 = a12;
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn mirroring(
|
pub fn mirroring(self: *const Mmc3) Mirroring {
|
||||||
self: *const Mmc3,
|
|
||||||
) Mirroring {
|
|
||||||
return self.mirroring_mode;
|
return self.mirroring_mode;
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn irqAsserted(
|
pub fn irqLine(self: *const Mmc3) bool {
|
||||||
self: *const Mmc3,
|
return self.irq_asserted;
|
||||||
) bool {
|
}
|
||||||
return self.irq_pending;
|
|
||||||
|
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());
|
||||||
}
|
}
|
||||||
|
|||||||
+124
-94
@@ -1,117 +1,147 @@
|
|||||||
const types = @import("types.zig");
|
const std = @import("std");
|
||||||
const Mirroring = types.Mirroring;
|
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,
|
prg_bank: u8 = 0,
|
||||||
|
chr_bank_0_fd: u8 = 0,
|
||||||
chr_banks_fd: [2]u8 = [_]u8{ 0, 0 },
|
chr_bank_0_fe: u8 = 0,
|
||||||
chr_banks_fe: [2]u8 = [_]u8{ 0, 0 },
|
chr_bank_1_fd: u8 = 0,
|
||||||
|
chr_bank_1_fe: u8 = 0,
|
||||||
latch: [2]u8 = [_]u8{ 0xfe, 0xfe },
|
latch_0: u1 = 0,
|
||||||
|
latch_1: u1 = 0,
|
||||||
mirroring_mode: Mirroring = .vertical,
|
mirroring_mode: Mirroring = .vertical,
|
||||||
prg_bank_count: usize,
|
|
||||||
chr_bank_count: usize,
|
|
||||||
|
|
||||||
pub fn init(
|
pub fn init() Mmc4 {
|
||||||
prg_size: usize,
|
return .{};
|
||||||
chr_size: usize,
|
|
||||||
) Mmc4 {
|
|
||||||
return .{
|
|
||||||
.prg_bank_count = prg_size / 0x4000,
|
|
||||||
.chr_bank_count = if (chr_size > 0) chr_size / 0x1000 else 2,
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn cpuMapRead(
|
pub fn cpuRead(self: *const Mmc4, address: u16, prg_rom: []const u8, prg_ram: []const u8) ?u8 {
|
||||||
self: *const Mmc4,
|
if (address >= 0x6000 and address <= 0x7fff) {
|
||||||
address: u16,
|
const offset = address - 0x6000;
|
||||||
) ?usize {
|
if (offset < prg_ram.len) return prg_ram[offset];
|
||||||
if (address < 0x8000) return null;
|
return 0;
|
||||||
|
}
|
||||||
const offset = @as(usize, address & 0x3fff);
|
if (address >= 0x8000 and address <= 0xffff) {
|
||||||
const last_bank = self.prg_bank_count - 1;
|
const total_16k_banks = @max(1, prg_rom.len / 0x4000);
|
||||||
|
var bank: usize = 0;
|
||||||
|
|
||||||
if (address < 0xc000) {
|
if (address < 0xc000) {
|
||||||
const bank = @as(usize, self.prg_bank) % self.prg_bank_count;
|
bank = @as(usize, self.prg_bank) % total_16k_banks;
|
||||||
return bank * 0x4000 + offset;
|
const offset = (bank * 0x4000) + (address - 0x8000);
|
||||||
|
if (offset < prg_rom.len) return prg_rom[offset];
|
||||||
} else {
|
} else {
|
||||||
return last_bank * 0x4000 + offset;
|
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: *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(
|
pub fn ppuRead(self: *const Mmc4, address: u16, chr_rom: []const u8, chr_ram: []const u8, chr_is_ram: bool) ?u8 {
|
||||||
self: *Mmc4,
|
if (address < 0x2000) {
|
||||||
address: u16,
|
if (chr_is_ram) {
|
||||||
value: u8,
|
if (address < chr_ram.len) return chr_ram[address];
|
||||||
) void {
|
return 0;
|
||||||
if (address < 0xa000) return;
|
}
|
||||||
|
if (chr_rom.len == 0) return 0;
|
||||||
|
|
||||||
switch (address & 0xf000) {
|
const total_4k_banks = @max(1, chr_rom.len / 0x1000);
|
||||||
0xa000 => self.prg_bank = value & 0x0f,
|
const bank = if (address < 0x1000)
|
||||||
0xb000 => self.chr_banks_fd[0] = value & 0x1f,
|
(if (self.latch_0 == 0) self.chr_bank_0_fd else self.chr_bank_0_fe)
|
||||||
0xc000 => self.chr_banks_fe[0] = value & 0x1f,
|
else
|
||||||
0xd000 => self.chr_banks_fd[1] = value & 0x1f,
|
(if (self.latch_1 == 0) self.chr_bank_1_fd else self.chr_bank_1_fe);
|
||||||
0xe000 => self.chr_banks_fe[1] = value & 0x1f,
|
|
||||||
0xf000 => self.mirroring_mode = if ((value & 1) != 0) .horizontal else .vertical,
|
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 => {},
|
else => {},
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn ppuMapRead(
|
pub fn mirroring(self: *const Mmc4) Mirroring {
|
||||||
self: *Mmc4,
|
|
||||||
address: u16,
|
|
||||||
) ?usize {
|
|
||||||
if (address >= 0x2000) return null;
|
|
||||||
|
|
||||||
const slot = address / 0x1000;
|
|
||||||
const offset = @as(usize, address & 0x0fff);
|
|
||||||
|
|
||||||
const bank_raw = if (self.latch[slot] == 0xfd)
|
|
||||||
self.chr_banks_fd[slot]
|
|
||||||
else
|
|
||||||
self.chr_banks_fe[slot];
|
|
||||||
|
|
||||||
const bank = @as(usize, bank_raw) % self.chr_bank_count;
|
|
||||||
const result = bank * 0x1000 + offset;
|
|
||||||
|
|
||||||
// Check PPU tile latch triggers ($0FD0-$0FDF, $0FE0-$0FEF, $1FD0-$1FDF, $1FE0-$1FEF)
|
|
||||||
if (address == 0x0fd0 or address == 0x0fe0) {
|
|
||||||
self.latch[0] = @truncate(address >> 4);
|
|
||||||
} else if (address == 0x1fd0 or address == 0x1fe0) {
|
|
||||||
self.latch[1] = @truncate(address >> 4);
|
|
||||||
}
|
|
||||||
|
|
||||||
return result;
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn ppuMapWrite(
|
|
||||||
self: *const Mmc4,
|
|
||||||
address: u16,
|
|
||||||
) ?usize {
|
|
||||||
if (address >= 0x2000) return null;
|
|
||||||
|
|
||||||
const slot = address / 0x1000;
|
|
||||||
const offset = @as(usize, address & 0x0fff);
|
|
||||||
|
|
||||||
const bank_raw = if (self.latch[slot] == 0xfd)
|
|
||||||
self.chr_banks_fd[slot]
|
|
||||||
else
|
|
||||||
self.chr_banks_fe[slot];
|
|
||||||
|
|
||||||
const bank = @as(usize, bank_raw) % self.chr_bank_count;
|
|
||||||
return bank * 0x1000 + offset;
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn mirroring(
|
|
||||||
self: *const Mmc4,
|
|
||||||
) Mirroring {
|
|
||||||
return self.mirroring_mode;
|
return self.mirroring_mode;
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn irqAsserted(
|
pub fn irqLine(_: *const Mmc4) bool {
|
||||||
_: *const Mmc4,
|
|
||||||
) bool {
|
|
||||||
return false;
|
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 std = @import("std");
|
||||||
const Mirroring = types.Mirroring;
|
const common = @import("../common.zig");
|
||||||
|
const Mirroring = common.Mirroring;
|
||||||
|
|
||||||
const Nrom = @This();
|
pub const Nrom = @This();
|
||||||
|
|
||||||
prg_16k: bool,
|
|
||||||
mirroring_mode: Mirroring,
|
mirroring_mode: Mirroring,
|
||||||
|
|
||||||
pub fn init(
|
pub fn init(initial_mirroring: Mirroring) Nrom {
|
||||||
prg_size: usize,
|
return .{ .mirroring_mode = initial_mirroring };
|
||||||
_mirroring: Mirroring,
|
|
||||||
) Nrom {
|
|
||||||
return .{
|
|
||||||
.prg_16k = prg_size == 0x4000,
|
|
||||||
.mirroring_mode = _mirroring,
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn cpuMapRead(
|
pub fn cpuRead(_: *const Nrom, address: u16, prg_rom: []const u8, prg_ram: []const u8) ?u8 {
|
||||||
self: *const Nrom,
|
if (address >= 0x6000 and address <= 0x7fff) {
|
||||||
address: u16,
|
const offset = address - 0x6000;
|
||||||
) ?usize {
|
if (offset < prg_ram.len) return prg_ram[offset];
|
||||||
if (address < 0x8000)
|
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;
|
return null;
|
||||||
|
|
||||||
const offset =
|
|
||||||
@as(usize, address) - 0x8000;
|
|
||||||
|
|
||||||
return if (self.prg_16k)
|
|
||||||
offset & 0x3fff
|
|
||||||
else
|
|
||||||
offset;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn cpuWrite(
|
pub fn cpuWrite(_: *Nrom, address: u16, value: u8, prg_ram: []u8) bool {
|
||||||
_: *Nrom,
|
if (address >= 0x6000 and address <= 0x7fff) {
|
||||||
_: u16,
|
const offset = address - 0x6000;
|
||||||
_: u8,
|
if (offset < prg_ram.len) {
|
||||||
) void {
|
prg_ram[offset] = value;
|
||||||
// NROM has no mapper registers.
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn ppuMapRead(
|
pub fn ppuRead(_: *const Nrom, address: u16, chr_rom: []const u8, chr_ram: []const u8, chr_is_ram: bool) ?u8 {
|
||||||
_: *const Nrom,
|
if (address < 0x2000) {
|
||||||
address: u16,
|
if (chr_is_ram) {
|
||||||
) ?usize {
|
if (address < chr_ram.len) return chr_ram[address];
|
||||||
if (address >= 0x2000)
|
} else {
|
||||||
|
if (chr_rom.len > 0) return chr_rom[address & (chr_rom.len - 1)];
|
||||||
|
}
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
return null;
|
return null;
|
||||||
|
|
||||||
return address;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn ppuMapWrite(
|
pub fn ppuWrite(_: *Nrom, address: u16, value: u8, chr_ram: []u8, chr_is_ram: bool) bool {
|
||||||
_: *const Nrom,
|
if (address < 0x2000 and chr_is_ram) {
|
||||||
address: u16,
|
if (address < chr_ram.len) {
|
||||||
) ?usize {
|
chr_ram[address] = value;
|
||||||
if (address >= 0x2000)
|
return true;
|
||||||
return null;
|
}
|
||||||
|
}
|
||||||
return address;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn mirroring(
|
pub fn mirroring(self: *const Nrom) Mirroring {
|
||||||
self: *const Nrom,
|
|
||||||
) Mirroring {
|
|
||||||
return self.mirroring_mode;
|
return self.mirroring_mode;
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn irqAsserted(_: *const Nrom) bool {
|
pub fn irqLine(_: *const Nrom) bool {
|
||||||
return false;
|
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 Nrom = @import("nrom.zig");
|
||||||
pub const Uxrom = @import("uxrom.zig");
|
|
||||||
pub const Mmc1 = @import("mmc1.zig");
|
pub const Mmc1 = @import("mmc1.zig");
|
||||||
|
pub const Uxrom = @import("uxrom.zig");
|
||||||
pub const Cnrom = @import("cnrom.zig");
|
pub const Cnrom = @import("cnrom.zig");
|
||||||
pub const Mmc3 = @import("mmc3.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 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 Gxrom = @import("gxrom.zig");
|
||||||
pub const Mmc2 = @import("mmc2.zig");
|
|
||||||
pub const ColorDreams = @import("colordreams.zig");
|
|
||||||
pub const Camerica = @import("camerica.zig");
|
pub const Camerica = @import("camerica.zig");
|
||||||
pub const Mapper13 = @import("mapper13.zig");
|
pub const Mmc2 = @import("mmc2.zig");
|
||||||
pub const Mapper34 = @import("mapper34.zig");
|
pub const Mmc4 = @import("mmc4.zig");
|
||||||
pub const Mapper65 = @import("mapper65.zig");
|
pub const Action53 = @import("action53.zig");
|
||||||
pub const Mapper68 = @import("mapper68.zig");
|
|
||||||
pub const Mapper69 = @import("mapper69.zig");
|
|
||||||
pub const Nina03 = @import("nina03.zig");
|
|
||||||
pub const Mapper76 = @import("mapper76.zig");
|
|
||||||
pub const Irem78 = @import("irem78.zig");
|
|
||||||
pub const Mapper87 = @import("mapper87.zig");
|
|
||||||
pub const Mapper88 = @import("mapper88.zig");
|
|
||||||
pub const Mapper89 = @import("mapper89.zig");
|
|
||||||
pub const Mapper93 = @import("mapper93.zig");
|
|
||||||
pub const Mapper94 = @import("mapper94.zig");
|
|
||||||
pub const Mapper118 = @import("mapper118.zig");
|
|
||||||
pub const Mapper119 = @import("mapper119.zig");
|
|
||||||
pub const Mapper140 = @import("mapper140.zig");
|
|
||||||
pub const Mapper180 = @import("mapper180.zig");
|
pub const 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) {
|
pub const Mapper = union(enum) {
|
||||||
nrom: Nrom,
|
nrom: Nrom,
|
||||||
uxrom: Uxrom,
|
|
||||||
mmc1: Mmc1,
|
mmc1: Mmc1,
|
||||||
|
uxrom: Uxrom,
|
||||||
cnrom: Cnrom,
|
cnrom: Cnrom,
|
||||||
mmc3: Mmc3,
|
mmc3: Mmc3,
|
||||||
mmc4: Mmc4,
|
|
||||||
mmc5: Mmc5,
|
|
||||||
axrom: Axrom,
|
axrom: Axrom,
|
||||||
gxrom: Gxrom,
|
|
||||||
mmc2: Mmc2,
|
|
||||||
color_dreams: ColorDreams,
|
color_dreams: ColorDreams,
|
||||||
|
unrom512: Unrom512,
|
||||||
|
bnrom: Bnrom,
|
||||||
|
gxrom: Gxrom,
|
||||||
camerica: Camerica,
|
camerica: Camerica,
|
||||||
mapper13: Mapper13,
|
mmc2: Mmc2,
|
||||||
mapper34: Mapper34,
|
mmc4: Mmc4,
|
||||||
mapper65: Mapper65,
|
action53: Action53,
|
||||||
mapper68: Mapper68,
|
|
||||||
mapper69: Mapper69,
|
|
||||||
nina03: Nina03,
|
|
||||||
mapper76: Mapper76,
|
|
||||||
irem78: Irem78,
|
|
||||||
mapper87: Mapper87,
|
|
||||||
mapper88: Mapper88,
|
|
||||||
mapper89: Mapper89,
|
|
||||||
mapper93: Mapper93,
|
|
||||||
mapper94: Mapper94,
|
|
||||||
mapper118: Mapper118,
|
|
||||||
mapper119: Mapper119,
|
|
||||||
mapper140: Mapper140,
|
|
||||||
mapper180: Mapper180,
|
mapper180: Mapper180,
|
||||||
vrc1: Vrc1,
|
|
||||||
vrc2: Vrc2,
|
|
||||||
mapper232: Mapper232,
|
|
||||||
vrc3: Vrc3,
|
|
||||||
vrc4: Vrc4,
|
|
||||||
vrc6: Vrc6,
|
|
||||||
vrc7: Vrc7,
|
|
||||||
mapper210: Mapper210,
|
|
||||||
mapper206: Mapper206,
|
|
||||||
|
|
||||||
pub fn cpuMapRead(
|
pub fn cpuRead(self: *const Mapper, address: u16, prg_rom: []const u8, prg_ram: []const u8) ?u8 {
|
||||||
self: *const Mapper,
|
|
||||||
address: u16,
|
|
||||||
) ?usize {
|
|
||||||
return switch (self.*) {
|
return switch (self.*) {
|
||||||
inline else => |*mapper| mapper.cpuMapRead(address),
|
inline else => |*m| m.cpuRead(address, prg_rom, prg_ram),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn cpuWrite(
|
pub fn cpuWrite(self: *Mapper, address: u16, value: u8, prg_ram: []u8) bool {
|
||||||
self: *Mapper,
|
return switch (self.*) {
|
||||||
address: u16,
|
inline else => |*m| m.cpuWrite(address, value, prg_ram),
|
||||||
value: u8,
|
};
|
||||||
) void {
|
}
|
||||||
|
|
||||||
|
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.*) {
|
switch (self.*) {
|
||||||
inline else => |*mapper| mapper.cpuWrite(address, value),
|
inline else => |*m| m.notifyPpuAddress(address),
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
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 => {},
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
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 std = @import("std");
|
||||||
const Mirroring = types.Mirroring;
|
const common = @import("../common.zig");
|
||||||
|
const Mirroring = common.Mirroring;
|
||||||
|
|
||||||
const Uxrom = @This();
|
pub const Uxrom = @This();
|
||||||
|
|
||||||
prg_bank_select: u8 = 0,
|
prg_bank: u8 = 0,
|
||||||
prg_bank_count: usize,
|
|
||||||
mirroring_mode: Mirroring,
|
mirroring_mode: Mirroring,
|
||||||
|
|
||||||
pub fn init(
|
pub fn init(initial_mirroring: Mirroring) Uxrom {
|
||||||
prg_size: usize,
|
return .{ .mirroring_mode = initial_mirroring };
|
||||||
_mirroring: Mirroring,
|
|
||||||
) Uxrom {
|
|
||||||
return .{
|
|
||||||
.prg_bank_count = prg_size / 0x4000,
|
|
||||||
.mirroring_mode = _mirroring,
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn cpuMapRead(
|
pub fn cpuRead(self: *const Uxrom, address: u16, prg_rom: []const u8, prg_ram: []const u8) ?u8 {
|
||||||
self: *const Uxrom,
|
if (address >= 0x6000 and address <= 0x7fff) {
|
||||||
address: u16,
|
const offset = address - 0x6000;
|
||||||
) ?usize {
|
if (offset < prg_ram.len) return prg_ram[offset];
|
||||||
if (address < 0x8000) return null;
|
return 0;
|
||||||
|
}
|
||||||
const offset = @as(usize, address & 0x3fff);
|
if (address >= 0x8000 and address <= 0xffff) {
|
||||||
|
const total_16k_banks = @max(1, prg_rom.len / 0x4000);
|
||||||
|
var bank: usize = 0;
|
||||||
if (address < 0xc000) {
|
if (address < 0xc000) {
|
||||||
// Switchable 16 KB bank
|
bank = @as(usize, self.prg_bank) % total_16k_banks;
|
||||||
const bank = @as(usize, self.prg_bank_select) % self.prg_bank_count;
|
const offset = (bank * 0x4000) + (address - 0x8000);
|
||||||
return bank * 0x4000 + offset;
|
if (offset < prg_rom.len) return prg_rom[offset];
|
||||||
} else {
|
} else {
|
||||||
// Fixed to last 16 KB bank
|
bank = total_16k_banks - 1;
|
||||||
const last_bank = self.prg_bank_count - 1;
|
const offset = (bank * 0x4000) + (address - 0xc000);
|
||||||
return last_bank * 0x4000 + offset;
|
if (offset < prg_rom.len) return prg_rom[offset];
|
||||||
}
|
}
|
||||||
}
|
return 0;
|
||||||
|
|
||||||
pub fn cpuWrite(
|
|
||||||
self: *Uxrom,
|
|
||||||
address: u16,
|
|
||||||
value: u8,
|
|
||||||
) void {
|
|
||||||
if (address >= 0x8000) {
|
|
||||||
self.prg_bank_select = value;
|
|
||||||
}
|
}
|
||||||
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn ppuMapRead(
|
pub fn cpuWrite(self: *Uxrom, address: u16, value: u8, prg_ram: []u8) bool {
|
||||||
_: *const Uxrom,
|
if (address >= 0x6000 and address <= 0x7fff) {
|
||||||
address: u16,
|
const offset = address - 0x6000;
|
||||||
) ?usize {
|
if (offset < prg_ram.len) {
|
||||||
if (address >= 0x2000) return null;
|
prg_ram[offset] = value;
|
||||||
return address;
|
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(
|
pub fn ppuRead(_: *const Uxrom, address: u16, chr_rom: []const u8, chr_ram: []const u8, chr_is_ram: bool) ?u8 {
|
||||||
_: *const Uxrom,
|
if (address < 0x2000) {
|
||||||
address: u16,
|
if (chr_is_ram) {
|
||||||
) ?usize {
|
if (address < chr_ram.len) return chr_ram[address];
|
||||||
if (address >= 0x2000) return null;
|
} else {
|
||||||
return address;
|
if (address < chr_rom.len) return chr_rom[address];
|
||||||
|
}
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn mirroring(
|
pub fn ppuWrite(_: *Uxrom, address: u16, value: u8, chr_ram: []u8, chr_is_ram: bool) bool {
|
||||||
self: *const Uxrom,
|
if (address < 0x2000 and chr_is_ram) {
|
||||||
) Mirroring {
|
if (address < chr_ram.len) {
|
||||||
|
chr_ram[address] = value;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn mirroring(self: *const Uxrom) Mirroring {
|
||||||
return self.mirroring_mode;
|
return self.mirroring_mode;
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn irqAsserted(_: *const Uxrom) bool {
|
pub fn irqLine(_: *const Uxrom) bool {
|
||||||
return false;
|
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());
|
||||||
|
}
|
||||||
+154
-323
@@ -1,45 +1,64 @@
|
|||||||
const std = @import("std");
|
const std = @import("std");
|
||||||
|
|
||||||
const contract = @import("contract");
|
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 _cpu = @import("cpu");
|
||||||
|
|
||||||
const Bus = @import("bus.zig");
|
pub const common = @import("common.zig");
|
||||||
const Cartridge = @import("cartridge.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 ppu = @import("ppu/root.zig");
|
||||||
pub const Button = controller.Button;
|
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
|
const Cpu = _cpu.m6502.Cpu(Bus, .ricoh2a03, .cycle);
|
||||||
// currently exports for the Ricoh 2A03.
|
|
||||||
const Cpu = _cpu.m6502.Cpu(
|
|
||||||
Bus,
|
|
||||||
.ricoh2a03,
|
|
||||||
.cycle,
|
|
||||||
);
|
|
||||||
|
|
||||||
const Nes = @This();
|
pub const Nes = @This();
|
||||||
pub const spec = contract.SystemSpec{
|
|
||||||
|
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",
|
.name = "Nintendo Entertainment System",
|
||||||
|
|
||||||
.video_outputs = &.{
|
.video_outputs = &.{
|
||||||
.{
|
.{
|
||||||
.max_width = 256,
|
.max_width = common.screen_width,
|
||||||
.max_height = 240,
|
.max_height = common.screen_height,
|
||||||
|
|
||||||
.format = .rgb565,
|
.format = .rgb565,
|
||||||
|
|
||||||
.aspect_ratio = .{
|
.aspect_ratio = .{
|
||||||
.numerator = 4,
|
.numerator = 4,
|
||||||
.denominator = 3,
|
.denominator = 3,
|
||||||
},
|
},
|
||||||
|
|
||||||
.refresh_rate = .{
|
.refresh_rate = .{
|
||||||
.numerator = 60,
|
.numerator = 60,
|
||||||
.denominator = 1,
|
.denominator = 1,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|
||||||
.audio_outputs = &.{
|
.audio_outputs = &.{
|
||||||
.{
|
.{
|
||||||
.sample_rate = 44_100,
|
.sample_rate = 44_100,
|
||||||
@@ -47,16 +66,10 @@ pub const spec = contract.SystemSpec{
|
|||||||
.format = .i16,
|
.format = .i16,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|
||||||
.input_devices = &.{
|
.input_devices = &.{
|
||||||
.{
|
.{ .button_count = 8 },
|
||||||
.button_count = 8,
|
.{ .button_count = 8 },
|
||||||
},
|
},
|
||||||
.{
|
|
||||||
.button_count = 8,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
|
|
||||||
.storage_devices = &.{
|
.storage_devices = &.{
|
||||||
.{
|
.{
|
||||||
.name = "Battery PRG RAM",
|
.name = "Battery PRG RAM",
|
||||||
@@ -67,355 +80,173 @@ pub const spec = contract.SystemSpec{
|
|||||||
.writable = true,
|
.writable = true,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|
||||||
.save_state = .{
|
.save_state = .{
|
||||||
.max_size = 32 * 1024,
|
.max_size = 64 * 1024,
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
bus: Bus = undefined,
|
bus: Bus = undefined,
|
||||||
cpu: Cpu = undefined,
|
cpu: Cpu = undefined,
|
||||||
|
rgb565_framebuffer: [common.screen_width * common.screen_height]u16 = [_]u16{0} ** (common.screen_width * common.screen_height),
|
||||||
|
|
||||||
/// Initialize in place.
|
pub fn init(self: *Nes, rom_bytes: []const u8) !void {
|
||||||
///
|
const cart = try Cartridge.init(rom_bytes);
|
||||||
/// `rom` must remain valid for the lifetime of this NES
|
self.bus = Bus.init(.ntsc, cart);
|
||||||
/// because Cartridge stores slices into it.
|
self.cpu = Cpu.init(&self.bus);
|
||||||
pub fn init(
|
self.cpu.reset();
|
||||||
self: *Nes,
|
|
||||||
rom: []const u8,
|
|
||||||
) Cartridge.Error!void {
|
|
||||||
self.bus =
|
|
||||||
try Bus.init(rom);
|
|
||||||
|
|
||||||
self.cpu =
|
|
||||||
Cpu.init(&self.bus);
|
|
||||||
|
|
||||||
self.reset();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn reset(
|
pub fn reset(self: *Nes) void {
|
||||||
self: *Nes,
|
self.cpu.bus = &self.bus;
|
||||||
) void {
|
|
||||||
self.bus.reset();
|
self.bus.reset();
|
||||||
self.cpu.reset();
|
self.cpu.reset();
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn step(
|
pub fn runFrame(self: *Nes) void {
|
||||||
self: *Nes,
|
self.bus.ppu.frame_complete = false;
|
||||||
) ?u8 {
|
self.bus.apu.clearSamples();
|
||||||
const cycles = self.cpu.step() orelse return null;
|
|
||||||
self.serviceOamDma();
|
|
||||||
return cycles;
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn runFrame(
|
while (!self.bus.ppu.frame_complete) {
|
||||||
self: *Nes,
|
if (self.bus.dma_active) {
|
||||||
) void {
|
self.bus.stepDma();
|
||||||
self.bus.ppu.frame_ready = false;
|
} else {
|
||||||
self.bus.apu.sample_count = 0;
|
_ = self.cpu.step();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
while (!self.bus.ppu.frame_ready) {
|
// Convert PPU framebuffer to RGB565 for frontend driver
|
||||||
_ = self.step() orelse
|
for (self.bus.ppu.framebuffer, 0..) |color_idx, i| {
|
||||||
return;
|
self.rgb565_framebuffer[i] = default_palette_565[color_idx & 0x3f];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub const conformance = @import("conformance.zig");
|
pub fn videoFrame(self: *const Nes, _: usize) video.VideoFrame {
|
||||||
|
|
||||||
test {
|
|
||||||
_ = conformance;
|
|
||||||
}
|
|
||||||
|
|
||||||
comptime {
|
|
||||||
contract.validateSystem(Nes);
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn videoFrame(
|
|
||||||
self: *const Nes,
|
|
||||||
output: usize,
|
|
||||||
) contract.VideoFrame {
|
|
||||||
std.debug.assert(output == 0);
|
|
||||||
|
|
||||||
return .{
|
return .{
|
||||||
.data = std.mem.sliceAsBytes(
|
.data = std.mem.sliceAsBytes(&self.rgb565_framebuffer),
|
||||||
self.bus.ppu.framebuffer[0..],
|
.width = common.screen_width,
|
||||||
),
|
.height = common.screen_height,
|
||||||
|
.pitch = common.screen_width * 2,
|
||||||
.width = 256,
|
|
||||||
.height = 240,
|
|
||||||
|
|
||||||
.pitch = 256 * @sizeOf(u16),
|
|
||||||
|
|
||||||
.format = .rgb565,
|
.format = .rgb565,
|
||||||
|
.frame_number = self.bus.ppu.frame_count,
|
||||||
.frame_number = self.bus.ppu.frame_number,
|
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn audioBuffer(
|
pub fn audioBuffer(self: *const Nes, _: usize) audio.Buffer {
|
||||||
self: *const Nes,
|
const samples = self.bus.apu.getSamples();
|
||||||
output: usize,
|
|
||||||
) contract.AudioBuffer {
|
|
||||||
std.debug.assert(output == 0);
|
|
||||||
|
|
||||||
const samples = self.bus.apu.sample_buffer[0..self.bus.apu.sample_count];
|
|
||||||
|
|
||||||
return .{
|
return .{
|
||||||
.data = std.mem.sliceAsBytes(samples),
|
.data = std.mem.sliceAsBytes(samples),
|
||||||
.frames = self.bus.apu.sample_count,
|
.frames = samples.len,
|
||||||
|
|
||||||
.sample_rate = 44_100,
|
.sample_rate = 44_100,
|
||||||
.channels = 1,
|
.channels = 1,
|
||||||
.format = .i16,
|
.format = .i16,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn setInput(
|
pub fn setInput(self: *Nes, device_index: usize, device_input: input.DeviceInput) void {
|
||||||
self: *Nes,
|
if (device_index < 2) {
|
||||||
device: usize,
|
self.bus.controllers[device_index].setButtons(@truncate(device_input.buttons));
|
||||||
input: contract.DeviceInput,
|
}
|
||||||
) void {
|
|
||||||
std.debug.assert(device < 2);
|
|
||||||
|
|
||||||
self.bus.controllers[device]
|
|
||||||
.setButtons(
|
|
||||||
@truncate(input.buttons),
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn storageView(
|
pub fn storageView(self: *const Nes, index: usize) storage.View {
|
||||||
self: *const Nes,
|
if (index == 0 and self.bus.cartridge != null) {
|
||||||
slot: usize,
|
|
||||||
) contract.StorageView {
|
|
||||||
std.debug.assert(slot == 0);
|
|
||||||
return .{
|
return .{
|
||||||
.data = &self.bus.cartridge.prg_ram,
|
.data = &self.bus.cartridge.?.prg_ram,
|
||||||
.generation = 1,
|
.generation = 0,
|
||||||
};
|
};
|
||||||
}
|
|
||||||
|
|
||||||
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();
|
|
||||||
}
|
}
|
||||||
|
return .{ .data = &.{}, .generation = 0 };
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn saveState(
|
pub fn loadStorage(self: *Nes, index: usize, data: []const u8) storage.LoadError!void {
|
||||||
self: *const Nes,
|
if (index != 0 or self.bus.cartridge == null) return error.InvalidSlot;
|
||||||
buffer: []u8,
|
if (data.len > self.bus.cartridge.?.prg_ram.len) return error.InvalidSize;
|
||||||
) contract.state.Error!usize {
|
@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;
|
var offset: usize = 0;
|
||||||
|
@memcpy(dest[offset .. offset + 8], "NESSTATE");
|
||||||
if (buffer.len < 8) return error.BufferTooSmall;
|
|
||||||
@memcpy(buffer[offset..][0..8], "6SOZNES1");
|
|
||||||
offset += 8;
|
offset += 8;
|
||||||
|
|
||||||
// CPU
|
const regs = self.cpu.registers;
|
||||||
if (offset + 15 > buffer.len) return error.BufferTooSmall;
|
dest[offset] = regs.a;
|
||||||
std.mem.writeInt(u16, buffer[offset..][0..2], self.cpu.registers.pc, .little);
|
dest[offset + 1] = regs.x;
|
||||||
offset += 2;
|
dest[offset + 2] = regs.y;
|
||||||
buffer[offset] = self.cpu.registers.a;
|
dest[offset + 3] = regs.sp;
|
||||||
offset += 1;
|
dest[offset + 4] = @bitCast(regs.status);
|
||||||
buffer[offset] = self.cpu.registers.x;
|
dest[offset + 5] = @truncate(regs.pc);
|
||||||
offset += 1;
|
dest[offset + 6] = @truncate(regs.pc >> 8);
|
||||||
buffer[offset] = self.cpu.registers.y;
|
offset += 7;
|
||||||
offset += 1;
|
|
||||||
buffer[offset] = self.cpu.registers.sp;
|
|
||||||
offset += 1;
|
|
||||||
buffer[offset] = @bitCast(self.cpu.registers.status);
|
|
||||||
offset += 1;
|
|
||||||
std.mem.writeInt(u64, buffer[offset..][0..8], self.cpu.cycles, .little);
|
|
||||||
offset += 8;
|
|
||||||
|
|
||||||
// Bus RAM & PRG RAM
|
@memcpy(dest[offset .. offset + 0x800], &self.bus.ram);
|
||||||
if (offset + self.bus.ram.len + self.bus.cartridge.prg_ram.len > buffer.len) return error.BufferTooSmall;
|
offset += 0x800;
|
||||||
@memcpy(buffer[offset..][0..self.bus.ram.len], &self.bus.ram);
|
|
||||||
offset += self.bus.ram.len;
|
|
||||||
@memcpy(buffer[offset..][0..self.bus.cartridge.prg_ram.len], &self.bus.cartridge.prg_ram);
|
|
||||||
offset += self.bus.cartridge.prg_ram.len;
|
|
||||||
|
|
||||||
if (self.bus.cartridge.chr_is_ram) {
|
@memcpy(dest[offset .. offset + 0x800], &self.bus.ciram);
|
||||||
if (offset + self.bus.cartridge.chr_ram.len > buffer.len) return error.BufferTooSmall;
|
offset += 0x800;
|
||||||
@memcpy(buffer[offset..][0..self.bus.cartridge.chr_ram.len], &self.bus.cartridge.chr_ram);
|
|
||||||
offset += self.bus.cartridge.chr_ram.len;
|
|
||||||
}
|
|
||||||
|
|
||||||
// PPU State
|
@memcpy(dest[offset .. offset + 256], &self.bus.ppu.registers.oam);
|
||||||
const ppu_size = 1 + 1 + 1 + 1 + 2 + 2 + 1 + 1 + 2 + 2 + 4096 + 32 + 256;
|
|
||||||
if (offset + ppu_size > buffer.len) return error.BufferTooSmall;
|
|
||||||
|
|
||||||
buffer[offset] = self.bus.ppu.ctrl;
|
|
||||||
offset += 1;
|
|
||||||
buffer[offset] = self.bus.ppu.mask;
|
|
||||||
offset += 1;
|
|
||||||
buffer[offset] = self.bus.ppu.status;
|
|
||||||
offset += 1;
|
|
||||||
buffer[offset] = self.bus.ppu.oam_addr;
|
|
||||||
offset += 1;
|
|
||||||
std.mem.writeInt(u16, buffer[offset..][0..2], self.bus.ppu.vram_addr, .little);
|
|
||||||
offset += 2;
|
|
||||||
std.mem.writeInt(u16, buffer[offset..][0..2], self.bus.ppu.temp_addr, .little);
|
|
||||||
offset += 2;
|
|
||||||
buffer[offset] = self.bus.ppu.fine_x;
|
|
||||||
offset += 1;
|
|
||||||
buffer[offset] = if (self.bus.ppu.write_latch) 1 else 0;
|
|
||||||
offset += 1;
|
|
||||||
std.mem.writeInt(u16, buffer[offset..][0..2], self.bus.ppu.dot, .little);
|
|
||||||
offset += 2;
|
|
||||||
std.mem.writeInt(u16, buffer[offset..][0..2], self.bus.ppu.scanline, .little);
|
|
||||||
offset += 2;
|
|
||||||
|
|
||||||
@memcpy(buffer[offset..][0..4096], &self.bus.ppu.nametable);
|
|
||||||
offset += 4096;
|
|
||||||
@memcpy(buffer[offset..][0..32], &self.bus.ppu.palette);
|
|
||||||
offset += 32;
|
|
||||||
@memcpy(buffer[offset..][0..256], &self.bus.ppu.oam);
|
|
||||||
offset += 256;
|
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;
|
return offset;
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn loadState(
|
pub fn loadState(self: *Nes, src: []const u8) state.Error!void {
|
||||||
self: *Nes,
|
if (src.len < 8 or !std.mem.eql(u8, src[0..8], "NESSTATE")) {
|
||||||
buffer: []const u8,
|
return error.InvalidState;
|
||||||
) 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;
|
|
||||||
}
|
}
|
||||||
|
var offset: usize = 8;
|
||||||
|
|
||||||
// PPU State
|
self.cpu.registers.a = src[offset];
|
||||||
const ppu_size = 1 + 1 + 1 + 1 + 2 + 2 + 1 + 1 + 2 + 2 + 4096 + 32 + 256;
|
self.cpu.registers.x = src[offset + 1];
|
||||||
if (offset + ppu_size > buffer.len) return error.InvalidState;
|
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];
|
@memcpy(&self.bus.ram, src[offset .. offset + 0x800]);
|
||||||
offset += 1;
|
offset += 0x800;
|
||||||
self.bus.ppu.mask = buffer[offset];
|
|
||||||
offset += 1;
|
|
||||||
self.bus.ppu.status = buffer[offset];
|
|
||||||
offset += 1;
|
|
||||||
self.bus.ppu.oam_addr = buffer[offset];
|
|
||||||
offset += 1;
|
|
||||||
self.bus.ppu.vram_addr = std.mem.readInt(u16, buffer[offset..][0..2], .little);
|
|
||||||
offset += 2;
|
|
||||||
self.bus.ppu.temp_addr = std.mem.readInt(u16, buffer[offset..][0..2], .little);
|
|
||||||
offset += 2;
|
|
||||||
self.bus.ppu.fine_x = @truncate(buffer[offset]);
|
|
||||||
offset += 1;
|
|
||||||
self.bus.ppu.write_latch = buffer[offset] != 0;
|
|
||||||
offset += 1;
|
|
||||||
self.bus.ppu.dot = std.mem.readInt(u16, buffer[offset..][0..2], .little);
|
|
||||||
offset += 2;
|
|
||||||
self.bus.ppu.scanline = std.mem.readInt(u16, buffer[offset..][0..2], .little);
|
|
||||||
offset += 2;
|
|
||||||
|
|
||||||
@memcpy(&self.bus.ppu.nametable, buffer[offset..][0..4096]);
|
@memcpy(&self.bus.ciram, src[offset .. offset + 0x800]);
|
||||||
offset += 4096;
|
offset += 0x800;
|
||||||
@memcpy(&self.bus.ppu.palette, buffer[offset..][0..32]);
|
|
||||||
offset += 32;
|
@memcpy(&self.bus.ppu.registers.oam, src[offset .. offset + 256]);
|
||||||
@memcpy(&self.bus.ppu.oam, buffer[offset..][0..256]);
|
|
||||||
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" {
|
test {
|
||||||
// Construct dummy iNES NROM header + 16K PRG + 8K CHR
|
contract.system.validate(Nes);
|
||||||
var rom: [16 + 0x4000 + 0x2000]u8 = [_]u8{0} ** (16 + 0x4000 + 0x2000);
|
_ = common;
|
||||||
@memcpy(rom[0..4], "NES\x1a");
|
_ = ppu;
|
||||||
rom[4] = 1; // 16K PRG
|
_ = apu;
|
||||||
rom[5] = 1; // 8K CHR
|
_ = bus_mod;
|
||||||
|
_ = cartridge_mod;
|
||||||
var nes: Nes = undefined;
|
_ = controller_mod;
|
||||||
try nes.init(&rom);
|
_ = mapper_mod;
|
||||||
|
_ = conformance;
|
||||||
// Mutate state
|
|
||||||
nes.cpu.registers.pc = 0x1234;
|
|
||||||
nes.cpu.registers.a = 0x42;
|
|
||||||
nes.bus.ram[0x05] = 0xAA;
|
|
||||||
nes.bus.ppu.vram_addr = 0x2050;
|
|
||||||
|
|
||||||
var save_buf: [32768]u8 = undefined;
|
|
||||||
const bytes_written = try nes.saveState(&save_buf);
|
|
||||||
|
|
||||||
// Reset nes
|
|
||||||
nes.reset();
|
|
||||||
try std.testing.expect(nes.cpu.registers.pc != 0x1234);
|
|
||||||
|
|
||||||
// Load state
|
|
||||||
try nes.loadState(save_buf[0..bytes_written]);
|
|
||||||
try std.testing.expectEqual(@as(u16, 0x1234), nes.cpu.registers.pc);
|
|
||||||
try std.testing.expectEqual(@as(u8, 0x42), nes.cpu.registers.a);
|
|
||||||
try std.testing.expectEqual(@as(u8, 0xAA), nes.bus.ram[0x05]);
|
|
||||||
try std.testing.expectEqual(@as(u16, 0x2050), nes.bus.ppu.vram_addr);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
comptime {
|
pub const conformance = @import("conformance/root.zig");
|
||||||
contract.validateSystem(Nes);
|
|
||||||
}
|
|
||||||
|
|||||||
Reference in New Issue
Block a user