From 3fff4fd74200bebbae6a3870d9a4cae255d9752d Mon Sep 17 00:00:00 2001 From: Rekai Musuka Date: Thu, 23 Feb 2023 18:25:05 -0600 Subject: [PATCH] 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; - } -};