From 5e94cbfbead8b720b4eae348f25afc6e79443aa7 Mon Sep 17 00:00:00 2001 From: Rekai Musuka Date: Sun, 1 Jan 2023 03:42:02 -0600 Subject: [PATCH 01/24] feat: add imgui support using zgui --- .gitignore | 6 +- .gitmodules | 3 + build.zig | 12 +++- lib/zgui | 1 + src/main.zig | 8 +-- src/platform.zig | 173 +++++++++++++++++++++++++++++++++++++---------- 6 files changed, 161 insertions(+), 42 deletions(-) create mode 160000 lib/zgui 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..9ce0f12 100644 --- a/.gitmodules +++ b/.gitmodules @@ -16,3 +16,6 @@ [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 diff --git a/build.zig b/build.zig index 2cf2bb2..b2df81a 100644 --- a/build.zig +++ b/build.zig @@ -2,7 +2,8 @@ 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"); pub fn build(b: *std.build.Builder) void { // Minimum Zig Version @@ -43,13 +44,20 @@ pub fn build(b: *std.build.Builder) void { exe.addAnonymousModule("gl", .{ .source_file = .{ .path = "lib/gl.zig" } }); // gdbstub - Gdbstub.link(exe); + gdbstub.link(exe); // 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_options = zgui.BuildOptionsStep.init(b, .{ .backend = .sdl2_opengl3 }); + const zgui_pkg = zgui.getPkg(&.{zgui_options.getPkg()}); + exe.addPackage(zgui_pkg); + zgui.link(exe, zgui_options); + + exe.setBuildMode(mode); exe.install(); const run_cmd = exe.run(); diff --git a/lib/zgui b/lib/zgui new file mode 160000 index 0000000..7b01afc --- /dev/null +++ b/lib/zgui @@ -0,0 +1 @@ +Subproject commit 7b01afc274db7231853dfd65c94dcd32f3f36b74 diff --git a/src/main.zig b/src/main.zig index ba2666e..4120b31 100644 --- a/src/main.zig +++ b/src/main.zig @@ -16,8 +16,6 @@ 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 @@ -91,10 +89,12 @@ 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.pak.title, &bus.apu) catch |e| exitln("failed to init gui: {}", .{e}); defer gui.deinit(); + var quit = Atomic(bool).init(false); + if (result.args.gdb) { const Server = @import("gdbstub").Server; const EmuThing = @import("core/emu.zig").EmuThing; diff --git a/src/platform.zig b/src/platform.zig index c5edb98..443e1cb 100644 --- a/src/platform.zig +++ b/src/platform.zig @@ -1,6 +1,8 @@ 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"); @@ -12,6 +14,14 @@ const FpsTracker = @import("util.zig").FpsTracker; 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; @@ -19,7 +29,6 @@ const default_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 @@ -44,20 +53,20 @@ pub const Gui = struct { program_id: gl.GLuint, - pub fn init(title: *const [12]u8, apu: *Apu, width: i32, height: i32) !Self { + pub fn init(allocator: Allocator, title: *const [12]u8, 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 win_scale = @intCast(c_int, config.config().host.win_scale); const window = SDL.SDL_CreateWindow( default_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 +76,39 @@ 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.backend.init(window, ctx, "#version 330 core"); return Self{ .window = window, .title = std.mem.sliceTo(title, 0), .ctx = ctx, - .program_id = program_id, + .program_id = try compileShaders(), .audio = Audio.init(apu), }; } - fn compileShaders() !gl.GLuint { - // TODO: Panic on Shader Compiler Failure + Error Message + 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,20 +137,25 @@ 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 @@ -137,23 +171,72 @@ 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; + } + + fn draw(self: *Self, tex_id: GLuint) void { + _ = self; + + { + _ = zgui.begin("Game Boy Advance Screen", .{ .flags = .{ .no_resize = true } }); + defer zgui.end(); + + const args = .{ + .w = gba_width, + .h = gba_height, + .uv0 = .{ 0.0, 1.0 }, + .uv1 = .{ 1.0, 0.0 }, + }; + + zgui.image(@intToPtr(*anyopaque, tex_id), args); + } + } + const RunOptions = struct { quit: *std.atomic.Atomic(bool), tracker: ?*FpsTracker = null, @@ -166,16 +249,18 @@ pub const Gui = struct { const tracker = opt.tracker; const quit = opt.quit; - 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 }); + + const fbo_id = try Self.genFrameBufObject(out_tex); + defer gl.deleteFramebuffers(1, &fbo_id); + + var quit = std.atomic.Atomic(bool).init(false); + var tracker = FpsTracker.init(); var title_buf: [0x100]u8 = undefined; @@ -187,6 +272,8 @@ pub const Gui = struct { 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 => { @@ -239,13 +326,25 @@ 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); + { + 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 + 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); + self.draw(out_tex); + zgui.backend.draw(); - gl.useProgram(self.program_id); - gl.bindVertexArray(vao_id); - gl.drawElements(gl.TRIANGLES, 6, gl.UNSIGNED_INT, null); SDL.SDL_GL_SwapWindow(self.window); if (tracker) |t| { @@ -259,6 +358,10 @@ pub const Gui = struct { pub fn deinit(self: *Self) void { self.audio.deinit(); + + zgui.backend.deinit(); + zgui.deinit(); + gl.deleteProgram(self.program_id); SDL.SDL_GL_DeleteContext(self.ctx); SDL.SDL_DestroyWindow(self.window); From 3dcc4cb385d17e9a44e9458d251c9fd3d818cc5f Mon Sep 17 00:00:00 2001 From: Rekai Musuka Date: Sun, 1 Jan 2023 12:58:08 -0600 Subject: [PATCH 02/24] fix: update zgui to work with sdl2 vcpkg package --- lib/zgui | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/zgui b/lib/zgui index 7b01afc..30fa220 160000 --- a/lib/zgui +++ b/lib/zgui @@ -1 +1 @@ -Subproject commit 7b01afc274db7231853dfd65c94dcd32f3f36b74 +Subproject commit 30fa220dfe8dd6a5e94012799f56de62d3bb32f6 From fe6fc0e517a6bf26604eb601a1bb350ee7a7372a Mon Sep 17 00:00:00 2001 From: Rekai Musuka Date: Fri, 6 Jan 2023 21:17:32 -0600 Subject: [PATCH 03/24] feat: add system information window --- src/platform.zig | 146 +++++++++++++++++++++++++++++++++++++++-------- src/util.zig | 75 ++++++++++++++++++++++++ 2 files changed, 197 insertions(+), 24 deletions(-) diff --git a/src/platform.zig b/src/platform.zig index 443e1cb..d673e04 100644 --- a/src/platform.zig +++ b/src/platform.zig @@ -10,6 +10,7 @@ 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 RingBuffer = @import("util.zig").RingBuffer; const gba_width = @import("core/ppu.zig").width; const gba_height = @import("core/ppu.zig").height; @@ -31,6 +32,25 @@ pub const Gui = struct { const Self = @This(); const log = std.log.scoped(.Gui); + const State = struct { + fps_hist: RingBuffer(u32), + allocator: Allocator, + + pub fn init(allocator: Allocator) !@This() { + const history = try allocator.alloc(u32, 0x400); + + return .{ + .fps_hist = RingBuffer(u32).init(history), + .allocator = allocator, + }; + } + + fn deinit(self: *@This()) void { + self.fps_hist.deinit(self.allocator); + self.* = undefined; + } + }; + // zig fmt: off const vertices: [32]f32 = [_]f32{ // Positions // Colours // Texture Coords @@ -51,6 +71,8 @@ pub const Gui = struct { title: []const u8, audio: Audio, + state: State, + program_id: gl.GLuint, pub fn init(allocator: Allocator, title: *const [12]u8, apu: *Apu) !Self { @@ -59,8 +81,6 @@ pub const Gui = struct { 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, SDL.SDL_WINDOWPOS_CENTERED, @@ -77,17 +97,37 @@ pub const Gui = struct { if (SDL.SDL_GL_SetSwapInterval(@boolToInt(config.config().host.vsync)) < 0) panic(); 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 = try compileShaders(), .audio = Audio.init(apu), + + .state = try State.init(allocator), }; } + pub fn deinit(self: *Self) void { + self.audio.deinit(); + self.state.deinit(); + + 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); @@ -219,21 +259,89 @@ pub const Gui = struct { return fbo_id; } - fn draw(self: *Self, tex_id: GLuint) void { - _ = self; + fn draw(self: *Self, tex_id: GLuint, cpu: *const Arm7tdmi) void { + _ = cpu; + const win_scale = config.config().host.win_scale; { - _ = zgui.begin("Game Boy Advance Screen", .{ .flags = .{ .no_resize = true } }); + _ = zgui.begin("Game Boy Advance Screen", .{ .flags = .{ .no_resize = true, .always_auto_resize = true } }); defer zgui.end(); - const args = .{ - .w = gba_width, - .h = gba_height, + const img_args = .{ + .w = @intToFloat(f32, gba_width * win_scale), + .h = @intToFloat(f32, gba_height * win_scale), .uv0 = .{ 0.0, 1.0 }, .uv1 = .{ 1.0, 0.0 }, }; - zgui.image(@intToPtr(*anyopaque, tex_id), args); + zgui.image(@intToPtr(*anyopaque, tex_id), img_args); + } + + { + _ = zgui.begin("Emulator Performance", .{}); + + const tmp = blk: { + var buf: [0x400]u32 = undefined; + const len = self.state.fps_hist.copy(&buf); + + break :blk .{ buf, len }; + }; + const values = tmp[0]; + const len = tmp[1]; + + if (len == values.len) _ = self.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 } })) { + 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] }); + zgui.plot.endPlot(); + } + + 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]}); + + defer zgui.end(); + } + + { + zgui.showDemoWindow(null); } } @@ -342,13 +450,16 @@ pub const Gui = struct { gl.clear(gl.COLOR_BUFFER_BIT); zgui.backend.newFrame(width, height); - self.draw(out_tex); + self.draw(out_tex, cpu); zgui.backend.draw(); SDL.SDL_GL_SwapWindow(self.window); if (tracker) |t| { - const dyn_title = std.fmt.bufPrintZ(&title_buf, "ZBA | {s} [Emu: {}fps] ", .{ self.title, t.value() }) catch unreachable; + const emu_fps = t.value(); + self.state.fps_hist.push(emu_fps) catch {}; + + const dyn_title = std.fmt.bufPrintZ(&title_buf, "ZBA | {s} [Emu: {}fps] ", .{ self.title, emu_fps }) catch unreachable; SDL.SDL_SetWindowTitle(self.window, dyn_title.ptr); } } @@ -356,19 +467,6 @@ pub const Gui = struct { quit.store(true, .Monotonic); // Terminate Emulator Thread } - pub fn deinit(self: *Self) void { - self.audio.deinit(); - - zgui.backend.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 glGetProcAddress(ctx: SDL.SDL_GLContext, proc: [:0]const u8) ?*anyopaque { _ = ctx; return SDL.SDL_GL_GetProcAddress(proc.ptr); diff --git a/src/util.zig b/src/util.zig index 9e5eca1..b135e2a 100644 --- a/src/util.zig +++ b/src/util.zig @@ -317,3 +317,78 @@ pub const FrameBuffer = struct { 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) |*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); + } + }; +} From ae78588b807296e7e8d636a7dffcae38c815fadf Mon Sep 17 00:00:00 2001 From: Rekai Musuka Date: Sat, 7 Jan 2023 01:24:51 -0600 Subject: [PATCH 04/24] feat: implement ui for register, interrupt --- src/core/bus/io.zig | 8 +-- src/core/cpu.zig | 4 +- src/platform.zig | 134 +++++++++++++++++++++++++++++++++++++++----- 3 files changed, 126 insertions(+), 20 deletions(-) diff --git a/src/core/bus/io.zig b/src/core/bus/io.zig index b4761b4..3b62d1f 100644 --- a/src/core/bus/io.zig +++ b/src/core/bus/io.zig @@ -346,10 +346,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/cpu.zig b/src/core/cpu.zig index 90e00d4..669d195 100644 --- a/src/core/cpu.zig +++ b/src/core/cpu.zig @@ -642,7 +642,7 @@ pub const PSR = extern union { } }; -const Mode = enum(u5) { +pub const Mode = enum(u5) { User = 0b10000, Fiq = 0b10001, Irq = 0b10010, @@ -651,7 +651,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/platform.zig b/src/platform.zig index d673e04..1b3c533 100644 --- a/src/platform.zig +++ b/src/platform.zig @@ -100,7 +100,7 @@ pub const Gui = struct { zgui.plot.init(); zgui.backend.init(window, ctx, "#version 330 core"); - zgui.io.setIniFilename(null); + // zgui.io.setIniFilename(null); return Self{ .window = window, @@ -260,25 +260,48 @@ pub const Gui = struct { } fn draw(self: *Self, tex_id: GLuint, cpu: *const Arm7tdmi) void { - _ = cpu; const win_scale = config.config().host.win_scale; { + const w = @intToFloat(f32, gba_width * win_scale); + const h = @intToFloat(f32, gba_height * win_scale); + _ = zgui.begin("Game Boy Advance Screen", .{ .flags = .{ .no_resize = true, .always_auto_resize = true } }); defer zgui.end(); - const img_args = .{ - .w = @intToFloat(f32, gba_width * win_scale), - .h = @intToFloat(f32, gba_height * win_scale), - .uv0 = .{ 0.0, 1.0 }, - .uv1 = .{ 1.0, 0.0 }, - }; - - zgui.image(@intToPtr(*anyopaque, tex_id), img_args); + zgui.image(@intToPtr(*anyopaque, tex_id), .{ .w = w, .h = h, .uv0 = .{ 0, 1 }, .uv1 = .{ 1, 0 } }); } { - _ = zgui.begin("Emulator Performance", .{}); + _ = zgui.begin("Information", .{}); + defer zgui.end(); + + { + var i: usize = 0; + while (i < 8) : (i += 1) { + zgui.text("R{}: 0x{X:0>8}", .{ i, cpu.r[i] }); + + zgui.sameLine(.{}); + + const prefix = if (8 + i < 10) " " else ""; + zgui.text("{s}R{}: 0x{X:0>8}", .{ prefix, 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); // space is for padding + widgets.interrupts("IRQ", cpu.bus.io.irq); + } + + { + _ = zgui.begin("Performance", .{}); + defer zgui.end(); const tmp = blk: { var buf: [0x400]u32 = undefined; @@ -307,6 +330,8 @@ pub const Gui = struct { 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); @@ -315,7 +340,6 @@ pub const Gui = struct { zgui.plot.setupFinish(); zgui.plot.plotLineValues("FPS", u32, .{ .v = values[0..len] }); - zgui.plot.endPlot(); } const stats: struct { u32, u32, u32 } = blk: { @@ -336,8 +360,6 @@ pub const Gui = struct { 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]}); - - defer zgui.end(); } { @@ -542,3 +564,87 @@ fn panic() noreturn { const str = @as(?[*:0]const u8, SDL.SDL_GetError()) orelse "unknown error"; @panic(std.mem.sliceTo(str, 0)); } + +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}); + } +}; From a8fac5f3c6727413377fa95fab9a02fcf53c13d9 Mon Sep 17 00:00:00 2001 From: Rekai Musuka Date: Sun, 8 Jan 2023 19:26:48 -0600 Subject: [PATCH 05/24] feat: pause emu when UI reads emu state --- src/core/emu.zig | 14 ++++++---- src/platform.zig | 66 +++++++++++++++++++++++++----------------------- 2 files changed, 43 insertions(+), 37 deletions(-) diff --git a/src/core/emu.zig b/src/core/emu.zig index 1a84c11..11931d7 100644 --- a/src/core/emu.zig +++ b/src/core/emu.zig @@ -4,7 +4,7 @@ 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 Timer = std.time.Timer; const Atomic = std.atomic.Atomic; @@ -35,18 +35,18 @@ const RunKind = enum { LimitedFPS, }; -pub fn run(quit: *Atomic(bool), scheduler: *Scheduler, cpu: *Arm7tdmi, tracker: *FpsTracker) void { +pub fn run(quit: *Atomic(bool), pause: *Atomic(bool), cpu: *Arm7tdmi, scheduler: *Scheduler, tracker: *Tracker) 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, quit, pause, cpu, scheduler, tracker); } else { - inner(.UnlimitedFPS, audio_sync, quit, scheduler, cpu, tracker); + inner(.UnlimitedFPS, audio_sync, quit, pause, cpu, scheduler, tracker); } } -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, quit: *Atomic(bool), pause: *Atomic(bool), cpu: *Arm7tdmi, scheduler: *Scheduler, tracker: ?*Tracker) void { if (kind == .UnlimitedFPS or kind == .LimitedFPS) { std.debug.assert(tracker != null); log.info("FPS tracking enabled", .{}); @@ -57,6 +57,8 @@ fn inner(comptime kind: RunKind, audio_sync: bool, quit: *Atomic(bool), schedule log.info("Emulation w/out video sync", .{}); while (!quit.load(.Monotonic)) { + if (pause.load(.Monotonic)) continue; + runFrame(scheduler, cpu); audioSync(audio_sync, cpu.bus.apu.stream, &cpu.bus.apu.is_buffer_full); @@ -69,6 +71,8 @@ fn inner(comptime kind: RunKind, audio_sync: bool, quit: *Atomic(bool), schedule var wake_time: u64 = frame_period; while (!quit.load(.Monotonic)) { + if (pause.load(.Monotonic)) continue; + runFrame(scheduler, cpu); const new_wake_time = videoSync(&timer, wake_time); diff --git a/src/platform.zig b/src/platform.zig index 1b3c533..3fe8ab8 100644 --- a/src/platform.zig +++ b/src/platform.zig @@ -389,8 +389,9 @@ pub const Gui = struct { const fbo_id = try Self.genFrameBufObject(out_tex); defer gl.deleteFramebuffers(1, &fbo_id); - var quit = std.atomic.Atomic(bool).init(false); var tracker = FpsTracker.init(); + var quit = std.atomic.Atomic(bool).init(false); + var pause = std.atomic.Atomic(bool).init(false); var title_buf: [0x100]u8 = undefined; @@ -441,12 +442,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 => {}, } @@ -456,37 +451,44 @@ pub const Gui = struct { } } + // We Access non-atomic parts of the Emulator here { - gl.bindFramebuffer(gl.FRAMEBUFFER, fbo_id); - defer gl.bindFramebuffer(gl.FRAMEBUFFER, 0); + pause.store(true, .Monotonic); + defer pause.store(false, .Monotonic); - const buf = cpu.bus.ppu.framebuf.get(.Renderer); - gl.viewport(0, 0, gba_width, gba_height); - self.drawGbaTexture(obj_ids, emu_tex, buf); + self.state.fps_hist.push(tracker.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); + + if (tracker) |t| { + const emu_fps = t.value(); + self.state.fps_hist.push(emu_fps) catch {}; + + const dyn_title = std.fmt.bufPrintZ(&title_buf, "ZBA | {s} [Emu: {}fps] ", .{ self.title, emu_fps }) catch unreachable; + SDL.SDL_SetWindowTitle(self.window, dyn_title.ptr); + } + + zgui.backend.newFrame(width, height); + self.draw(out_tex, cpu); + zgui.backend.draw(); } - // Background - 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); - self.draw(out_tex, cpu); - zgui.backend.draw(); - SDL.SDL_GL_SwapWindow(self.window); - - if (tracker) |t| { - const emu_fps = t.value(); - self.state.fps_hist.push(emu_fps) catch {}; - - const dyn_title = std.fmt.bufPrintZ(&title_buf, "ZBA | {s} [Emu: {}fps] ", .{ self.title, emu_fps }) catch unreachable; - SDL.SDL_SetWindowTitle(self.window, dyn_title.ptr); - } } - - quit.store(true, .Monotonic); // Terminate Emulator Thread } fn glGetProcAddress(ctx: SDL.SDL_GLContext, proc: [:0]const u8) ?*anyopaque { From 1d601dba39040f1135af11c1154167226cc0ae87 Mon Sep 17 00:00:00 2001 From: Rekai Musuka Date: Sun, 8 Jan 2023 19:48:30 -0600 Subject: [PATCH 06/24] feat: add scheduler ui --- src/platform.zig | 30 ++++++++++++++++++++++++++++++ 1 file changed, 30 insertions(+) diff --git a/src/platform.zig b/src/platform.zig index 3fe8ab8..039c5ce 100644 --- a/src/platform.zig +++ b/src/platform.zig @@ -362,6 +362,28 @@ pub const Gui = struct { 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); } @@ -649,4 +671,12 @@ const widgets = struct { 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; + } }; From 3e98f4053aadac8b8ac5b4882810734113eb2f53 Mon Sep 17 00:00:00 2001 From: Rekai Musuka Date: Mon, 16 Jan 2023 03:35:50 -0600 Subject: [PATCH 07/24] chore: update zgui --- lib/zgui | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/zgui b/lib/zgui index 30fa220..f9744ec 160000 --- a/lib/zgui +++ b/lib/zgui @@ -1 +1 @@ -Subproject commit 30fa220dfe8dd6a5e94012799f56de62d3bb32f6 +Subproject commit f9744ec8d613028b908896c410be937b5513f73b From ff609c85baf5ba9381811cf59a2e8fcd5994e76c Mon Sep 17 00:00:00 2001 From: Rekai Musuka Date: Mon, 16 Jan 2023 04:49:55 -0600 Subject: [PATCH 08/24] feat: show game title as imgui screen title --- src/core/bus/GamePak.zig | 16 +++++++--------- src/main.zig | 2 +- src/platform.zig | 25 ++++++++++++------------- 3 files changed, 20 insertions(+), 23 deletions(-) diff --git a/src/core/bus/GamePak.zig b/src/core/bus/GamePak.zig index 2698c44..750202e 100644 --- a/src/core/bus/GamePak.zig +++ b/src/core/bus/GamePak.zig @@ -188,7 +188,7 @@ pub fn init(allocator: Allocator, cpu: *Arm7tdmi, rom_path: []const u8, save_pat const kind = Backup.guess(file_buf); const device = if (config.config().guest.force_rtc) .Rtc else guessDevice(file_buf); - logHeader(file_buf, &title); + logHeader(file_buf, title); return .{ .buf = file_buf, @@ -220,19 +220,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]; +fn logHeader(buf: []const u8, title: [12]u8) void { + const ver = 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}); + if (ver != 0) log.info("Version: {}", .{ver}); + + log.info("Game Code: {s}", .{buf[0xAC..0xB0]}); + log.info("Maker Code: {s}", .{buf[0xB0..0xB2]}); } test "OOB Access" { diff --git a/src/main.zig b/src/main.zig index 4120b31..087bfc3 100644 --- a/src/main.zig +++ b/src/main.zig @@ -90,7 +90,7 @@ pub fn main() void { } // 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.pak.title, &bus.apu) catch |e| exitln("failed to init gui: {}", .{e}); defer gui.deinit(); var quit = Atomic(bool).init(false); diff --git a/src/platform.zig b/src/platform.zig index 039c5ce..132895d 100644 --- a/src/platform.zig +++ b/src/platform.zig @@ -34,19 +34,14 @@ pub const Gui = struct { const State = struct { fps_hist: RingBuffer(u32), - allocator: Allocator, pub fn init(allocator: Allocator) !@This() { const history = try allocator.alloc(u32, 0x400); - - return .{ - .fps_hist = RingBuffer(u32).init(history), - .allocator = allocator, - }; + return .{ .fps_hist = RingBuffer(u32).init(history) }; } - fn deinit(self: *@This()) void { - self.fps_hist.deinit(self.allocator); + fn deinit(self: *@This(), allocator: Allocator) void { + self.fps_hist.deinit(allocator); self.* = undefined; } }; @@ -68,14 +63,15 @@ pub const Gui = struct { window: *SDL.SDL_Window, ctx: SDL_GLContext, - title: []const u8, + title: [:0]const u8, audio: Audio, state: State, + allocator: Allocator, program_id: gl.GLuint, - pub fn init(allocator: Allocator, title: *const [12]u8, apu: *Apu) !Self { + pub fn init(allocator: Allocator, title: [12]u8, 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(); @@ -104,18 +100,19 @@ pub const Gui = struct { return Self{ .window = window, - .title = std.mem.sliceTo(title, 0), + .title = try allocator.dupeZ(u8, &title), .ctx = ctx, .program_id = try compileShaders(), .audio = Audio.init(apu), + .allocator = allocator, .state = try State.init(allocator), }; } pub fn deinit(self: *Self) void { self.audio.deinit(); - self.state.deinit(); + self.state.deinit(self.allocator); zgui.backend.deinit(); zgui.plot.deinit(); @@ -125,6 +122,8 @@ pub const Gui = struct { SDL.SDL_GL_DeleteContext(self.ctx); SDL.SDL_DestroyWindow(self.window); SDL.SDL_Quit(); + + self.allocator.free(self.title); self.* = undefined; } @@ -266,7 +265,7 @@ pub const Gui = struct { const w = @intToFloat(f32, gba_width * win_scale); const h = @intToFloat(f32, gba_height * win_scale); - _ = zgui.begin("Game Boy Advance Screen", .{ .flags = .{ .no_resize = true, .always_auto_resize = true } }); + _ = zgui.begin(self.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 } }); From 6048458f9b1e15388cf5332ea4c01b4832cc8faf Mon Sep 17 00:00:00 2001 From: Rekai Musuka Date: Tue, 17 Jan 2023 17:55:45 -0600 Subject: [PATCH 09/24] feat: implement menu bar + add file picker dep --- .gitmodules | 3 +++ build.zig | 6 +++++- lib/nfd-zig | 1 + src/platform.zig | 34 ++++++++++++++++++++++++++++++++++ 4 files changed, 43 insertions(+), 1 deletion(-) create mode 160000 lib/nfd-zig diff --git a/.gitmodules b/.gitmodules index 9ce0f12..9d0cf56 100644 --- a/.gitmodules +++ b/.gitmodules @@ -19,3 +19,6 @@ [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 diff --git a/build.zig b/build.zig index b2df81a..51b7d55 100644 --- a/build.zig +++ b/build.zig @@ -4,6 +4,7 @@ const builtin = @import("builtin"); const Sdk = @import("lib/SDL.zig/Sdk.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 @@ -45,6 +46,9 @@ pub fn build(b: *std.build.Builder) void { // gdbstub gdbstub.link(exe); + // NativeFileDialog(ue) Bindings + exe.linkLibrary(nfd.makeLib(b, mode, target)); + exe.addPackage(nfd.getPackage("nfd")); // Zig SDL Bindings: https://github.com/MasterQ32/SDL.zig const sdk = Sdk.init(b, null); @@ -54,8 +58,8 @@ pub fn build(b: *std.build.Builder) void { // Dear ImGui bindings const zgui_options = zgui.BuildOptionsStep.init(b, .{ .backend = .sdl2_opengl3 }); const zgui_pkg = zgui.getPkg(&.{zgui_options.getPkg()}); - exe.addPackage(zgui_pkg); zgui.link(exe, zgui_options); + exe.addPackage(zgui_pkg); exe.setBuildMode(mode); exe.install(); diff --git a/lib/nfd-zig b/lib/nfd-zig new file mode 160000 index 0000000..75acefd --- /dev/null +++ b/lib/nfd-zig @@ -0,0 +1 @@ +Subproject commit 75acefd571ede5361514b3a8ae9c8b36c6908d36 diff --git a/src/platform.zig b/src/platform.zig index 132895d..49aaded 100644 --- a/src/platform.zig +++ b/src/platform.zig @@ -2,6 +2,7 @@ const std = @import("std"); const SDL = @import("sdl2"); const gl = @import("gl"); const zgui = @import("zgui"); +const nfd = @import("nfd"); const emu = @import("core/emu.zig"); const config = @import("config.zig"); @@ -34,6 +35,7 @@ pub const Gui = struct { const State = struct { fps_hist: RingBuffer(u32), + should_quit: bool = false, pub fn init(allocator: Allocator) !@This() { const history = try allocator.alloc(u32, 0x400); @@ -261,6 +263,36 @@ pub const Gui = struct { fn draw(self: *Self, tex_id: GLuint, cpu: *const 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", .{})) self.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.loadRom(cpu, file_path); + } + } + } + + if (zgui.beginMenu("Emulation", true)) { + defer zgui.endMenu(); + + if (zgui.menuItem("Restart", .{})) log.warn("TODO: Restart Emulator", .{}); + } + } + { const w = @intToFloat(f32, gba_width * win_scale); const h = @intToFloat(f32, gba_height * win_scale); @@ -422,6 +454,8 @@ pub const Gui = struct { // 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; + // Quit Signal from Dear Imgui + if (self.state.should_quit) break :emu_loop; while (SDL.SDL_PollEvent(&event) != 0) { _ = zgui.backend.processEvent(&event); From eef5a238a0a0307babccbfa787677a63f0adf06c Mon Sep 17 00:00:00 2001 From: Rekai Musuka Date: Sat, 4 Feb 2023 19:13:46 -0600 Subject: [PATCH 10/24] chore: update nfd-zig respond to build.zig changes in zig master --- build.zig | 3 +-- lib/nfd-zig | 2 +- 2 files changed, 2 insertions(+), 3 deletions(-) diff --git a/build.zig b/build.zig index 51b7d55..df164f6 100644 --- a/build.zig +++ b/build.zig @@ -47,7 +47,7 @@ pub fn build(b: *std.build.Builder) void { // gdbstub gdbstub.link(exe); // NativeFileDialog(ue) Bindings - exe.linkLibrary(nfd.makeLib(b, mode, target)); + exe.linkLibrary(nfd.makeLib(b, target, optimize)); exe.addPackage(nfd.getPackage("nfd")); // Zig SDL Bindings: https://github.com/MasterQ32/SDL.zig @@ -61,7 +61,6 @@ pub fn build(b: *std.build.Builder) void { zgui.link(exe, zgui_options); exe.addPackage(zgui_pkg); - exe.setBuildMode(mode); exe.install(); const run_cmd = exe.run(); diff --git a/lib/nfd-zig b/lib/nfd-zig index 75acefd..b8d5626 160000 --- a/lib/nfd-zig +++ b/lib/nfd-zig @@ -1 +1 @@ -Subproject commit 75acefd571ede5361514b3a8ae9c8b36c6908d36 +Subproject commit b8d56260294636f9d7bf596cb558b697e873793f From 57c7437f77725e839efcf364ecc4b3e110746d2d Mon Sep 17 00:00:00 2001 From: Rekai Musuka Date: Sat, 4 Feb 2023 19:30:55 -0600 Subject: [PATCH 11/24] chore: add gui deps to README.md --- README.md | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index fcec683..ce7537b 100644 --- a/README.md +++ b/README.md @@ -84,10 +84,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` | From baa3fb7905ad4dc7a8e77778f4c40c17a694cfd7 Mon Sep 17 00:00:00 2001 From: Rekai Musuka Date: Tue, 7 Feb 2023 17:33:54 -0600 Subject: [PATCH 12/24] chore: update gui libs to latest zig master --- build.zig | 9 ++++----- lib/nfd-zig | 2 +- lib/zgui | 2 +- 3 files changed, 6 insertions(+), 7 deletions(-) diff --git a/build.zig b/build.zig index df164f6..35fefea 100644 --- a/build.zig +++ b/build.zig @@ -48,7 +48,7 @@ pub fn build(b: *std.build.Builder) void { gdbstub.link(exe); // NativeFileDialog(ue) Bindings exe.linkLibrary(nfd.makeLib(b, target, optimize)); - exe.addPackage(nfd.getPackage("nfd")); + exe.addModule("nfd", nfd.getModule(b)); // Zig SDL Bindings: https://github.com/MasterQ32/SDL.zig const sdk = Sdk.init(b, null); @@ -56,10 +56,9 @@ pub fn build(b: *std.build.Builder) void { exe.addModule("sdl2", sdk.getNativeModule()); // Dear ImGui bindings - const zgui_options = zgui.BuildOptionsStep.init(b, .{ .backend = .sdl2_opengl3 }); - const zgui_pkg = zgui.getPkg(&.{zgui_options.getPkg()}); - zgui.link(exe, zgui_options); - exe.addPackage(zgui_pkg); + const zgui_pkg = zgui.package(b, .{ .options = .{ .backend = .sdl2_opengl3 } }); + exe.addModule("zgui", zgui_pkg.module); + zgui.link(exe, zgui_pkg.options); exe.install(); diff --git a/lib/nfd-zig b/lib/nfd-zig index b8d5626..5e5098b 160000 --- a/lib/nfd-zig +++ b/lib/nfd-zig @@ -1 +1 @@ -Subproject commit b8d56260294636f9d7bf596cb558b697e873793f +Subproject commit 5e5098bcaf2643d35199ce556da8091626a4d2ef diff --git a/lib/zgui b/lib/zgui index f9744ec..12e480f 160000 --- a/lib/zgui +++ b/lib/zgui @@ -1 +1 @@ -Subproject commit f9744ec8d613028b908896c410be937b5513f73b +Subproject commit 12e480f30d1192eda18bd9245f41f6ff2d4055bd From 54143332abc1bafb6e92d36a6a2722e75576a1d7 Mon Sep 17 00:00:00 2001 From: Rekai Musuka Date: Wed, 22 Feb 2023 16:45:06 -0600 Subject: [PATCH 13/24] chore: update for loop in RingBuffer impl --- src/util.zig | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/util.zig b/src/util.zig index b135e2a..85177bf 100644 --- a/src/util.zig +++ b/src/util.zig @@ -365,7 +365,7 @@ pub fn RingBuffer(comptime T: type) type { const count = std.math.min(self.len(), cpy.len); var start: Index = self.read; - for (cpy) |*v, i| { + for (cpy, 0..) |*v, i| { if (i >= count) break; v.* = self.buf[self.mask(start)]; From e90d5a17baaa0d1f913e1435b116abcc0acb764d Mon Sep 17 00:00:00 2001 From: Rekai Musuka Date: Thu, 23 Feb 2023 17:23:51 -0600 Subject: [PATCH 14/24] fix: ensure code builds + works the gdbstub branch got merged into main, rebasing on top of main led to a bunch of merge conflicts that had to be resolved. Unfortunately some things got missed, and this commit covers the immediate problems that the rebase caused --- src/main.zig | 4 +++- src/platform.zig | 61 +++++++++++++++++++++--------------------------- 2 files changed, 30 insertions(+), 35 deletions(-) diff --git a/src/main.zig b/src/main.zig index 087bfc3..765cd27 100644 --- a/src/main.zig +++ b/src/main.zig @@ -120,8 +120,9 @@ pub fn main() void { }) catch |e| exitln("main thread panicked: {}", .{e}); } else { var tracker = FpsTracker.init(); + var pause = Atomic(bool).init(false); - 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, .{ &quit, &pause, &cpu, &scheduler, &tracker }) catch |e| exitln("emu thread panicked: {}", .{e}); defer thread.join(); gui.run(.{ @@ -129,6 +130,7 @@ pub fn main() void { .scheduler = &scheduler, .tracker = &tracker, .quit = &quit, + .pause = &pause, }) catch |e| exitln("main thread panicked: {}", .{e}); } } diff --git a/src/platform.zig b/src/platform.zig index 49aaded..6262db3 100644 --- a/src/platform.zig +++ b/src/platform.zig @@ -307,16 +307,13 @@ pub const Gui = struct { _ = zgui.begin("Information", .{}); defer zgui.end(); - { - var i: usize = 0; - while (i < 8) : (i += 1) { - zgui.text("R{}: 0x{X:0>8}", .{ i, cpu.r[i] }); + for (0..8) |i| { + zgui.text("R{}: 0x{X:0>8}", .{ i, cpu.r[i] }); - zgui.sameLine(.{}); + zgui.sameLine(.{}); - const prefix = if (8 + i < 10) " " else ""; - zgui.text("{s}R{}: 0x{X:0>8}", .{ prefix, 8 + i, cpu.r[8 + i] }); - } + const prefix = if (8 + i < 10) " " else ""; + zgui.text("{s}R{}: 0x{X:0>8}", .{ prefix, 8 + i, cpu.r[8 + i] }); } zgui.separator(); @@ -415,13 +412,14 @@ pub const Gui = struct { } } - { - zgui.showDemoWindow(null); - } + // { + // zgui.showDemoWindow(null); + // } } const RunOptions = struct { quit: *std.atomic.Atomic(bool), + pause: ?*std.atomic.Atomic(bool) = null, tracker: ?*FpsTracker = null, cpu: *Arm7tdmi, scheduler: *Scheduler, @@ -431,6 +429,7 @@ pub const Gui = struct { const cpu = opt.cpu; const tracker = opt.tracker; const quit = opt.quit; + const pause = opt.pause; const obj_ids = Self.genBufferObjects(); defer gl.deleteBuffers(3, @as(*const [3]c_uint, &obj_ids)); @@ -442,21 +441,16 @@ pub const Gui = struct { const fbo_id = try Self.genFrameBufObject(out_tex); defer gl.deleteFramebuffers(1, &fbo_id); - var tracker = FpsTracker.init(); - var quit = std.atomic.Atomic(bool).init(false); - var pause = std.atomic.Atomic(bool).init(false); - - var title_buf: [0x100]u8 = undefined; - emu_loop: while (true) { - 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 + // `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 (quit.load(.Monotonic)) break :emu_loop; - // Quit Signal from Dear Imgui + + // 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; while (SDL.SDL_PollEvent(&event) != 0) { _ = zgui.backend.processEvent(&event); @@ -506,12 +500,17 @@ pub const Gui = struct { } } - // We Access non-atomic parts of the Emulator here + // If `Gui.run` has been passed with a `pause` atomic, we should + // pause the emulation thread while we access the data there { - pause.store(true, .Monotonic); - defer pause.store(false, .Monotonic); + // TODO: Is there a nicer way to express this? + if (pause) |val| val.store(true, .Monotonic); + defer if (pause) |val| val.store(false, .Monotonic); - self.state.fps_hist.push(tracker.value()) catch {}; + // Add FPS count to the histogram + if (tracker) |t| { + self.state.fps_hist.push(t.value()) catch {}; + } // Draw GBA Screen to Texture { @@ -529,14 +528,6 @@ pub const Gui = struct { gl.clearColor(0, 0, 0, 1.0); gl.clear(gl.COLOR_BUFFER_BIT); - if (tracker) |t| { - const emu_fps = t.value(); - self.state.fps_hist.push(emu_fps) catch {}; - - const dyn_title = std.fmt.bufPrintZ(&title_buf, "ZBA | {s} [Emu: {}fps] ", .{ self.title, emu_fps }) catch unreachable; - SDL.SDL_SetWindowTitle(self.window, dyn_title.ptr); - } - zgui.backend.newFrame(width, height); self.draw(out_tex, cpu); zgui.backend.draw(); @@ -544,6 +535,8 @@ pub const Gui = struct { SDL.SDL_GL_SwapWindow(self.window); } + + quit.store(true, .Monotonic); // Signals to emu thread to exit } fn glGetProcAddress(ctx: SDL.SDL_GLContext, proc: [:0]const u8) ?*anyopaque { From 3fff4fd74200bebbae6a3870d9a4cae255d9752d Mon Sep 17 00:00:00 2001 From: Rekai Musuka Date: Thu, 23 Feb 2023 18:25:05 -0600 Subject: [PATCH 15/24] chore: move imgui-specific code to its own file --- src/imgui.zig | 298 +++++++++++++++++++++++++++++++++++++++++++++++ src/platform.zig | 279 +------------------------------------------- 2 files changed, 304 insertions(+), 273 deletions(-) create mode 100644 src/imgui.zig diff --git a/src/imgui.zig b/src/imgui.zig new file mode 100644 index 0000000..2dcf60f --- /dev/null +++ b/src/imgui.zig @@ -0,0 +1,298 @@ +//! 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("util.zig").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: [:0]const u8, + + fps_hist: RingBuffer(u32), + should_quit: bool = false, + + pub fn init(allocator: Allocator, title: [12]u8) !@This() { + const history = try allocator.alloc(u32, histogram_len); + + return .{ + .title = try allocator.dupeZ(u8, &title), + .fps_hist = RingBuffer(u32).init(history), + }; + } + + pub fn deinit(self: *@This(), allocator: Allocator) void { + allocator.free(self.title); + self.fps_hist.deinit(allocator); + + self.* = undefined; + } +}; + +pub fn draw(state: *State, tex_id: GLuint, cpu: *const 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.loadRom(cpu, file_path); + } + } + } + + if (zgui.beginMenu("Emulation", true)) { + defer zgui.endMenu(); + + if (zgui.menuItem("Restart", .{})) log.warn("TODO: Restart Emulator", .{}); + } + } + + { + const w = @intToFloat(f32, gba_width * win_scale); + const h = @intToFloat(f32, gba_height * win_scale); + + _ = zgui.begin(state.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/platform.zig b/src/platform.zig index 6262db3..5ef333b 100644 --- a/src/platform.zig +++ b/src/platform.zig @@ -2,10 +2,10 @@ const std = @import("std"); const SDL = @import("sdl2"); const gl = @import("gl"); const zgui = @import("zgui"); -const nfd = @import("nfd"); 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; @@ -27,27 +27,12 @@ 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 log = std.log.scoped(.Gui); - const State = struct { - fps_hist: RingBuffer(u32), - should_quit: bool = false, - - pub fn init(allocator: Allocator) !@This() { - const history = try allocator.alloc(u32, 0x400); - return .{ .fps_hist = RingBuffer(u32).init(history) }; - } - - fn deinit(self: *@This(), allocator: Allocator) void { - self.fps_hist.deinit(allocator); - self.* = undefined; - } - }; - // zig fmt: off const vertices: [32]f32 = [_]f32{ // Positions // Colours // Texture Coords @@ -65,10 +50,9 @@ pub const Gui = struct { window: *SDL.SDL_Window, ctx: SDL_GLContext, - title: [:0]const u8, audio: Audio, - state: State, + state: imgui.State, allocator: Allocator, program_id: gl.GLuint, @@ -80,7 +64,7 @@ pub const Gui = struct { if (SDL.SDL_GL_SetAttribute(SDL.SDL_GL_CONTEXT_MAJOR_VERSION, 3) < 0) panic(); const window = SDL.SDL_CreateWindow( - default_title, + window_title, SDL.SDL_WINDOWPOS_CENTERED, SDL.SDL_WINDOWPOS_CENTERED, width, @@ -102,13 +86,12 @@ pub const Gui = struct { return Self{ .window = window, - .title = try allocator.dupeZ(u8, &title), .ctx = ctx, .program_id = try compileShaders(), .audio = Audio.init(apu), .allocator = allocator, - .state = try State.init(allocator), + .state = try imgui.State.init(allocator, title), }; } @@ -125,7 +108,6 @@ pub const Gui = struct { SDL.SDL_DestroyWindow(self.window); SDL.SDL_Quit(); - self.allocator.free(self.title); self.* = undefined; } @@ -260,163 +242,6 @@ pub const Gui = struct { return fbo_id; } - fn draw(self: *Self, tex_id: GLuint, cpu: *const 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", .{})) self.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.loadRom(cpu, file_path); - } - } - } - - if (zgui.beginMenu("Emulation", true)) { - defer zgui.endMenu(); - - if (zgui.menuItem("Restart", .{})) log.warn("TODO: Restart Emulator", .{}); - } - } - - { - const w = @intToFloat(f32, gba_width * win_scale); - const h = @intToFloat(f32, gba_height * win_scale); - - _ = zgui.begin(self.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 prefix = if (8 + i < 10) " " else ""; - zgui.text("{s}R{}: 0x{X:0>8}", .{ prefix, 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); // space is for padding - widgets.interrupts("IRQ", cpu.bus.io.irq); - } - - { - _ = zgui.begin("Performance", .{}); - defer zgui.end(); - - const tmp = blk: { - var buf: [0x400]u32 = undefined; - const len = self.state.fps_hist.copy(&buf); - - break :blk .{ buf, len }; - }; - const values = tmp[0]; - const len = tmp[1]; - - if (len == values.len) _ = self.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 RunOptions = struct { quit: *std.atomic.Atomic(bool), pause: ?*std.atomic.Atomic(bool) = null, @@ -529,7 +354,7 @@ pub const Gui = struct { gl.clear(gl.COLOR_BUFFER_BIT); zgui.backend.newFrame(width, height); - self.draw(out_tex, cpu); + imgui.draw(&self.state, out_tex, cpu); zgui.backend.draw(); } @@ -614,95 +439,3 @@ fn panic() noreturn { const str = @as(?[*:0]const u8, SDL.SDL_GetError()) orelse "unknown error"; @panic(std.mem.sliceTo(str, 0)); } - -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; - } -}; From d985eac0fc9e6b707d857f16a509caac8b56c81a Mon Sep 17 00:00:00 2001 From: Rekai Musuka Date: Thu, 23 Feb 2023 22:12:06 -0600 Subject: [PATCH 16/24] tmp: implement mechanisms for a emu reset fn (currently crashes) --- src/core/Bus.zig | 26 ++++++++++++++++++++++ src/core/apu.zig | 38 ++++++++++++++++++++++++++++---- src/core/apu/device/Envelope.zig | 9 ++++---- src/core/apu/device/Length.zig | 6 ++--- src/core/apu/device/Sweep.zig | 20 +++++------------ src/core/bus/Bios.zig | 4 ++++ src/core/bus/Ewram.zig | 4 ++++ src/core/bus/Iwram.zig | 4 ++++ src/core/bus/dma.zig | 4 ++++ src/core/bus/io.zig | 4 ++++ src/core/bus/timer.zig | 6 +++++ src/core/cpu.zig | 8 +++++++ src/core/emu.zig | 7 ++++++ src/core/scheduler.zig | 15 +++++++++++-- src/imgui.zig | 6 +++-- 15 files changed, 131 insertions(+), 30 deletions(-) diff --git a/src/core/Bus.zig b/src/core/Bus.zig index 29e9076..925f1ba 100644 --- a/src/core/Bus.zig +++ b/src/core/Bus.zig @@ -102,6 +102,32 @@ pub fn deinit(self: *Self) void { self.* = undefined; } +pub fn reset(self: *Self) void { + self.bios.reset(); + // TODO: deinit ppu + 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(); +} + 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..8730290 100644 --- a/src/core/apu.zig +++ b/src/core/apu.zig @@ -289,7 +289,28 @@ pub const Apu = struct { return apu; } - fn reset(self: *Self) void { + /// 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(); + } + + /// 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 +372,7 @@ pub const Apu = struct { // Rest Noise self.ch4.lfsr.reset(); } else { - self.reset(); + self._reset(); } } @@ -528,6 +549,11 @@ 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(); @@ -562,10 +588,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..6743ffc 100644 --- a/src/core/bus/Bios.zig +++ b/src/core/bus/Bios.zig @@ -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/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/dma.zig b/src/core/bus/dma.zig index e1f1b1c..7d75d18 100644 --- a/src/core/bus/dma.zig +++ b/src/core/bus/dma.zig @@ -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 3b62d1f..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); 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 669d195..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) { diff --git a/src/core/emu.zig b/src/core/emu.zig index 11931d7..215173f 100644 --- a/src/core/emu.zig +++ b/src/core/emu.zig @@ -222,3 +222,10 @@ 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(); +} 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 index 2dcf60f..1d62181 100644 --- a/src/imgui.zig +++ b/src/imgui.zig @@ -48,7 +48,7 @@ pub const State = struct { } }; -pub fn draw(state: *State, tex_id: GLuint, cpu: *const Arm7tdmi) void { +pub fn draw(state: *State, tex_id: GLuint, cpu: *Arm7tdmi) void { const win_scale = config.config().host.win_scale; { @@ -77,7 +77,9 @@ pub fn draw(state: *State, tex_id: GLuint, cpu: *const Arm7tdmi) void { if (zgui.beginMenu("Emulation", true)) { defer zgui.endMenu(); - if (zgui.menuItem("Restart", .{})) log.warn("TODO: Restart Emulator", .{}); + if (zgui.menuItem("Restart", .{})) { + emu.reset(cpu); + } } } From 72b702cb21c8cc22a7bc56faa64cb6df69086f81 Mon Sep 17 00:00:00 2001 From: Rekai Musuka Date: Sat, 4 Mar 2023 18:02:12 -0600 Subject: [PATCH 17/24] fix: handle null GBA ROM titles when passing to imgui --- src/imgui.zig | 6 ++++-- src/platform.zig | 2 +- 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/src/imgui.zig b/src/imgui.zig index 1d62181..5a6a82e 100644 --- a/src/imgui.zig +++ b/src/imgui.zig @@ -33,9 +33,10 @@ pub const State = struct { pub fn init(allocator: Allocator, title: [12]u8) !@This() { const history = try allocator.alloc(u32, histogram_len); + const without_null = std.mem.sliceTo(&title, 0); return .{ - .title = try allocator.dupeZ(u8, &title), + .title = try allocator.dupeZ(u8, without_null), .fps_hist = RingBuffer(u32).init(history), }; } @@ -87,7 +88,8 @@ pub fn draw(state: *State, tex_id: GLuint, cpu: *Arm7tdmi) void { const w = @intToFloat(f32, gba_width * win_scale); const h = @intToFloat(f32, gba_height * win_scale); - _ = zgui.begin(state.title, .{ .flags = .{ .no_resize = true, .always_auto_resize = true } }); + const window_title = if (state.title.len != 0) state.title else "[No Title]"; + _ = 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 } }); diff --git a/src/platform.zig b/src/platform.zig index 5ef333b..57c395b 100644 --- a/src/platform.zig +++ b/src/platform.zig @@ -182,7 +182,7 @@ pub const Gui = struct { 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)))); From 11eae091db6a5ee82f6aa3a1274e2209c0f1928c Mon Sep 17 00:00:00 2001 From: Rekai Musuka Date: Fri, 10 Mar 2023 00:05:31 -0600 Subject: [PATCH 18/24] chore: introduce zba-util In an effort to reuse code between zba and zba-gdbstub, move common util code (like the SPSC Channel I implemented in this commit) in a new lib --- .gitmodules | 3 + build.zig | 5 +- lib/{util => }/bitfield.zig | 0 lib/zba-util | 1 + src/core/Bus.zig | 2 +- src/core/apu.zig | 3 +- src/core/bus/Bios.zig | 2 +- src/core/bus/dma.zig | 2 +- src/core/cpu/arm/branch.zig | 2 +- .../cpu/arm/half_signed_data_transfer.zig | 4 +- src/core/cpu/arm/psr_transfer.zig | 2 +- src/core/cpu/arm/single_data_swap.zig | 2 +- src/core/cpu/arm/single_data_transfer.zig | 2 +- src/core/cpu/barrel_shifter.zig | 2 +- src/core/cpu/thumb/branch.zig | 2 +- src/core/cpu/thumb/data_transfer.zig | 4 +- src/imgui.zig | 4 +- src/main.zig | 7 +- src/util.zig | 107 ------------------ 19 files changed, 28 insertions(+), 128 deletions(-) rename lib/{util => }/bitfield.zig (100%) create mode 160000 lib/zba-util diff --git a/.gitmodules b/.gitmodules index 9d0cf56..475af46 100644 --- a/.gitmodules +++ b/.gitmodules @@ -22,3 +22,6 @@ [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/build.zig b/build.zig index 35fefea..1eabf6d 100644 --- a/build.zig +++ b/build.zig @@ -33,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" } }); @@ -44,6 +44,9 @@ 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); // NativeFileDialog(ue) Bindings 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/zba-util b/lib/zba-util new file mode 160000 index 0000000..52ac8d9 --- /dev/null +++ b/lib/zba-util @@ -0,0 +1 @@ +Subproject commit 52ac8d952fe7c722e33a3237e465dcf258076613 diff --git a/src/core/Bus.zig b/src/core/Bus.zig index 925f1ba..0c2d3a8 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 diff --git a/src/core/apu.zig b/src/core/apu.zig index 8730290..89204f9 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); @@ -557,7 +556,7 @@ pub fn DmaSound(comptime kind: DmaSoundKind) type { 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 { diff --git a/src/core/bus/Bios.zig b/src/core/bus/Bios.zig index 6743ffc..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 diff --git a/src/core/bus/dma.zig b/src/core/bus/dma.zig index 7d75d18..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() }; 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/imgui.zig b/src/imgui.zig index 5a6a82e..14454d5 100644 --- a/src/imgui.zig +++ b/src/imgui.zig @@ -11,7 +11,7 @@ const emu = @import("core/emu.zig"); const Gui = @import("platform.zig").Gui; 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 GLuint = gl.GLuint; @@ -43,7 +43,7 @@ pub const State = struct { pub fn deinit(self: *@This(), allocator: Allocator) void { allocator.free(self.title); - self.fps_hist.deinit(allocator); + allocator.free(self.fps_hist.buf); self.* = undefined; } diff --git a/src/main.zig b/src/main.zig index 765cd27..541ad81 100644 --- a/src/main.zig +++ b/src/main.zig @@ -70,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 diff --git a/src/util.zig b/src/util.zig index 85177bf..db49131 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 @@ -317,78 +285,3 @@ pub const FrameBuffer = struct { 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); - } - }; -} From bd872ee1c0de006e9140bfa1ee43298c6f50b2af Mon Sep 17 00:00:00 2001 From: Rekai Musuka Date: Fri, 10 Mar 2023 02:02:34 -0600 Subject: [PATCH 19/24] fix: drop select atomics in favour of a thread-safe channel --- lib/zba-util | 2 +- src/core/emu.zig | 38 +++++++++++++++++++++++++++++--------- src/main.zig | 15 ++++++++------- src/platform.zig | 32 +++++++++++++++++--------------- 4 files changed, 55 insertions(+), 32 deletions(-) diff --git a/lib/zba-util b/lib/zba-util index 52ac8d9..d5e66ca 160000 --- a/lib/zba-util +++ b/lib/zba-util @@ -1 +1 @@ -Subproject commit 52ac8d952fe7c722e33a3237e465dcf258076613 +Subproject commit d5e66caf2180324d83ad9be30e887849f5ed74da diff --git a/src/core/emu.zig b/src/core/emu.zig index 215173f..c029943 100644 --- a/src/core/emu.zig +++ b/src/core/emu.zig @@ -5,9 +5,9 @@ const config = @import("../config.zig"); const Scheduler = @import("scheduler.zig").Scheduler; const Arm7tdmi = @import("cpu.zig").Arm7tdmi; 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,29 +35,40 @@ const RunKind = enum { 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; if (audio_sync) log.info("Audio sync enabled", .{}); if (config.config().guest.video_sync) { - inner(.LimitedFPS, audio_sync, quit, pause, cpu, scheduler, tracker); + inner(.LimitedFPS, audio_sync, cpu, scheduler, tracker, channel); } 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) { 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)) { - if (pause.load(.Monotonic)) continue; + 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); @@ -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 wake_time: u64 = frame_period; - while (!quit.load(.Monotonic)) { - if (pause.load(.Monotonic)) continue; + 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); diff --git a/src/main.zig b/src/main.zig index 541ad81..17940ee 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,7 +14,6 @@ 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); pub const log_level = if (builtin.mode != .Debug) .info else std.log.default_level; @@ -94,7 +94,10 @@ pub fn main() void { var gui = Gui.init(allocator, bus.pak.title, &bus.apu) catch |e| exitln("failed to init gui: {}", .{e}); 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) { const Server = @import("gdbstub").Server; @@ -117,21 +120,19 @@ 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(); - 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(); gui.run(.{ .cpu = &cpu, .scheduler = &scheduler, + .channel = &channel, .tracker = &tracker, - .quit = &quit, - .pause = &pause, }) catch |e| exitln("main thread panicked: {}", .{e}); } } diff --git a/src/platform.zig b/src/platform.zig index 57c395b..30c9e04 100644 --- a/src/platform.zig +++ b/src/platform.zig @@ -11,7 +11,7 @@ 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 RingBuffer = @import("util.zig").RingBuffer; +const TwoWayChannel = @import("zba-util").TwoWayChannel; const gba_width = @import("core/ppu.zig").width; const gba_height = @import("core/ppu.zig").height; @@ -243,8 +243,7 @@ pub const Gui = struct { } const RunOptions = struct { - quit: *std.atomic.Atomic(bool), - pause: ?*std.atomic.Atomic(bool) = null, + channel: *TwoWayChannel, tracker: ?*FpsTracker = null, cpu: *Arm7tdmi, scheduler: *Scheduler, @@ -253,8 +252,7 @@ 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 pause = opt.pause; + const channel = opt.channel; const obj_ids = Self.genBufferObjects(); defer gl.deleteBuffers(3, @as(*const [3]c_uint, &obj_ids)); @@ -269,7 +267,10 @@ pub const Gui = struct { 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 (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 // 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? - if (pause) |val| val.store(true, .Monotonic); - defer if (pause) |val| val.store(false, .Monotonic); + channel.emu.push(.Pause); + defer channel.emu.push(.Resume); + + // 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 - if (tracker) |t| { - self.state.fps_hist.push(t.value()) catch {}; - } + if (tracker) |t| self.state.fps_hist.push(t.value()) catch {}; // Draw GBA Screen to Texture { @@ -361,7 +363,7 @@ pub const Gui = struct { 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 { From f8477714ae8bf1df0819877e4c544cb5608f0188 Mon Sep 17 00:00:00 2001 From: Rekai Musuka Date: Fri, 10 Mar 2023 02:27:31 -0600 Subject: [PATCH 20/24] feat: implement resetting --- src/core/Bus.zig | 2 +- src/core/apu.zig | 18 ++++++++++++------ src/core/ppu.zig | 20 ++++++++++++++++++++ src/core/ppu/Oam.zig | 4 ++++ src/core/ppu/Palette.zig | 4 ++++ src/core/ppu/Vram.zig | 4 ++++ src/util.zig | 8 ++++++-- 7 files changed, 51 insertions(+), 9 deletions(-) diff --git a/src/core/Bus.zig b/src/core/Bus.zig index 0c2d3a8..22b0b31 100644 --- a/src/core/Bus.zig +++ b/src/core/Bus.zig @@ -104,7 +104,7 @@ pub fn deinit(self: *Self) void { pub fn reset(self: *Self) void { self.bios.reset(); - // TODO: deinit ppu + self.ppu.reset(); self.apu.reset(); self.iwram.reset(); self.ewram.reset(); diff --git a/src/core/apu.zig b/src/core/apu.zig index 89204f9..a93ad0b 100644 --- a/src/core/apu.zig +++ b/src/core/apu.zig @@ -278,16 +278,20 @@ 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 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 @@ -306,6 +310,8 @@ pub const Apu = struct { self.sampling_cycle = 0; self.fs.reset(); + + Self.initEvents(self.sched, self.interval()); } /// Emulates the reset behaviour of the APU 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/util.zig b/src/util.zig index db49131..276a658 100644 --- a/src/util.zig +++ b/src/util.zig @@ -251,7 +251,7 @@ pub const FrameBuffer = struct { layers: [2][]u8, buf: []u8, - current: u1, + current: u1 = 0, allocator: Allocator, @@ -266,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; From 5adbc354d649d69cda2c803e65261378e1485ed6 Mon Sep 17 00:00:00 2001 From: Rekai Musuka Date: Fri, 10 Mar 2023 02:50:05 -0600 Subject: [PATCH 21/24] feat: replace Gamepak --- src/core/Bus.zig | 15 +++++++++++++++ src/core/emu.zig | 5 +++++ src/imgui.zig | 5 ++++- 3 files changed, 24 insertions(+), 1 deletion(-) diff --git a/src/core/Bus.zig b/src/core/Bus.zig index 22b0b31..055e6cb 100644 --- a/src/core/Bus.zig +++ b/src/core/Bus.zig @@ -128,6 +128,21 @@ pub fn reset(self: *Self) void { 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/emu.zig b/src/core/emu.zig index c029943..e009a67 100644 --- a/src/core/emu.zig +++ b/src/core/emu.zig @@ -249,3 +249,8 @@ pub fn reset(cpu: *Arm7tdmi) void { 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/imgui.zig b/src/imgui.zig index 14454d5..a2dc076 100644 --- a/src/imgui.zig +++ b/src/imgui.zig @@ -70,7 +70,10 @@ pub fn draw(state: *State, tex_id: GLuint, cpu: *Arm7tdmi) void { defer nfd.freePath(file_path); log.info("user chose: \"{s}\"", .{file_path}); - // emu.loadRom(cpu, file_path); + emu.replaceGamepak(cpu, file_path) catch |e| { + log.err("failed to replace GamePak: {}", .{e}); + break :blk; + }; } } } From 85ec9a84c492be12154241870904d6f0f4b463b4 Mon Sep 17 00:00:00 2001 From: Rekai Musuka Date: Fri, 10 Mar 2023 19:37:28 -0600 Subject: [PATCH 22/24] chore: add screenshot to README.md --- README.md | 2 ++ assets/screenshot.png | Bin 0 -> 35354 bytes 2 files changed, 2 insertions(+) create mode 100644 assets/screenshot.png diff --git a/README.md b/README.md index ce7537b..cf7ea2f 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). diff --git a/assets/screenshot.png b/assets/screenshot.png new file mode 100644 index 0000000000000000000000000000000000000000..ff6cb5cbf1cd985479fd141cff9db3c154d26df0 GIT binary patch literal 35354 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 K^Y2)`{J#JLKC5d0 literal 0 HcmV?d00001 From c7b62d32028063bd4543b8eb888d87f893e12491 Mon Sep 17 00:00:00 2001 From: Rekai Musuka Date: Fri, 10 Mar 2023 20:41:49 -0600 Subject: [PATCH 23/24] chore: dynamically update window title on ROM replace --- src/imgui.zig | 21 +++++++++++++-------- src/main.zig | 2 +- src/platform.zig | 4 ++-- 3 files changed, 16 insertions(+), 11 deletions(-) diff --git a/src/imgui.zig b/src/imgui.zig index a2dc076..a710275 100644 --- a/src/imgui.zig +++ b/src/imgui.zig @@ -26,25 +26,25 @@ const histogram_len = 0x400; /// Immediate-Mode GUI State pub const State = struct { - title: [:0]const u8, + title: [12:0]u8, fps_hist: RingBuffer(u32), should_quit: bool = false, - pub fn init(allocator: Allocator, title: [12]u8) !@This() { + pub fn init(allocator: Allocator) !@This() { const history = try allocator.alloc(u32, histogram_len); - const without_null = std.mem.sliceTo(&title, 0); + + var title: [12:0]u8 = [_:0]u8{0} ** 12; + std.mem.copy(u8, &title, "[No Title]"); return .{ - .title = try allocator.dupeZ(u8, without_null), + .title = title, .fps_hist = RingBuffer(u32).init(history), }; } pub fn deinit(self: *@This(), allocator: Allocator) void { - allocator.free(self.title); allocator.free(self.fps_hist.buf); - self.* = undefined; } }; @@ -60,9 +60,10 @@ pub fn draw(state: *State, tex_id: GLuint, cpu: *Arm7tdmi) void { defer zgui.endMenu(); if (zgui.menuItem("Quit", .{})) state.should_quit = true; + if (zgui.menuItem("Insert ROM", .{})) blk: { const maybe_path = nfd.openFileDialog("gba", null) catch |e| { - log.err("failed to open file dialog: {?}", .{e}); + log.err("failed to open file dialog: {}", .{e}); break :blk; }; @@ -74,6 +75,10 @@ pub fn draw(state: *State, tex_id: GLuint, cpu: *Arm7tdmi) void { 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); } } } @@ -91,7 +96,7 @@ pub fn draw(state: *State, tex_id: GLuint, cpu: *Arm7tdmi) void { const w = @intToFloat(f32, gba_width * win_scale); const h = @intToFloat(f32, gba_height * win_scale); - const window_title = if (state.title.len != 0) state.title else "[No Title]"; + const window_title = std.mem.sliceTo(&state.title, 0); _ = zgui.begin(window_title, .{ .flags = .{ .no_resize = true, .always_auto_resize = true } }); defer zgui.end(); diff --git a/src/main.zig b/src/main.zig index 17940ee..39faa08 100644 --- a/src/main.zig +++ b/src/main.zig @@ -91,7 +91,7 @@ pub fn main() void { } // 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) catch |e| exitln("failed to init gui: {}", .{e}); defer gui.deinit(); var quit = std.atomic.Atomic(bool).init(false); diff --git a/src/platform.zig b/src/platform.zig index 30c9e04..25e2a5d 100644 --- a/src/platform.zig +++ b/src/platform.zig @@ -57,7 +57,7 @@ pub const Gui = struct { allocator: Allocator, program_id: gl.GLuint, - pub fn init(allocator: Allocator, title: [12]u8, apu: *Apu) !Self { + pub fn init(allocator: Allocator, apu: *Apu) !Self { if (SDL.SDL_Init(SDL.SDL_INIT_VIDEO | SDL.SDL_INIT_EVENTS | SDL.SDL_INIT_AUDIO) < 0) panic(); if (SDL.SDL_GL_SetAttribute(SDL.SDL_GL_CONTEXT_PROFILE_MASK, SDL.SDL_GL_CONTEXT_PROFILE_CORE) < 0) panic(); if (SDL.SDL_GL_SetAttribute(SDL.SDL_GL_CONTEXT_MAJOR_VERSION, 3) < 0) panic(); @@ -91,7 +91,7 @@ pub const Gui = struct { .audio = Audio.init(apu), .allocator = allocator, - .state = try imgui.State.init(allocator, title), + .state = try imgui.State.init(allocator), }; } From 2629d15e2f63aee5ecd33fe180e3d072ddb7e482 Mon Sep 17 00:00:00 2001 From: Rekai Musuka Date: Fri, 10 Mar 2023 21:13:38 -0600 Subject: [PATCH 24/24] feat: don't require path to ROM in CLI --- README.md | 3 +-- src/core/bus/GamePak.zig | 35 +++++++++++++++++++++-------------- src/core/bus/backup.zig | 2 +- src/main.zig | 6 +++--- src/util.zig | 2 +- 5 files changed, 27 insertions(+), 21 deletions(-) diff --git a/README.md b/README.md index cf7ea2f..ba251da 100644 --- a/README.md +++ b/README.md @@ -15,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)? diff --git a/src/core/bus/GamePak.zig b/src/core/bus/GamePak.zig index 750202e..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]), }; } @@ -223,11 +230,11 @@ fn guessDevice(buf: []const u8) Gpio.Device.Kind { return .None; } -fn logHeader(buf: []const u8, title: [12]u8) void { - const ver = buf[0xBC]; +fn logHeader(buf: []const u8, title: *const [12]u8) void { + const version = buf[0xBC]; log.info("Title: {s}", .{title}); - if (ver != 0) log.info("Version: {}", .{ver}); + if (version != 0) log.info("Version: {}", .{version}); log.info("Game Code: {s}", .{buf[0xAC..0xB0]}); log.info("Maker Code: {s}", .{buf[0xB0..0xB2]}); 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/main.zig b/src/main.zig index 39faa08..eff677a 100644 --- a/src/main.zig +++ b/src/main.zig @@ -139,7 +139,7 @@ pub fn main() void { fn handleArguments(allocator: Allocator, data_path: []const u8, result: *const clap.Result(clap.Help, ¶ms, clap.parsers.default)) !FilePaths { const rom_path = romPath(result); - log.info("ROM path: {s}", .{rom_path}); + log.info("ROM path: {?s}", .{rom_path}); const bios_path = result.args.bios; if (bios_path) |path| log.info("BIOS path: {s}", .{path}) else log.warn("No BIOS provided", .{}); @@ -188,10 +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/util.zig b/src/util.zig index 276a658..9b6bbc9 100644 --- a/src/util.zig +++ b/src/util.zig @@ -50,7 +50,7 @@ pub fn escape(title: [12]u8) [12]u8 { } pub const FilePaths = struct { - rom: []const u8, + rom: ?[]const u8, bios: ?[]const u8, save: ?[]const u8, };