diff --git a/.gitignore b/.gitignore index 2bcaeb9..62d52e5 100644 --- a/.gitignore +++ b/.gitignore @@ -11,4 +11,8 @@ /lib/SDL2 # Any Custom Scripts for Debugging purposes -*.sh \ No newline at end of file +*.sh + + +# Dear ImGui +**/imgui.ini \ No newline at end of file diff --git a/.gitmodules b/.gitmodules index f45d1ad..475af46 100644 --- a/.gitmodules +++ b/.gitmodules @@ -16,3 +16,12 @@ [submodule "lib/zba-gdbstub"] path = lib/zba-gdbstub url = https://git.musuka.dev/paoda/zba-gdbstub +[submodule "lib/zgui"] + path = lib/zgui + url = https://git.musuka.dev/paoda/zgui +[submodule "lib/nfd-zig"] + path = lib/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 diff --git a/README.md b/README.md index fcec683..ba251da 100644 --- a/README.md +++ b/README.md @@ -2,6 +2,8 @@ A Game Boy Advance Emulator written in Zig ⚡! +![ZBA running リズム天国](assets/screenshot.png) + ## 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)? @@ -84,10 +85,12 @@ Most recently built on Zig [v0.11.0-dev.1580+a5b34a61a](https://github.com/zigla Dependency | Source --- | --- SDL.zig | -zig-clap | known-folders | -zig-toml | +nfd-zig | +zgui | +zig-clap | zig-datetime | +zig-toml | `bitfields.zig` | [https://github.com/FlorenceOS/Florence](https://github.com/FlorenceOS/Florence/blob/aaa5a9e568/lib/util/bitfields.zig) `gl.zig` | diff --git a/assets/screenshot.png b/assets/screenshot.png new file mode 100644 index 0000000..ff6cb5c Binary files /dev/null and b/assets/screenshot.png differ diff --git a/build.zig b/build.zig index 2cf2bb2..1eabf6d 100644 --- a/build.zig +++ b/build.zig @@ -2,7 +2,9 @@ const std = @import("std"); const builtin = @import("builtin"); const Sdk = @import("lib/SDL.zig/Sdk.zig"); -const Gdbstub = @import("lib/zba-gdbstub/build.zig"); +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 { // Minimum Zig Version @@ -31,7 +33,7 @@ pub fn build(b: *std.build.Builder) void { exe.addAnonymousModule("datetime", .{ .source_file = .{ .path = "lib/zig-datetime/src/main.zig" } }); // 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 exe.addAnonymousModule("clap", .{ .source_file = .{ .path = "lib/zig-clap/clap.zig" } }); @@ -42,14 +44,25 @@ pub fn build(b: *std.build.Builder) void { // OpenGL 3.3 Bindings 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.link(exe); + gdbstub.link(exe); + // NativeFileDialog(ue) Bindings + exe.linkLibrary(nfd.makeLib(b, target, optimize)); + exe.addModule("nfd", nfd.getModule(b)); // Zig SDL Bindings: https://github.com/MasterQ32/SDL.zig const sdk = Sdk.init(b, null); sdk.link(exe, .dynamic); 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); + exe.install(); const run_cmd = exe.run(); diff --git a/lib/util/bitfield.zig b/lib/bitfield.zig similarity index 100% rename from lib/util/bitfield.zig rename to lib/bitfield.zig diff --git a/lib/nfd-zig b/lib/nfd-zig new file mode 160000 index 0000000..5e5098b --- /dev/null +++ b/lib/nfd-zig @@ -0,0 +1 @@ +Subproject commit 5e5098bcaf2643d35199ce556da8091626a4d2ef diff --git a/lib/zba-util b/lib/zba-util new file mode 160000 index 0000000..d5e66ca --- /dev/null +++ b/lib/zba-util @@ -0,0 +1 @@ +Subproject commit d5e66caf2180324d83ad9be30e887849f5ed74da diff --git a/lib/zgui b/lib/zgui new file mode 160000 index 0000000..12e480f --- /dev/null +++ b/lib/zgui @@ -0,0 +1 @@ +Subproject commit 12e480f30d1192eda18bd9245f41f6ff2d4055bd diff --git a/src/core/Bus.zig b/src/core/Bus.zig index 29e9076..055e6cb 100644 --- a/src/core/Bus.zig +++ b/src/core/Bus.zig @@ -19,7 +19,7 @@ const log = std.log.scoped(.Bus); const createDmaTuple = @import("bus/dma.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{ // BIOS, Unused, EWRAM, IWRAM, I/0, PALRAM, VRAM, OAM, ROM0, ROM0, ROM1, ROM1, ROM2, ROM2, SRAM, Unused @@ -102,6 +102,47 @@ pub fn deinit(self: *Self) void { self.* = undefined; } +pub fn reset(self: *Self) void { + self.bios.reset(); + self.ppu.reset(); + self.apu.reset(); + self.iwram.reset(); + self.ewram.reset(); + + // https://github.com/ziglang/zig/issues/14705 + { + comptime var i: usize = 0; + inline while (i < self.dma.len) : (i += 1) { + self.dma[0].reset(); + } + } + + // https://github.com/ziglang/zig/issues/14705 + { + comptime var i: usize = 0; + inline while (i < self.tim.len) : (i += 1) { + self.tim[0].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 { const vramMirror = @import("ppu/Vram.zig").mirror; diff --git a/src/core/apu.zig b/src/core/apu.zig index 05b6cea..a93ad0b 100644 --- a/src/core/apu.zig +++ b/src/core/apu.zig @@ -14,7 +14,6 @@ const SoundFifo = std.fifo.LinearFifo(u8, .{ .Static = 0x20 }); const getHalf = util.getHalf; const setHalf = util.setHalf; -const intToBytes = util.intToBytes; const log = std.log.scoped(.APU); @@ -279,17 +278,44 @@ pub const Apu = struct { .is_buffer_full = false, }; - sched.push(.SampleAudio, 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); + Self.initEvents(apu.sched, apu.interval()); return apu; } - fn reset(self: *Self) void { + 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 + pub fn reset(self: *Self) void { + // FIXME: These reset functions are meant to emulate obscure APU behaviour. Write proper emu reset fns + self.ch1.reset(); + self.ch2.reset(); + self.ch3.reset(); + self.ch4.reset(); + + self.chA.reset(); + self.chB.reset(); + + self.psg_cnt = .{ .raw = 0 }; + self.dma_cnt = .{ .raw = 0 }; + self.cnt = .{ .raw = 0 }; + self.bias = .{ .raw = 0x200 }; + + self.sampling_cycle = 0; + self.fs.reset(); + + Self.initEvents(self.sched, self.interval()); + } + + /// Emulates the reset behaviour of the APU + fn _reset(self: *Self) void { // All PSG Registers between 0x0400_0060..0x0400_0081 are zeroed // 0x0400_0082 and 0x0400_0088 retain their values self.ch1.reset(); @@ -351,7 +377,7 @@ pub const Apu = struct { // Rest Noise self.ch4.lfsr.reset(); } else { - self.reset(); + self._reset(); } } @@ -528,10 +554,15 @@ pub fn DmaSound(comptime kind: DmaSoundKind) type { }; } + /// Used when resetting hte emulator (not emulation code) + fn reset(self: *Self) void { + self.* = Self.init(); + } + pub fn push(self: *Self, value: u32) void { 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 { @@ -562,10 +593,14 @@ pub const FrameSequencer = struct { const Self = @This(); pub const interval = (1 << 24) / 512; - step: u3, + step: u3 = 0, pub fn init() Self { - return .{ .step = 0 }; + return .{}; + } + + pub fn reset(self: *Self) void { + self.* = .{}; } pub fn tick(self: *Self) void { diff --git a/src/core/apu/device/Envelope.zig b/src/core/apu/device/Envelope.zig index 9eee8fe..49ea53c 100644 --- a/src/core/apu/device/Envelope.zig +++ b/src/core/apu/device/Envelope.zig @@ -3,17 +3,16 @@ const io = @import("../../bus/io.zig"); const Self = @This(); /// Period Timer -timer: u3, +timer: u3 = 0, /// Current Volume -vol: u4, +vol: u4 = 0, pub fn create() Self { - return .{ .timer = 0, .vol = 0 }; + return .{}; } pub fn reset(self: *Self) void { - self.timer = 0; - self.vol = 0; + self.* = .{}; } pub fn tick(self: *Self, nrx2: io.Envelope) void { diff --git a/src/core/apu/device/Length.zig b/src/core/apu/device/Length.zig index 420daf0..8478188 100644 --- a/src/core/apu/device/Length.zig +++ b/src/core/apu/device/Length.zig @@ -1,13 +1,13 @@ const Self = @This(); -timer: u9, +timer: u9 = 0, pub fn create() Self { - return .{ .timer = 0 }; + return .{}; } pub fn reset(self: *Self) void { - self.timer = 0; + self.* = .{}; } pub fn tick(self: *Self, enabled: bool, ch_enable: *bool) void { diff --git a/src/core/apu/device/Sweep.zig b/src/core/apu/device/Sweep.zig index 605697d..82877e8 100644 --- a/src/core/apu/device/Sweep.zig +++ b/src/core/apu/device/Sweep.zig @@ -3,26 +3,18 @@ const ToneSweep = @import("../ToneSweep.zig"); const Self = @This(); -timer: u8, -enabled: bool, -shadow: u11, +timer: u8 = 0, +enabled: bool = false, +shadow: u11 = 0, -calc_performed: bool, +calc_performed: bool = false, pub fn create() Self { - return .{ - .timer = 0, - .enabled = false, - .shadow = 0, - .calc_performed = false, - }; + return .{}; } pub fn reset(self: *Self) void { - self.timer = 0; - self.enabled = false; - self.shadow = 0; - self.calc_performed = false; + self.* = .{}; } pub fn tick(self: *Self, ch1: *ToneSweep) void { diff --git a/src/core/bus/Bios.zig b/src/core/bus/Bios.zig index 84e6e7d..51c6095 100644 --- a/src/core/bus/Bios.zig +++ b/src/core/bus/Bios.zig @@ -3,7 +3,7 @@ const std = @import("std"); const Allocator = std.mem.Allocator; const log = std.log.scoped(.Bios); -const rotr = @import("../../util.zig").rotr; +const rotr = @import("zba-util").rotr; const forceAlign = @import("../Bus.zig").forceAlign; /// Size of the BIOS in bytes @@ -77,6 +77,10 @@ pub fn init(allocator: Allocator, maybe_path: ?[]const u8) !Self { return Self{ .buf = buf, .allocator = allocator }; } +pub fn reset(self: *Self) void { + self.addr_latch = 0; +} + pub fn deinit(self: *Self) void { if (self.buf) |buf| self.allocator.free(buf); self.* = undefined; diff --git a/src/core/bus/Ewram.zig b/src/core/bus/Ewram.zig index c36d998..a1ad801 100644 --- a/src/core/bus/Ewram.zig +++ b/src/core/bus/Ewram.zig @@ -35,6 +35,10 @@ pub fn init(allocator: Allocator) !Self { }; } +pub fn reset(self: *Self) void { + std.mem.set(u8, self.buf, 0); +} + pub fn deinit(self: *Self) void { self.allocator.free(self.buf); self.* = undefined; diff --git a/src/core/bus/GamePak.zig b/src/core/bus/GamePak.zig index 2698c44..d2203a1 100644 --- a/src/core/bus/GamePak.zig +++ b/src/core/bus/GamePak.zig @@ -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, .{}); - defer file.close(); +pub fn init(allocator: Allocator, cpu: *Arm7tdmi, maybe_rom: ?[]const u8, maybe_save: ?[]const u8) !Self { + const Device = Gpio.Device; - 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 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(); - 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 .{ - .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]), }; } @@ -220,19 +227,17 @@ fn guessDevice(buf: []const u8) Gpio.Device.Kind { } // TODO: Detect other GPIO devices - return .None; } fn logHeader(buf: []const u8, title: *const [12]u8) void { - const code = buf[0xAC..0xB0]; - const maker = buf[0xB0..0xB2]; const version = buf[0xBC]; log.info("Title: {s}", .{title}); if (version != 0) log.info("Version: {}", .{version}); - log.info("Game Code: {s}", .{code}); - log.info("Maker Code: {s}", .{maker}); + + log.info("Game Code: {s}", .{buf[0xAC..0xB0]}); + log.info("Maker Code: {s}", .{buf[0xB0..0xB2]}); } test "OOB Access" { diff --git a/src/core/bus/Iwram.zig b/src/core/bus/Iwram.zig index 383075e..0356460 100644 --- a/src/core/bus/Iwram.zig +++ b/src/core/bus/Iwram.zig @@ -35,6 +35,10 @@ pub fn init(allocator: Allocator) !Self { }; } +pub fn reset(self: *Self) void { + std.mem.set(u8, self.buf, 0); +} + pub fn deinit(self: *Self) void { self.allocator.free(self.buf); self.* = undefined; diff --git a/src/core/bus/backup.zig b/src/core/bus/backup.zig index d1d708d..8db2571 100644 --- a/src/core/bus/backup.zig +++ b/src/core/bus/backup.zig @@ -32,7 +32,7 @@ pub const Backup = struct { flash: Flash, eeprom: Eeprom, - const Kind = enum { + pub const Kind = enum { Eeprom, Sram, Flash, diff --git a/src/core/bus/dma.zig b/src/core/bus/dma.zig index e1f1b1c..9bc3274 100644 --- a/src/core/bus/dma.zig +++ b/src/core/bus/dma.zig @@ -12,7 +12,7 @@ const getHalf = util.getHalf; const setHalf = util.setHalf; const setQuart = util.setQuart; -const rotr = @import("../../util.zig").rotr; +const rotr = @import("zba-util").rotr; pub fn create() DmaTuple { return .{ DmaController(0).init(), DmaController(1).init(), DmaController(2).init(), DmaController(3).init() }; @@ -195,6 +195,10 @@ fn DmaController(comptime id: u2) type { }; } + pub fn reset(self: *Self) void { + self.* = Self.init(); + } + pub fn setDmasad(self: *Self, addr: u32) void { self.sad = addr & sad_mask; } diff --git a/src/core/bus/io.zig b/src/core/bus/io.zig index b4761b4..2cbbb32 100644 --- a/src/core/bus/io.zig +++ b/src/core/bus/io.zig @@ -38,6 +38,10 @@ pub const Io = struct { }; } + pub fn reset(self: *Self) void { + self.* = Self.init(); + } + fn setIrqs(self: *Io, word: u32) void { self.ie.raw = @truncate(u16, word); self.irq.raw &= ~@truncate(u16, word >> 16); @@ -346,10 +350,10 @@ const InterruptEnable = extern union { vblank: Bit(u16, 0), hblank: Bit(u16, 1), coincidence: Bit(u16, 2), - tm0_overflow: Bit(u16, 3), - tm1_overflow: Bit(u16, 4), - tm2_overflow: Bit(u16, 5), - tm3_overflow: Bit(u16, 6), + tim0: Bit(u16, 3), + tim1: Bit(u16, 4), + tim2: Bit(u16, 5), + tim3: Bit(u16, 6), serial: Bit(u16, 7), dma0: Bit(u16, 8), dma1: Bit(u16, 9), diff --git a/src/core/bus/timer.zig b/src/core/bus/timer.zig index ad7b5f0..93ff7c4 100644 --- a/src/core/bus/timer.zig +++ b/src/core/bus/timer.zig @@ -128,6 +128,12 @@ fn Timer(comptime id: u2) type { }; } + pub fn reset(self: *Self) void { + const scheduler = self.sched; + + self.* = Self.init(scheduler); + } + /// TIMCNT_L Getter pub fn timcntL(self: *const Self) u16 { if (self.cnt.cascade.read() or !self.cnt.enabled.read()) return self._counter; diff --git a/src/core/cpu.zig b/src/core/cpu.zig index 90e00d4..e005c9e 100644 --- a/src/core/cpu.zig +++ b/src/core/cpu.zig @@ -314,6 +314,14 @@ pub const Arm7tdmi = struct { }; } + // FIXME: Resetting disables logging (if enabled) + pub fn reset(self: *Self) void { + const bus_ptr = self.bus; + const scheduler_ptr = self.sched; + + self.* = Self.init(scheduler_ptr, bus_ptr, null); + } + pub inline fn hasSPSR(self: *const Self) bool { const mode = getModeChecked(self, self.cpsr.mode.read()); return switch (mode) { @@ -642,7 +650,7 @@ pub const PSR = extern union { } }; -const Mode = enum(u5) { +pub const Mode = enum(u5) { User = 0b10000, Fiq = 0b10001, Irq = 0b10010, @@ -651,7 +659,7 @@ const Mode = enum(u5) { Undefined = 0b11011, System = 0b11111, - fn toString(self: Mode) []const u8 { + pub fn toString(self: Mode) []const u8 { return switch (self) { .User => "usr", .Fiq => "fiq", diff --git a/src/core/cpu/arm/branch.zig b/src/core/cpu/arm/branch.zig index b55782d..18d7b6e 100644 --- a/src/core/cpu/arm/branch.zig +++ b/src/core/cpu/arm/branch.zig @@ -2,7 +2,7 @@ const Bus = @import("../../Bus.zig"); const Arm7tdmi = @import("../../cpu.zig").Arm7tdmi; 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 { return struct { diff --git a/src/core/cpu/arm/half_signed_data_transfer.zig b/src/core/cpu/arm/half_signed_data_transfer.zig index cd205d9..e95c9c2 100644 --- a/src/core/cpu/arm/half_signed_data_transfer.zig +++ b/src/core/cpu/arm/half_signed_data_transfer.zig @@ -2,8 +2,8 @@ const Bus = @import("../../Bus.zig"); const Arm7tdmi = @import("../../cpu.zig").Arm7tdmi; const InstrFn = @import("../../cpu.zig").arm.InstrFn; -const sext = @import("../../../util.zig").sext; -const rotr = @import("../../../util.zig").rotr; +const sext = @import("zba-util").sext; +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 { return struct { diff --git a/src/core/cpu/arm/psr_transfer.zig b/src/core/cpu/arm/psr_transfer.zig index 3ee8791..8eba6f6 100644 --- a/src/core/cpu/arm/psr_transfer.zig +++ b/src/core/cpu/arm/psr_transfer.zig @@ -7,7 +7,7 @@ const PSR = @import("../../cpu.zig").PSR; 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 { return struct { diff --git a/src/core/cpu/arm/single_data_swap.zig b/src/core/cpu/arm/single_data_swap.zig index 511cb7b..7a588f3 100644 --- a/src/core/cpu/arm/single_data_swap.zig +++ b/src/core/cpu/arm/single_data_swap.zig @@ -2,7 +2,7 @@ const Bus = @import("../../Bus.zig"); const Arm7tdmi = @import("../../cpu.zig").Arm7tdmi; 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 { return struct { diff --git a/src/core/cpu/arm/single_data_transfer.zig b/src/core/cpu/arm/single_data_transfer.zig index 328699e..8a2e2d2 100644 --- a/src/core/cpu/arm/single_data_transfer.zig +++ b/src/core/cpu/arm/single_data_transfer.zig @@ -3,7 +3,7 @@ const Bus = @import("../../Bus.zig"); const Arm7tdmi = @import("../../cpu.zig").Arm7tdmi; 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 { return struct { diff --git a/src/core/cpu/barrel_shifter.zig b/src/core/cpu/barrel_shifter.zig index ecca44b..c5a6c81 100644 --- a/src/core/cpu/barrel_shifter.zig +++ b/src/core/cpu/barrel_shifter.zig @@ -1,7 +1,7 @@ const Arm7tdmi = @import("../cpu.zig").Arm7tdmi; 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 { var result: u32 = undefined; diff --git a/src/core/cpu/thumb/branch.zig b/src/core/cpu/thumb/branch.zig index 4d00577..c531ac9 100644 --- a/src/core/cpu/thumb/branch.zig +++ b/src/core/cpu/thumb/branch.zig @@ -3,7 +3,7 @@ const Arm7tdmi = @import("../../cpu.zig").Arm7tdmi; const InstrFn = @import("../../cpu.zig").thumb.InstrFn; 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 { return struct { diff --git a/src/core/cpu/thumb/data_transfer.zig b/src/core/cpu/thumb/data_transfer.zig index b6c5246..b7cfd7f 100644 --- a/src/core/cpu/thumb/data_transfer.zig +++ b/src/core/cpu/thumb/data_transfer.zig @@ -2,8 +2,8 @@ const Bus = @import("../../Bus.zig"); const Arm7tdmi = @import("../../cpu.zig").Arm7tdmi; const InstrFn = @import("../../cpu.zig").thumb.InstrFn; -const rotr = @import("../../../util.zig").rotr; -const sext = @import("../../../util.zig").sext; +const rotr = @import("zba-util").rotr; +const sext = @import("zba-util").sext; pub fn fmt6(comptime rd: u3) InstrFn { return struct { diff --git a/src/core/emu.zig b/src/core/emu.zig index 1a84c11..e009a67 100644 --- a/src/core/emu.zig +++ b/src/core/emu.zig @@ -4,10 +4,10 @@ const config = @import("../config.zig"); const Scheduler = @import("scheduler.zig").Scheduler; const Arm7tdmi = @import("cpu.zig").Arm7tdmi; -const FpsTracker = @import("../util.zig").FpsTracker; +const Tracker = @import("../util.zig").FpsTracker; +const TwoWayChannel = @import("zba-util").TwoWayChannel; const Timer = std.time.Timer; -const Atomic = std.atomic.Atomic; /// 4 Cycles in 1 dot const cycles_per_dot = 4; @@ -35,28 +35,41 @@ const RunKind = enum { LimitedFPS, }; -pub fn run(quit: *Atomic(bool), scheduler: *Scheduler, cpu: *Arm7tdmi, tracker: *FpsTracker) 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; if (audio_sync) log.info("Audio sync enabled", .{}); if (config.config().guest.video_sync) { - inner(.LimitedFPS, audio_sync, quit, scheduler, cpu, tracker); + inner(.LimitedFPS, audio_sync, cpu, scheduler, tracker, channel); } else { - inner(.UnlimitedFPS, audio_sync, quit, scheduler, cpu, tracker); + inner(.UnlimitedFPS, audio_sync, cpu, scheduler, tracker, channel); } } -fn inner(comptime kind: RunKind, audio_sync: bool, quit: *Atomic(bool), scheduler: *Scheduler, cpu: *Arm7tdmi, tracker: ?*FpsTracker) void { +fn inner(comptime kind: RunKind, audio_sync: bool, cpu: *Arm7tdmi, scheduler: *Scheduler, tracker: ?*Tracker, channel: *TwoWayChannel) void { if (kind == .UnlimitedFPS or kind == .LimitedFPS) { std.debug.assert(tracker != null); log.info("FPS tracking enabled", .{}); } + var paused: bool = false; + switch (kind) { .Unlimited, .UnlimitedFPS => { log.info("Emulation w/out video sync", .{}); - while (!quit.load(.Monotonic)) { + while (true) { + 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); audioSync(audio_sync, cpu.bus.apu.stream, &cpu.bus.apu.is_buffer_full); @@ -68,7 +81,18 @@ fn inner(comptime kind: RunKind, audio_sync: bool, quit: *Atomic(bool), schedule var timer = Timer.start() catch @panic("failed to initalize std.timer.Timer"); var wake_time: u64 = frame_period; - while (!quit.load(.Monotonic)) { + while (true) { + 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); const new_wake_time = videoSync(&timer, wake_time); @@ -218,3 +242,15 @@ pub const EmuThing = struct { } } }; + +pub fn reset(cpu: *Arm7tdmi) void { + // @breakpoint(); + cpu.sched.reset(); // Yes this is order sensitive, see the PPU reset for why + cpu.bus.reset(); + cpu.reset(); +} + +pub fn replaceGamepak(cpu: *Arm7tdmi, file_path: []const u8) !void { + try cpu.bus.replaceGamepak(file_path); + reset(cpu); +} diff --git a/src/core/ppu.zig b/src/core/ppu.zig index a6da4a9..35cb7ad 100644 --- a/src/core/ppu.zig +++ b/src/core/ppu.zig @@ -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 { self.allocator.destroy(self.scanline_sprites); self.framebuf.deinit(); diff --git a/src/core/ppu/Oam.zig b/src/core/ppu/Oam.zig index 2e0a78d..f09c0ff 100644 --- a/src/core/ppu/Oam.zig +++ b/src/core/ppu/Oam.zig @@ -34,6 +34,10 @@ pub fn init(allocator: Allocator) !Self { 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 { self.allocator.free(self.buf); self.* = undefined; diff --git a/src/core/ppu/Palette.zig b/src/core/ppu/Palette.zig index d23978a..9e6f36a 100644 --- a/src/core/ppu/Palette.zig +++ b/src/core/ppu/Palette.zig @@ -37,6 +37,10 @@ pub fn init(allocator: Allocator) !Self { 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 { self.allocator.free(self.buf); self.* = undefined; diff --git a/src/core/ppu/Vram.zig b/src/core/ppu/Vram.zig index f599212..f148dc7 100644 --- a/src/core/ppu/Vram.zig +++ b/src/core/ppu/Vram.zig @@ -45,6 +45,10 @@ pub fn init(allocator: Allocator) !Self { 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 { self.allocator.free(self.buf); self.* = undefined; diff --git a/src/core/scheduler.zig b/src/core/scheduler.zig index 8a57183..86ebab1 100644 --- a/src/core/scheduler.zig +++ b/src/core/scheduler.zig @@ -11,11 +11,11 @@ const log = std.log.scoped(.Scheduler); pub const Scheduler = struct { const Self = @This(); - tick: u64, + tick: u64 = 0, queue: PriorityQueue(Event, void, lessThan), pub fn init(allocator: Allocator) Self { - var sched = Self{ .tick = 0, .queue = PriorityQueue(Event, void, lessThan).init(allocator, {}) }; + var sched = Self{ .queue = PriorityQueue(Event, void, lessThan).init(allocator, {}) }; sched.queue.add(.{ .kind = .HeatDeath, .tick = std.math.maxInt(u64) }) catch unreachable; return sched; @@ -26,6 +26,17 @@ pub const Scheduler = struct { self.* = undefined; } + pub fn reset(self: *Self) void { + // `std.PriorityQueue` provides no reset function, so we will just create a new one + const allocator = self.queue.allocator; + self.queue.deinit(); + + var new_queue = PriorityQueue(Event, void, lessThan).init(allocator, {}); + new_queue.add(.{ .kind = .HeatDeath, .tick = std.math.maxInt(u64) }) catch unreachable; + + self.* = .{ .queue = new_queue }; + } + pub inline fn now(self: *const Self) u64 { return self.tick; } diff --git a/src/imgui.zig b/src/imgui.zig new file mode 100644 index 0000000..a710275 --- /dev/null +++ b/src/imgui.zig @@ -0,0 +1,310 @@ +//! Namespace for dealing with ZBA's immediate-mode GUI +//! Currently, ZBA uses zgui from https://github.com/michal-z/zig-gamedev +//! which provides Zig bindings for https://github.com/ocornut/imgui under the hood + +const std = @import("std"); +const zgui = @import("zgui"); +const gl = @import("gl"); +const nfd = @import("nfd"); +const config = @import("config.zig"); +const emu = @import("core/emu.zig"); + +const Gui = @import("platform.zig").Gui; +const Arm7tdmi = @import("core/cpu.zig").Arm7tdmi; +const RingBuffer = @import("zba-util").RingBuffer; + +const Allocator = std.mem.Allocator; +const GLuint = gl.GLuint; + +const gba_width = @import("core/ppu.zig").width; +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; + +/// Immediate-Mode GUI State +pub const State = struct { + title: [12:0]u8, + + fps_hist: RingBuffer(u32), + should_quit: bool = false, + + pub fn init(allocator: Allocator) !@This() { + const history = try allocator.alloc(u32, histogram_len); + + var title: [12:0]u8 = [_:0]u8{0} ** 12; + std.mem.copy(u8, &title, "[No Title]"); + + return .{ + .title = title, + .fps_hist = RingBuffer(u32).init(history), + }; + } + + pub fn deinit(self: *@This(), allocator: Allocator) void { + allocator.free(self.fps_hist.buf); + self.* = undefined; + } +}; + +pub fn draw(state: *State, tex_id: GLuint, cpu: *Arm7tdmi) void { + const win_scale = config.config().host.win_scale; + + { + _ = zgui.beginMainMenuBar(); + defer zgui.endMainMenuBar(); + + if (zgui.beginMenu("File", true)) { + 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}); + break :blk; + }; + + if (maybe_path) |file_path| { + defer nfd.freePath(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; + }; + + // Ideally, state.title = cpu.bus.pak.title + // since state.title is a [12:0]u8 and cpu.bus.pak.title is a [12]u8 + std.mem.copy(u8, &state.title, &cpu.bus.pak.title); + } + } + } + + if (zgui.beginMenu("Emulation", true)) { + defer zgui.endMenu(); + + if (zgui.menuItem("Restart", .{})) { + emu.reset(cpu); + } + } + } + + { + const w = @intToFloat(f32, gba_width * win_scale); + const h = @intToFloat(f32, gba_height * win_scale); + + const window_title = std.mem.sliceTo(&state.title, 0); + _ = zgui.begin(window_title, .{ .flags = .{ .no_resize = true, .always_auto_resize = true } }); + defer zgui.end(); + + zgui.image(@intToPtr(*anyopaque, tex_id), .{ .w = w, .h = h, .uv0 = .{ 0, 1 }, .uv1 = .{ 1, 0 } }); + } + + { + _ = zgui.begin("Information", .{}); + defer zgui.end(); + + for (0..8) |i| { + zgui.text("R{}: 0x{X:0>8}", .{ i, cpu.r[i] }); + + zgui.sameLine(.{}); + + const padding = if (8 + i < 10) " " else ""; + zgui.text("{s}R{}: 0x{X:0>8}", .{ padding, 8 + i, cpu.r[8 + i] }); + } + + zgui.separator(); + + widgets.psr("CPSR", cpu.cpsr); + widgets.psr("SPSR", cpu.spsr); + + zgui.separator(); + + widgets.interrupts(" IE", cpu.bus.io.ie); + widgets.interrupts("IRQ", cpu.bus.io.irq); + } + + { + _ = zgui.begin("Performance", .{}); + defer zgui.end(); + + const tmp = blk: { + var buf: [0x400]u32 = undefined; + const len = state.fps_hist.copy(&buf); + + break :blk .{ buf, len }; + }; + const values = tmp[0]; + const len = tmp[1]; + + if (len == values.len) _ = state.fps_hist.pop(); + + const sorted = blk: { + var buf: @TypeOf(values) = undefined; + + std.mem.copy(u32, buf[0..len], values[0..len]); + std.sort.sort(u32, buf[0..len], {}, std.sort.asc(u32)); + + break :blk buf; + }; + + const y_max = 2 * if (len != 0) @intToFloat(f64, sorted[len - 1]) else emu.frame_rate; + const x_max = @intToFloat(f64, values.len); + + const y_args = .{ .flags = .{ .no_grid_lines = true } }; + const x_args = .{ .flags = .{ .no_grid_lines = true, .no_tick_labels = true, .no_tick_marks = true } }; + + if (zgui.plot.beginPlot("Emulation FPS", .{ .w = 0.0, .flags = .{ .no_title = true, .no_frame = true } })) { + defer zgui.plot.endPlot(); + + zgui.plot.setupLegend(.{ .north = true, .east = true }, .{}); + zgui.plot.setupAxis(.x1, x_args); + zgui.plot.setupAxis(.y1, y_args); + zgui.plot.setupAxisLimits(.y1, .{ .min = 0.0, .max = y_max, .cond = .always }); + zgui.plot.setupAxisLimits(.x1, .{ .min = 0.0, .max = x_max, .cond = .always }); + zgui.plot.setupFinish(); + + zgui.plot.plotLineValues("FPS", u32, .{ .v = values[0..len] }); + } + + const stats: struct { u32, u32, u32 } = blk: { + if (len == 0) break :blk .{ 0, 0, 0 }; + + const average = average: { + var sum: u32 = 0; + for (sorted[0..len]) |value| sum += value; + + break :average @intCast(u32, sum / len); + }; + const median = sorted[len / 2]; + const low = sorted[len / 100]; // 1% Low + + break :blk .{ average, median, low }; + }; + + zgui.text("Average: {:0>3} fps", .{stats[0]}); + zgui.text(" Median: {:0>3} fps", .{stats[1]}); + zgui.text(" 1% Low: {:0>3} fps", .{stats[2]}); + } + + { + _ = zgui.begin("Scheduler", .{}); + defer zgui.end(); + + const scheduler = cpu.sched; + + zgui.text("tick: {X:0>16}", .{scheduler.tick}); + zgui.separator(); + + const Event = std.meta.Child(@TypeOf(scheduler.queue.items)); + + var items: [20]Event = undefined; + const len = scheduler.queue.len; + + std.mem.copy(Event, &items, scheduler.queue.items); + std.sort.sort(Event, items[0..len], {}, widgets.eventDesc(Event)); + + for (items[0..len]) |event| { + zgui.text("{X:0>16} | {?}", .{ event.tick, event.kind }); + } + } + + // { + // zgui.showDemoWindow(null); + // } +} + +const widgets = struct { + fn interrupts(comptime label: []const u8, int: anytype) void { + const h = 15.0; + const w = 9.0 * 2 + 3.5; + const ww = 9.0 * 3; + + { + zgui.text(label ++ ":", .{}); + + zgui.sameLine(.{}); + _ = zgui.selectable("VBL", .{ .w = w, .h = h, .selected = int.vblank.read() }); + + zgui.sameLine(.{}); + _ = zgui.selectable("HBL", .{ .w = w, .h = h, .selected = int.hblank.read() }); + + zgui.sameLine(.{}); + _ = zgui.selectable("VCT", .{ .w = w, .h = h, .selected = int.coincidence.read() }); + + { + zgui.sameLine(.{}); + _ = zgui.selectable("TIM0", .{ .w = ww, .h = h, .selected = int.tim0.read() }); + + zgui.sameLine(.{}); + _ = zgui.selectable("TIM1", .{ .w = ww, .h = h, .selected = int.tim1.read() }); + + zgui.sameLine(.{}); + _ = zgui.selectable("TIM2", .{ .w = ww, .h = h, .selected = int.tim2.read() }); + + zgui.sameLine(.{}); + _ = zgui.selectable("TIM3", .{ .w = ww, .h = h, .selected = int.tim3.read() }); + } + + zgui.sameLine(.{}); + _ = zgui.selectable("SRL", .{ .w = w, .h = h, .selected = int.serial.read() }); + + { + zgui.sameLine(.{}); + _ = zgui.selectable("DMA0", .{ .w = ww, .h = h, .selected = int.dma0.read() }); + + zgui.sameLine(.{}); + _ = zgui.selectable("DMA1", .{ .w = ww, .h = h, .selected = int.dma1.read() }); + + zgui.sameLine(.{}); + _ = zgui.selectable("DMA2", .{ .w = ww, .h = h, .selected = int.dma2.read() }); + + zgui.sameLine(.{}); + _ = zgui.selectable("DMA3", .{ .w = ww, .h = h, .selected = int.dma3.read() }); + } + + zgui.sameLine(.{}); + _ = zgui.selectable("KPD", .{ .w = w, .h = h, .selected = int.keypad.read() }); + + zgui.sameLine(.{}); + _ = zgui.selectable("GPK", .{ .w = w, .h = h, .selected = int.game_pak.read() }); + } + } + + fn psr(comptime label: []const u8, register: anytype) void { + const Mode = @import("core/cpu.zig").Mode; + + const maybe_mode = std.meta.intToEnum(Mode, register.mode.read()) catch null; + const mode = if (maybe_mode) |mode| mode.toString() else "???"; + const w = 9.0; + const h = 15.0; + + zgui.text(label ++ ": 0x{X:0>8}", .{register.raw}); + + zgui.sameLine(.{}); + _ = zgui.selectable("N", .{ .w = w, .h = h, .selected = register.n.read() }); + + zgui.sameLine(.{}); + _ = zgui.selectable("Z", .{ .w = w, .h = h, .selected = register.z.read() }); + + zgui.sameLine(.{}); + _ = zgui.selectable("C", .{ .w = w, .h = h, .selected = register.c.read() }); + + zgui.sameLine(.{}); + _ = zgui.selectable("V", .{ .w = w, .h = h, .selected = register.v.read() }); + + zgui.sameLine(.{}); + zgui.text("{s}", .{mode}); + } + + fn eventDesc(comptime T: type) fn (void, T, T) bool { + return struct { + fn inner(_: void, left: T, right: T) bool { + return left.tick > right.tick; + } + }.inner; + } +}; diff --git a/src/main.zig b/src/main.zig index ba2666e..eff677a 100644 --- a/src/main.zig +++ b/src/main.zig @@ -6,6 +6,7 @@ const clap = @import("clap"); const config = @import("config.zig"); const emu = @import("core/emu.zig"); +const TwoWayChannel = @import("zba-util").TwoWayChannel; const Gui = @import("platform.zig").Gui; const Bus = @import("core/Bus.zig"); const Arm7tdmi = @import("core/cpu.zig").Arm7tdmi; @@ -13,11 +14,8 @@ const Scheduler = @import("core/scheduler.zig").Scheduler; const FilePaths = @import("util.zig").FilePaths; const FpsTracker = @import("util.zig").FpsTracker; const Allocator = std.mem.Allocator; -const Atomic = std.atomic.Atomic; const log = std.log.scoped(.Cli); -const width = @import("core/ppu.zig").width; -const height = @import("core/ppu.zig").height; pub const log_level = if (builtin.mode != .Debug) .info else std.log.default_level; // CLI Arguments + Help Text @@ -72,9 +70,10 @@ pub fn main() void { const paths = handleArguments(allocator, data_path, &result) catch |e| exitln("failed to handle cli arguments: {}", .{e}); defer if (paths.save) |path| allocator.free(path); - const log_file = if (config.config().debug.cpu_trace) blk: { - break :blk std.fs.cwd().createFile("zba.log", .{}) catch |e| exitln("failed to create trace log file: {}", .{e}); - } else null; + const log_file = switch (config.config().debug.cpu_trace) { + true => std.fs.cwd().createFile("zba.log", .{}) catch |e| exitln("failed to create trace log file: {}", .{e}), + false => null, + }; defer if (log_file) |file| file.close(); // TODO: Take Emulator Init Code out of main.zig @@ -91,10 +90,15 @@ pub fn main() void { cpu.fastBoot(); } - var quit = Atomic(bool).init(false); - var gui = Gui.init(&bus.pak.title, &bus.apu, width, height) catch |e| exitln("failed to init gui: {}", .{e}); + // TODO: Just copy the title instead of grabbing a pointer to it + var gui = Gui.init(allocator, &bus.apu) catch |e| exitln("failed to init gui: {}", .{e}); defer gui.deinit(); + var quit = std.atomic.Atomic(bool).init(false); + + var items: [0x100]u8 = undefined; + var channel = TwoWayChannel.init(&items); + if (result.args.gdb) { const Server = @import("gdbstub").Server; const EmuThing = @import("core/emu.zig").EmuThing; @@ -116,26 +120,26 @@ pub fn main() void { gui.run(.{ .cpu = &cpu, .scheduler = &scheduler, - .quit = &quit, + .channel = &channel, }) catch |e| exitln("main thread panicked: {}", .{e}); } else { var tracker = FpsTracker.init(); - const thread = std.Thread.spawn(.{}, emu.run, .{ &quit, &scheduler, &cpu, &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(); gui.run(.{ .cpu = &cpu, .scheduler = &scheduler, + .channel = &channel, .tracker = &tracker, - .quit = &quit, }) 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 { 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", .{}); @@ -184,10 +188,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.", .{}), }; } diff --git a/src/platform.zig b/src/platform.zig index c5edb98..25e2a5d 100644 --- a/src/platform.zig +++ b/src/platform.zig @@ -1,25 +1,36 @@ const std = @import("std"); const SDL = @import("sdl2"); const gl = @import("gl"); +const zgui = @import("zgui"); + const emu = @import("core/emu.zig"); const config = @import("config.zig"); +const imgui = @import("imgui.zig"); const Apu = @import("core/apu.zig").Apu; const Arm7tdmi = @import("core/cpu.zig").Arm7tdmi; const Scheduler = @import("core/scheduler.zig").Scheduler; const FpsTracker = @import("util.zig").FpsTracker; +const TwoWayChannel = @import("zba-util").TwoWayChannel; const gba_width = @import("core/ppu.zig").width; const gba_height = @import("core/ppu.zig").height; +const GLuint = gl.GLuint; +const GLsizei = gl.GLsizei; +const SDL_GLContext = *anyopaque; +const Allocator = std.mem.Allocator; + +const width = 1280; +const height = 720; + pub const sample_rate = 1 << 15; pub const sample_format = SDL.AUDIO_U16; -const default_title = "ZBA"; +const window_title = "ZBA"; pub const Gui = struct { const Self = @This(); - const SDL_GLContext = *anyopaque; // SDL.SDL_GLContext is a ?*anyopaque const log = std.log.scoped(.Gui); // zig fmt: off @@ -39,25 +50,25 @@ pub const Gui = struct { window: *SDL.SDL_Window, ctx: SDL_GLContext, - title: []const u8, audio: Audio, + state: imgui.State, + + allocator: Allocator, program_id: gl.GLuint, - pub fn init(title: *const [12]u8, apu: *Apu, width: i32, height: i32) !Self { + pub fn init(allocator: Allocator, apu: *Apu) !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(); if (SDL.SDL_GL_SetAttribute(SDL.SDL_GL_CONTEXT_MAJOR_VERSION, 3) < 0) panic(); - const win_scale = @intCast(c_int, config.config().host.win_scale); - const window = SDL.SDL_CreateWindow( - default_title, + window_title, SDL.SDL_WINDOWPOS_CENTERED, SDL.SDL_WINDOWPOS_CENTERED, - @as(c_int, width * win_scale), - @as(c_int, height * win_scale), + width, + height, SDL.SDL_WINDOW_OPENGL | SDL.SDL_WINDOW_SHOWN, ) orelse panic(); @@ -67,19 +78,60 @@ pub const Gui = struct { gl.load(ctx, Self.glGetProcAddress) catch {}; if (SDL.SDL_GL_SetSwapInterval(@boolToInt(config.config().host.vsync)) < 0) panic(); - const program_id = try compileShaders(); + zgui.init(allocator); + zgui.plot.init(); + zgui.backend.init(window, ctx, "#version 330 core"); + + // zgui.io.setIniFilename(null); return Self{ .window = window, - .title = std.mem.sliceTo(title, 0), .ctx = ctx, - .program_id = program_id, + .program_id = try compileShaders(), .audio = Audio.init(apu), + + .allocator = allocator, + .state = try imgui.State.init(allocator), }; } - fn compileShaders() !gl.GLuint { - // TODO: Panic on Shader Compiler Failure + Error Message + pub fn deinit(self: *Self) void { + self.audio.deinit(); + self.state.deinit(self.allocator); + + zgui.backend.deinit(); + zgui.plot.deinit(); + zgui.deinit(); + + gl.deleteProgram(self.program_id); + SDL.SDL_GL_DeleteContext(self.ctx); + SDL.SDL_DestroyWindow(self.window); + SDL.SDL_Quit(); + + self.* = undefined; + } + + fn drawGbaTexture(self: *const Self, obj_ids: struct { GLuint, GLuint, GLuint }, tex_id: GLuint, buf: []const u8) void { + gl.bindTexture(gl.TEXTURE_2D, tex_id); + defer gl.bindTexture(gl.TEXTURE_2D, 0); + + gl.texSubImage2D(gl.TEXTURE_2D, 0, 0, 0, gba_width, gba_height, gl.RGBA, gl.UNSIGNED_INT_8_8_8_8, buf.ptr); + + // Bind VAO, EBO. VBO not bound + gl.bindVertexArray(obj_ids[0]); // VAO + defer gl.bindVertexArray(0); + + gl.bindBuffer(gl.ELEMENT_ARRAY_BUFFER, obj_ids[2]); // EBO + defer gl.bindBuffer(gl.ELEMENT_ARRAY_BUFFER, 0); + + // Use compiled frag + vertex shader + gl.useProgram(self.program_id); + defer gl.useProgram(0); + + gl.drawElements(gl.TRIANGLES, 6, gl.UNSIGNED_INT, null); + } + + fn compileShaders() !GLuint { const vert_shader = @embedFile("shader/pixelbuf.vert"); const frag_shader = @embedFile("shader/pixelbuf.frag"); @@ -108,24 +160,29 @@ pub const Gui = struct { } // Returns the VAO ID since it's used in run() - fn generateBuffers() struct { c_uint, c_uint, c_uint } { - var vao_id: c_uint = undefined; - var vbo_id: c_uint = undefined; - var ebo_id: c_uint = undefined; + fn genBufferObjects() struct { GLuint, GLuint, GLuint } { + var vao_id: GLuint = undefined; + var vbo_id: GLuint = undefined; + var ebo_id: GLuint = undefined; + gl.genVertexArrays(1, &vao_id); gl.genBuffers(1, &vbo_id); gl.genBuffers(1, &ebo_id); gl.bindVertexArray(vao_id); + defer gl.bindVertexArray(0); gl.bindBuffer(gl.ARRAY_BUFFER, vbo_id); - gl.bufferData(gl.ARRAY_BUFFER, @sizeOf(@TypeOf(vertices)), &vertices, gl.STATIC_DRAW); + defer gl.bindBuffer(gl.ARRAY_BUFFER, 0); gl.bindBuffer(gl.ELEMENT_ARRAY_BUFFER, ebo_id); + defer gl.bindBuffer(gl.ELEMENT_ARRAY_BUFFER, 0); + + gl.bufferData(gl.ARRAY_BUFFER, @sizeOf(@TypeOf(vertices)), &vertices, gl.STATIC_DRAW); gl.bufferData(gl.ELEMENT_ARRAY_BUFFER, @sizeOf(@TypeOf(indices)), &indices, gl.STATIC_DRAW); // Position - gl.vertexAttribPointer(0, 3, gl.FLOAT, gl.FALSE, 8 * @sizeOf(f32), @intToPtr(?*anyopaque, 0)); // lmao + gl.vertexAttribPointer(0, 3, gl.FLOAT, gl.FALSE, 8 * @sizeOf(f32), null); // lmao gl.enableVertexAttribArray(0); // Colour gl.vertexAttribPointer(1, 3, gl.FLOAT, gl.FALSE, 8 * @sizeOf(f32), @intToPtr(?*anyopaque, (3 * @sizeOf(f32)))); @@ -137,25 +194,56 @@ pub const Gui = struct { return .{ vao_id, vbo_id, ebo_id }; } - fn generateTexture(buf: []const u8) c_uint { - var tex_id: c_uint = undefined; + fn genGbaTexture(buf: []const u8) GLuint { + var tex_id: GLuint = undefined; gl.genTextures(1, &tex_id); - gl.bindTexture(gl.TEXTURE_2D, tex_id); - // gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, gl.CLAMP_TO_EDGE); - // gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, gl.CLAMP_TO_EDGE); + gl.bindTexture(gl.TEXTURE_2D, tex_id); + defer gl.bindTexture(gl.TEXTURE_2D, 0); gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.NEAREST); gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.NEAREST); gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA, gba_width, gba_height, 0, gl.RGBA, gl.UNSIGNED_INT_8_8_8_8, buf.ptr); - // gl.generateMipmap(gl.TEXTURE_2D); // TODO: Remove? return tex_id; } + fn genOutTexture() GLuint { + var tex_id: GLuint = undefined; + gl.genTextures(1, &tex_id); + + gl.bindTexture(gl.TEXTURE_2D, tex_id); + defer gl.bindTexture(gl.TEXTURE_2D, 0); + + gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.NEAREST); + gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.NEAREST); + + gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA, gba_width, gba_height, 0, gl.RGBA, gl.UNSIGNED_INT_8_8_8_8, null); + + return tex_id; + } + + fn genFrameBufObject(tex_id: c_uint) !GLuint { + var fbo_id: GLuint = undefined; + gl.genFramebuffers(1, &fbo_id); + + gl.bindFramebuffer(gl.FRAMEBUFFER, fbo_id); + defer gl.bindFramebuffer(gl.FRAMEBUFFER, 0); + + gl.framebufferTexture(gl.FRAMEBUFFER, gl.COLOR_ATTACHMENT0, tex_id, 0); + + const draw_buffers: [1]GLuint = .{gl.COLOR_ATTACHMENT0}; + gl.drawBuffers(1, &draw_buffers); + + if (gl.checkFramebufferStatus(gl.FRAMEBUFFER) != gl.FRAMEBUFFER_COMPLETE) + return error.FrameBufferObejctInitFailed; + + return fbo_id; + } + const RunOptions = struct { - quit: *std.atomic.Atomic(bool), + channel: *TwoWayChannel, tracker: ?*FpsTracker = null, cpu: *Arm7tdmi, scheduler: *Scheduler, @@ -164,29 +252,34 @@ pub const Gui = struct { pub fn run(self: *Self, opt: RunOptions) !void { const cpu = opt.cpu; const tracker = opt.tracker; - const quit = opt.quit; + const channel = opt.channel; - var buffer_ids = Self.generateBuffers(); - defer { - gl.deleteBuffers(1, &buffer_ids[2]); // EBO - gl.deleteBuffers(1, &buffer_ids[1]); // VBO - gl.deleteVertexArrays(1, &buffer_ids[0]); // VAO - } - const vao_id = buffer_ids[0]; + const obj_ids = Self.genBufferObjects(); + defer gl.deleteBuffers(3, @as(*const [3]c_uint, &obj_ids)); - const tex_id = Self.generateTexture(cpu.bus.ppu.framebuf.get(.Renderer)); - defer gl.deleteTextures(1, &tex_id); + const emu_tex = Self.genGbaTexture(cpu.bus.ppu.framebuf.get(.Renderer)); + const out_tex = Self.genOutTexture(); + defer gl.deleteTextures(2, &[_]c_uint{ emu_tex, out_tex }); - var title_buf: [0x100]u8 = undefined; + const fbo_id = try Self.genFrameBufObject(out_tex); + defer gl.deleteFramebuffers(1, &fbo_id); emu_loop: while (true) { + // `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 + 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 + // should exit, in which case we should also handle this + if (self.state.should_quit) break :emu_loop; + var event: SDL.SDL_Event = undefined; - - // This might be true if the emu is running via a gdbstub server - // and the gdb stub exits first - if (quit.load(.Monotonic)) break :emu_loop; - while (SDL.SDL_PollEvent(&event) != 0) { + _ = zgui.backend.processEvent(&event); + switch (event.type) { SDL.SDL_QUIT => break :emu_loop, SDL.SDL_KEYDOWN => { @@ -224,12 +317,6 @@ pub const Gui = struct { SDL.SDLK_s => keyinput.shoulder_r.set(), SDL.SDLK_RETURN => keyinput.start.set(), SDL.SDLK_RSHIFT => keyinput.select.set(), - SDL.SDLK_i => { - comptime std.debug.assert(sample_format == SDL.AUDIO_U16); - log.err("Sample Count: {}", .{@intCast(u32, SDL.SDL_AudioStreamAvailable(cpu.bus.apu.stream)) / (2 * @sizeOf(u16))}); - }, - // SDL.SDLK_j => log.err("Scheduler Capacity: {} | Scheduler Event Count: {}", .{ scheduler.queue.capacity(), scheduler.queue.count() }), - SDL.SDLK_k => {}, else => {}, } @@ -239,31 +326,44 @@ pub const Gui = struct { } } - // Emulator has an internal Double Buffer - const framebuf = cpu.bus.ppu.framebuf.get(.Renderer); - gl.texSubImage2D(gl.TEXTURE_2D, 0, 0, 0, gba_width, gba_height, gl.RGBA, gl.UNSIGNED_INT_8_8_8_8, framebuf.ptr); + { + channel.emu.push(.Pause); + defer channel.emu.push(.Resume); - gl.useProgram(self.program_id); - gl.bindVertexArray(vao_id); - gl.drawElements(gl.TRIANGLES, 6, gl.UNSIGNED_INT, null); - SDL.SDL_GL_SwapWindow(self.window); + // 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}), + }; - if (tracker) |t| { - const dyn_title = std.fmt.bufPrintZ(&title_buf, "ZBA | {s} [Emu: {}fps] ", .{ self.title, t.value() }) catch unreachable; - SDL.SDL_SetWindowTitle(self.window, dyn_title.ptr); + // Add FPS count to the histogram + if (tracker) |t| self.state.fps_hist.push(t.value()) catch {}; + + // Draw GBA Screen to Texture + { + gl.bindFramebuffer(gl.FRAMEBUFFER, fbo_id); + defer gl.bindFramebuffer(gl.FRAMEBUFFER, 0); + + const buf = cpu.bus.ppu.framebuf.get(.Renderer); + gl.viewport(0, 0, gba_width, gba_height); + self.drawGbaTexture(obj_ids, emu_tex, buf); + } + + // Background Colour + const size = zgui.io.getDisplaySize(); + gl.viewport(0, 0, @floatToInt(c_int, size[0]), @floatToInt(c_int, size[1])); + gl.clearColor(0, 0, 0, 1.0); + gl.clear(gl.COLOR_BUFFER_BIT); + + zgui.backend.newFrame(width, height); + imgui.draw(&self.state, out_tex, cpu); + zgui.backend.draw(); } + + SDL.SDL_GL_SwapWindow(self.window); } - quit.store(true, .Monotonic); // Terminate Emulator Thread - } - - pub fn deinit(self: *Self) void { - self.audio.deinit(); - gl.deleteProgram(self.program_id); - SDL.SDL_GL_DeleteContext(self.ctx); - SDL.SDL_DestroyWindow(self.window); - SDL.SDL_Quit(); - self.* = undefined; + channel.emu.push(.Quit); } fn glGetProcAddress(ctx: SDL.SDL_GLContext, proc: [:0]const u8) ?*anyopaque { diff --git a/src/util.zig b/src/util.zig index 9e5eca1..9b6bbc9 100644 --- a/src/util.zig +++ b/src/util.zig @@ -7,27 +7,6 @@ const Arm7tdmi = @import("core/cpu.zig").Arm7tdmi; 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 { 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 /// /// 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 { - rom: []const u8, + rom: ?[]const u8, bios: ?[]const u8, save: ?[]const u8, }; @@ -283,7 +251,7 @@ pub const FrameBuffer = struct { layers: [2][]u8, buf: []u8, - current: u1, + current: u1 = 0, allocator: Allocator, @@ -298,12 +266,16 @@ pub const FrameBuffer = struct { // Front and Back Framebuffers .layers = [_][]u8{ buf[0..][0..len], buf[len..][0..len] }, .buf = buf, - .current = 0, .allocator = allocator, }; } + pub fn reset(self: *Self) void { + std.mem.set(u8, self.buf, 0); + self.current = 0; + } + pub fn deinit(self: *Self) void { self.allocator.free(self.buf); self.* = undefined;