Compare commits
18 Commits
72b702cb21
...
april-fool
| Author | SHA1 | Date | |
|---|---|---|---|
| 9844d657b0 | |||
| 1d8b21d6b4 | |||
| b879c76510 | |||
| 0dbba2fb9a | |||
| 49b0620c48 | |||
| a6a9e3ac72 | |||
| aeefff86f8 | |||
| 91aa98eef7 | |||
| f3b6c4f3fe | |||
| 5aa5ac2a8b | |||
| b1827ccea0 | |||
| 2629d15e2f | |||
| c7b62d3202 | |||
| 85ec9a84c4 | |||
| 5adbc354d6 | |||
| f8477714ae | |||
| bd872ee1c0 | |||
| 11eae091db |
4
.github/workflows/main.yml
vendored
4
.github/workflows/main.yml
vendored
@@ -24,8 +24,8 @@ jobs:
|
|||||||
- name: prepare-linux
|
- name: prepare-linux
|
||||||
if: runner.os == 'Linux'
|
if: runner.os == 'Linux'
|
||||||
run: |
|
run: |
|
||||||
sudo apt-get update
|
sudo apt update
|
||||||
sudo apt-get install libsdl2-dev
|
sudo apt install libgtk-3-dev libsdl2-dev
|
||||||
- name: prepare-windows
|
- name: prepare-windows
|
||||||
if: runner.os == 'Windows'
|
if: runner.os == 'Windows'
|
||||||
run: |
|
run: |
|
||||||
|
|||||||
3
.gitmodules
vendored
3
.gitmodules
vendored
@@ -22,3 +22,6 @@
|
|||||||
[submodule "lib/nfd-zig"]
|
[submodule "lib/nfd-zig"]
|
||||||
path = lib/nfd-zig
|
path = lib/nfd-zig
|
||||||
url = https://github.com/fabioarnold/nfd-zig
|
url = https://github.com/fabioarnold/nfd-zig
|
||||||
|
[submodule "lib/zba-util"]
|
||||||
|
path = lib/zba-util
|
||||||
|
url = https://git.musuka.dev/paoda/zba-util.git
|
||||||
|
|||||||
@@ -2,6 +2,8 @@
|
|||||||
|
|
||||||
A Game Boy Advance Emulator written in Zig ⚡!
|
A Game Boy Advance Emulator written in Zig ⚡!
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
## Scope
|
## 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).
|
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
|
- [x] Affine Sprites
|
||||||
- [ ] Windowing (see [this branch](https://git.musuka.dev/paoda/zba/src/branch/window))
|
- [ ] Windowing (see [this branch](https://git.musuka.dev/paoda/zba/src/branch/window))
|
||||||
- [ ] Audio Resampler (Having issues with SDL2's)
|
- [ ] 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
|
- [ ] Refactoring for easy-ish perf boosts
|
||||||
|
|
||||||
## Usage
|
## 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.
|
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)?
|
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
|
## 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
|
### Dependencies
|
||||||
|
|
||||||
|
|||||||
BIN
assets/screenshot.png
Normal file
BIN
assets/screenshot.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 34 KiB |
19
build.zig
19
build.zig
@@ -6,9 +6,9 @@ const gdbstub = @import("lib/zba-gdbstub/build.zig");
|
|||||||
const zgui = @import("lib/zgui/build.zig");
|
const zgui = @import("lib/zgui/build.zig");
|
||||||
const nfd = @import("lib/nfd-zig/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
|
// 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)) {
|
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.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);
|
std.os.exit(1);
|
||||||
@@ -33,7 +33,7 @@ pub fn build(b: *std.build.Builder) void {
|
|||||||
exe.addAnonymousModule("datetime", .{ .source_file = .{ .path = "lib/zig-datetime/src/main.zig" } });
|
exe.addAnonymousModule("datetime", .{ .source_file = .{ .path = "lib/zig-datetime/src/main.zig" } });
|
||||||
|
|
||||||
// Bitfield type from FlorenceOS: https://github.com/FlorenceOS/
|
// Bitfield type from FlorenceOS: https://github.com/FlorenceOS/
|
||||||
exe.addAnonymousModule("bitfield", .{ .source_file = .{ .path = "lib/util/bitfield.zig" } });
|
exe.addAnonymousModule("bitfield", .{ .source_file = .{ .path = "lib/bitfield.zig" } });
|
||||||
|
|
||||||
// Argument Parsing Library
|
// Argument Parsing Library
|
||||||
exe.addAnonymousModule("clap", .{ .source_file = .{ .path = "lib/zig-clap/clap.zig" } });
|
exe.addAnonymousModule("clap", .{ .source_file = .{ .path = "lib/zig-clap/clap.zig" } });
|
||||||
@@ -44,8 +44,12 @@ pub fn build(b: *std.build.Builder) void {
|
|||||||
// OpenGL 3.3 Bindings
|
// OpenGL 3.3 Bindings
|
||||||
exe.addAnonymousModule("gl", .{ .source_file = .{ .path = "lib/gl.zig" } });
|
exe.addAnonymousModule("gl", .{ .source_file = .{ .path = "lib/gl.zig" } });
|
||||||
|
|
||||||
|
// ZBA utility code
|
||||||
|
exe.addAnonymousModule("zba-util", .{ .source_file = .{ .path = "lib/zba-util/src/lib.zig" } });
|
||||||
|
|
||||||
// gdbstub
|
// gdbstub
|
||||||
gdbstub.link(exe);
|
exe.addModule("gdbstub", gdbstub.getModule(b));
|
||||||
|
|
||||||
// NativeFileDialog(ue) Bindings
|
// NativeFileDialog(ue) Bindings
|
||||||
exe.linkLibrary(nfd.makeLib(b, target, optimize));
|
exe.linkLibrary(nfd.makeLib(b, target, optimize));
|
||||||
exe.addModule("nfd", nfd.getModule(b));
|
exe.addModule("nfd", nfd.getModule(b));
|
||||||
@@ -56,9 +60,10 @@ pub fn build(b: *std.build.Builder) void {
|
|||||||
exe.addModule("sdl2", sdk.getNativeModule());
|
exe.addModule("sdl2", sdk.getNativeModule());
|
||||||
|
|
||||||
// Dear ImGui bindings
|
// Dear ImGui bindings
|
||||||
const zgui_pkg = zgui.package(b, .{ .options = .{ .backend = .sdl2_opengl3 } });
|
|
||||||
exe.addModule("zgui", zgui_pkg.module);
|
// .shared option should stay in sync with SDL.zig call above where true == .dynamic, and false == .static
|
||||||
zgui.link(exe, zgui_pkg.options);
|
const zgui_pkg = zgui.package(b, target, optimize, .{ .options = .{ .backend = .sdl2_opengl3, .shared = true } });
|
||||||
|
zgui_pkg.link(exe);
|
||||||
|
|
||||||
exe.install();
|
exe.install();
|
||||||
|
|
||||||
|
|||||||
Submodule lib/SDL.zig updated: 59c90e8ea2...cc3b023f50
Submodule lib/known-folders updated: 53fe3b676f...d13ba61370
Submodule lib/zba-gdbstub updated: acb59994fc...215e053b9a
1
lib/zba-util
Submodule
1
lib/zba-util
Submodule
Submodule lib/zba-util added at d5e66caf21
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
@@ -19,7 +19,7 @@ const log = std.log.scoped(.Bus);
|
|||||||
|
|
||||||
const createDmaTuple = @import("bus/dma.zig").create;
|
const createDmaTuple = @import("bus/dma.zig").create;
|
||||||
const createTimerTuple = @import("bus/timer.zig").create;
|
const createTimerTuple = @import("bus/timer.zig").create;
|
||||||
const rotr = @import("../util.zig").rotr;
|
const rotr = @import("zba-util").rotr;
|
||||||
|
|
||||||
const timings: [2][0x10]u8 = [_][0x10]u8{
|
const timings: [2][0x10]u8 = [_][0x10]u8{
|
||||||
// BIOS, Unused, EWRAM, IWRAM, I/0, PALRAM, VRAM, OAM, ROM0, ROM0, ROM1, ROM1, ROM2, ROM2, SRAM, Unused
|
// BIOS, Unused, EWRAM, IWRAM, I/0, PALRAM, VRAM, OAM, ROM0, ROM0, ROM1, ROM1, ROM2, ROM2, SRAM, Unused
|
||||||
@@ -104,7 +104,7 @@ pub fn deinit(self: *Self) void {
|
|||||||
|
|
||||||
pub fn reset(self: *Self) void {
|
pub fn reset(self: *Self) void {
|
||||||
self.bios.reset();
|
self.bios.reset();
|
||||||
// TODO: deinit ppu
|
self.ppu.reset();
|
||||||
self.apu.reset();
|
self.apu.reset();
|
||||||
self.iwram.reset();
|
self.iwram.reset();
|
||||||
self.ewram.reset();
|
self.ewram.reset();
|
||||||
@@ -128,6 +128,21 @@ pub fn reset(self: *Self) void {
|
|||||||
self.io.reset();
|
self.io.reset();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn replaceGamepak(self: *Self, file_path: []const u8) !void {
|
||||||
|
// Note: `save_path` isn't owned by `Backup`
|
||||||
|
const save_path = self.pak.backup.save_path;
|
||||||
|
self.pak.deinit();
|
||||||
|
|
||||||
|
self.pak = try GamePak.init(self.allocator, self.cpu, file_path, save_path);
|
||||||
|
|
||||||
|
const read_ptr: *[table_len]?*const anyopaque = @constCast(self.read_table);
|
||||||
|
const write_ptrs: [2]*[table_len]?*anyopaque = .{ @constCast(self.write_tables[0]), @constCast(self.write_tables[1]) };
|
||||||
|
|
||||||
|
self.fillReadTable(read_ptr);
|
||||||
|
self.fillWriteTable(u32, write_ptrs[0]);
|
||||||
|
self.fillWriteTable(u8, write_ptrs[1]);
|
||||||
|
}
|
||||||
|
|
||||||
fn fillReadTable(self: *Self, table: *[table_len]?*const anyopaque) void {
|
fn fillReadTable(self: *Self, table: *[table_len]?*const anyopaque) void {
|
||||||
const vramMirror = @import("ppu/Vram.zig").mirror;
|
const vramMirror = @import("ppu/Vram.zig").mirror;
|
||||||
|
|
||||||
|
|||||||
@@ -14,7 +14,6 @@ const SoundFifo = std.fifo.LinearFifo(u8, .{ .Static = 0x20 });
|
|||||||
|
|
||||||
const getHalf = util.getHalf;
|
const getHalf = util.getHalf;
|
||||||
const setHalf = util.setHalf;
|
const setHalf = util.setHalf;
|
||||||
const intToBytes = util.intToBytes;
|
|
||||||
|
|
||||||
const log = std.log.scoped(.APU);
|
const log = std.log.scoped(.APU);
|
||||||
|
|
||||||
@@ -279,16 +278,20 @@ pub const Apu = struct {
|
|||||||
.is_buffer_full = false,
|
.is_buffer_full = false,
|
||||||
};
|
};
|
||||||
|
|
||||||
sched.push(.SampleAudio, apu.interval());
|
Self.initEvents(apu.sched, apu.interval());
|
||||||
sched.push(.{ .ApuChannel = 0 }, @import("apu/signal/Square.zig").interval);
|
|
||||||
sched.push(.{ .ApuChannel = 1 }, @import("apu/signal/Square.zig").interval);
|
|
||||||
sched.push(.{ .ApuChannel = 2 }, @import("apu/signal/Wave.zig").interval);
|
|
||||||
sched.push(.{ .ApuChannel = 3 }, @import("apu/signal/Lfsr.zig").interval);
|
|
||||||
sched.push(.FrameSequencer, FrameSequencer.interval);
|
|
||||||
|
|
||||||
return apu;
|
return apu;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn initEvents(scheduler: *Scheduler, apu_interval: u64) void {
|
||||||
|
scheduler.push(.SampleAudio, apu_interval);
|
||||||
|
scheduler.push(.{ .ApuChannel = 0 }, @import("apu/signal/Square.zig").interval);
|
||||||
|
scheduler.push(.{ .ApuChannel = 1 }, @import("apu/signal/Square.zig").interval);
|
||||||
|
scheduler.push(.{ .ApuChannel = 2 }, @import("apu/signal/Wave.zig").interval);
|
||||||
|
scheduler.push(.{ .ApuChannel = 3 }, @import("apu/signal/Lfsr.zig").interval);
|
||||||
|
scheduler.push(.FrameSequencer, FrameSequencer.interval);
|
||||||
|
}
|
||||||
|
|
||||||
/// Used when resetting the emulator
|
/// Used when resetting the emulator
|
||||||
pub fn reset(self: *Self) void {
|
pub fn reset(self: *Self) void {
|
||||||
// FIXME: These reset functions are meant to emulate obscure APU behaviour. Write proper emu reset fns
|
// FIXME: These reset functions are meant to emulate obscure APU behaviour. Write proper emu reset fns
|
||||||
@@ -307,6 +310,8 @@ pub const Apu = struct {
|
|||||||
|
|
||||||
self.sampling_cycle = 0;
|
self.sampling_cycle = 0;
|
||||||
self.fs.reset();
|
self.fs.reset();
|
||||||
|
|
||||||
|
Self.initEvents(self.sched, self.interval());
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Emulates the reset behaviour of the APU
|
/// Emulates the reset behaviour of the APU
|
||||||
@@ -557,7 +562,7 @@ pub fn DmaSound(comptime kind: DmaSoundKind) type {
|
|||||||
pub fn push(self: *Self, value: u32) void {
|
pub fn push(self: *Self, value: u32) void {
|
||||||
if (!self.enabled) self.enable();
|
if (!self.enabled) self.enable();
|
||||||
|
|
||||||
self.fifo.write(&intToBytes(u32, value)) catch |e| log.err("{} Error: {}", .{ kind, e });
|
self.fifo.write(std.mem.asBytes(&value)) catch |e| log.err("{} Error: {}", .{ kind, e });
|
||||||
}
|
}
|
||||||
|
|
||||||
fn enable(self: *Self) void {
|
fn enable(self: *Self) void {
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ const std = @import("std");
|
|||||||
const Allocator = std.mem.Allocator;
|
const Allocator = std.mem.Allocator;
|
||||||
const log = std.log.scoped(.Bios);
|
const log = std.log.scoped(.Bios);
|
||||||
|
|
||||||
const rotr = @import("../../util.zig").rotr;
|
const rotr = @import("zba-util").rotr;
|
||||||
const forceAlign = @import("../Bus.zig").forceAlign;
|
const forceAlign = @import("../Bus.zig").forceAlign;
|
||||||
|
|
||||||
/// Size of the BIOS in bytes
|
/// Size of the BIOS in bytes
|
||||||
|
|||||||
@@ -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 {
|
pub fn init(allocator: Allocator, cpu: *Arm7tdmi, maybe_rom: ?[]const u8, maybe_save: ?[]const u8) !Self {
|
||||||
const file = try std.fs.cwd().openFile(rom_path, .{});
|
const Device = Gpio.Device;
|
||||||
defer file.close();
|
|
||||||
|
|
||||||
const file_buf = try file.readToEndAlloc(allocator, try file.getEndPos());
|
const items: struct { []u8, [12]u8, Backup.Kind, Device.Kind } = if (maybe_rom) |file_path| blk: {
|
||||||
const title = file_buf[0xA0..0xAC].*;
|
const file = try std.fs.cwd().openFile(file_path, .{});
|
||||||
const kind = Backup.guess(file_buf);
|
defer file.close();
|
||||||
const device = if (config.config().guest.force_rtc) .Rtc else guessDevice(file_buf);
|
|
||||||
|
|
||||||
logHeader(file_buf, title);
|
const buffer = try file.readToEndAlloc(allocator, try file.getEndPos());
|
||||||
|
const title = buffer[0xA0..0xAC];
|
||||||
|
logHeader(buffer, 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 .{
|
return .{
|
||||||
.buf = file_buf,
|
.buf = items[0],
|
||||||
.allocator = allocator,
|
.allocator = allocator,
|
||||||
.title = title,
|
.title = title,
|
||||||
.backup = try Backup.init(allocator, kind, title, save_path),
|
.backup = try Backup.init(allocator, items[2], title, maybe_save),
|
||||||
.gpio = try Gpio.init(allocator, cpu, device),
|
.gpio = try Gpio.init(allocator, cpu, items[3]),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -223,11 +230,11 @@ fn guessDevice(buf: []const u8) Gpio.Device.Kind {
|
|||||||
return .None;
|
return .None;
|
||||||
}
|
}
|
||||||
|
|
||||||
fn logHeader(buf: []const u8, title: [12]u8) void {
|
fn logHeader(buf: []const u8, title: *const [12]u8) void {
|
||||||
const ver = buf[0xBC];
|
const version = buf[0xBC];
|
||||||
|
|
||||||
log.info("Title: {s}", .{title});
|
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("Game Code: {s}", .{buf[0xAC..0xB0]});
|
||||||
log.info("Maker Code: {s}", .{buf[0xB0..0xB2]});
|
log.info("Maker Code: {s}", .{buf[0xB0..0xB2]});
|
||||||
|
|||||||
@@ -32,7 +32,7 @@ pub const Backup = struct {
|
|||||||
flash: Flash,
|
flash: Flash,
|
||||||
eeprom: Eeprom,
|
eeprom: Eeprom,
|
||||||
|
|
||||||
const Kind = enum {
|
pub const Kind = enum {
|
||||||
Eeprom,
|
Eeprom,
|
||||||
Sram,
|
Sram,
|
||||||
Flash,
|
Flash,
|
||||||
@@ -205,6 +205,10 @@ pub const Backup = struct {
|
|||||||
const file_path = try self.savePath(allocator, path);
|
const file_path = try self.savePath(allocator, path);
|
||||||
defer allocator.free(file_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) {
|
switch (self.kind) {
|
||||||
.Sram, .Flash, .Flash1M, .Eeprom => {
|
.Sram, .Flash, .Flash1M, .Eeprom => {
|
||||||
const file = try std.fs.createFileAbsolute(file_path, .{});
|
const file = try std.fs.createFileAbsolute(file_path, .{});
|
||||||
|
|||||||
@@ -12,7 +12,7 @@ const getHalf = util.getHalf;
|
|||||||
const setHalf = util.setHalf;
|
const setHalf = util.setHalf;
|
||||||
const setQuart = util.setQuart;
|
const setQuart = util.setQuart;
|
||||||
|
|
||||||
const rotr = @import("../../util.zig").rotr;
|
const rotr = @import("zba-util").rotr;
|
||||||
|
|
||||||
pub fn create() DmaTuple {
|
pub fn create() DmaTuple {
|
||||||
return .{ DmaController(0).init(), DmaController(1).init(), DmaController(2).init(), DmaController(3).init() };
|
return .{ DmaController(0).init(), DmaController(1).init(), DmaController(2).init(), DmaController(3).init() };
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ const Bus = @import("../../Bus.zig");
|
|||||||
const Arm7tdmi = @import("../../cpu.zig").Arm7tdmi;
|
const Arm7tdmi = @import("../../cpu.zig").Arm7tdmi;
|
||||||
const InstrFn = @import("../../cpu.zig").arm.InstrFn;
|
const InstrFn = @import("../../cpu.zig").arm.InstrFn;
|
||||||
|
|
||||||
const sext = @import("../../../util.zig").sext;
|
const sext = @import("zba-util").sext;
|
||||||
|
|
||||||
pub fn branch(comptime L: bool) InstrFn {
|
pub fn branch(comptime L: bool) InstrFn {
|
||||||
return struct {
|
return struct {
|
||||||
|
|||||||
@@ -2,8 +2,8 @@ const Bus = @import("../../Bus.zig");
|
|||||||
const Arm7tdmi = @import("../../cpu.zig").Arm7tdmi;
|
const Arm7tdmi = @import("../../cpu.zig").Arm7tdmi;
|
||||||
const InstrFn = @import("../../cpu.zig").arm.InstrFn;
|
const InstrFn = @import("../../cpu.zig").arm.InstrFn;
|
||||||
|
|
||||||
const sext = @import("../../../util.zig").sext;
|
const sext = @import("zba-util").sext;
|
||||||
const rotr = @import("../../../util.zig").rotr;
|
const rotr = @import("zba-util").rotr;
|
||||||
|
|
||||||
pub fn halfAndSignedDataTransfer(comptime P: bool, comptime U: bool, comptime I: bool, comptime W: bool, comptime L: bool) InstrFn {
|
pub fn halfAndSignedDataTransfer(comptime P: bool, comptime U: bool, comptime I: bool, comptime W: bool, comptime L: bool) InstrFn {
|
||||||
return struct {
|
return struct {
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ const PSR = @import("../../cpu.zig").PSR;
|
|||||||
|
|
||||||
const log = std.log.scoped(.PsrTransfer);
|
const log = std.log.scoped(.PsrTransfer);
|
||||||
|
|
||||||
const rotr = @import("../../../util.zig").rotr;
|
const rotr = @import("zba-util").rotr;
|
||||||
|
|
||||||
pub fn psrTransfer(comptime I: bool, comptime R: bool, comptime kind: u2) InstrFn {
|
pub fn psrTransfer(comptime I: bool, comptime R: bool, comptime kind: u2) InstrFn {
|
||||||
return struct {
|
return struct {
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ const Bus = @import("../../Bus.zig");
|
|||||||
const Arm7tdmi = @import("../../cpu.zig").Arm7tdmi;
|
const Arm7tdmi = @import("../../cpu.zig").Arm7tdmi;
|
||||||
const InstrFn = @import("../../cpu.zig").arm.InstrFn;
|
const InstrFn = @import("../../cpu.zig").arm.InstrFn;
|
||||||
|
|
||||||
const rotr = @import("../../../util.zig").rotr;
|
const rotr = @import("zba-util").rotr;
|
||||||
|
|
||||||
pub fn singleDataSwap(comptime B: bool) InstrFn {
|
pub fn singleDataSwap(comptime B: bool) InstrFn {
|
||||||
return struct {
|
return struct {
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ const Bus = @import("../../Bus.zig");
|
|||||||
const Arm7tdmi = @import("../../cpu.zig").Arm7tdmi;
|
const Arm7tdmi = @import("../../cpu.zig").Arm7tdmi;
|
||||||
const InstrFn = @import("../../cpu.zig").arm.InstrFn;
|
const InstrFn = @import("../../cpu.zig").arm.InstrFn;
|
||||||
|
|
||||||
const rotr = @import("../../../util.zig").rotr;
|
const rotr = @import("zba-util").rotr;
|
||||||
|
|
||||||
pub fn singleDataTransfer(comptime I: bool, comptime P: bool, comptime U: bool, comptime B: bool, comptime W: bool, comptime L: bool) InstrFn {
|
pub fn singleDataTransfer(comptime I: bool, comptime P: bool, comptime U: bool, comptime B: bool, comptime W: bool, comptime L: bool) InstrFn {
|
||||||
return struct {
|
return struct {
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
const Arm7tdmi = @import("../cpu.zig").Arm7tdmi;
|
const Arm7tdmi = @import("../cpu.zig").Arm7tdmi;
|
||||||
const CPSR = @import("../cpu.zig").PSR;
|
const CPSR = @import("../cpu.zig").PSR;
|
||||||
|
|
||||||
const rotr = @import("../../util.zig").rotr;
|
const rotr = @import("zba-util").rotr;
|
||||||
|
|
||||||
pub fn exec(comptime S: bool, cpu: *Arm7tdmi, opcode: u32) u32 {
|
pub fn exec(comptime S: bool, cpu: *Arm7tdmi, opcode: u32) u32 {
|
||||||
var result: u32 = undefined;
|
var result: u32 = undefined;
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ const Arm7tdmi = @import("../../cpu.zig").Arm7tdmi;
|
|||||||
const InstrFn = @import("../../cpu.zig").thumb.InstrFn;
|
const InstrFn = @import("../../cpu.zig").thumb.InstrFn;
|
||||||
|
|
||||||
const checkCond = @import("../../cpu.zig").checkCond;
|
const checkCond = @import("../../cpu.zig").checkCond;
|
||||||
const sext = @import("../../../util.zig").sext;
|
const sext = @import("zba-util").sext;
|
||||||
|
|
||||||
pub fn fmt16(comptime cond: u4) InstrFn {
|
pub fn fmt16(comptime cond: u4) InstrFn {
|
||||||
return struct {
|
return struct {
|
||||||
|
|||||||
@@ -2,8 +2,8 @@ const Bus = @import("../../Bus.zig");
|
|||||||
const Arm7tdmi = @import("../../cpu.zig").Arm7tdmi;
|
const Arm7tdmi = @import("../../cpu.zig").Arm7tdmi;
|
||||||
const InstrFn = @import("../../cpu.zig").thumb.InstrFn;
|
const InstrFn = @import("../../cpu.zig").thumb.InstrFn;
|
||||||
|
|
||||||
const rotr = @import("../../../util.zig").rotr;
|
const rotr = @import("zba-util").rotr;
|
||||||
const sext = @import("../../../util.zig").sext;
|
const sext = @import("zba-util").sext;
|
||||||
|
|
||||||
pub fn fmt6(comptime rd: u3) InstrFn {
|
pub fn fmt6(comptime rd: u3) InstrFn {
|
||||||
return struct {
|
return struct {
|
||||||
|
|||||||
@@ -5,9 +5,9 @@ const config = @import("../config.zig");
|
|||||||
const Scheduler = @import("scheduler.zig").Scheduler;
|
const Scheduler = @import("scheduler.zig").Scheduler;
|
||||||
const Arm7tdmi = @import("cpu.zig").Arm7tdmi;
|
const Arm7tdmi = @import("cpu.zig").Arm7tdmi;
|
||||||
const Tracker = @import("../util.zig").FpsTracker;
|
const Tracker = @import("../util.zig").FpsTracker;
|
||||||
|
const TwoWayChannel = @import("zba-util").TwoWayChannel;
|
||||||
|
|
||||||
const Timer = std.time.Timer;
|
const Timer = std.time.Timer;
|
||||||
const Atomic = std.atomic.Atomic;
|
|
||||||
|
|
||||||
/// 4 Cycles in 1 dot
|
/// 4 Cycles in 1 dot
|
||||||
const cycles_per_dot = 4;
|
const cycles_per_dot = 4;
|
||||||
@@ -35,29 +35,40 @@ const RunKind = enum {
|
|||||||
LimitedFPS,
|
LimitedFPS,
|
||||||
};
|
};
|
||||||
|
|
||||||
pub fn run(quit: *Atomic(bool), pause: *Atomic(bool), cpu: *Arm7tdmi, scheduler: *Scheduler, tracker: *Tracker) void {
|
pub fn run(cpu: *Arm7tdmi, scheduler: *Scheduler, tracker: *Tracker, channel: *TwoWayChannel) void {
|
||||||
const audio_sync = config.config().guest.audio_sync and !config.config().host.mute;
|
const audio_sync = config.config().guest.audio_sync and !config.config().host.mute;
|
||||||
if (audio_sync) log.info("Audio sync enabled", .{});
|
if (audio_sync) log.info("Audio sync enabled", .{});
|
||||||
|
|
||||||
if (config.config().guest.video_sync) {
|
if (config.config().guest.video_sync) {
|
||||||
inner(.LimitedFPS, audio_sync, quit, pause, cpu, scheduler, tracker);
|
inner(.LimitedFPS, audio_sync, cpu, scheduler, tracker, channel);
|
||||||
} else {
|
} else {
|
||||||
inner(.UnlimitedFPS, audio_sync, quit, pause, cpu, scheduler, tracker);
|
inner(.UnlimitedFPS, audio_sync, cpu, scheduler, tracker, channel);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn inner(comptime kind: RunKind, audio_sync: bool, quit: *Atomic(bool), pause: *Atomic(bool), cpu: *Arm7tdmi, scheduler: *Scheduler, tracker: ?*Tracker) void {
|
fn inner(comptime kind: RunKind, audio_sync: bool, cpu: *Arm7tdmi, scheduler: *Scheduler, tracker: ?*Tracker, channel: *TwoWayChannel) void {
|
||||||
if (kind == .UnlimitedFPS or kind == .LimitedFPS) {
|
if (kind == .UnlimitedFPS or kind == .LimitedFPS) {
|
||||||
std.debug.assert(tracker != null);
|
std.debug.assert(tracker != null);
|
||||||
log.info("FPS tracking enabled", .{});
|
log.info("FPS tracking enabled", .{});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var paused: bool = false;
|
||||||
|
|
||||||
switch (kind) {
|
switch (kind) {
|
||||||
.Unlimited, .UnlimitedFPS => {
|
.Unlimited, .UnlimitedFPS => {
|
||||||
log.info("Emulation w/out video sync", .{});
|
log.info("Emulation w/out video sync", .{});
|
||||||
|
|
||||||
while (!quit.load(.Monotonic)) {
|
while (true) {
|
||||||
if (pause.load(.Monotonic)) continue;
|
if (channel.emu.pop()) |e| switch (e) {
|
||||||
|
.Quit => break,
|
||||||
|
.Resume => paused = false,
|
||||||
|
.Pause => {
|
||||||
|
paused = true;
|
||||||
|
channel.gui.push(.Paused);
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
if (paused) continue;
|
||||||
|
|
||||||
runFrame(scheduler, cpu);
|
runFrame(scheduler, cpu);
|
||||||
audioSync(audio_sync, cpu.bus.apu.stream, &cpu.bus.apu.is_buffer_full);
|
audioSync(audio_sync, cpu.bus.apu.stream, &cpu.bus.apu.is_buffer_full);
|
||||||
@@ -70,8 +81,17 @@ fn inner(comptime kind: RunKind, audio_sync: bool, quit: *Atomic(bool), pause: *
|
|||||||
var timer = Timer.start() catch @panic("failed to initalize std.timer.Timer");
|
var timer = Timer.start() catch @panic("failed to initalize std.timer.Timer");
|
||||||
var wake_time: u64 = frame_period;
|
var wake_time: u64 = frame_period;
|
||||||
|
|
||||||
while (!quit.load(.Monotonic)) {
|
while (true) {
|
||||||
if (pause.load(.Monotonic)) continue;
|
if (channel.emu.pop()) |e| switch (e) {
|
||||||
|
.Quit => break,
|
||||||
|
.Resume => paused = false,
|
||||||
|
.Pause => {
|
||||||
|
paused = true;
|
||||||
|
channel.gui.push(.Paused);
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
if (paused) continue;
|
||||||
|
|
||||||
runFrame(scheduler, cpu);
|
runFrame(scheduler, cpu);
|
||||||
const new_wake_time = videoSync(&timer, wake_time);
|
const new_wake_time = videoSync(&timer, wake_time);
|
||||||
@@ -229,3 +249,8 @@ pub fn reset(cpu: *Arm7tdmi) void {
|
|||||||
cpu.bus.reset();
|
cpu.bus.reset();
|
||||||
cpu.reset();
|
cpu.reset();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn replaceGamepak(cpu: *Arm7tdmi, file_path: []const u8) !void {
|
||||||
|
try cpu.bus.replaceGamepak(file_path);
|
||||||
|
reset(cpu);
|
||||||
|
}
|
||||||
|
|||||||
@@ -290,6 +290,26 @@ pub const Ppu = struct {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn reset(self: *Self) void {
|
||||||
|
self.sched.push(.Draw, 240 * 4);
|
||||||
|
|
||||||
|
self.vram.reset();
|
||||||
|
self.palette.reset();
|
||||||
|
self.oam.reset();
|
||||||
|
self.framebuf.reset();
|
||||||
|
|
||||||
|
self.win = Window.init();
|
||||||
|
self.bg = [_]Background{Background.init()} ** 4;
|
||||||
|
self.aff_bg = [_]AffineBackground{AffineBackground.init()} ** 2;
|
||||||
|
self.bld = Blend.create();
|
||||||
|
self.dispcnt = .{ .raw = 0x0000 };
|
||||||
|
self.dispstat = .{ .raw = 0x0000 };
|
||||||
|
self.vcount = .{ .raw = 0x0000 };
|
||||||
|
|
||||||
|
self.scanline.reset();
|
||||||
|
std.mem.set(?Sprite, self.scanline_sprites, null);
|
||||||
|
}
|
||||||
|
|
||||||
pub fn deinit(self: *Self) void {
|
pub fn deinit(self: *Self) void {
|
||||||
self.allocator.destroy(self.scanline_sprites);
|
self.allocator.destroy(self.scanline_sprites);
|
||||||
self.framebuf.deinit();
|
self.framebuf.deinit();
|
||||||
|
|||||||
@@ -34,6 +34,10 @@ pub fn init(allocator: Allocator) !Self {
|
|||||||
return Self{ .buf = buf, .allocator = allocator };
|
return Self{ .buf = buf, .allocator = allocator };
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn reset(self: *Self) void {
|
||||||
|
std.mem.set(u8, self.buf, 0);
|
||||||
|
}
|
||||||
|
|
||||||
pub fn deinit(self: *Self) void {
|
pub fn deinit(self: *Self) void {
|
||||||
self.allocator.free(self.buf);
|
self.allocator.free(self.buf);
|
||||||
self.* = undefined;
|
self.* = undefined;
|
||||||
|
|||||||
@@ -37,6 +37,10 @@ pub fn init(allocator: Allocator) !Self {
|
|||||||
return Self{ .buf = buf, .allocator = allocator };
|
return Self{ .buf = buf, .allocator = allocator };
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn reset(self: *Self) void {
|
||||||
|
std.mem.set(u8, self.buf, 0);
|
||||||
|
}
|
||||||
|
|
||||||
pub fn deinit(self: *Self) void {
|
pub fn deinit(self: *Self) void {
|
||||||
self.allocator.free(self.buf);
|
self.allocator.free(self.buf);
|
||||||
self.* = undefined;
|
self.* = undefined;
|
||||||
|
|||||||
@@ -45,6 +45,10 @@ pub fn init(allocator: Allocator) !Self {
|
|||||||
return Self{ .buf = buf, .allocator = allocator };
|
return Self{ .buf = buf, .allocator = allocator };
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn reset(self: *Self) void {
|
||||||
|
std.mem.set(u8, self.buf, 0);
|
||||||
|
}
|
||||||
|
|
||||||
pub fn deinit(self: *Self) void {
|
pub fn deinit(self: *Self) void {
|
||||||
self.allocator.free(self.buf);
|
self.allocator.free(self.buf);
|
||||||
self.* = undefined;
|
self.* = undefined;
|
||||||
|
|||||||
@@ -11,7 +11,7 @@ const emu = @import("core/emu.zig");
|
|||||||
|
|
||||||
const Gui = @import("platform.zig").Gui;
|
const Gui = @import("platform.zig").Gui;
|
||||||
const Arm7tdmi = @import("core/cpu.zig").Arm7tdmi;
|
const Arm7tdmi = @import("core/cpu.zig").Arm7tdmi;
|
||||||
const RingBuffer = @import("util.zig").RingBuffer;
|
const RingBuffer = @import("zba-util").RingBuffer;
|
||||||
|
|
||||||
const Allocator = std.mem.Allocator;
|
const Allocator = std.mem.Allocator;
|
||||||
const GLuint = gl.GLuint;
|
const GLuint = gl.GLuint;
|
||||||
@@ -21,30 +21,27 @@ const gba_height = @import("core/ppu.zig").height;
|
|||||||
|
|
||||||
const log = std.log.scoped(.Imgui);
|
const log = std.log.scoped(.Imgui);
|
||||||
|
|
||||||
// TODO: Document how I decided on this value (I forgot 😅)
|
// two seconds worth of fps values into the past
|
||||||
const histogram_len = 0x400;
|
const histogram_len = 0x80;
|
||||||
|
|
||||||
/// Immediate-Mode GUI State
|
/// Immediate-Mode GUI State
|
||||||
pub const State = struct {
|
pub const State = struct {
|
||||||
title: [:0]const u8,
|
title: [12:0]u8,
|
||||||
|
|
||||||
fps_hist: RingBuffer(u32),
|
fps_hist: RingBuffer(u32),
|
||||||
should_quit: bool = false,
|
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 history = try allocator.alloc(u32, histogram_len);
|
||||||
const without_null = std.mem.sliceTo(&title, 0);
|
|
||||||
|
|
||||||
return .{
|
const title: [12:0]u8 = if (title_opt) |t| t.* ++ [_:0]u8{} else "[No Title]\x00\x00".*;
|
||||||
.title = try allocator.dupeZ(u8, without_null),
|
return .{ .title = title, .fps_hist = RingBuffer(u32).init(history) };
|
||||||
.fps_hist = RingBuffer(u32).init(history),
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn deinit(self: *@This(), allocator: Allocator) void {
|
pub fn deinit(self: *@This(), allocator: Allocator) void {
|
||||||
allocator.free(self.title);
|
allocator.free(self.fps_hist.buf);
|
||||||
self.fps_hist.deinit(allocator);
|
|
||||||
|
|
||||||
self.* = undefined;
|
self.* = undefined;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
@@ -60,18 +57,26 @@ pub fn draw(state: *State, tex_id: GLuint, cpu: *Arm7tdmi) void {
|
|||||||
defer zgui.endMenu();
|
defer zgui.endMenu();
|
||||||
|
|
||||||
if (zgui.menuItem("Quit", .{})) state.should_quit = true;
|
if (zgui.menuItem("Quit", .{})) state.should_quit = true;
|
||||||
|
|
||||||
if (zgui.menuItem("Insert ROM", .{})) blk: {
|
if (zgui.menuItem("Insert ROM", .{})) blk: {
|
||||||
const maybe_path = nfd.openFileDialog("gba", null) catch |e| {
|
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;
|
break :blk;
|
||||||
};
|
};
|
||||||
|
|
||||||
if (maybe_path) |file_path| {
|
const file_path = maybe_path orelse {
|
||||||
defer nfd.freePath(file_path);
|
log.warn("did not receive a file path", .{});
|
||||||
log.info("user chose: \"{s}\"", .{file_path});
|
break :blk;
|
||||||
|
};
|
||||||
|
defer nfd.freePath(file_path);
|
||||||
|
|
||||||
// emu.loadRom(cpu, 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{};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -88,7 +93,7 @@ pub fn draw(state: *State, tex_id: GLuint, cpu: *Arm7tdmi) void {
|
|||||||
const w = @intToFloat(f32, gba_width * win_scale);
|
const w = @intToFloat(f32, gba_width * win_scale);
|
||||||
const h = @intToFloat(f32, gba_height * 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 } });
|
_ = zgui.begin(window_title, .{ .flags = .{ .no_resize = true, .always_auto_resize = true } });
|
||||||
defer zgui.end();
|
defer zgui.end();
|
||||||
|
|
||||||
@@ -124,7 +129,7 @@ pub fn draw(state: *State, tex_id: GLuint, cpu: *Arm7tdmi) void {
|
|||||||
defer zgui.end();
|
defer zgui.end();
|
||||||
|
|
||||||
const tmp = blk: {
|
const tmp = blk: {
|
||||||
var buf: [0x400]u32 = undefined;
|
var buf: [histogram_len]u32 = undefined;
|
||||||
const len = state.fps_hist.copy(&buf);
|
const len = state.fps_hist.copy(&buf);
|
||||||
|
|
||||||
break :blk .{ buf, len };
|
break :blk .{ buf, len };
|
||||||
|
|||||||
32
src/main.zig
32
src/main.zig
@@ -6,6 +6,7 @@ const clap = @import("clap");
|
|||||||
const config = @import("config.zig");
|
const config = @import("config.zig");
|
||||||
const emu = @import("core/emu.zig");
|
const emu = @import("core/emu.zig");
|
||||||
|
|
||||||
|
const TwoWayChannel = @import("zba-util").TwoWayChannel;
|
||||||
const Gui = @import("platform.zig").Gui;
|
const Gui = @import("platform.zig").Gui;
|
||||||
const Bus = @import("core/Bus.zig");
|
const Bus = @import("core/Bus.zig");
|
||||||
const Arm7tdmi = @import("core/cpu.zig").Arm7tdmi;
|
const Arm7tdmi = @import("core/cpu.zig").Arm7tdmi;
|
||||||
@@ -13,7 +14,6 @@ const Scheduler = @import("core/scheduler.zig").Scheduler;
|
|||||||
const FilePaths = @import("util.zig").FilePaths;
|
const FilePaths = @import("util.zig").FilePaths;
|
||||||
const FpsTracker = @import("util.zig").FpsTracker;
|
const FpsTracker = @import("util.zig").FpsTracker;
|
||||||
const Allocator = std.mem.Allocator;
|
const Allocator = std.mem.Allocator;
|
||||||
const Atomic = std.atomic.Atomic;
|
|
||||||
|
|
||||||
const log = std.log.scoped(.Cli);
|
const log = std.log.scoped(.Cli);
|
||||||
pub const log_level = if (builtin.mode != .Debug) .info else std.log.default_level;
|
pub const log_level = if (builtin.mode != .Debug) .info else std.log.default_level;
|
||||||
@@ -70,9 +70,10 @@ pub fn main() void {
|
|||||||
const paths = handleArguments(allocator, data_path, &result) catch |e| exitln("failed to handle cli arguments: {}", .{e});
|
const paths = handleArguments(allocator, data_path, &result) catch |e| exitln("failed to handle cli arguments: {}", .{e});
|
||||||
defer if (paths.save) |path| allocator.free(path);
|
defer if (paths.save) |path| allocator.free(path);
|
||||||
|
|
||||||
const log_file = if (config.config().debug.cpu_trace) blk: {
|
const log_file = switch (config.config().debug.cpu_trace) {
|
||||||
break :blk std.fs.cwd().createFile("zba.log", .{}) catch |e| exitln("failed to create trace log file: {}", .{e});
|
true => std.fs.cwd().createFile("zba.log", .{}) catch |e| exitln("failed to create trace log file: {}", .{e}),
|
||||||
} else null;
|
false => null,
|
||||||
|
};
|
||||||
defer if (log_file) |file| file.close();
|
defer if (log_file) |file| file.close();
|
||||||
|
|
||||||
// TODO: Take Emulator Init Code out of main.zig
|
// TODO: Take Emulator Init Code out of main.zig
|
||||||
@@ -89,11 +90,16 @@ pub fn main() void {
|
|||||||
cpu.fastBoot();
|
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
|
// 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();
|
defer gui.deinit();
|
||||||
|
|
||||||
var quit = Atomic(bool).init(false);
|
var quit = std.atomic.Atomic(bool).init(false);
|
||||||
|
|
||||||
|
var items: [0x100]u8 = undefined;
|
||||||
|
var channel = TwoWayChannel.init(&items);
|
||||||
|
|
||||||
if (result.args.gdb) {
|
if (result.args.gdb) {
|
||||||
const Server = @import("gdbstub").Server;
|
const Server = @import("gdbstub").Server;
|
||||||
@@ -116,28 +122,26 @@ pub fn main() void {
|
|||||||
gui.run(.{
|
gui.run(.{
|
||||||
.cpu = &cpu,
|
.cpu = &cpu,
|
||||||
.scheduler = &scheduler,
|
.scheduler = &scheduler,
|
||||||
.quit = &quit,
|
.channel = &channel,
|
||||||
}) catch |e| exitln("main thread panicked: {}", .{e});
|
}) catch |e| exitln("main thread panicked: {}", .{e});
|
||||||
} else {
|
} else {
|
||||||
var tracker = FpsTracker.init();
|
var tracker = FpsTracker.init();
|
||||||
var pause = Atomic(bool).init(false);
|
|
||||||
|
|
||||||
const thread = std.Thread.spawn(.{}, emu.run, .{ &quit, &pause, &cpu, &scheduler, &tracker }) catch |e| exitln("emu thread panicked: {}", .{e});
|
const thread = std.Thread.spawn(.{}, emu.run, .{ &cpu, &scheduler, &tracker, &channel }) catch |e| exitln("emu thread panicked: {}", .{e});
|
||||||
defer thread.join();
|
defer thread.join();
|
||||||
|
|
||||||
gui.run(.{
|
gui.run(.{
|
||||||
.cpu = &cpu,
|
.cpu = &cpu,
|
||||||
.scheduler = &scheduler,
|
.scheduler = &scheduler,
|
||||||
|
.channel = &channel,
|
||||||
.tracker = &tracker,
|
.tracker = &tracker,
|
||||||
.quit = &quit,
|
|
||||||
.pause = &pause,
|
|
||||||
}) catch |e| exitln("main thread panicked: {}", .{e});
|
}) catch |e| exitln("main thread panicked: {}", .{e});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn handleArguments(allocator: Allocator, data_path: []const u8, result: *const clap.Result(clap.Help, ¶ms, clap.parsers.default)) !FilePaths {
|
fn handleArguments(allocator: Allocator, data_path: []const u8, result: *const clap.Result(clap.Help, ¶ms, clap.parsers.default)) !FilePaths {
|
||||||
const rom_path = romPath(result);
|
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;
|
const bios_path = result.args.bios;
|
||||||
if (bios_path) |path| log.info("BIOS path: {s}", .{path}) else log.warn("No BIOS provided", .{});
|
if (bios_path) |path| log.info("BIOS path: {s}", .{path}) else log.warn("No BIOS provided", .{});
|
||||||
@@ -186,10 +190,10 @@ fn ensureConfigDirExists(config_path: []const u8) !void {
|
|||||||
try dir.makePath("zba");
|
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) {
|
return switch (result.positionals.len) {
|
||||||
|
0 => null,
|
||||||
1 => result.positionals[0],
|
1 => result.positionals[0],
|
||||||
0 => exitln("ZBA requires a path to a GamePak ROM", .{}),
|
|
||||||
else => exitln("ZBA received too many positional arguments.", .{}),
|
else => exitln("ZBA received too many positional arguments.", .{}),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -11,7 +11,7 @@ const Apu = @import("core/apu.zig").Apu;
|
|||||||
const Arm7tdmi = @import("core/cpu.zig").Arm7tdmi;
|
const Arm7tdmi = @import("core/cpu.zig").Arm7tdmi;
|
||||||
const Scheduler = @import("core/scheduler.zig").Scheduler;
|
const Scheduler = @import("core/scheduler.zig").Scheduler;
|
||||||
const FpsTracker = @import("util.zig").FpsTracker;
|
const FpsTracker = @import("util.zig").FpsTracker;
|
||||||
const RingBuffer = @import("util.zig").RingBuffer;
|
const TwoWayChannel = @import("zba-util").TwoWayChannel;
|
||||||
|
|
||||||
const gba_width = @import("core/ppu.zig").width;
|
const gba_width = @import("core/ppu.zig").width;
|
||||||
const gba_height = @import("core/ppu.zig").height;
|
const gba_height = @import("core/ppu.zig").height;
|
||||||
@@ -57,7 +57,7 @@ pub const Gui = struct {
|
|||||||
allocator: Allocator,
|
allocator: Allocator,
|
||||||
program_id: gl.GLuint,
|
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_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_PROFILE_MASK, SDL.SDL_GL_CONTEXT_PROFILE_CORE) < 0) panic();
|
||||||
if (SDL.SDL_GL_SetAttribute(SDL.SDL_GL_CONTEXT_MAJOR_VERSION, 3) < 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),
|
.audio = Audio.init(apu),
|
||||||
|
|
||||||
.allocator = allocator,
|
.allocator = allocator,
|
||||||
.state = try imgui.State.init(allocator, title),
|
.state = try imgui.State.init(allocator, title_opt),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -243,8 +243,7 @@ pub const Gui = struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const RunOptions = struct {
|
const RunOptions = struct {
|
||||||
quit: *std.atomic.Atomic(bool),
|
channel: *TwoWayChannel,
|
||||||
pause: ?*std.atomic.Atomic(bool) = null,
|
|
||||||
tracker: ?*FpsTracker = null,
|
tracker: ?*FpsTracker = null,
|
||||||
cpu: *Arm7tdmi,
|
cpu: *Arm7tdmi,
|
||||||
scheduler: *Scheduler,
|
scheduler: *Scheduler,
|
||||||
@@ -253,8 +252,7 @@ pub const Gui = struct {
|
|||||||
pub fn run(self: *Self, opt: RunOptions) !void {
|
pub fn run(self: *Self, opt: RunOptions) !void {
|
||||||
const cpu = opt.cpu;
|
const cpu = opt.cpu;
|
||||||
const tracker = opt.tracker;
|
const tracker = opt.tracker;
|
||||||
const quit = opt.quit;
|
const channel = opt.channel;
|
||||||
const pause = opt.pause;
|
|
||||||
|
|
||||||
const obj_ids = Self.genBufferObjects();
|
const obj_ids = Self.genBufferObjects();
|
||||||
defer gl.deleteBuffers(3, @as(*const [3]c_uint, &obj_ids));
|
defer gl.deleteBuffers(3, @as(*const [3]c_uint, &obj_ids));
|
||||||
@@ -269,7 +267,10 @@ pub const Gui = struct {
|
|||||||
emu_loop: while (true) {
|
emu_loop: while (true) {
|
||||||
// `quit` from RunOptions may be modified by the GDBSTUB thread,
|
// `quit` from RunOptions may be modified by the GDBSTUB thread,
|
||||||
// so we want to recognize that it may change to `true` and exit the GUI thread
|
// so we want to recognize that it may change to `true` and exit the GUI thread
|
||||||
if (quit.load(.Monotonic)) break :emu_loop;
|
if (channel.gui.pop()) |event| switch (event) {
|
||||||
|
.Quit => break :emu_loop,
|
||||||
|
.Paused => @panic("TODO: We want to peek (and then pop if it's .Quit), not always pop"),
|
||||||
|
};
|
||||||
|
|
||||||
// Outside of `SDL.SDL_QUIT` below, the DearImgui UI might signal that the program
|
// Outside of `SDL.SDL_QUIT` below, the DearImgui UI might signal that the program
|
||||||
// should exit, in which case we should also handle this
|
// should exit, in which case we should also handle this
|
||||||
@@ -325,17 +326,18 @@ pub const Gui = struct {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// If `Gui.run` has been passed with a `pause` atomic, we should
|
|
||||||
// pause the emulation thread while we access the data there
|
|
||||||
{
|
{
|
||||||
// TODO: Is there a nicer way to express this?
|
channel.emu.push(.Pause);
|
||||||
if (pause) |val| val.store(true, .Monotonic);
|
defer channel.emu.push(.Resume);
|
||||||
defer if (pause) |val| val.store(false, .Monotonic);
|
|
||||||
|
// Spin Loop until we know that the emu is paused
|
||||||
|
wait: while (true) switch (channel.gui.pop() orelse continue) {
|
||||||
|
.Paused => break :wait,
|
||||||
|
else => |any| std.debug.panic("[Gui/Channel]: Unhandled Event: {}", .{any}),
|
||||||
|
};
|
||||||
|
|
||||||
// Add FPS count to the histogram
|
// Add FPS count to the histogram
|
||||||
if (tracker) |t| {
|
if (tracker) |t| self.state.fps_hist.push(t.value()) catch {};
|
||||||
self.state.fps_hist.push(t.value()) catch {};
|
|
||||||
}
|
|
||||||
|
|
||||||
// Draw GBA Screen to Texture
|
// Draw GBA Screen to Texture
|
||||||
{
|
{
|
||||||
@@ -361,7 +363,7 @@ pub const Gui = struct {
|
|||||||
SDL.SDL_GL_SwapWindow(self.window);
|
SDL.SDL_GL_SwapWindow(self.window);
|
||||||
}
|
}
|
||||||
|
|
||||||
quit.store(true, .Monotonic); // Signals to emu thread to exit
|
channel.emu.push(.Quit);
|
||||||
}
|
}
|
||||||
|
|
||||||
fn glGetProcAddress(ctx: SDL.SDL_GLContext, proc: [:0]const u8) ?*anyopaque {
|
fn glGetProcAddress(ctx: SDL.SDL_GLContext, proc: [:0]const u8) ?*anyopaque {
|
||||||
|
|||||||
117
src/util.zig
117
src/util.zig
@@ -7,27 +7,6 @@ const Arm7tdmi = @import("core/cpu.zig").Arm7tdmi;
|
|||||||
|
|
||||||
const Allocator = std.mem.Allocator;
|
const Allocator = std.mem.Allocator;
|
||||||
|
|
||||||
// Sign-Extend value of type `T` to type `U`
|
|
||||||
pub fn sext(comptime T: type, comptime U: type, value: T) T {
|
|
||||||
// U must have less bits than T
|
|
||||||
comptime std.debug.assert(@typeInfo(U).Int.bits <= @typeInfo(T).Int.bits);
|
|
||||||
|
|
||||||
const iT = std.meta.Int(.signed, @typeInfo(T).Int.bits);
|
|
||||||
const ExtU = if (@typeInfo(U).Int.signedness == .unsigned) T else iT;
|
|
||||||
const shift_amt = @intCast(Log2Int(T), @typeInfo(T).Int.bits - @typeInfo(U).Int.bits);
|
|
||||||
|
|
||||||
return @bitCast(T, @bitCast(iT, @as(ExtU, @truncate(U, value)) << shift_amt) >> shift_amt);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// See https://godbolt.org/z/W3en9Eche
|
|
||||||
pub inline fn rotr(comptime T: type, x: T, r: anytype) T {
|
|
||||||
if (@typeInfo(T).Int.signedness == .signed)
|
|
||||||
@compileError("cannot rotate signed integer");
|
|
||||||
|
|
||||||
const ar = @intCast(Log2Int(T), @mod(r, @typeInfo(T).Int.bits));
|
|
||||||
return x >> ar | x << (1 +% ~ar);
|
|
||||||
}
|
|
||||||
|
|
||||||
pub const FpsTracker = struct {
|
pub const FpsTracker = struct {
|
||||||
const Self = @This();
|
const Self = @This();
|
||||||
|
|
||||||
@@ -57,17 +36,6 @@ pub const FpsTracker = struct {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
pub fn intToBytes(comptime T: type, value: anytype) [@sizeOf(T)]u8 {
|
|
||||||
comptime std.debug.assert(@typeInfo(T) == .Int);
|
|
||||||
|
|
||||||
var result: [@sizeOf(T)]u8 = undefined;
|
|
||||||
|
|
||||||
var i: Log2Int(T) = 0;
|
|
||||||
while (i < result.len) : (i += 1) result[i] = @truncate(u8, value >> i * @bitSizeOf(u8));
|
|
||||||
|
|
||||||
return result;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Creates a copy of a title with all Filesystem-invalid characters replaced
|
/// Creates a copy of a title with all Filesystem-invalid characters replaced
|
||||||
///
|
///
|
||||||
/// e.g. POKEPIN R/S to POKEPIN R_S
|
/// e.g. POKEPIN R/S to POKEPIN R_S
|
||||||
@@ -82,7 +50,7 @@ pub fn escape(title: [12]u8) [12]u8 {
|
|||||||
}
|
}
|
||||||
|
|
||||||
pub const FilePaths = struct {
|
pub const FilePaths = struct {
|
||||||
rom: []const u8,
|
rom: ?[]const u8,
|
||||||
bios: ?[]const u8,
|
bios: ?[]const u8,
|
||||||
save: ?[]const u8,
|
save: ?[]const u8,
|
||||||
};
|
};
|
||||||
@@ -283,7 +251,7 @@ pub const FrameBuffer = struct {
|
|||||||
|
|
||||||
layers: [2][]u8,
|
layers: [2][]u8,
|
||||||
buf: []u8,
|
buf: []u8,
|
||||||
current: u1,
|
current: u1 = 0,
|
||||||
|
|
||||||
allocator: Allocator,
|
allocator: Allocator,
|
||||||
|
|
||||||
@@ -298,12 +266,16 @@ pub const FrameBuffer = struct {
|
|||||||
// Front and Back Framebuffers
|
// Front and Back Framebuffers
|
||||||
.layers = [_][]u8{ buf[0..][0..len], buf[len..][0..len] },
|
.layers = [_][]u8{ buf[0..][0..len], buf[len..][0..len] },
|
||||||
.buf = buf,
|
.buf = buf,
|
||||||
.current = 0,
|
|
||||||
|
|
||||||
.allocator = allocator,
|
.allocator = allocator,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn reset(self: *Self) void {
|
||||||
|
std.mem.set(u8, self.buf, 0);
|
||||||
|
self.current = 0;
|
||||||
|
}
|
||||||
|
|
||||||
pub fn deinit(self: *Self) void {
|
pub fn deinit(self: *Self) void {
|
||||||
self.allocator.free(self.buf);
|
self.allocator.free(self.buf);
|
||||||
self.* = undefined;
|
self.* = undefined;
|
||||||
@@ -317,78 +289,3 @@ pub const FrameBuffer = struct {
|
|||||||
return self.layers[if (dev == .Emulator) self.current else ~self.current];
|
return self.layers[if (dev == .Emulator) self.current else ~self.current];
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
pub fn RingBuffer(comptime T: type) type {
|
|
||||||
return struct {
|
|
||||||
const Self = @This();
|
|
||||||
const Index = usize;
|
|
||||||
const max_capacity = (@as(Index, 1) << @typeInfo(Index).Int.bits - 1) - 1; // half the range of index type
|
|
||||||
|
|
||||||
const log = std.log.scoped(.RingBuffer);
|
|
||||||
|
|
||||||
read: Index,
|
|
||||||
write: Index,
|
|
||||||
buf: []T,
|
|
||||||
|
|
||||||
const Error = error{buffer_full};
|
|
||||||
|
|
||||||
pub fn init(buf: []T) Self {
|
|
||||||
std.debug.assert(std.math.isPowerOfTwo(buf.len)); // capacity must be a power of two
|
|
||||||
std.debug.assert(buf.len <= max_capacity);
|
|
||||||
|
|
||||||
std.mem.set(T, buf, 0);
|
|
||||||
|
|
||||||
return .{ .read = 0, .write = 0, .buf = buf };
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn deinit(self: *Self, allocator: Allocator) void {
|
|
||||||
allocator.free(self.buf);
|
|
||||||
self.* = undefined;
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn push(self: *Self, value: T) Error!void {
|
|
||||||
if (self.isFull()) return error.buffer_full;
|
|
||||||
defer self.write += 1;
|
|
||||||
|
|
||||||
self.buf[self.mask(self.write)] = value;
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn pop(self: *Self) ?T {
|
|
||||||
if (self.isEmpty()) return null;
|
|
||||||
defer self.read += 1;
|
|
||||||
|
|
||||||
return self.buf[self.mask(self.read)];
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Returns the number of entries read
|
|
||||||
pub fn copy(self: *const Self, cpy: []T) Index {
|
|
||||||
const count = std.math.min(self.len(), cpy.len);
|
|
||||||
var start: Index = self.read;
|
|
||||||
|
|
||||||
for (cpy, 0..) |*v, i| {
|
|
||||||
if (i >= count) break;
|
|
||||||
|
|
||||||
v.* = self.buf[self.mask(start)];
|
|
||||||
start += 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
return count;
|
|
||||||
}
|
|
||||||
|
|
||||||
fn len(self: *const Self) Index {
|
|
||||||
return self.write - self.read;
|
|
||||||
}
|
|
||||||
|
|
||||||
fn isFull(self: *const Self) bool {
|
|
||||||
return self.len() == self.buf.len;
|
|
||||||
}
|
|
||||||
|
|
||||||
fn isEmpty(self: *const Self) bool {
|
|
||||||
return self.read == self.write;
|
|
||||||
}
|
|
||||||
|
|
||||||
fn mask(self: *const Self, idx: Index) Index {
|
|
||||||
return idx & (self.buf.len - 1);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|||||||
Reference in New Issue
Block a user