Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
51 changes: 49 additions & 2 deletions build.zig
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,39 @@ fn sdl2PrefixFound(b: *std.Build, prefix: []const u8) bool {
return true;
}

// A step that prints a notice and does nothing else. It lets optional
// steps like the fixture regenerator skip cleanly when their inputs are
// not present in the checkout.
const NoticeStep = struct {
step: std.Build.Step,
message: []const u8,

fn make(step: *std.Build.Step, options: std.Build.Step.MakeOptions) !void {
_ = options;
const self: *NoticeStep = @fieldParentPtr("step", step);
std.debug.print("SKIP: {s}\n", .{self.message});
}

fn create(b: *std.Build, message: []const u8) *std.Build.Step {
const self = b.allocator.create(NoticeStep) catch @panic("OOM");
self.* = .{
.step = std.Build.Step.init(.{
.id = .custom,
.name = "notice",
.owner = b,
.makeFn = make,
}),
.message = message,
};
return &self.step;
}
};

fn dirExists(b: *std.Build, path: []const u8) bool {
b.build_root.handle.access(b.graph.io, path, .{}) catch return false;
return true;
}

pub fn build(b: *std.Build) void {
const target = b.standardTargetOptions(.{});
const optimize = b.standardOptimizeOption(.{});
Expand Down Expand Up @@ -131,19 +164,33 @@ pub fn build(b: *std.Build) void {
const run_asm_tests = b.addRunArtifact(asm_tests);
test_step.dependOn(&run_asm_tests.step);

const joypad_tests = b.addTest(.{
.root_module = b.createModule(.{
.root_source_file = b.path("src/joypad.zig"),
.target = target,
.optimize = optimize,
}),
});
const run_joypad_tests = b.addRunArtifact(joypad_tests);
test_step.dependOn(&run_joypad_tests.step);

const fixtures_step = b.step("fixtures", "Regenerate bundled test ROMs from their assembly sources");
{
if (dirExists(b, "fixtures/asm")) {
const run = b.addRunArtifact(gbasm);
run.addDirectoryArg(b.path("fixtures/asm"));
run.addDirectoryArg(b.path("fixtures/roms"));
fixtures_step.dependOn(&run.step);
} else {
fixtures_step.dependOn(NoticeStep.create(b, "fixtures/asm is not present; nothing to regenerate"));
}

const blargg_step = b.step("test-blargg", "Run the blargg cpu_instrs ROMs in headless mode and check their serial output");
{
if (dirExists(b, "fixtures/blargg")) {
const run = b.addRunArtifact(headless);
run.addArg("--blargg-suite");
run.addDirectoryArg(b.path("fixtures/blargg"));
blargg_step.dependOn(&run.step);
} else {
blargg_step.dependOn(NoticeStep.create(b, "fixtures/blargg is not present; add the ROMs to run the suite"));
}
}
38 changes: 38 additions & 0 deletions src/emulator.zig
Original file line number Diff line number Diff line change
Expand Up @@ -89,3 +89,41 @@ test "page size sanity" {
try std.testing.expectEqual(@as(usize, 160), ppu.ScreenWidth);
try std.testing.expectEqual(@as(usize, 144), ppu.ScreenHeight);
}

fn frameChecksum(frame: []const u32) u64 {
var hash: u64 = 0xcbf29ce484222325;
for (frame) |pixel| {
var bytes = [4]u8{ @truncate(pixel), @truncate(pixel >> 8), @truncate(pixel >> 16), @truncate(pixel >> 24) };
for (&bytes) |byte| {
hash ^= byte;
hash *%= 0x100000001b3;
}
}
return hash;
}

test "a frame completes within the frame cycle budget" {
const allocator = std.testing.allocator;
var rom: [0x8000]u8 = [_]u8{0x00} ** 0x8000;
var emulator = try Emulator.init(allocator, &rom);
defer emulator.deinit();
emulator.runFrame(70_224);
try std.testing.expectEqual(@as(u64, 0), emulator.total_cycles % 4);
try std.testing.expectEqual(@as(u64, 65_664), emulator.total_cycles);
}

test "frame output is deterministic across runs" {
const allocator = std.testing.allocator;
var rom: [0x8000]u8 = [_]u8{0x00} ** 0x8000;
var first = try Emulator.init(allocator, &rom);
defer first.deinit();
first.runFrame(70_224);
const hash_a = frameChecksum(&first.bus.ppu.frame);

var second = try Emulator.init(allocator, &rom);
defer second.deinit();
second.runFrame(70_224);
const hash_b = frameChecksum(&second.bus.ppu.frame);

try std.testing.expectEqual(hash_a, hash_b);
}
89 changes: 89 additions & 0 deletions src/joypad.zig
Original file line number Diff line number Diff line change
@@ -0,0 +1,89 @@
const std = @import("std");

// Joypad key model for the frontend.
//
// The Game Boy joypad register is low-active. A pressed button reads 0
// and a released button reads 1. The windowed frontend converts SDL
// scancodes into `Key` values, then `apply` updates the button byte that
// the memory bus exposes at 0xff00.

pub const Key = enum {
a,
b,
start,
select,
up,
down,
left,
right,
};

pub const Button = struct {
pub const right: u8 = 1 << 0;
pub const left: u8 = 1 << 1;
pub const up: u8 = 1 << 2;
pub const down: u8 = 1 << 3;
pub const a: u8 = 1 << 4;
pub const b: u8 = 1 << 5;
pub const select: u8 = 1 << 6;
pub const start: u8 = 1 << 7;
};

pub fn mask(key: Key) u8 {
return switch (key) {
.a => Button.a,
.b => Button.b,
.start => Button.start,
.select => Button.select,
.up => Button.up,
.down => Button.down,
.left => Button.left,
.right => Button.right,
};
}

// Updates the button byte for one key. A press clears the low-active bit.
// A release sets the bit back to 1.
pub fn apply(key: Key, pressed: bool, buttons: *u8) void {
const bit = mask(key);
if (pressed) {
buttons.* &= ~bit;
} else {
buttons.* |= bit;
}
}

test "each key maps to its own bit" {
try std.testing.expectEqual(@as(u8, 0x01), mask(.right));
try std.testing.expectEqual(@as(u8, 0x02), mask(.left));
try std.testing.expectEqual(@as(u8, 0x04), mask(.up));
try std.testing.expectEqual(@as(u8, 0x08), mask(.down));
try std.testing.expectEqual(@as(u8, 0x10), mask(.a));
try std.testing.expectEqual(@as(u8, 0x20), mask(.b));
try std.testing.expectEqual(@as(u8, 0x40), mask(.select));
try std.testing.expectEqual(@as(u8, 0x80), mask(.start));
}

test "a press clears the button bit" {
var buttons: u8 = 0xff;
apply(.a, true, &buttons);
try std.testing.expectEqual(@as(u8, 0xef), buttons);
apply(.right, true, &buttons);
try std.testing.expectEqual(@as(u8, 0xee), buttons);
}

test "a release restores the button bit" {
var buttons: u8 = 0x00;
apply(.start, false, &buttons);
try std.testing.expectEqual(@as(u8, 0x80), buttons);
apply(.down, false, &buttons);
try std.testing.expectEqual(@as(u8, 0x88), buttons);
}

test "keys do not disturb each other" {
var buttons: u8 = 0xff;
apply(.b, true, &buttons);
apply(.select, true, &buttons);
apply(.up, false, &buttons);
try std.testing.expectEqual(@as(u8, 0x9b), buttons);
}
Loading