//! Namespace for dealing with ZBA's immediate-mode GUI //! Currently, ZBA uses zgui from https://github.com/michal-z/zig-gamedev //! which provides Zig bindings for https://github.com/ocornut/imgui under the hood const std = @import("std"); const zgui = @import("zgui"); const gl = @import("gl"); const nfd = @import("nfd"); const config = @import("config.zig"); const emu = @import("core/emu.zig"); const Gui = @import("platform.zig").Gui; const Arm7tdmi = @import("core/cpu.zig").Arm7tdmi; const RingBuffer = @import("zba-util").RingBuffer; const Dimensions = @import("platform.zig").Dimensions; 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); // two seconds worth of fps values into the past const histogram_len = 0x80; /// Immediate-Mode GUI State pub const State = struct { title: [12:0]u8, fps_hist: RingBuffer(u32), should_quit: bool = false, emulation: Emulation, win_stat: WindowStatus = .{}, const WindowStatus = struct { show_deps: bool = false, show_regs: bool = false, show_schedule: bool = false, show_perf: bool = false, show_palette: bool = false, }; const Emulation = union(enum) { Active, Inactive, Transition: enum { Active, Inactive }, }; /// if zba is initialized with a ROM already provided, this initializer should be called /// with `title_opt` being non-null pub fn init(allocator: Allocator, title_opt: ?*const [12]u8) !@This() { const history = try allocator.alloc(u32, histogram_len); return .{ .title = handleTitle(title_opt), .emulation = if (title_opt == null) .Inactive else .{ .Transition = .Active }, .fps_hist = RingBuffer(u32).init(history), }; } pub fn deinit(self: *@This(), allocator: Allocator) void { allocator.free(self.fps_hist.buf); self.* = undefined; } }; pub fn draw(state: *State, win_dim: Dimensions, tex_id: GLuint, cpu: *Arm7tdmi) bool { const scn_scale = config.config().host.win_scale; zgui.backend.newFrame(@intToFloat(f32, win_dim.width), @intToFloat(f32, win_dim.height)); { _ = 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; }; const file_path = maybe_path orelse { log.warn("did not receive a file path", .{}); break :blk; }; defer nfd.freePath(file_path); log.info("user chose: \"{s}\"", .{file_path}); emu.replaceGamepak(cpu, file_path) catch |e| { log.err("failed to replace GamePak: {}", .{e}); break :blk; }; state.title = handleTitle(&cpu.bus.pak.title); state.emulation = .{ .Transition = .Active }; } } if (zgui.beginMenu("Emulation", true)) { defer zgui.endMenu(); if (zgui.menuItem("Registers", .{ .selected = state.win_stat.show_regs })) state.win_stat.show_regs = true; if (zgui.menuItem("Palette", .{ .selected = state.win_stat.show_palette })) state.win_stat.show_palette = true; if (zgui.menuItem("Schedule", .{ .selected = state.win_stat.show_schedule })) state.win_stat.show_schedule = true; if (zgui.menuItem("Paused", .{ .selected = state.emulation == .Inactive })) { state.emulation = switch (state.emulation) { .Active => .{ .Transition = .Inactive }, .Inactive => .{ .Transition = .Active }, else => state.emulation, }; } if (zgui.menuItem("Restart", .{})) emu.reset(cpu); } if (zgui.beginMenu("Stats", true)) { defer zgui.endMenu(); if (zgui.menuItem("Performance", .{ .selected = state.win_stat.show_perf })) state.win_stat.show_perf = true; } if (zgui.beginMenu("Help", true)) { defer zgui.endMenu(); if (zgui.menuItem("Dependencies", .{ .selected = state.win_stat.show_deps })) state.win_stat.show_deps = true; } } { const w = @intToFloat(f32, gba_width * scn_scale); const h = @intToFloat(f32, gba_height * scn_scale); const window_title = std.mem.sliceTo(&state.title, 0); _ = zgui.begin(window_title, .{ .flags = .{ .no_resize = true, .always_auto_resize = true } }); defer zgui.end(); zgui.image(@intToPtr(*anyopaque, tex_id), .{ .w = w, .h = h, .uv0 = .{ 0, 1 }, .uv1 = .{ 1, 0 } }); } // TODO: Any other steps to respect the copyright of the libraries I use? if (state.win_stat.show_deps) { _ = zgui.begin("Dependencies", .{ .popen = &state.win_stat.show_deps }); defer zgui.end(); zgui.bulletText("SDL.zig by Felix Queißner", .{}); { zgui.indent(.{}); defer zgui.unindent(.{}); zgui.bulletText("SDL by Sam Lantinga", .{}); } zgui.bulletText("known-folders by ziglibs", .{}); zgui.bulletText("nfd-zig by Fabio Arnold", .{}); { zgui.indent(.{}); defer zgui.unindent(.{}); zgui.bulletText("nativefiledialog by Michael Labbe", .{}); } zgui.bulletText("zba-gdbstub by Rekai Musuka", .{}); zgui.bulletText("zba-util by Rekai Musuka", .{}); zgui.bulletText("zgui by Michal Ziulek", .{}); { zgui.indent(.{}); defer zgui.unindent(.{}); zgui.bulletText("DearImGui by Omar Cornut", .{}); } zgui.bulletText("zig-clap by Jimmi Holst Christensen", .{}); zgui.bulletText("zig-datetime by Jairus Martin", .{}); zgui.bulletText("zig-opengl by Felix Queißner", .{}); { zgui.indent(.{}); defer zgui.unindent(.{}); zgui.bulletText("OpenGL-Registry by The Khronos Group", .{}); } zgui.bulletText("zig-toml by Aeron Avery", .{}); zgui.bulletText("bitfield.zig by Hannes Bredberg and FlorenceOS contributors", .{}); } if (state.win_stat.show_regs) { _ = zgui.begin("Guest Registers", .{ .popen = &state.win_stat.show_regs }); 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); } if (state.win_stat.show_perf) { _ = zgui.begin("Performance", .{ .popen = &state.win_stat.show_perf }); defer zgui.end(); const tmp = blk: { var buf: [histogram_len]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; @memcpy(buf[0..len], values[0..len]); std.mem.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]}); } if (state.win_stat.show_schedule) { _ = zgui.begin("Schedule", .{ .popen = &state.win_stat.show_schedule }); 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; @memcpy(&items, scheduler.queue.items); std.mem.sort(Event, items[0..len], {}, widgets.eventDesc(Event)); for (items[0..len]) |event| { zgui.text("{X:0>16} | {?}", .{ event.tick, event.kind }); } } if (state.win_stat.show_palette) { _ = zgui.begin("Palette", .{ .popen = &state.win_stat.show_palette }); defer zgui.end(); widgets.paletteGrid(.Background, cpu); zgui.sameLine(.{ .spacing = 20.0 }); widgets.paletteGrid(.Object, cpu); } { zgui.showDemoWindow(null); } return true; // request redraw } const widgets = struct { const PaletteKind = enum { Background, Object }; fn paletteGrid(comptime kind: PaletteKind, cpu: *const Arm7tdmi) void { _ = zgui.beginGroup(); defer zgui.endGroup(); const address: u32 = switch (kind) { .Background => 0x0500_0000, .Object => 0x0500_0200, }; for (0..0x100) |i| { const offset = @truncate(u32, i); const bgr555 = cpu.bus.dbgRead(u16, address + offset * @sizeOf(u16)); widgets.colourSquare(bgr555); if ((i + 1) % 0x10 != 0) zgui.sameLine(.{}); } zgui.text(@tagName(kind), .{}); } fn colourSquare(bgr555: u16) void { // FIXME: working with the packed struct enum is currently broken :pensive: const ImguiColorEditFlags_NoInputs: u32 = 1 << 5; const ImguiColorEditFlags_NoPicker: u32 = 1 << 2; const flags = @bitCast(zgui.ColorEditFlags, ImguiColorEditFlags_NoInputs | ImguiColorEditFlags_NoPicker); const b = @intToFloat(f32, bgr555 >> 10 & 0x1f); const g = @intToFloat(f32, bgr555 >> 5 & 0x1F); const r = @intToFloat(f32, bgr555 & 0x1F); var col = [_]f32{ r / 31.0, g / 31.0, b / 31.0 }; _ = zgui.colorEdit3("", .{ .col = &col, .flags = flags }); } 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; } }; fn handleTitle(title_opt: ?*const [12]u8) [12:0]u8 { if (title_opt == null) return "[N/A Title]\x00".*; // No ROM present const title = title_opt.?; // ROM Title is an empty string (ImGui hates these) if (title[0] == '\x00') return "[No Title]\x00\x00".*; return title.* ++ [_:0]u8{}; }