Compare commits
14 Commits
5adbc354d6
...
april-fool
| Author | SHA1 | Date | |
|---|---|---|---|
| 9844d657b0 | |||
| 1d8b21d6b4 | |||
| b879c76510 | |||
| 0dbba2fb9a | |||
| 49b0620c48 | |||
| a6a9e3ac72 | |||
| aeefff86f8 | |||
| 91aa98eef7 | |||
| f3b6c4f3fe | |||
| 5aa5ac2a8b | |||
| b1827ccea0 | |||
| 2629d15e2f | |||
| c7b62d3202 | |||
| 85ec9a84c4 |
4
.github/workflows/main.yml
vendored
4
.github/workflows/main.yml
vendored
@@ -24,8 +24,8 @@ jobs:
|
||||
- name: prepare-linux
|
||||
if: runner.os == 'Linux'
|
||||
run: |
|
||||
sudo apt-get update
|
||||
sudo apt-get install libsdl2-dev
|
||||
sudo apt update
|
||||
sudo apt install libgtk-3-dev libsdl2-dev
|
||||
- name: prepare-windows
|
||||
if: runner.os == 'Windows'
|
||||
run: |
|
||||
|
||||
@@ -2,6 +2,8 @@
|
||||
|
||||
A Game Boy Advance Emulator written in Zig ⚡!
|
||||
|
||||

|
||||
|
||||
## Scope
|
||||
|
||||
I'm hardly the first to write a Game Boy Advance Emulator nor will I be the last. This project isn't going to compete with the GOATs like [mGBA](https://github.com/mgba-emu) or [NanoBoyAdvance](https://github.com/nba-emu/NanoBoyAdvance). There aren't any interesting ideas either like in [DSHBA](https://github.com/DenSinH/DSHBA).
|
||||
@@ -13,14 +15,13 @@ This is a simple (read: incomplete) for-fun long-term project. I hope to get "mo
|
||||
- [x] Affine Sprites
|
||||
- [ ] Windowing (see [this branch](https://git.musuka.dev/paoda/zba/src/branch/window))
|
||||
- [ ] Audio Resampler (Having issues with SDL2's)
|
||||
- [x] Immediate Mode GUI (see [this branch](https://git.musuka.dev/paoda/zba/src/branch/imgui))
|
||||
- [ ] Refactoring for easy-ish perf boosts
|
||||
|
||||
## Usage
|
||||
|
||||
As it currently exists, ZBA is run from the terminal. In your console of choice, type `./zba --help` to see what you can do.
|
||||
|
||||
I typically find myself typing `./zba -b ./bin/bios.bin ./bin/test/suite.gba` to see how badly my "cool new feature" broke everything else.
|
||||
I typically find myself typing `./zba -b ./bin/bios.bin` and then going to File -> Insert ROM to load the title of my choice.
|
||||
|
||||
Need a BIOS? Why not try using the open-source [Cult-Of-GBA BIOS](https://github.com/Cult-of-GBA/BIOS) written by [fleroviux](https://github.com/fleroviux) and [DenSinH](https://github.com/DenSinH)?
|
||||
|
||||
@@ -77,7 +78,7 @@ arm7wrestler GBA Fixed | [destoer](https://github.com/destoer)
|
||||
|
||||
## Compiling
|
||||
|
||||
Most recently built on Zig [v0.11.0-dev.1580+a5b34a61a](https://github.com/ziglang/zig/tree/a5b34a61a)
|
||||
Most recently built on Zig [v0.11.0-dev.2168+322ace70f](https://github.com/ziglang/zig/tree/322ace70f)
|
||||
|
||||
### Dependencies
|
||||
|
||||
|
||||
BIN
assets/screenshot.png
Normal file
BIN
assets/screenshot.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 34 KiB |
14
build.zig
14
build.zig
@@ -6,9 +6,9 @@ const gdbstub = @import("lib/zba-gdbstub/build.zig");
|
||||
const zgui = @import("lib/zgui/build.zig");
|
||||
const nfd = @import("lib/nfd-zig/build.zig");
|
||||
|
||||
pub fn build(b: *std.build.Builder) void {
|
||||
pub fn build(b: *std.Build) void {
|
||||
// Minimum Zig Version
|
||||
const min_ver = std.SemanticVersion.parse("0.11.0-dev.1580+a5b34a61a") catch return; // https://github.com/ziglang/zig/commit/a5b34a61a
|
||||
const min_ver = std.SemanticVersion.parse("0.11.0-dev.2168+322ace70f") catch return; // https://github.com/ziglang/zig/commit/322ace70f
|
||||
if (builtin.zig_version.order(min_ver).compare(.lt)) {
|
||||
std.log.err("{s}", .{b.fmt("Zig v{} does not meet the minimum version requirement. (Zig v{})", .{ builtin.zig_version, min_ver })});
|
||||
std.os.exit(1);
|
||||
@@ -48,7 +48,8 @@ pub fn build(b: *std.build.Builder) void {
|
||||
exe.addAnonymousModule("zba-util", .{ .source_file = .{ .path = "lib/zba-util/src/lib.zig" } });
|
||||
|
||||
// gdbstub
|
||||
gdbstub.link(exe);
|
||||
exe.addModule("gdbstub", gdbstub.getModule(b));
|
||||
|
||||
// NativeFileDialog(ue) Bindings
|
||||
exe.linkLibrary(nfd.makeLib(b, target, optimize));
|
||||
exe.addModule("nfd", nfd.getModule(b));
|
||||
@@ -59,9 +60,10 @@ pub fn build(b: *std.build.Builder) void {
|
||||
exe.addModule("sdl2", sdk.getNativeModule());
|
||||
|
||||
// Dear ImGui bindings
|
||||
const zgui_pkg = zgui.package(b, .{ .options = .{ .backend = .sdl2_opengl3 } });
|
||||
exe.addModule("zgui", zgui_pkg.module);
|
||||
zgui.link(exe, zgui_pkg.options);
|
||||
|
||||
// .shared option should stay in sync with SDL.zig call above where true == .dynamic, and false == .static
|
||||
const zgui_pkg = zgui.package(b, target, optimize, .{ .options = .{ .backend = .sdl2_opengl3, .shared = true } });
|
||||
zgui_pkg.link(exe);
|
||||
|
||||
exe.install();
|
||||
|
||||
|
||||
Submodule lib/SDL.zig updated: 59c90e8ea2...cc3b023f50
Submodule lib/known-folders updated: 53fe3b676f...d13ba61370
Submodule lib/zba-gdbstub updated: acb59994fc...215e053b9a
2
lib/zgui
2
lib/zgui
Submodule lib/zgui updated: 12e480f30d...5b2b64a9de
Submodule lib/zig-clap updated: 861de651f3...6310cbd576
Submodule lib/zig-datetime updated: bf0ae0c27c...b570d61187
@@ -179,23 +179,30 @@ pub fn write(self: *Self, comptime T: type, word_count: u16, address: u32, value
|
||||
}
|
||||
}
|
||||
|
||||
pub fn init(allocator: Allocator, cpu: *Arm7tdmi, rom_path: []const u8, save_path: ?[]const u8) !Self {
|
||||
const file = try std.fs.cwd().openFile(rom_path, .{});
|
||||
pub fn init(allocator: Allocator, cpu: *Arm7tdmi, maybe_rom: ?[]const u8, maybe_save: ?[]const u8) !Self {
|
||||
const Device = Gpio.Device;
|
||||
|
||||
const items: struct { []u8, [12]u8, Backup.Kind, Device.Kind } = if (maybe_rom) |file_path| blk: {
|
||||
const file = try std.fs.cwd().openFile(file_path, .{});
|
||||
defer file.close();
|
||||
|
||||
const file_buf = try file.readToEndAlloc(allocator, try file.getEndPos());
|
||||
const title = file_buf[0xA0..0xAC].*;
|
||||
const kind = Backup.guess(file_buf);
|
||||
const device = if (config.config().guest.force_rtc) .Rtc else guessDevice(file_buf);
|
||||
const buffer = try file.readToEndAlloc(allocator, try file.getEndPos());
|
||||
const title = buffer[0xA0..0xAC];
|
||||
logHeader(buffer, title);
|
||||
|
||||
logHeader(file_buf, title);
|
||||
const device_kind = if (config.config().guest.force_rtc) .Rtc else guessDevice(buffer);
|
||||
|
||||
break :blk .{ buffer, title.*, Backup.guess(buffer), device_kind };
|
||||
} else .{ try allocator.alloc(u8, 0), [_]u8{0} ** 12, .None, .None };
|
||||
|
||||
const title = items[1];
|
||||
|
||||
return .{
|
||||
.buf = file_buf,
|
||||
.buf = items[0],
|
||||
.allocator = allocator,
|
||||
.title = title,
|
||||
.backup = try Backup.init(allocator, kind, title, save_path),
|
||||
.gpio = try Gpio.init(allocator, cpu, device),
|
||||
.backup = try Backup.init(allocator, items[2], title, maybe_save),
|
||||
.gpio = try Gpio.init(allocator, cpu, items[3]),
|
||||
};
|
||||
}
|
||||
|
||||
@@ -223,11 +230,11 @@ fn guessDevice(buf: []const u8) Gpio.Device.Kind {
|
||||
return .None;
|
||||
}
|
||||
|
||||
fn logHeader(buf: []const u8, title: [12]u8) void {
|
||||
const ver = buf[0xBC];
|
||||
fn logHeader(buf: []const u8, title: *const [12]u8) void {
|
||||
const version = buf[0xBC];
|
||||
|
||||
log.info("Title: {s}", .{title});
|
||||
if (ver != 0) log.info("Version: {}", .{ver});
|
||||
if (version != 0) log.info("Version: {}", .{version});
|
||||
|
||||
log.info("Game Code: {s}", .{buf[0xAC..0xB0]});
|
||||
log.info("Maker Code: {s}", .{buf[0xB0..0xB2]});
|
||||
|
||||
@@ -32,7 +32,7 @@ pub const Backup = struct {
|
||||
flash: Flash,
|
||||
eeprom: Eeprom,
|
||||
|
||||
const Kind = enum {
|
||||
pub const Kind = enum {
|
||||
Eeprom,
|
||||
Sram,
|
||||
Flash,
|
||||
@@ -205,6 +205,10 @@ pub const Backup = struct {
|
||||
const file_path = try self.savePath(allocator, path);
|
||||
defer allocator.free(file_path);
|
||||
|
||||
// FIXME: communicate edge case to the user?
|
||||
if (std.mem.eql(u8, &self.title, "ACE LIGHTNIN"))
|
||||
return;
|
||||
|
||||
switch (self.kind) {
|
||||
.Sram, .Flash, .Flash1M, .Eeprom => {
|
||||
const file = try std.fs.createFileAbsolute(file_path, .{});
|
||||
|
||||
@@ -21,30 +21,27 @@ const gba_height = @import("core/ppu.zig").height;
|
||||
|
||||
const log = std.log.scoped(.Imgui);
|
||||
|
||||
// TODO: Document how I decided on this value (I forgot 😅)
|
||||
const histogram_len = 0x400;
|
||||
// two seconds worth of fps values into the past
|
||||
const histogram_len = 0x80;
|
||||
|
||||
/// Immediate-Mode GUI State
|
||||
pub const State = struct {
|
||||
title: [:0]const u8,
|
||||
title: [12:0]u8,
|
||||
|
||||
fps_hist: RingBuffer(u32),
|
||||
should_quit: bool = false,
|
||||
|
||||
pub fn init(allocator: Allocator, title: [12]u8) !@This() {
|
||||
/// if zba is initialized with a ROM already provided, this initializer should be called
|
||||
/// with `title_opt` being non-null
|
||||
pub fn init(allocator: Allocator, title_opt: ?*const [12]u8) !@This() {
|
||||
const history = try allocator.alloc(u32, histogram_len);
|
||||
const without_null = std.mem.sliceTo(&title, 0);
|
||||
|
||||
return .{
|
||||
.title = try allocator.dupeZ(u8, without_null),
|
||||
.fps_hist = RingBuffer(u32).init(history),
|
||||
};
|
||||
const title: [12:0]u8 = if (title_opt) |t| t.* ++ [_:0]u8{} else "[No Title]\x00\x00".*;
|
||||
return .{ .title = title, .fps_hist = RingBuffer(u32).init(history) };
|
||||
}
|
||||
|
||||
pub fn deinit(self: *@This(), allocator: Allocator) void {
|
||||
allocator.free(self.title);
|
||||
allocator.free(self.fps_hist.buf);
|
||||
|
||||
self.* = undefined;
|
||||
}
|
||||
};
|
||||
@@ -60,21 +57,26 @@ pub fn draw(state: *State, tex_id: GLuint, cpu: *Arm7tdmi) void {
|
||||
defer zgui.endMenu();
|
||||
|
||||
if (zgui.menuItem("Quit", .{})) state.should_quit = true;
|
||||
|
||||
if (zgui.menuItem("Insert ROM", .{})) blk: {
|
||||
const maybe_path = nfd.openFileDialog("gba", null) catch |e| {
|
||||
log.err("failed to open file dialog: {?}", .{e});
|
||||
log.err("failed to open file dialog: {}", .{e});
|
||||
break :blk;
|
||||
};
|
||||
|
||||
if (maybe_path) |file_path| {
|
||||
const file_path = maybe_path orelse {
|
||||
log.warn("did not receive a file path", .{});
|
||||
break :blk;
|
||||
};
|
||||
defer nfd.freePath(file_path);
|
||||
log.info("user chose: \"{s}\"", .{file_path});
|
||||
|
||||
log.info("user chose: \"{s}\"", .{file_path});
|
||||
emu.replaceGamepak(cpu, file_path) catch |e| {
|
||||
log.err("failed to replace GamePak: {}", .{e});
|
||||
break :blk;
|
||||
};
|
||||
}
|
||||
|
||||
state.title = cpu.bus.pak.title ++ [_:0]u8{};
|
||||
}
|
||||
}
|
||||
|
||||
@@ -91,7 +93,7 @@ pub fn draw(state: *State, tex_id: GLuint, cpu: *Arm7tdmi) void {
|
||||
const w = @intToFloat(f32, gba_width * win_scale);
|
||||
const h = @intToFloat(f32, gba_height * win_scale);
|
||||
|
||||
const window_title = if (state.title.len != 0) state.title else "[No Title]";
|
||||
const window_title = std.mem.sliceTo(&state.title, 0);
|
||||
_ = zgui.begin(window_title, .{ .flags = .{ .no_resize = true, .always_auto_resize = true } });
|
||||
defer zgui.end();
|
||||
|
||||
@@ -127,7 +129,7 @@ pub fn draw(state: *State, tex_id: GLuint, cpu: *Arm7tdmi) void {
|
||||
defer zgui.end();
|
||||
|
||||
const tmp = blk: {
|
||||
var buf: [0x400]u32 = undefined;
|
||||
var buf: [histogram_len]u32 = undefined;
|
||||
const len = state.fps_hist.copy(&buf);
|
||||
|
||||
break :blk .{ buf, len };
|
||||
|
||||
10
src/main.zig
10
src/main.zig
@@ -90,8 +90,10 @@ pub fn main() void {
|
||||
cpu.fastBoot();
|
||||
}
|
||||
|
||||
const title_ptr = if (paths.rom != null) &bus.pak.title else null;
|
||||
|
||||
// TODO: Just copy the title instead of grabbing a pointer to it
|
||||
var gui = Gui.init(allocator, bus.pak.title, &bus.apu) catch |e| exitln("failed to init gui: {}", .{e});
|
||||
var gui = Gui.init(allocator, &bus.apu, title_ptr) catch |e| exitln("failed to init gui: {}", .{e});
|
||||
defer gui.deinit();
|
||||
|
||||
var quit = std.atomic.Atomic(bool).init(false);
|
||||
@@ -139,7 +141,7 @@ pub fn main() void {
|
||||
|
||||
fn handleArguments(allocator: Allocator, data_path: []const u8, result: *const clap.Result(clap.Help, ¶ms, clap.parsers.default)) !FilePaths {
|
||||
const rom_path = romPath(result);
|
||||
log.info("ROM path: {s}", .{rom_path});
|
||||
log.info("ROM path: {?s}", .{rom_path});
|
||||
|
||||
const bios_path = result.args.bios;
|
||||
if (bios_path) |path| log.info("BIOS path: {s}", .{path}) else log.warn("No BIOS provided", .{});
|
||||
@@ -188,10 +190,10 @@ fn ensureConfigDirExists(config_path: []const u8) !void {
|
||||
try dir.makePath("zba");
|
||||
}
|
||||
|
||||
fn romPath(result: *const clap.Result(clap.Help, ¶ms, clap.parsers.default)) []const u8 {
|
||||
fn romPath(result: *const clap.Result(clap.Help, ¶ms, clap.parsers.default)) ?[]const u8 {
|
||||
return switch (result.positionals.len) {
|
||||
0 => null,
|
||||
1 => result.positionals[0],
|
||||
0 => exitln("ZBA requires a path to a GamePak ROM", .{}),
|
||||
else => exitln("ZBA received too many positional arguments.", .{}),
|
||||
};
|
||||
}
|
||||
|
||||
@@ -57,7 +57,7 @@ pub const Gui = struct {
|
||||
allocator: Allocator,
|
||||
program_id: gl.GLuint,
|
||||
|
||||
pub fn init(allocator: Allocator, title: [12]u8, apu: *Apu) !Self {
|
||||
pub fn init(allocator: Allocator, apu: *Apu, title_opt: ?*const [12]u8) !Self {
|
||||
if (SDL.SDL_Init(SDL.SDL_INIT_VIDEO | SDL.SDL_INIT_EVENTS | SDL.SDL_INIT_AUDIO) < 0) panic();
|
||||
if (SDL.SDL_GL_SetAttribute(SDL.SDL_GL_CONTEXT_PROFILE_MASK, SDL.SDL_GL_CONTEXT_PROFILE_CORE) < 0) panic();
|
||||
if (SDL.SDL_GL_SetAttribute(SDL.SDL_GL_CONTEXT_MAJOR_VERSION, 3) < 0) panic();
|
||||
@@ -91,7 +91,7 @@ pub const Gui = struct {
|
||||
.audio = Audio.init(apu),
|
||||
|
||||
.allocator = allocator,
|
||||
.state = try imgui.State.init(allocator, title),
|
||||
.state = try imgui.State.init(allocator, title_opt),
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -50,7 +50,7 @@ pub fn escape(title: [12]u8) [12]u8 {
|
||||
}
|
||||
|
||||
pub const FilePaths = struct {
|
||||
rom: []const u8,
|
||||
rom: ?[]const u8,
|
||||
bios: ?[]const u8,
|
||||
save: ?[]const u8,
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user