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
53 changes: 53 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
name: ci

on:
push:
branches: [main]
pull_request:

permissions:
contents: read

concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true

jobs:
test:
name: Test (${{ matrix.os }})
strategy:
fail-fast: false
matrix:
os: [ubuntu-latest, windows-latest]
runs-on: ${{ matrix.os }}
steps:
- uses: actions/checkout@v4
- uses: mlugg/setup-zig@v2
with:
version: 0.16.0
- name: Format check
run: zig fmt --check .
- name: Unit and integration tests
run: zig build test -Dsdl2=off --summary all
- name: Regenerate bundled ROMs
run: zig build fixtures -Dsdl2=off
- name: Committed fixtures match regeneration
if: runner.os == 'Linux'
run: git diff --exit-code -- fixtures
- name: Demo ROM reports PASS
run: zig build run-demo -Dsdl2=off
- name: Build headless tools
run: zig build -Dsdl2=off --summary all

windowed:
name: Windowed frontend (ubuntu)
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: mlugg/setup-zig@v2
with:
version: 0.16.0
- name: Install SDL2
run: sudo apt-get update && sudo apt-get install -y libsdl2-dev
- name: Build windowed frontend
run: zig build --summary all
4 changes: 4 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -10,3 +10,7 @@ zig-out/
*.gb
*.sav
*.swp

# The bundled demo and test ROMs are generated from fixtures/asm and
# committed so the demo works without running the build step first.
!fixtures/roms/*.gb
21 changes: 21 additions & 0 deletions LICENSE
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
MIT License

Copyright (c) 2026 DanielCuevas1208

Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:

The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
3 changes: 3 additions & 0 deletions NOTICE
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
This project links the SDL2 library at build time. SDL2 is a
third-party library. It is not part of this source tree. The SDL2 source
is available from the SDL website. This project does not bundle SDL2.
97 changes: 61 additions & 36 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,46 +1,54 @@
# Dot Matrix Deck

Dot Matrix Deck is a Game Boy emulator workbench written in Zig. It models the
SM83 CPU, the memory bus, timers, serial output, and the pixel pipeline. A
headless runner makes emulator behavior easy to inspect in scripts and CI.

## Current status

This project is under active development. The headless core is the most useful
entry point today. The SDL2 windowed frontend is available when SDL2 is
installed, but it is still being built out.
Dot Matrix Deck is a Game Boy emulator. It is written in Zig. It
emulates the SM83 CPU, the memory bus, the timers, and the pixel
pipeline. A window shows the screen with SDL2. A headless mode runs
test ROMs without a window.

## Features

- SM83 CPU and instruction execution
- Memory bus with timer, serial, and pixel-processing components
- Headless ROM runner with cycle limits and serial verdicts
- Optional instruction trace output
- Small assembler tool for test ROM sources
- Deterministic unit tests for the emulator core and assembler
- SM83 CPU with the full base and CB instruction sets
- Memory bus with timer, serial, joypad, and pixel components
- Pixel pipeline with background, window, and sprites
- MBC1 and ROM-only cartridges
- Headless runner with serial verdicts
- SDL2 window with keyboard controls
- gbasm assembler for test ROMs
- Bundled demo ROM that draws and reports PASS
- Deterministic unit and end-to-end tests

## Requirements

- Zig 0.16 or later
- SDL2 for the optional windowed frontend
- SDL2 for the windowed frontend

The headless build does not require SDL2. On Windows, the build searches common
MSYS2 and vcpkg prefixes. You can also set `SDL2_DIR` or pass an SDL2 prefix.
The headless build does not need SDL2. On Windows, the build searches
common MSYS2 and vcpkg prefixes. Set `SDL2_DIR` to use another prefix.

## Build

Build the headless runner and assembler:
Build the headless tools:

```text
zig build -Dsdl2=off
```

Build with the windowed frontend when SDL2 is available:
Build the windowed frontend:

```text
zig build
```

## Run the demo

Run the demo ROM and check its verdict:

```text
zig build run-demo -Dsdl2=off
```

The demo draws a pattern and reports PASS over the link port.

## Run a ROM

Run a ROM without a window:
Expand All @@ -49,41 +57,58 @@ Run a ROM without a window:
zig build run-headless -- path/to/rom.gb
```

Useful options are `--max-cycles N`, `--trace`, and `--expect pass|fail|any`.
The runner prints serial output and returns a status from the detected verdict.
The runner prints the serial output. It sets the exit code from the
verdict. Useful options are `--max-cycles N`, `--trace`, and
`--expect pass|fail|any`.

Run a ROM in the SDL2 frontend:
Run a ROM in a window:

```text
zig build run -- path/to/rom.gb
```

## Controls

Use the arrow keys to move. Use Z and X for the A and B buttons. Use
Enter and Backspace for Start and Select. Press Escape to quit.

## Test

Run the core and assembler tests:
Run all tests:

```text
zig build test -Dsdl2=off
```

The repository also contains build steps for generated fixtures and the Blargg
CPU instruction suite. Add the required fixture files before using those steps.
The tests cover the CPU, the timer, the serial port, the disassembler,
the assembler, and the ROM fixtures.

## Project layout

- `src/cpu.zig` - SM83 CPU implementation
- `src/emulator.zig` - emulator composition and core tests
- `src/bus.zig` - memory and device routing
- `src/headless.zig` - command-line ROM runner
- `src/main.zig` - SDL2 frontend
- `tools/gbasm.zig` - small assembler for fixture ROMs
- `src/cpu.zig` - the SM83 CPU
- `src/bus.zig` - the memory bus
- `src/ppu.zig` - the pixel pipeline
- `src/timer.zig` - the timer
- `src/cartridge.zig` - cartridges and MBC1
- `src/headless.zig` - the headless runner
- `src/frontend.zig` - the SDL2 window
- `src/joypad.zig` - the joypad
- `src/disasm.zig` - the disassembler
- `tools/gbasm.zig` - the assembler
- `fixtures/` - bundled test ROMs and sources
- `docs/` - roadmap and architecture notes

## Test status

The project runs 44 tests in CI. The CI checks formatting, builds, and
the demo verdict. It runs on Linux and Windows.

## Limitations

Hardware coverage is incomplete. Timing accuracy, cartridge support, audio,
and frontend features will improve as the project grows.
Timing accuracy is incomplete. Audio is not implemented. Only MBC1
cartridges work. The windowed frontend is basic. See `docs/roadmap.md`
for the remaining work.

## License

No license file is published yet. Treat this repository as an experimental
project until a license is added.
MIT. See `LICENSE`.
92 changes: 80 additions & 12 deletions build.zig
Original file line number Diff line number Diff line change
Expand Up @@ -57,22 +57,22 @@ pub fn build(b: *std.Build) void {
};

if (build_windowed) {
var sdl_prefix: ?[]const u8 = null;
if (sdl2_opt) |value| {
if (value.len != 0 and !std.mem.eql(u8, value, "off")) sdl_prefix = value;
} else {
if (b.graph.environ_map.get("SDL2_DIR")) |dir| {
if (dir.len != 0) sdl_prefix = dir;
}
if (sdl_prefix == null and target.result.os.tag == .windows) {
var sdl_prefix: ?[]const u8 = null;
if (sdl2_opt) |value| {
if (value.len != 0 and !std.mem.eql(u8, value, "off")) sdl_prefix = value;
} else {
if (b.graph.environ_map.get("SDL2_DIR")) |dir| {
if (dir.len != 0) sdl_prefix = dir;
}
if (sdl_prefix == null and target.result.os.tag == .windows) {
for (sdl_prefix_candidates) |candidate| {
if (sdl2PrefixFound(b, candidate)) {
sdl_prefix = candidate;
break;
}
}
}
}
}

if (sdl_prefix != null or target.result.os.tag != .windows) {
const exe = b.addExecutable(.{
Expand All @@ -91,9 +91,20 @@ pub fn build(b: *std.Build) void {
if (sdl_prefix) |prefix| {
exe.root_module.addIncludePath(.{ .cwd_relative = b.pathJoin(&.{ prefix, "include" }) });
exe.root_module.addLibraryPath(.{ .cwd_relative = b.pathJoin(&.{ prefix, "lib" }) });
// On Windows prefer the DLL import library. The static
// mingw archive drags in CRT symbols Zig does not ship.
if (target.result.os.tag == .windows) {
exe.root_module.addObjectFile(.{ .cwd_relative = b.pathJoin(&.{ prefix, "lib", "libSDL2.dll.a" }) });
const shim = b.addObject(.{ .name = "sdl_mingw_shim", .root_module = b.createModule(.{
.root_source_file = b.path("src/sdl_mingw_shim.zig"),
.target = target,
.optimize = optimize,
}) });
exe.root_module.addObject(shim);
}
const dll_path = b.pathJoin(&.{ prefix, "bin", "SDL2.dll" });
if (sdl2PrefixFound(b, prefix)) {
const install_dll = b.addInstallBinFile(.{ .cwd_relative = dll_path }, "bin/SDL2.dll");
const install_dll = b.addInstallBinFile(.{ .cwd_relative = dll_path }, "SDL2.dll");
b.getInstallStep().dependOn(&install_dll.step);
}
}
Expand All @@ -118,8 +129,6 @@ pub fn build(b: *std.Build) void {
}),
});
const run_core_tests = b.addRunArtifact(core_tests);
const test_step = b.step("test", "Run deterministic unit tests and bundled ROM tests");
test_step.dependOn(&run_core_tests.step);

const asm_tests = b.addTest(.{
.root_module = b.createModule(.{
Expand All @@ -129,7 +138,56 @@ pub fn build(b: *std.Build) void {
}),
});
const run_asm_tests = b.addRunArtifact(asm_tests);

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);

const serial_tests = b.addTest(.{
.root_module = b.createModule(.{
.root_source_file = b.path("src/serial.zig"),
.target = target,
.optimize = optimize,
}),
});
const run_serial_tests = b.addRunArtifact(serial_tests);

const disasm_tests = b.addTest(.{
.root_module = b.createModule(.{
.root_source_file = b.path("src/disasm.zig"),
.target = target,
.optimize = optimize,
}),
});
const run_disasm_tests = b.addRunArtifact(disasm_tests);

const gbasm_mod = b.createModule(.{
.root_source_file = b.path("tools/gbasm.zig"),
.target = target,
.optimize = optimize,
});
const end_to_end_tests = b.addTest(.{
.root_module = b.createModule(.{
.root_source_file = b.path("src/tests.zig"),
.target = target,
.optimize = optimize,
.imports = &.{.{ .name = "gbasm", .module = gbasm_mod }},
}),
});
const run_end_to_end_tests = b.addRunArtifact(end_to_end_tests);

const test_step = b.step("test", "Run deterministic unit tests and bundled ROM tests");
test_step.dependOn(&run_core_tests.step);
test_step.dependOn(&run_asm_tests.step);
test_step.dependOn(&run_joypad_tests.step);
test_step.dependOn(&run_serial_tests.step);
test_step.dependOn(&run_disasm_tests.step);
test_step.dependOn(&run_end_to_end_tests.step);

const fixtures_step = b.step("fixtures", "Regenerate bundled test ROMs from their assembly sources");
{
Expand All @@ -139,6 +197,16 @@ pub fn build(b: *std.Build) void {
fixtures_step.dependOn(&run.step);
}

const demo_step = b.step("run-demo", "Run the bundled demo ROM and check it reports PASS");
{
const run = b.addRunArtifact(headless);
run.addArg("--expect");
run.addArg("pass");
run.addFileArg(b.path("fixtures/roms/demo.gb"));
demo_step.dependOn(&run.step);
}
demo_step.dependOn(fixtures_step);

const blargg_step = b.step("test-blargg", "Run the blargg cpu_instrs ROMs in headless mode and check their serial output");
{
const run = b.addRunArtifact(headless);
Expand Down
3 changes: 2 additions & 1 deletion build.zig.zon
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
.{
.name = .dot_matrix_deck,
.version = "0.1.0",
.version = "0.2.0",
.minimum_zig_version = "0.16.0",
.fingerprint = 0x3ad14c39cc8e69a1,
.paths = .{
Expand All @@ -9,6 +9,7 @@
"src",
"tools",
"fixtures",
"docs",
"README.md",
"LICENSE",
"NOTICE",
Expand Down
Loading
Loading