diff --git a/build.zig b/build.zig index 4d3a85b..83279ff 100644 --- a/build.zig +++ b/build.zig @@ -10,33 +10,91 @@ pub fn build(b: *std.Build) void { .root_source_file = b.path("src/cpu/root.zig"), }); + const lib = b.addLibrary(.{ + .name = "cpu", + .root_module = cpu_mod, + .linkage = .static, + }); + + b.installArtifact(lib); + + const contract_mod = b.createModule(.{ + .target = target, + .optimize = optimize, + .root_source_file = b.path("src/contract/root.zig"), + }); + const system_mod = b.createModule(.{ .target = target, .optimize = optimize, .root_source_file = b.path("src/system/root.zig"), }); + system_mod.addImport("contract", contract_mod); + system_mod.addImport("cpu", cpu_mod); const platform_mod = b.createModule(.{ .target = target, .optimize = optimize, .root_source_file = b.path("src/platform/root.zig"), }); + platform_mod.addImport("contract", contract_mod); const frontend_mod = b.createModule(.{ .target = target, .optimize = optimize, .root_source_file = b.path("src/frontend/root.zig"), }); + frontend_mod.addImport("contract", contract_mod); const exe_mod = b.createModule(.{ .target = target, .optimize = optimize, .root_source_file = b.path("src/main.zig"), }); + exe_mod.addImport("contract", contract_mod); + exe_mod.addImport("system", system_mod); + const exe = b.addExecutable(.{ + .name = "6soz", + .root_module = exe_mod, + }); + + b.installArtifact(exe); + + // Test Step Configuration const test_step = b.step("test", "Run 6soz tests"); - inline for (&.{ cpu_mod, system_mod, platform_mod, frontend_mod, exe_mod }) |mod| { + const test_options = b.addOptions(); + if (b.lazyDependency("nes_test_roms", .{})) |roms_dep| { + test_options.addOption(?[]const u8, "nes_test_roms_dir", roms_dep.path("").getPath(b)); + } else { + test_options.addOption(?[]const u8, "nes_test_roms_dir", null); + } + if (b.lazyDependency("m6502_functional_tests", .{})) |func_dep| { + test_options.addOption(?[]const u8, "m6502_functional_tests_dir", func_dep.path("").getPath(b)); + } else { + test_options.addOption(?[]const u8, "m6502_functional_tests_dir", null); + } + + const options_mod = test_options.createModule(); + + const cpu_test_mod = b.createModule(.{ + .target = target, + .optimize = optimize, + .root_source_file = b.path("src/cpu/root.zig"), + }); + cpu_test_mod.addImport("build_options", options_mod); + + const system_test_mod = b.createModule(.{ + .target = target, + .optimize = optimize, + .root_source_file = b.path("src/system/root.zig"), + }); + system_test_mod.addImport("contract", contract_mod); + system_test_mod.addImport("cpu", cpu_test_mod); + system_test_mod.addImport("build_options", options_mod); + + inline for (&.{ cpu_test_mod, contract_mod, system_test_mod, platform_mod, frontend_mod, exe_mod }) |mod| { const mod_test = b.addTest(.{ .root_module = mod }); const mod_cmd = b.addRunArtifact(mod_test); test_step.dependOn(&mod_cmd.step); diff --git a/build.zig.zon b/build.zig.zon index 2a74a6a..4a8f95a 100644 --- a/build.zig.zon +++ b/build.zig.zon @@ -2,6 +2,19 @@ .name = ._6soz, .version = "0.0.1", .minimum_zig_version = "0.16.0", + .dependencies = .{ + .nes_test_roms = .{ + .url = "https://github.com/christopherpow/nes-test-roms/archive/refs/heads/master.tar.gz", + .hash = "N-V-__8AANPxaAENPnXYuDVuqgUcOdrux1zKqz8bZZYf0RA2", + .lazy = true, + }, + .m6502_functional_tests = .{ + .url = "https://github.com/Klaus2m5/6502_65C02_functional_tests/archive/refs/heads/master.tar.gz", + .hash = "N-V-__8AAKmLHACkwIMCMRdqnsyLtL5hbQdi8cm3nGs8n-O7", + .lazy = true, + }, + }, + .paths = .{""}, .fingerprint = 0xad233139f14a4bea, } diff --git a/src/main.zig b/src/main.zig index bbb7b6e..1c35f6e 100644 --- a/src/main.zig +++ b/src/main.zig @@ -1 +1,157 @@ -pub fn main() !void {} +const std = @import("std"); +const Nes = @import("system").nes; + +pub fn main(init: std.process.Init) !void { + const allocator = init.gpa; + const cwd_dir: std.Io.Dir = .{ .handle = std.posix.AT.FDCWD }; + + var args_it = try std.process.Args.Iterator.initAllocator(init.minimal.args, allocator); + defer args_it.deinit(); + + var args: [32][]const u8 = undefined; + var args_count: usize = 0; + + while (args_it.next()) |arg| { + if (args_count < args.len) { + args[args_count] = arg; + args_count += 1; + } + } + + if (args_count < 2) { + printUsage(); + return; + } + + const command = args[1]; + if (std.mem.eql(u8, command, "help") or std.mem.eql(u8, command, "--help")) { + printUsage(); + return; + } + + if (!std.mem.eql(u8, command, "run")) { + std.debug.print("Unknown command '{s}'. Use '6soz --help' for usage.\n", .{command}); + return; + } + + if (args_count < 3) { + std.debug.print("Error: missing ROM file path.\nUsage: 6soz run \n", .{}); + return; + } + + const rom_path = args[2]; + var target_frames: u64 = 60; + var save_state_path: ?[]const u8 = null; + var load_state_path: ?[]const u8 = null; + var save_sram_path: ?[]const u8 = null; + var load_sram_path: ?[]const u8 = null; + + var i: usize = 3; + while (i < args_count) : (i += 1) { + const arg = args[i]; + if (std.mem.eql(u8, arg, "--frames") and i + 1 < args_count) { + i += 1; + target_frames = try std.fmt.parseInt(u64, args[i], 10); + } else if (std.mem.eql(u8, arg, "--save-state") and i + 1 < args_count) { + i += 1; + save_state_path = args[i]; + } else if (std.mem.eql(u8, arg, "--load-state") and i + 1 < args_count) { + i += 1; + load_state_path = args[i]; + } else if (std.mem.eql(u8, arg, "--save-sram") and i + 1 < args_count) { + i += 1; + save_sram_path = args[i]; + } else if (std.mem.eql(u8, arg, "--load-sram") and i + 1 < args_count) { + i += 1; + load_sram_path = args[i]; + } + } + + const rom_bytes = cwd_dir.readFileAlloc(init.io, rom_path, allocator, @enumFromInt(16 * 1024 * 1024)) catch |err| { + std.debug.print("Error reading ROM file '{s}': {s}\n", .{ rom_path, @errorName(err) }); + return; + }; + defer allocator.free(rom_bytes); + + var nes: Nes = undefined; + nes.init(rom_bytes) catch |err| { + std.debug.print("Failed to initialize NES cartridge: {s}\n", .{@errorName(err)}); + return; + }; + + if (load_sram_path) |path| { + if (cwd_dir.readFileAlloc(init.io, path, allocator, @enumFromInt(8192))) |sram_bytes| { + defer allocator.free(sram_bytes); + nes.loadStorage(0, sram_bytes) catch {}; + std.debug.print("[6soz] Loaded SRAM from '{s}'\n", .{path}); + } else |err| { + std.debug.print("Warning: Failed to read SRAM file '{s}': {s}\n", .{ path, @errorName(err) }); + } + } + + if (load_state_path) |path| { + if (cwd_dir.readFileAlloc(init.io, path, allocator, @enumFromInt(64 * 1024))) |state_bytes| { + defer allocator.free(state_bytes); + nes.loadState(state_bytes) catch |err| { + std.debug.print("Failed to load save state: {s}\n", .{@errorName(err)}); + }; + std.debug.print("[6soz] Loaded Save State from '{s}'\n", .{path}); + } else |err| { + std.debug.print("Warning: Failed to read state file '{s}': {s}\n", .{ path, @errorName(err) }); + } + } + + std.debug.print("[6soz] Running NES emulator for {d} frames...\n", .{target_frames}); + const start_time = std.Io.Clock.Timestamp.now(init.io, .awake); + + var frame: u64 = 0; + while (frame < target_frames) : (frame += 1) { + nes.runFrame(); + } + + const end_time = std.Io.Clock.Timestamp.now(init.io, .awake); + const elapsed_ns = end_time.raw.nanoseconds - start_time.raw.nanoseconds; + const elapsed_sec = @as(f64, @floatFromInt(elapsed_ns)) / 1_000_000_000.0; + const fps = if (elapsed_sec > 0) @as(f64, @floatFromInt(target_frames)) / elapsed_sec else 0.0; + + std.debug.print("[6soz] Benchmark Complete:\n", .{}); + std.debug.print(" - Frames: {d}\n", .{target_frames}); + std.debug.print(" - Elapsed Time: {d:.3}s\n", .{elapsed_sec}); + std.debug.print(" - Performance: {d:.1} FPS\n", .{fps}); + + if (save_state_path) |path| { + var state_buf: [64 * 1024]u8 = undefined; + if (nes.saveState(&state_buf)) |written| { + cwd_dir.writeFile(init.io, .{ .sub_path = path, .data = state_buf[0..written] }) catch |err| { + std.debug.print("Error writing save state to '{s}': {s}\n", .{ path, @errorName(err) }); + }; + std.debug.print("[6soz] Saved State to '{s}'\n", .{path}); + } else |err| { + std.debug.print("Error serializing save state: {s}\n", .{@errorName(err)}); + } + } + + if (save_sram_path) |path| { + const sram_view = nes.storageView(0); + cwd_dir.writeFile(init.io, .{ .sub_path = path, .data = sram_view.data }) catch |err| { + std.debug.print("Error writing SRAM to '{s}': {s}\n", .{ path, @errorName(err) }); + }; + std.debug.print("[6soz] Saved SRAM to '{s}'\n", .{path}); + } +} + +fn printUsage() void { + std.debug.print( + \\6soz - 6502 / 6510 & NES Emulator Engine + \\Usage: + \\ 6soz run [options] + \\ + \\Options: + \\ --frames Number of frames to execute (default: 60) + \\ --save-state Save state binary file output + \\ --load-state Load state binary file input + \\ --save-sram Save battery PRG RAM output + \\ --load-sram Load battery PRG RAM input + \\ + , .{}); +} diff --git a/src/system/nes/conformance.zig b/src/system/nes/conformance.zig new file mode 100644 index 0000000..de324da --- /dev/null +++ b/src/system/nes/conformance.zig @@ -0,0 +1,345 @@ +// 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); +}