From 8277d19dd510fb0be462a345c3bd1f24f9eba5de Mon Sep 17 00:00:00 2001 From: Gunter Hager Date: Sun, 16 Mar 2025 15:37:57 +0100 Subject: [PATCH 01/20] Implemented render callback so that we can use IMGUI --- build.zig.zon | 4 ++-- src/host/gfx.zig | 4 ++++ 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/build.zig.zon b/build.zig.zon index c60b7c3..f9f792f 100644 --- a/build.zig.zon +++ b/build.zig.zon @@ -4,8 +4,8 @@ .fingerprint = 0x3ecb34175baf6f81, .dependencies = .{ .sokol = .{ - .url = "git+https://github.com/floooh/sokol-zig.git#13d22dd4442cfc5daf496b1db3be6c16eedb9c9d", - .hash = "sokol-0.1.0-pb1HK3tgNgDugWnzeI4S8PQ1usvj559ADkdFiRyB9cxt", + .url = "git+https://github.com/floooh/sokol-zig.git#9fd3e75019b5015ab45aa5dee3a699955721f802", + .hash = "sokol-0.1.0-pb1HK0rZOQCGojU9hkELvtwS3O7FsQYgWy2g2ZKyaeLs", }, }, .paths = .{ diff --git a/src/host/gfx.zig b/src/host/gfx.zig index c31ecbd..3e5de6d 100644 --- a/src/host/gfx.zig +++ b/src/host/gfx.zig @@ -45,6 +45,7 @@ pub const Status = struct { const DrawOptions = struct { display: DisplayInfo, status: ?Status = null, + render_cb: ?*const fn () void = null, }; const DrawFunc = *const fn () void; @@ -289,6 +290,9 @@ pub fn draw(opts: DrawOptions) void { } }; sg.applyUniforms(shaders.UB_offscreen_vs_params, sg.asRange(&vs_params)); sg.draw(0, 4, 1); + if (opts.render_cb) |cb| { + cb(); + } sg.endPass(); // draw display pass with linear filtering From 7898bc53723990da466a7c18e0ada0d1c8331298 Mon Sep 17 00:00:00 2001 From: Gunter Hager Date: Sun, 16 Mar 2025 15:50:42 +0100 Subject: [PATCH 02/20] Added IMGUI dependency --- build.zig | 9 +++++++++ build.zig.zon | 4 ++++ emus/build.zig | 11 +++++++++++ 3 files changed, 24 insertions(+) diff --git a/build.zig b/build.zig index efb0028..953d246 100644 --- a/build.zig +++ b/build.zig @@ -11,7 +11,15 @@ pub fn build(b: *Build) void { const dep_sokol = b.dependency("sokol", .{ .target = target, .optimize = optimize, + .with_sokol_imgui = true, }); + const dep_cimgui = b.dependency("cimgui", .{ + .target = target, + .optimize = optimize, + }); + + // inject the cimgui header search path into the sokol C library compile step + dep_sokol.artifact("sokol_clib").addIncludePath(dep_cimgui.path("src")); // internal module definitions const mod_common = b.addModule("common", .{ @@ -42,6 +50,7 @@ pub fn build(b: *Build) void { .optimize = optimize, .imports = &.{ .{ .name = "sokol", .module = dep_sokol.module("sokol") }, + .{ .name = "cimgui", .module = dep_cimgui.module("cimgui") }, .{ .name = "common", .module = mod_common }, }, }); diff --git a/build.zig.zon b/build.zig.zon index f9f792f..a0ddbae 100644 --- a/build.zig.zon +++ b/build.zig.zon @@ -7,6 +7,10 @@ .url = "git+https://github.com/floooh/sokol-zig.git#9fd3e75019b5015ab45aa5dee3a699955721f802", .hash = "sokol-0.1.0-pb1HK0rZOQCGojU9hkELvtwS3O7FsQYgWy2g2ZKyaeLs", }, + .cimgui = .{ + .url = "git+https://github.com/floooh/dcimgui.git#3969c14f7c7abda0e4b59d2616b17b7fb9eb0827", + .hash = "cimgui-0.1.0-44ClkTt5hgBU8BelH8W_G8mso3ys_hrqNUWwJvaxXDs5", + }, }, .paths = .{ "build.zig", diff --git a/emus/build.zig b/emus/build.zig index 6dee501..6ceff9d 100644 --- a/emus/build.zig +++ b/emus/build.zig @@ -37,7 +37,15 @@ pub fn build(b: *Build, opts: Options) void { const dep_sokol = b.dependency("sokol", .{ .target = opts.target, .optimize = opts.optimize, + .with_sokol_imgui = true, }); + const dep_cimgui = b.dependency("cimgui", .{ + .target = opts.target, + .optimize = opts.optimize, + }); + + // inject the cimgui header search path into the sokol C library compile step + dep_sokol.artifact("sokol_clib").addIncludePath(dep_cimgui.path("src")); inline for (emulators) |emu| { addEmulator(b, .{ @@ -48,6 +56,7 @@ pub fn build(b: *Build, opts: Options) void { .optimize = opts.optimize, .mod_chipz = opts.mod_chipz, .mod_sokol = dep_sokol.module("sokol"), + .mod_cimgui = dep_cimgui.module("cimgui"), }); } } @@ -60,6 +69,7 @@ const EmuOptions = struct { optimize: OptimizeMode, mod_chipz: *Module, mod_sokol: *Module, + mod_cimgui: *Module, }; fn addEmulator(b: *Build, opts: EmuOptions) void { @@ -70,6 +80,7 @@ fn addEmulator(b: *Build, opts: EmuOptions) void { .imports = &.{ .{ .name = "chipz", .module = opts.mod_chipz }, .{ .name = "sokol", .module = opts.mod_sokol }, + .{ .name = "cimgui", .module = opts.mod_cimgui }, }, }); if (opts.model != .NONE) { From 7b45e39988e9535595389439896a7eb80129e74f Mon Sep 17 00:00:00 2001 From: Gunter Hager Date: Sun, 16 Mar 2025 20:47:12 +0100 Subject: [PATCH 03/20] Removed render_cb since it's obsolete --- src/host/gfx.zig | 4 ---- 1 file changed, 4 deletions(-) diff --git a/src/host/gfx.zig b/src/host/gfx.zig index 3e5de6d..c31ecbd 100644 --- a/src/host/gfx.zig +++ b/src/host/gfx.zig @@ -45,7 +45,6 @@ pub const Status = struct { const DrawOptions = struct { display: DisplayInfo, status: ?Status = null, - render_cb: ?*const fn () void = null, }; const DrawFunc = *const fn () void; @@ -290,9 +289,6 @@ pub fn draw(opts: DrawOptions) void { } }; sg.applyUniforms(shaders.UB_offscreen_vs_params, sg.asRange(&vs_params)); sg.draw(0, 4, 1); - if (opts.render_cb) |cb| { - cb(); - } sg.endPass(); // draw display pass with linear filtering From c9a5772069ed181a3173cd449c28030e13bffa65 Mon Sep 17 00:00:00 2001 From: Gunter Hager Date: Mon, 17 Mar 2025 18:02:59 +0100 Subject: [PATCH 04/20] Started to implement debug UI --- emus/kc85/kc85.zig | 106 +++++++++++++++++++++++++ src/ui/ui_chip.zig | 189 +++++++++++++++++++++++++++++++++++++++++++++ src/ui/ui_util.zig | 9 +++ src/ui/ui_z80.zig | 0 4 files changed, 304 insertions(+) create mode 100644 src/ui/ui_chip.zig create mode 100644 src/ui/ui_util.zig create mode 100644 src/ui/ui_z80.zig diff --git a/emus/kc85/kc85.zig b/emus/kc85/kc85.zig index fe3fe33..d4bb366 100644 --- a/emus/kc85/kc85.zig +++ b/emus/kc85/kc85.zig @@ -9,6 +9,9 @@ const slog = sokol.log; const host = @import("chipz").host; const kc85 = @import("chipz").systems.kc85; +const ig = @import("cimgui"); +const simgui = sokol.imgui; + const model: kc85.Model = switch (build_options.model) { .KC852 => .KC852, .KC853 => .KC853, @@ -58,6 +61,11 @@ export fn init() void { }, }); host.gfx.init(.{ .display = sys.displayInfo() }); + // initialize sokol-imgui + simgui.setup(.{ + .logger = .{ .func = slog.func }, + }); + host.gfx.addDrawFunc(renderGUI); // insert modules inline for (.{ &args.slot8, &args.slotc }, .{ 0x08, 0x0C }) |slot, slot_addr| { @@ -70,12 +78,105 @@ export fn init() void { } } +fn renderGUI() void { + simgui.render(); +} + +fn uiDrawMenu() void { + if (ig.igBeginMainMenuBar()) { + if (ig.igBeginMenu("System")) { + if (ig.igMenuItem("Reset")) { + // TODO: implement reset + } + if (ig.igMenuItem("Cold Boot")) { + // TODO: implement cold boot + } + ig.igEndMenu(); + } + if (ig.igBeginMenu("Hardware")) { + if (ig.igMenuItem("Memory Map")) { + // TODO: open window + } + if (ig.igMenuItem("System State")) { + // TODO: open window + } + if (ig.igMenuItem("Audio Output")) { + // TODO: open window + } + if (ig.igMenuItem("Display")) { + // TODO: open window + } + if (ig.igMenuItem("Z80")) { + // TODO: open chip window + } + if (ig.igMenuItem("Z80 PIO")) { + // TODO: open chip window + } + if (ig.igMenuItem("Z80 CTC")) { + // TODO: open chip window + } + ig.igEndMenu(); + } + if (ig.igBeginMenu("Debug")) { + if (ig.igMenuItem("CPU Debugger")) { + // TODO: open window + } + if (ig.igMenuItem("Breakpoints")) { + // TODO: open window + } + if (ig.igBeginMenu("Memory Editor")) { + if (ig.igMenuItem("Window #1")) { + // TODO: open window + } + if (ig.igMenuItem("Window #2")) { + // TODO: open window + } + if (ig.igMenuItem("Window #3")) { + // TODO: open window + } + if (ig.igMenuItem("Window #4")) { + // TODO: open window + } + ig.igEndMenu(); + } + if (ig.igBeginMenu("Disassembler")) { + if (ig.igMenuItem("Window #1")) { + // TODO: open window + } + if (ig.igMenuItem("Window #2")) { + // TODO: open window + } + if (ig.igMenuItem("Window #3")) { + // TODO: open window + } + if (ig.igMenuItem("Window #4")) { + // TODO: open window + } + ig.igEndMenu(); + } + ig.igEndMenu(); + } + ig.igEndMainMenuBar(); + } +} + export fn frame() void { const frame_time_us = host.time.frameTime(); host.prof.pushMicroSeconds(.FRAME, frame_time_us); host.time.emuStart(); const num_ticks = sys.exec(frame_time_us); host.prof.pushMicroSeconds(.EMU, host.time.emuEnd()); + + // call simgui.newFrame() before any ImGui calls + simgui.newFrame(.{ + .width = sapp.width(), + .height = sapp.height(), + .delta_time = sapp.frameDuration(), + .dpi_scale = sapp.dpiScale(), + }); + + uiDrawMenu(); + host.gfx.draw(.{ .display = sys.displayInfo(), .status = .{ @@ -95,6 +196,7 @@ export fn frame() void { } export fn cleanup() void { + simgui.shutdown(); host.gfx.shutdown(); host.prof.shutdown(); host.audio.shutdown(); @@ -107,6 +209,10 @@ export fn cleanup() void { export fn input(ev: ?*const sapp.Event) void { const event = ev.?; const shift = 0 != (event.modifiers & sapp.modifier_shift); + + // forward input events to sokol-imgui + _ = simgui.handleEvent(event.*); + switch (event.type) { .CHAR => { var c: u8 = @truncate(event.char_code); diff --git a/src/ui/ui_chip.zig b/src/ui/ui_chip.zig new file mode 100644 index 0000000..6bdcc22 --- /dev/null +++ b/src/ui/ui_chip.zig @@ -0,0 +1,189 @@ +const std = @import("std"); +const math = std.math; +const ig = @import("cimgui"); +const ui_util = @import("ui_util.zig"); + +pub const TypeConfig = struct { + bus: type, +}; + +pub fn Type(comptime cfg: TypeConfig) type { + const Bus = cfg.bus; + + const ChipPin = struct { + name: []u8, + slot: comptime_int, + mask: Bus, + }; + + const MAX_PINS = 64; + + return struct { + const Self = @This(); + + name: []u8, + num_slots: comptime_int = 0, + num_slots_left: comptime_int = 0, + num_slots_right: comptime_int = 0, + num_slots_top: comptime_int = 0, + num_slots_bottom: comptime_int = 0, + chip_width: comptime_float = 0, + chip_height: comptime_float = 0, + pin_slot_dist: comptime_float = 0, + pin_width: comptime_float = 0, + pin_height: comptime_float = 0, + are_pin_names_inside: bool = false, + is_name_outside: bool = false, + pins: [MAX_PINS]ChipPin, + + pub fn init(chip: Self) Self { + var self = chip; + if (self.num_slots == 0) { + self.num_slots = self.num_slots_left + self.num_slots_right + self.num_slots_top + self.num_slots_bottom; + } else { + self.num_slots_right = self.num_slots / 2; + self.num_slots_left = self.num_slots - self.num_slots_right; + } + if (self.pin_slot_dist == 0) { + self.pin_slot_dist = 16; + } + if (self.pin_width == 0) { + self.pin_width = 12; + } + if (self.pin_height == 0) { + self.pin_height = 12; + } + if (self.chip_width == 0) { + const slots = @max(self.num_slots_top, self.num_slots_bottom); + self.chip_width = if (slots > 0) slots * self.pin_slot_dist else 64; + } + if (self.chip_height == 0) { + const slots = @max(self.num_slots_left, self.num_slots_right); + self.chip_height = if (slots > 0) slots * self.pin_slot_dist else 64; + } + } + + /// Get screen pos of center of pin (by pin index) with chip center at cx, cy + fn pinPos(self: *Self, pin_index: comptime_int, c: ig.ImVec2) ig.ImVec2 { + var pos = ig.ImVec2{ .x = 0, .y = 0 }; + if (pin_index < self.num_slots) { + const w = self.chip_width; + const h = self.chip_height; + const zero = ig.ImVec2{ .x = math.floor(c.x - w / 2), .y = math.floor(c.y - h / 2) }; + const slot_dist = self.pin_slot_dist; + const pwh: f32 = self.pin_width / 2; + const phh: f32 = self.pin_height / 2; + const l = self.num_slots_left; + const r = self.num_slots_right; + const t = self.num_slots_top; + const b = self.num_slots_bottom; + const pin = self.pins[pin_index]; + if (pin.slot < l) { + // left side + pos.x = zero.x - pwh; + pos.y = zero.y + slot_dist / 2 + pin.slot * slot_dist; + } else if (pin.slot < (l + r)) { + // right side + pos.x = zero.x + w + pwh; + pos.y = zero.y + slot_dist / 2 + (pin.slot - l) * slot_dist; + } else if (pin.slot < (l + r + t)) { + // top side + pos.x = zero.x + slot_dist / 2 + (pin.slot - (l + r)) * slot_dist; + pos.y = zero.y - phh; + } else if (pin.slot < (l + r + t + b)) { + pos.x = zero.x + slot_dist / 2 + (pin.slot - (l + r + t)) * slot_dist; + pos.y = zero.y + h + phh; + } + } + return pos; + } + + /// Find pin index by pin bit + fn pinIndex(self: *Self, mask: Bus) ?usize { + for (0..self.num_slots) |index| { + if (self.pins[index].mask == mask) { + return index; + } + } + return null; + } + + /// Get screen pos of center of pin (by pin index) with chip center at cx, cy with pin bit mask + fn pinMaskPos(self: *Self, pin_mask: Bus, c: ig.ImVec2) ?ig.ImVec2 { + if (self.pinIndex(pin_mask)) |pin_index| { + return self.pinPos(pin_index, c); + } else { + return null; + } + } + + /// Draw chip centered at screen pos + pub fn drawAt(self: *Self, pins: Bus, c: ig.ImVec2) void { + const dl = ig.igGetWindowDrawList(); + const w = self.pin_width; + const h = self.chip_height; + const p0 = ig.ImVec2{ .x = math.floor(c.x - w / 2), .y = math.floor(c.y - h / 2) }; + const p1 = ig.ImVec2{ .x = p0.x + w, .y = p0.y + h }; + const m = ig.ImVec2{ .x = math.floor((p0.x + p1.x) / 2), .y = math.floor((p0.y + p1.y) / 2) }; + const pw = self.pin_width; + const ph = self.pin_height; + const l = self.num_slots_left; + const r = self.num_slots_right; + const text_color = ui_util.color(ig.ImGuiCol_Text); + const line_color = text_color; + const pin_color_on = ig.ImColor{ig.ImVec4{ .x = 0, .y = 1, .z = 0, .w = ig.igGetStyle().Alpha }}; + const pin_color_off = ig.ImColor{ig.ImVec4{ .x = 0, .y = 0.25, .z = 0, .w = ig.igGetStyle().Alpha }}; + + dl.AddRect(p0, p1, line_color); + const ts = ig.igCalcTextSize(self.name); + if (self.is_name_outside) { + dl.AddText(.{ .x = m.x - ts.x / 2, .y = p0.y - ts.y }, text_color, self.name); + } else { + dl.AddText(.{ .x = m.x - ts.x / 2, .y = m.y - ts.y / 2 }, text_color, self.name); + } + var p = ig.ImVec2{}; + var t = ig.ImVec2{}; + for (0..self.num_slots) |index| { + const pin = self.pins[index]; + if (pin.name.len == 0) break; + const pin_pos = self.pinPos(index, c); + p.x = pin_pos.x - pw / 2; + p.y = pin_pos.y - ph / 2; + ts = ig.igCalcTextSize(pin.name); + if (pin.slot < l) { + // left side + if (self.are_pin_names_inside) { + t.x = p.x + pw + 4; + } else { + t.x = p.x - ts.x - 4; + } + t.y = p.y + ph / 2 - ts.y / 2; + } else if (pin.slot < (l + r)) { + // right side + if (self.are_pin_names_inside) { + t.x = p.x - ts.x - 4; + } else { + t.x = p.x + pw + 4; + } + t.y = p.y + ph / 2 - ts.y / 2; + } else { + // FIXME: top/bottom text (must be rendered vertical) + t = p; + } + const pin_color = if ((pins & pin.mask) != 0) pin_color_on else pin_color_off; + const pp = ig.ImVec2{ .x = p.x + pw, .y = p.y + ph }; + dl.AddRectFilled(p, pp, pin_color); + dl.AddRect(p, pp, line_color); + dl.AddText(t, text_color, pin.name); + } + } + + /// Draw chip centered at current ImGui cursor pos + pub fn draw(self: *Self, pins: Bus) void { + const canvas_pos = ig.igGetCursorScreenPos(); + const canvas_area = ig.igGetContentRegionAvail(); + const c = ig.ImVec2{ .x = canvas_pos.x + canvas_area.x / 2, .y = canvas_pos.y + canvas_area.y / 2 }; + self.drawAt(pins, c); + } + }; +} diff --git a/src/ui/ui_util.zig b/src/ui/ui_util.zig new file mode 100644 index 0000000..1dbd11b --- /dev/null +++ b/src/ui/ui_util.zig @@ -0,0 +1,9 @@ +//! UI helper functions +const ig = @import("cimgui"); + +pub fn color(imgui_color: c_int) ig.ImColor { + const style = ig.igGetStyle(); + var c = style.Colors[imgui_color]; + c.w *= style.Alpha; + return ig.ImColor{c}; +} diff --git a/src/ui/ui_z80.zig b/src/ui/ui_z80.zig new file mode 100644 index 0000000..e69de29 From 5a160be6c826deca1636338e598162c11d8164e7 Mon Sep 17 00:00:00 2001 From: Gunter Hager Date: Tue, 18 Mar 2025 12:22:46 +0100 Subject: [PATCH 05/20] Started to implement Z80 window --- build.zig | 20 +++++++++++ build.zig.zon | 4 +++ emus/build.zig | 11 ++++++ emus/kc85/kc85.zig | 49 +++++++++++++++++++++++++- src/chips/z80.zig | 2 +- src/chipz.zig | 1 + src/systems/kc85.zig | 14 ++++---- src/ui/ui.zig | 3 ++ src/ui/ui_chip.zig | 84 ++++++++++++++++++++++++-------------------- src/ui/ui_util.zig | 6 ++-- src/ui/ui_z80.zig | 80 +++++++++++++++++++++++++++++++++++++++++ 11 files changed, 222 insertions(+), 52 deletions(-) create mode 100644 src/ui/ui.zig diff --git a/build.zig b/build.zig index efb0028..6669934 100644 --- a/build.zig +++ b/build.zig @@ -11,8 +11,16 @@ pub fn build(b: *Build) void { const dep_sokol = b.dependency("sokol", .{ .target = target, .optimize = optimize, + .with_sokol_imgui = true, + }); + const dep_cimgui = b.dependency("cimgui", .{ + .target = target, + .optimize = optimize, }); + // inject the cimgui header search path into the sokol C library compile step + dep_sokol.artifact("sokol_clib").addIncludePath(dep_cimgui.path("src")); + // internal module definitions const mod_common = b.addModule("common", .{ .root_source_file = b.path("src/common/common.zig"), @@ -45,6 +53,17 @@ pub fn build(b: *Build) void { .{ .name = "common", .module = mod_common }, }, }); + const mod_ui = b.addModule("ui", .{ + .root_source_file = b.path("src/ui/ui.zig"), + .target = target, + .optimize = optimize, + .imports = &.{ + .{ .name = "sokol", .module = dep_sokol.module("sokol") }, + .{ .name = "cimgui", .module = dep_cimgui.module("cimgui") }, + .{ .name = "common", .module = mod_common }, + .{ .name = "chips", .module = mod_chips }, + }, + }); // top-level modules const mod_chipz = b.addModule("chipz", .{ @@ -56,6 +75,7 @@ pub fn build(b: *Build) void { .{ .name = "chips", .module = mod_chips }, .{ .name = "systems", .module = mod_systems }, .{ .name = "host", .module = mod_host }, + .{ .name = "ui", .module = mod_ui }, }, }); diff --git a/build.zig.zon b/build.zig.zon index f9f792f..a0ddbae 100644 --- a/build.zig.zon +++ b/build.zig.zon @@ -7,6 +7,10 @@ .url = "git+https://github.com/floooh/sokol-zig.git#9fd3e75019b5015ab45aa5dee3a699955721f802", .hash = "sokol-0.1.0-pb1HK0rZOQCGojU9hkELvtwS3O7FsQYgWy2g2ZKyaeLs", }, + .cimgui = .{ + .url = "git+https://github.com/floooh/dcimgui.git#3969c14f7c7abda0e4b59d2616b17b7fb9eb0827", + .hash = "cimgui-0.1.0-44ClkTt5hgBU8BelH8W_G8mso3ys_hrqNUWwJvaxXDs5", + }, }, .paths = .{ "build.zig", diff --git a/emus/build.zig b/emus/build.zig index 6dee501..6ceff9d 100644 --- a/emus/build.zig +++ b/emus/build.zig @@ -37,7 +37,15 @@ pub fn build(b: *Build, opts: Options) void { const dep_sokol = b.dependency("sokol", .{ .target = opts.target, .optimize = opts.optimize, + .with_sokol_imgui = true, }); + const dep_cimgui = b.dependency("cimgui", .{ + .target = opts.target, + .optimize = opts.optimize, + }); + + // inject the cimgui header search path into the sokol C library compile step + dep_sokol.artifact("sokol_clib").addIncludePath(dep_cimgui.path("src")); inline for (emulators) |emu| { addEmulator(b, .{ @@ -48,6 +56,7 @@ pub fn build(b: *Build, opts: Options) void { .optimize = opts.optimize, .mod_chipz = opts.mod_chipz, .mod_sokol = dep_sokol.module("sokol"), + .mod_cimgui = dep_cimgui.module("cimgui"), }); } } @@ -60,6 +69,7 @@ const EmuOptions = struct { optimize: OptimizeMode, mod_chipz: *Module, mod_sokol: *Module, + mod_cimgui: *Module, }; fn addEmulator(b: *Build, opts: EmuOptions) void { @@ -70,6 +80,7 @@ fn addEmulator(b: *Build, opts: EmuOptions) void { .imports = &.{ .{ .name = "chipz", .module = opts.mod_chipz }, .{ .name = "sokol", .module = opts.mod_sokol }, + .{ .name = "cimgui", .module = opts.mod_cimgui }, }, }); if (opts.model != .NONE) { diff --git a/emus/kc85/kc85.zig b/emus/kc85/kc85.zig index d4bb366..e083cf6 100644 --- a/emus/kc85/kc85.zig +++ b/emus/kc85/kc85.zig @@ -8,6 +8,7 @@ const sapp = sokol.app; const slog = sokol.log; const host = @import("chipz").host; const kc85 = @import("chipz").systems.kc85; +const ui = @import("chipz").ui; const ig = @import("cimgui"); const simgui = sokol.imgui; @@ -24,6 +25,44 @@ const name = switch (model) { .KC854 => "KC85/4", }; const KC85 = kc85.Type(model); +const UI_CHIP = ui.ui_chip.Type(.{ .bus = kc85.Bus }); +const UI_Z80 = ui.ui_z80.Type(.{ .bus = kc85.Bus, .cpu = kc85.Z80 }); +const UI_Z80_Pins = [_]UI_CHIP.Pin{ + .{ .name = "D0", .slot = 0, .mask = kc85.Z80.D0 }, + .{ .name = "D1", .slot = 1, .mask = kc85.Z80.D1 }, + .{ .name = "D2", .slot = 2, .mask = kc85.Z80.D2 }, + .{ .name = "D3", .slot = 3, .mask = kc85.Z80.D3 }, + .{ .name = "D4", .slot = 4, .mask = kc85.Z80.D4 }, + .{ .name = "D5", .slot = 5, .mask = kc85.Z80.D5 }, + .{ .name = "D6", .slot = 6, .mask = kc85.Z80.D6 }, + .{ .name = "D7", .slot = 7, .mask = kc85.Z80.D7 }, + .{ .name = "M1", .slot = 8, .mask = kc85.Z80.M1 }, + .{ .name = "MREQ", .slot = 9, .mask = kc85.Z80.MREQ }, + .{ .name = "IORQ", .slot = 10, .mask = kc85.Z80.IORQ }, + .{ .name = "RD", .slot = 11, .mask = kc85.Z80.RD }, + .{ .name = "WR", .slot = 12, .mask = kc85.Z80.WR }, + .{ .name = "RFSH", .slot = 13, .mask = kc85.Z80.RFSH }, + .{ .name = "HALT", .slot = 14, .mask = kc85.Z80.HALT }, + .{ .name = "INT", .slot = 15, .mask = kc85.Z80.INT }, + .{ .name = "NMI", .slot = 16, .mask = kc85.Z80.NMI }, + .{ .name = "WAIT", .slot = 17, .mask = kc85.Z80.WAIT }, + .{ .name = "A0", .slot = 18, .mask = kc85.Z80.A0 }, + .{ .name = "A1", .slot = 19, .mask = kc85.Z80.A1 }, + .{ .name = "A2", .slot = 20, .mask = kc85.Z80.A2 }, + .{ .name = "A3", .slot = 21, .mask = kc85.Z80.A3 }, + .{ .name = "A4", .slot = 22, .mask = kc85.Z80.A4 }, + .{ .name = "A5", .slot = 23, .mask = kc85.Z80.A5 }, + .{ .name = "A6", .slot = 24, .mask = kc85.Z80.A6 }, + .{ .name = "A7", .slot = 25, .mask = kc85.Z80.A7 }, + .{ .name = "A8", .slot = 26, .mask = kc85.Z80.A8 }, + .{ .name = "A9", .slot = 27, .mask = kc85.Z80.A9 }, + .{ .name = "A10", .slot = 28, .mask = kc85.Z80.A10 }, + .{ .name = "A11", .slot = 29, .mask = kc85.Z80.A11 }, + .{ .name = "A12", .slot = 30, .mask = kc85.Z80.A12 }, + .{ .name = "A13", .slot = 31, .mask = kc85.Z80.A13 }, + .{ .name = "A14", .slot = 32, .mask = kc85.Z80.A14 }, + .{ .name = "A15", .slot = 33, .mask = kc85.Z80.A15 }, +}; // a once-trigger for loading a file after booting has finished var file_loaded = host.time.Once.init(switch (model) { @@ -34,6 +73,7 @@ var file_loaded = host.time.Once.init(switch (model) { var sys: KC85 = undefined; var gpa = GeneralPurposeAllocator(.{}){}; var args: Args = undefined; +var ui_z80: UI_Z80 = undefined; export fn init() void { host.audio.init(.{}); @@ -60,6 +100,12 @@ export fn init() void { }, }, }); + ui_z80.initInPlace(.{ + .title = "Z80 CPU", + .cpu = &sys.cpu, + .origin = .{ .x = 30, .y = 30 }, + .chip = .{ .name = "Z80\nCPU", .num_slots = 36, .pins = &UI_Z80_Pins }, + }); host.gfx.init(.{ .display = sys.displayInfo() }); // initialize sokol-imgui simgui.setup(.{ @@ -107,7 +153,7 @@ fn uiDrawMenu() void { // TODO: open window } if (ig.igMenuItem("Z80")) { - // TODO: open chip window + ui_z80.open = true; } if (ig.igMenuItem("Z80 PIO")) { // TODO: open chip window @@ -176,6 +222,7 @@ export fn frame() void { }); uiDrawMenu(); + ui_z80.draw(sys.bus); host.gfx.draw(.{ .display = sys.displayInfo(), diff --git a/src/chips/z80.zig b/src/chips/z80.zig index 153b72c..0009747 100644 --- a/src/chips/z80.zig +++ b/src/chips/z80.zig @@ -87,7 +87,7 @@ pub fn Type(comptime cfg: TypeConfig) type { pub const A7 = mask(Bus, cfg.pins.ABUS[7]); pub const A8 = mask(Bus, cfg.pins.ABUS[8]); pub const A9 = mask(Bus, cfg.pins.ABUS[9]); - pub const A10 = mask(Bus, cfg.pins.AB[10]); + pub const A10 = mask(Bus, cfg.pins.ABUS[10]); pub const A11 = mask(Bus, cfg.pins.ABUS[11]); pub const A12 = mask(Bus, cfg.pins.ABUS[12]); pub const A13 = mask(Bus, cfg.pins.ABUS[13]); diff --git a/src/chipz.zig b/src/chipz.zig index 4eb2f96..85ff28b 100644 --- a/src/chipz.zig +++ b/src/chipz.zig @@ -2,3 +2,4 @@ pub const chips = @import("chips"); pub const common = @import("common"); pub const systems = @import("systems"); pub const host = @import("host"); +pub const ui = @import("ui"); diff --git a/src/systems/kc85.zig b/src/systems/kc85.zig index 88b4422..462ca21 100644 --- a/src/systems/kc85.zig +++ b/src/systems/kc85.zig @@ -82,13 +82,13 @@ const IO84_PINS = [8]comptime_int{ 80, 81, 82, 83, 84, 85, 86, 87 }; const IO86_PINS = [8]comptime_int{ 88, 89, 90, 91, 92, 93, 94, 95 }; // NOTE: 64 bits isn't enough for the system bus -const Bus = u128; -const Memory = memory.Type(.{ .page_size = 0x0400 }); -const Z80 = z80.Type(.{ .pins = CPU_PINS, .bus = Bus }); -const Z80PIO = z80pio.Type(.{ .pins = PIO_PINS, .bus = Bus }); -const Z80CTC = z80ctc.Type(.{ .pins = CTC_PINS, .bus = Bus }); -const KeyBuf = keybuf.Type(.{ .num_slots = 4 }); -const Audio = audio.Type(.{ .num_voices = 2 }); +pub const Bus = u128; +pub const Memory = memory.Type(.{ .page_size = 0x0400 }); +pub const Z80 = z80.Type(.{ .pins = CPU_PINS, .bus = Bus }); +pub const Z80PIO = z80pio.Type(.{ .pins = PIO_PINS, .bus = Bus }); +pub const Z80CTC = z80ctc.Type(.{ .pins = CTC_PINS, .bus = Bus }); +pub const KeyBuf = keybuf.Type(.{ .num_slots = 4 }); +pub const Audio = audio.Type(.{ .num_voices = 2 }); const getData = Z80.getData; const setData = Z80.setData; diff --git a/src/ui/ui.zig b/src/ui/ui.zig new file mode 100644 index 0000000..b330193 --- /dev/null +++ b/src/ui/ui.zig @@ -0,0 +1,3 @@ +pub const ui_chip = @import("ui_chip.zig"); +pub const ui_util = @import("ui_util.zig"); +pub const ui_z80 = @import("ui_z80.zig"); diff --git a/src/ui/ui_chip.zig b/src/ui/ui_chip.zig index 6bdcc22..a73c242 100644 --- a/src/ui/ui_chip.zig +++ b/src/ui/ui_chip.zig @@ -8,33 +8,30 @@ pub const TypeConfig = struct { }; pub fn Type(comptime cfg: TypeConfig) type { - const Bus = cfg.bus; - - const ChipPin = struct { - name: []u8, - slot: comptime_int, - mask: Bus, - }; - - const MAX_PINS = 64; - return struct { const Self = @This(); + const Bus = cfg.bus; + + pub const Pin = struct { + name: []const u8, + slot: usize, + mask: Bus, + }; - name: []u8, - num_slots: comptime_int = 0, - num_slots_left: comptime_int = 0, - num_slots_right: comptime_int = 0, - num_slots_top: comptime_int = 0, - num_slots_bottom: comptime_int = 0, - chip_width: comptime_float = 0, - chip_height: comptime_float = 0, - pin_slot_dist: comptime_float = 0, - pin_width: comptime_float = 0, - pin_height: comptime_float = 0, + name: []const u8, + num_slots: usize = 0, + num_slots_left: usize = 0, + num_slots_right: usize = 0, + num_slots_top: usize = 0, + num_slots_bottom: usize = 0, + chip_width: f32 = 0, + chip_height: f32 = 0, + pin_slot_dist: f32 = 0, + pin_width: f32 = 0, + pin_height: f32 = 0, are_pin_names_inside: bool = false, is_name_outside: bool = false, - pins: [MAX_PINS]ChipPin, + pins: []const Pin, pub fn init(chip: Self) Self { var self = chip; @@ -54,17 +51,18 @@ pub fn Type(comptime cfg: TypeConfig) type { self.pin_height = 12; } if (self.chip_width == 0) { - const slots = @max(self.num_slots_top, self.num_slots_bottom); + const slots: f32 = @floatFromInt(@max(self.num_slots_top, self.num_slots_bottom)); self.chip_width = if (slots > 0) slots * self.pin_slot_dist else 64; } if (self.chip_height == 0) { - const slots = @max(self.num_slots_left, self.num_slots_right); + const slots: f32 = @floatFromInt(@max(self.num_slots_left, self.num_slots_right)); self.chip_height = if (slots > 0) slots * self.pin_slot_dist else 64; } + return self; } /// Get screen pos of center of pin (by pin index) with chip center at cx, cy - fn pinPos(self: *Self, pin_index: comptime_int, c: ig.ImVec2) ig.ImVec2 { + fn pinPos(self: *Self, pin_index: usize, c: ig.ImVec2) ig.ImVec2 { var pos = ig.ImVec2{ .x = 0, .y = 0 }; if (pin_index < self.num_slots) { const w = self.chip_width; @@ -78,20 +76,25 @@ pub fn Type(comptime cfg: TypeConfig) type { const t = self.num_slots_top; const b = self.num_slots_bottom; const pin = self.pins[pin_index]; + const pin_slot_f: f32 = @floatFromInt(pin.slot); + const l_f: f32 = @floatFromInt(l); + const r_f: f32 = @floatFromInt(r); + const t_f: f32 = @floatFromInt(t); + if (pin.slot < l) { // left side pos.x = zero.x - pwh; - pos.y = zero.y + slot_dist / 2 + pin.slot * slot_dist; + pos.y = zero.y + slot_dist / 2 + pin_slot_f * slot_dist; } else if (pin.slot < (l + r)) { // right side pos.x = zero.x + w + pwh; - pos.y = zero.y + slot_dist / 2 + (pin.slot - l) * slot_dist; + pos.y = zero.y + slot_dist / 2 + (pin_slot_f - l_f) * slot_dist; } else if (pin.slot < (l + r + t)) { // top side - pos.x = zero.x + slot_dist / 2 + (pin.slot - (l + r)) * slot_dist; + pos.x = zero.x + slot_dist / 2 + (pin_slot_f - (l_f + r_f)) * slot_dist; pos.y = zero.y - phh; } else if (pin.slot < (l + r + t + b)) { - pos.x = zero.x + slot_dist / 2 + (pin.slot - (l + r + t)) * slot_dist; + pos.x = zero.x + slot_dist / 2 + (pin_slot_f - (l_f + r_f + t_f)) * slot_dist; pos.y = zero.y + h + phh; } } @@ -131,15 +134,18 @@ pub fn Type(comptime cfg: TypeConfig) type { const r = self.num_slots_right; const text_color = ui_util.color(ig.ImGuiCol_Text); const line_color = text_color; - const pin_color_on = ig.ImColor{ig.ImVec4{ .x = 0, .y = 1, .z = 0, .w = ig.igGetStyle().Alpha }}; - const pin_color_off = ig.ImColor{ig.ImVec4{ .x = 0, .y = 0.25, .z = 0, .w = ig.igGetStyle().Alpha }}; + const style = ig.igGetStyle(); + const pin_color_on = ig.igGetColorU32ImVec4(.{ .x = 0, .y = 1, .z = 0, .w = style.*.Alpha }); + const pin_color_off = ig.igGetColorU32ImVec4(.{ .x = 0, .y = 0.25, .z = 0, .w = style.*.Alpha }); - dl.AddRect(p0, p1, line_color); - const ts = ig.igCalcTextSize(self.name); + ig.ImDrawList_AddRect(dl, p0, p1, line_color); + const chip_ts = ig.igCalcTextSize(self.name.ptr); if (self.is_name_outside) { - dl.AddText(.{ .x = m.x - ts.x / 2, .y = p0.y - ts.y }, text_color, self.name); + const tpos = ig.ImVec2{ .x = m.x - chip_ts.x / 2, .y = p0.y - chip_ts.y }; + ig.ImDrawList_AddText(dl, tpos, text_color, self.name.ptr); } else { - dl.AddText(.{ .x = m.x - ts.x / 2, .y = m.y - ts.y / 2 }, text_color, self.name); + const tpos = ig.ImVec2{ .x = m.x - chip_ts.x / 2, .y = m.y - chip_ts.y / 2 }; + ig.ImDrawList_AddText(dl, tpos, text_color, self.name.ptr); } var p = ig.ImVec2{}; var t = ig.ImVec2{}; @@ -149,7 +155,7 @@ pub fn Type(comptime cfg: TypeConfig) type { const pin_pos = self.pinPos(index, c); p.x = pin_pos.x - pw / 2; p.y = pin_pos.y - ph / 2; - ts = ig.igCalcTextSize(pin.name); + const ts = ig.igCalcTextSize(pin.name.ptr); if (pin.slot < l) { // left side if (self.are_pin_names_inside) { @@ -172,9 +178,9 @@ pub fn Type(comptime cfg: TypeConfig) type { } const pin_color = if ((pins & pin.mask) != 0) pin_color_on else pin_color_off; const pp = ig.ImVec2{ .x = p.x + pw, .y = p.y + ph }; - dl.AddRectFilled(p, pp, pin_color); - dl.AddRect(p, pp, line_color); - dl.AddText(t, text_color, pin.name); + ig.ImDrawList_AddRectFilled(dl, p, pp, pin_color); + ig.ImDrawList_AddRect(dl, p, pp, line_color); + ig.ImDrawList_AddText(dl, t, text_color, pin.name.ptr); } } diff --git a/src/ui/ui_util.zig b/src/ui/ui_util.zig index 1dbd11b..a042ad9 100644 --- a/src/ui/ui_util.zig +++ b/src/ui/ui_util.zig @@ -1,9 +1,7 @@ //! UI helper functions const ig = @import("cimgui"); -pub fn color(imgui_color: c_int) ig.ImColor { +pub fn color(imgui_color: usize) ig.ImU32 { const style = ig.igGetStyle(); - var c = style.Colors[imgui_color]; - c.w *= style.Alpha; - return ig.ImColor{c}; + return ig.igGetColorU32Ex(@intCast(imgui_color), style.*.Alpha); } diff --git a/src/ui/ui_z80.zig b/src/ui/ui_z80.zig index e69de29..89d3cb7 100644 --- a/src/ui/ui_z80.zig +++ b/src/ui/ui_z80.zig @@ -0,0 +1,80 @@ +const chips = @import("chips"); +const z80 = chips.z80; +const ui_chip = @import("ui_chip.zig"); +const ig = @import("cimgui"); + +pub const TypeConfig = struct { + bus: type, + cpu: type, +}; + +pub fn Type(comptime cfg: TypeConfig) type { + return struct { + const Self = @This(); + const Bus = cfg.bus; + const Z80 = cfg.cpu; + const UI_Chip = ui_chip.Type(.{ .bus = cfg.bus }); + + pub const Options = struct { + title: []const u8, + cpu: *Z80, + origin: ig.ImVec2, + size: ig.ImVec2 = .{}, + open: bool = false, + chip: UI_Chip, + }; + + title: []const u8, + cpu: *Z80, + origin: ig.ImVec2, + size: ig.ImVec2, + open: bool, + last_open: bool, + valid: bool, + chip: UI_Chip, + + pub fn initInPlace(self: *Self, opts: Options) void { + self.* = .{ + .title = opts.title, + .cpu = opts.cpu, + .origin = opts.origin, + .size = .{ .x = if (opts.size.x == 0) 360 else opts.size.x, .y = if (opts.size.y == 0) 340 else opts.size.y }, + .open = opts.open, + .last_open = opts.open, + .valid = true, + .chip = opts.chip.init(), + }; + } + + pub fn discard(self: *Self) void { + self.valid = false; + } + + fn drawRegisters(self: *Self) void { + const cpu = self.cpu; + ig.igText("AF: %04X AF': %04X", cpu.AF(), cpu.af2); + ig.igSeparator(); + } + + pub fn draw(self: *Self, in_bus: Bus) void { + if (self.open != self.last_open) { + self.open = self.last_open; + } + if (!self.open) return; + ig.igSetNextWindowPos(self.origin, ig.ImGuiCond_FirstUseEver); + ig.igSetNextWindowSize(self.size, ig.ImGuiCond_FirstUseEver); + if (ig.igBegin(self.title.ptr, &self.open, ig.ImGuiWindowFlags_None)) { + if (ig.igBeginChild("##z80_chip", .{ .x = 176, .y = 0 }, ig.ImGuiChildFlags_None, ig.ImGuiWindowFlags_None)) { + self.chip.draw(in_bus); + } + ig.igEndChild(); + ig.igSameLine(); + if (ig.igBeginChild("##z80_regs", .{}, ig.ImGuiChildFlags_None, ig.ImGuiWindowFlags_None)) { + self.drawRegisters(); + } + ig.igEndChild(); + } + ig.igEnd(); + } + }; +} From 420c68653e9e40623bd13d0744e7be09bbadd77f Mon Sep 17 00:00:00 2001 From: Gunter Hager Date: Tue, 18 Mar 2025 14:37:23 +0100 Subject: [PATCH 06/20] Implemented Z80 debug UI --- emus/kc85/kc85.zig | 1 + src/ui/ui_chip.zig | 5 ++--- src/ui/ui_z80.zig | 39 ++++++++++++++++++++++++++++++++++++--- 3 files changed, 39 insertions(+), 6 deletions(-) diff --git a/emus/kc85/kc85.zig b/emus/kc85/kc85.zig index e083cf6..3e7c3b8 100644 --- a/emus/kc85/kc85.zig +++ b/emus/kc85/kc85.zig @@ -152,6 +152,7 @@ fn uiDrawMenu() void { if (ig.igMenuItem("Display")) { // TODO: open window } + ig.igSeparator(); if (ig.igMenuItem("Z80")) { ui_z80.open = true; } diff --git a/src/ui/ui_chip.zig b/src/ui/ui_chip.zig index a73c242..3a9424e 100644 --- a/src/ui/ui_chip.zig +++ b/src/ui/ui_chip.zig @@ -123,7 +123,7 @@ pub fn Type(comptime cfg: TypeConfig) type { /// Draw chip centered at screen pos pub fn drawAt(self: *Self, pins: Bus, c: ig.ImVec2) void { const dl = ig.igGetWindowDrawList(); - const w = self.pin_width; + const w = self.chip_width; const h = self.chip_height; const p0 = ig.ImVec2{ .x = math.floor(c.x - w / 2), .y = math.floor(c.y - h / 2) }; const p1 = ig.ImVec2{ .x = p0.x + w, .y = p0.y + h }; @@ -149,9 +149,8 @@ pub fn Type(comptime cfg: TypeConfig) type { } var p = ig.ImVec2{}; var t = ig.ImVec2{}; - for (0..self.num_slots) |index| { + for (0..self.pins.len) |index| { const pin = self.pins[index]; - if (pin.name.len == 0) break; const pin_pos = self.pinPos(index, c); p.x = pin_pos.x - pw / 2; p.y = pin_pos.y - ph / 2; diff --git a/src/ui/ui_z80.zig b/src/ui/ui_z80.zig index 89d3cb7..2f0ca92 100644 --- a/src/ui/ui_z80.zig +++ b/src/ui/ui_z80.zig @@ -50,15 +50,48 @@ pub fn Type(comptime cfg: TypeConfig) type { self.valid = false; } - fn drawRegisters(self: *Self) void { + fn drawRegisters(self: *Self, in_bus: Bus) void { const cpu = self.cpu; ig.igText("AF: %04X AF': %04X", cpu.AF(), cpu.af2); + ig.igText("BC: %04X BC': %04X", cpu.BC(), cpu.bc2); + ig.igText("DE: %04X DE': %04X", cpu.DE(), cpu.de2); + ig.igText("HL: %04X HL': %04X", cpu.HL(), cpu.hl2); ig.igSeparator(); + ig.igText("IX: %04X IY: %04X", cpu.IX(), cpu.IY()); + ig.igText("PC: %04X SP: %04X", cpu.pc, cpu.SP()); + ig.igText("IR: %04X WZ: %04X", cpu.ir, cpu.WZ()); + ig.igText("IM: %02X", cpu.im); + ig.igSeparator(); + const f = cpu.r[Z80.F]; + const flags = [_]u8{ + if ((f & Z80.SF) != 0) 'S' else '-', + if ((f & Z80.ZF) != 0) 'Z' else '-', + if ((f & Z80.YF) != 0) 'Y' else '-', + if ((f & Z80.HF) != 0) 'H' else '-', + if ((f & Z80.XF) != 0) 'X' else '-', + if ((f & Z80.VF) != 0) 'V' else '-', + if ((f & Z80.NF) != 0) 'N' else '-', + if ((f & Z80.CF) != 0) 'C' else '-', + }; + ig.igText("Flags: %s", &flags); + if (cpu.iff1 != 0) { + ig.igText("IFF1: ON"); + } else { + ig.igText("IFF1: OFF"); + } + if (cpu.iff2 != 0) { + ig.igText("IFF2: ON"); + } else { + ig.igText("IFF2: OFF"); + } + ig.igSeparator(); + ig.igText("Addr: %04X", Z80.getAddr(in_bus)); + ig.igText("Data: %02X", Z80.getData(in_bus)); } pub fn draw(self: *Self, in_bus: Bus) void { if (self.open != self.last_open) { - self.open = self.last_open; + self.last_open = self.open; } if (!self.open) return; ig.igSetNextWindowPos(self.origin, ig.ImGuiCond_FirstUseEver); @@ -70,7 +103,7 @@ pub fn Type(comptime cfg: TypeConfig) type { ig.igEndChild(); ig.igSameLine(); if (ig.igBeginChild("##z80_regs", .{}, ig.ImGuiChildFlags_None, ig.ImGuiWindowFlags_None)) { - self.drawRegisters(); + self.drawRegisters(in_bus); } ig.igEndChild(); } From ad21eb8b4447205365800801657c270912a6cd72 Mon Sep 17 00:00:00 2001 From: Gunter Hager Date: Tue, 18 Mar 2025 16:33:21 +0100 Subject: [PATCH 07/20] Implemented Z80 PIO --- emus/kc85/kc85.zig | 60 ++++++++++++++- src/ui/ui.zig | 1 + src/ui/ui_z80pio.zig | 171 +++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 230 insertions(+), 2 deletions(-) create mode 100644 src/ui/ui_z80pio.zig diff --git a/emus/kc85/kc85.zig b/emus/kc85/kc85.zig index 3e7c3b8..dc28f58 100644 --- a/emus/kc85/kc85.zig +++ b/emus/kc85/kc85.zig @@ -63,6 +63,44 @@ const UI_Z80_Pins = [_]UI_CHIP.Pin{ .{ .name = "A14", .slot = 32, .mask = kc85.Z80.A14 }, .{ .name = "A15", .slot = 33, .mask = kc85.Z80.A15 }, }; +const UI_Z80PIO = ui.ui_z80pio.Type(.{ .bus = kc85.Bus, .pio = kc85.Z80PIO }); +const UI_Z80PIO_Pins = [_]UI_CHIP.Pin{ + .{ .name = "D0", .slot = 0, .mask = kc85.Z80.D0 }, + .{ .name = "D1", .slot = 1, .mask = kc85.Z80.D1 }, + .{ .name = "D2", .slot = 2, .mask = kc85.Z80.D2 }, + .{ .name = "D3", .slot = 3, .mask = kc85.Z80.D3 }, + .{ .name = "D4", .slot = 4, .mask = kc85.Z80.D4 }, + .{ .name = "D5", .slot = 5, .mask = kc85.Z80.D5 }, + .{ .name = "D6", .slot = 6, .mask = kc85.Z80.D6 }, + .{ .name = "D7", .slot = 7, .mask = kc85.Z80.D7 }, + .{ .name = "CE", .slot = 9, .mask = kc85.Z80PIO.CE }, + .{ .name = "BASEL", .slot = 10, .mask = kc85.Z80PIO.BASEL }, + .{ .name = "CDSEL", .slot = 11, .mask = kc85.Z80PIO.CDSEL }, + .{ .name = "M1", .slot = 12, .mask = kc85.Z80PIO.M1 }, + .{ .name = "IORQ", .slot = 13, .mask = kc85.Z80PIO.IORQ }, + .{ .name = "RD", .slot = 14, .mask = kc85.Z80PIO.RD }, + .{ .name = "INT", .slot = 15, .mask = kc85.Z80PIO.INT }, + .{ .name = "ARDY", .slot = 20, .mask = kc85.Z80PIO.ARDY }, + .{ .name = "ASTB", .slot = 21, .mask = kc85.Z80PIO.ASTB }, + .{ .name = "PA0", .slot = 22, .mask = kc85.Z80PIO.PA0 }, + .{ .name = "PA1", .slot = 23, .mask = kc85.Z80PIO.PA1 }, + .{ .name = "PA2", .slot = 24, .mask = kc85.Z80PIO.PA2 }, + .{ .name = "PA3", .slot = 25, .mask = kc85.Z80PIO.PA3 }, + .{ .name = "PA4", .slot = 26, .mask = kc85.Z80PIO.PA4 }, + .{ .name = "PA5", .slot = 27, .mask = kc85.Z80PIO.PA5 }, + .{ .name = "PA6", .slot = 28, .mask = kc85.Z80PIO.PA6 }, + .{ .name = "PA7", .slot = 29, .mask = kc85.Z80PIO.PA7 }, + .{ .name = "BRDY", .slot = 30, .mask = kc85.Z80PIO.ARDY }, + .{ .name = "BSTB", .slot = 31, .mask = kc85.Z80PIO.ASTB }, + .{ .name = "PB0", .slot = 32, .mask = kc85.Z80PIO.PB0 }, + .{ .name = "PB1", .slot = 33, .mask = kc85.Z80PIO.PB1 }, + .{ .name = "PB2", .slot = 34, .mask = kc85.Z80PIO.PB2 }, + .{ .name = "PB3", .slot = 35, .mask = kc85.Z80PIO.PB3 }, + .{ .name = "PB4", .slot = 36, .mask = kc85.Z80PIO.PB4 }, + .{ .name = "PB5", .slot = 37, .mask = kc85.Z80PIO.PB5 }, + .{ .name = "PB6", .slot = 38, .mask = kc85.Z80PIO.PB6 }, + .{ .name = "PB7", .slot = 39, .mask = kc85.Z80PIO.PB7 }, +}; // a once-trigger for loading a file after booting has finished var file_loaded = host.time.Once.init(switch (model) { @@ -74,6 +112,7 @@ var sys: KC85 = undefined; var gpa = GeneralPurposeAllocator(.{}){}; var args: Args = undefined; var ui_z80: UI_Z80 = undefined; +var ui_z80pio: UI_Z80PIO = undefined; export fn init() void { host.audio.init(.{}); @@ -100,13 +139,29 @@ export fn init() void { }, }, }); + + // Setting up debug UI + var start = ig.ImVec2{ .x = 20, .y = 20 }; + const d = ig.ImVec2{ .x = 10, .y = 10 }; ui_z80.initInPlace(.{ .title = "Z80 CPU", .cpu = &sys.cpu, - .origin = .{ .x = 30, .y = 30 }, + .origin = start, .chip = .{ .name = "Z80\nCPU", .num_slots = 36, .pins = &UI_Z80_Pins }, }); + start.x += d.x; + start.y += d.y; + ui_z80pio.initInPlace(.{ + .title = "Z80 PIO", + .pio = &sys.pio, + .origin = start, + .chip = .{ .name = "Z80\nPIO", .num_slots = 40, .pins = &UI_Z80_Pins }, + }); + start.x += d.x; + start.y += d.y; + host.gfx.init(.{ .display = sys.displayInfo() }); + // initialize sokol-imgui simgui.setup(.{ .logger = .{ .func = slog.func }, @@ -157,7 +212,7 @@ fn uiDrawMenu() void { ui_z80.open = true; } if (ig.igMenuItem("Z80 PIO")) { - // TODO: open chip window + ui_z80pio.open = true; } if (ig.igMenuItem("Z80 CTC")) { // TODO: open chip window @@ -224,6 +279,7 @@ export fn frame() void { uiDrawMenu(); ui_z80.draw(sys.bus); + ui_z80pio.draw(sys.bus); host.gfx.draw(.{ .display = sys.displayInfo(), diff --git a/src/ui/ui.zig b/src/ui/ui.zig index b330193..2686863 100644 --- a/src/ui/ui.zig +++ b/src/ui/ui.zig @@ -1,3 +1,4 @@ pub const ui_chip = @import("ui_chip.zig"); pub const ui_util = @import("ui_util.zig"); pub const ui_z80 = @import("ui_z80.zig"); +pub const ui_z80pio = @import("ui_z80pio.zig"); diff --git a/src/ui/ui_z80pio.zig b/src/ui/ui_z80pio.zig new file mode 100644 index 0000000..c5ad833 --- /dev/null +++ b/src/ui/ui_z80pio.zig @@ -0,0 +1,171 @@ +const chips = @import("chips"); +const z80pio = chips.z80pio; +const ui_chip = @import("ui_chip.zig"); +const ig = @import("cimgui"); + +pub const TypeConfig = struct { + bus: type, + pio: type, +}; + +pub fn Type(comptime cfg: TypeConfig) type { + return struct { + const Self = @This(); + const Bus = cfg.bus; + const Z80PIO = cfg.pio; + const UI_Chip = ui_chip.Type(.{ .bus = cfg.bus }); + + pub const Options = struct { + title: []const u8, + pio: *Z80PIO, + origin: ig.ImVec2, + size: ig.ImVec2 = .{}, + open: bool = false, + chip: UI_Chip, + }; + + title: []const u8, + pio: *Z80PIO, + origin: ig.ImVec2, + size: ig.ImVec2, + open: bool, + last_open: bool, + valid: bool, + chip: UI_Chip, + + pub fn initInPlace(self: *Self, opts: Options) void { + self.* = .{ + .title = opts.title, + .pio = opts.pio, + .origin = opts.origin, + .size = .{ .x = if (opts.size.x == 0) 360 else opts.size.x, .y = if (opts.size.y == 0) 364 else opts.size.y }, + .open = opts.open, + .last_open = opts.open, + .valid = true, + .chip = opts.chip.init(), + }; + } + + pub fn discard(self: *Self) void { + self.valid = false; + } + + fn modeString(mode: u8) []const u8 { + switch (mode) { + 0 => return "OUT", + 1 => return "INP", + 2 => return "BDIR", + 3 => return "BITC", + else => return "INVALID", + } + } + + fn drawPorts(self: *Self) void { + const pio = self.pio; + if (ig.igBeginTable("##pio_columns", 3, ig.ImGuiTableFlags_None)) { + ig.igTableSetupColumnEx("", ig.ImGuiTableColumnFlags_WidthFixed, 64, 0); + ig.igTableSetupColumnEx("PA", ig.ImGuiTableColumnFlags_WidthFixed, 32, 0); + ig.igTableSetupColumnEx("PB", ig.ImGuiTableColumnFlags_WidthFixed, 32, 0); + ig.igTableHeadersRow(); + _ = ig.igTableNextColumn(); + ig.igText("Mode"); + _ = ig.igTableNextColumn(); + for (0..2) |index| { + ig.igText(modeString(pio.ports[index].mode).ptr); + _ = ig.igTableNextColumn(); + } + ig.igText("Output"); + _ = ig.igTableNextColumn(); + for (0..2) |index| { + ig.igText("%02X", pio.ports[index].output); + _ = ig.igTableNextColumn(); + } + ig.igText("Input"); + _ = ig.igTableNextColumn(); + for (0..2) |index| { + ig.igText("%02X", pio.ports[index].input); + _ = ig.igTableNextColumn(); + } + ig.igText("IO Select"); + _ = ig.igTableNextColumn(); + for (0..2) |index| { + ig.igText("%02X", pio.ports[index].io_select); + _ = ig.igTableNextColumn(); + } + ig.igText("INT Ctrl"); + _ = ig.igTableNextColumn(); + for (0..2) |index| { + ig.igText("%02X", pio.ports[index].int_control); + _ = ig.igTableNextColumn(); + } + ig.igText(" ei/di"); + _ = ig.igTableNextColumn(); + for (0..2) |index| { + const int_control = pio.ports[index].int_control; + if ((int_control & Z80PIO.INTCTRL.EI) != 0) { + ig.igText("EI"); + } else { + ig.igText("DI"); + } + _ = ig.igTableNextColumn(); + } + ig.igText(" and/or"); + _ = ig.igTableNextColumn(); + for (0..2) |index| { + const int_control = pio.ports[index].int_control; + if ((int_control & Z80PIO.INTCTRL.ANDOR) != 0) { + ig.igText("AND"); + } else { + ig.igText("OR"); + } + _ = ig.igTableNextColumn(); + } + ig.igText(" hi/lo"); + _ = ig.igTableNextColumn(); + for (0..2) |index| { + const int_control = pio.ports[index].int_control; + if ((int_control & Z80PIO.INTCTRL.HILO) != 0) { + ig.igText("HI"); + } else { + ig.igText("LO"); + } + _ = ig.igTableNextColumn(); + } + ig.igText("INT Vec"); + _ = ig.igTableNextColumn(); + for (0..2) |index| { + ig.igText("%02X", pio.ports[index].irq.vector); + _ = ig.igTableNextColumn(); + } + ig.igText("INT Mask"); + _ = ig.igTableNextColumn(); + for (0..2) |index| { + ig.igText("%02X", pio.ports[index].int_mask); + _ = ig.igTableNextColumn(); + } + ig.igEndTable(); + } + } + + pub fn draw(self: *Self, in_bus: Bus) void { + if (self.open != self.last_open) { + self.last_open = self.open; + } + if (!self.open) return; + ig.igSetNextWindowPos(self.origin, ig.ImGuiCond_FirstUseEver); + ig.igSetNextWindowSize(self.size, ig.ImGuiCond_FirstUseEver); + if (ig.igBegin(self.title.ptr, &self.open, ig.ImGuiWindowFlags_None)) { + if (ig.igBeginChild("##pio_chip", .{ .x = 176, .y = 0 }, ig.ImGuiChildFlags_None, ig.ImGuiWindowFlags_None)) { + self.chip.draw(in_bus); + } + ig.igEndChild(); + ig.igSameLine(); + if (ig.igBeginChild("##pio_vals", .{}, ig.ImGuiChildFlags_None, ig.ImGuiWindowFlags_None)) { + self.drawPorts(); + } + ig.igEndChild(); + } + ig.igEnd(); + } + }; +} From c1a938e03d6940f3f8a4ebe5eb0e1e82c5d395b0 Mon Sep 17 00:00:00 2001 From: Gunter Hager Date: Tue, 18 Mar 2025 17:21:47 +0100 Subject: [PATCH 08/20] Implemented CTC debug UI --- emus/kc85/kc85.zig | 39 ++++++++- src/ui/ui.zig | 1 + src/ui/ui_z80ctc.zig | 200 +++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 238 insertions(+), 2 deletions(-) create mode 100644 src/ui/ui_z80ctc.zig diff --git a/emus/kc85/kc85.zig b/emus/kc85/kc85.zig index dc28f58..203490d 100644 --- a/emus/kc85/kc85.zig +++ b/emus/kc85/kc85.zig @@ -101,6 +101,31 @@ const UI_Z80PIO_Pins = [_]UI_CHIP.Pin{ .{ .name = "PB6", .slot = 38, .mask = kc85.Z80PIO.PB6 }, .{ .name = "PB7", .slot = 39, .mask = kc85.Z80PIO.PB7 }, }; +const UI_Z80CTC = ui.ui_z80ctc.Type(.{ .bus = kc85.Bus, .ctc = kc85.Z80CTC }); +const UI_Z80CTC_Pins = [_]UI_CHIP.Pin{ + .{ .name = "D0", .slot = 0, .mask = kc85.Z80.D0 }, + .{ .name = "D1", .slot = 1, .mask = kc85.Z80.D1 }, + .{ .name = "D2", .slot = 2, .mask = kc85.Z80.D2 }, + .{ .name = "D3", .slot = 3, .mask = kc85.Z80.D3 }, + .{ .name = "D4", .slot = 4, .mask = kc85.Z80.D4 }, + .{ .name = "D5", .slot = 5, .mask = kc85.Z80.D5 }, + .{ .name = "D6", .slot = 6, .mask = kc85.Z80.D6 }, + .{ .name = "D7", .slot = 7, .mask = kc85.Z80.D7 }, + .{ .name = "CE", .slot = 9, .mask = kc85.Z80CTC.CE }, + .{ .name = "CS0", .slot = 10, .mask = kc85.Z80CTC.CS0 }, + .{ .name = "CS1", .slot = 11, .mask = kc85.Z80CTC.CS1 }, + .{ .name = "M1", .slot = 12, .mask = kc85.Z80CTC.M1 }, + .{ .name = "IORQ", .slot = 13, .mask = kc85.Z80CTC.IORQ }, + .{ .name = "RD", .slot = 14, .mask = kc85.Z80CTC.RD }, + .{ .name = "INT", .slot = 15, .mask = kc85.Z80CTC.INT }, + .{ .name = "CT0", .slot = 16, .mask = kc85.Z80CTC.CLKTRG0 }, + .{ .name = "ZT0", .slot = 17, .mask = kc85.Z80CTC.ZCTO0 }, + .{ .name = "CT1", .slot = 19, .mask = kc85.Z80CTC.CLKTRG1 }, + .{ .name = "ZT1", .slot = 20, .mask = kc85.Z80CTC.ZCTO1 }, + .{ .name = "CT2", .slot = 22, .mask = kc85.Z80CTC.CLKTRG2 }, + .{ .name = "ZT2", .slot = 23, .mask = kc85.Z80CTC.ZCTO2 }, + .{ .name = "CT3", .slot = 25, .mask = kc85.Z80CTC.CLKTRG3 }, +}; // a once-trigger for loading a file after booting has finished var file_loaded = host.time.Once.init(switch (model) { @@ -113,6 +138,7 @@ var gpa = GeneralPurposeAllocator(.{}){}; var args: Args = undefined; var ui_z80: UI_Z80 = undefined; var ui_z80pio: UI_Z80PIO = undefined; +var ui_z80ctc: UI_Z80CTC = undefined; export fn init() void { host.audio.init(.{}); @@ -155,7 +181,15 @@ export fn init() void { .title = "Z80 PIO", .pio = &sys.pio, .origin = start, - .chip = .{ .name = "Z80\nPIO", .num_slots = 40, .pins = &UI_Z80_Pins }, + .chip = .{ .name = "Z80\nPIO", .num_slots = 40, .pins = &UI_Z80PIO_Pins }, + }); + start.x += d.x; + start.y += d.y; + ui_z80ctc.initInPlace(.{ + .title = "Z80 CTC", + .ctc = &sys.ctc, + .origin = start, + .chip = .{ .name = "Z80\nCTC", .num_slots = 32, .pins = &UI_Z80CTC_Pins }, }); start.x += d.x; start.y += d.y; @@ -215,7 +249,7 @@ fn uiDrawMenu() void { ui_z80pio.open = true; } if (ig.igMenuItem("Z80 CTC")) { - // TODO: open chip window + ui_z80ctc.open = true; } ig.igEndMenu(); } @@ -280,6 +314,7 @@ export fn frame() void { uiDrawMenu(); ui_z80.draw(sys.bus); ui_z80pio.draw(sys.bus); + ui_z80ctc.draw(sys.bus); host.gfx.draw(.{ .display = sys.displayInfo(), diff --git a/src/ui/ui.zig b/src/ui/ui.zig index 2686863..34c5d80 100644 --- a/src/ui/ui.zig +++ b/src/ui/ui.zig @@ -2,3 +2,4 @@ pub const ui_chip = @import("ui_chip.zig"); pub const ui_util = @import("ui_util.zig"); pub const ui_z80 = @import("ui_z80.zig"); pub const ui_z80pio = @import("ui_z80pio.zig"); +pub const ui_z80ctc = @import("ui_z80ctc.zig"); diff --git a/src/ui/ui_z80ctc.zig b/src/ui/ui_z80ctc.zig new file mode 100644 index 0000000..2ed890a --- /dev/null +++ b/src/ui/ui_z80ctc.zig @@ -0,0 +1,200 @@ +const chips = @import("chips"); +const z80ctc = chips.z80ctc; +const ui_chip = @import("ui_chip.zig"); +const ig = @import("cimgui"); + +pub const TypeConfig = struct { + bus: type, + ctc: type, +}; + +pub fn Type(comptime cfg: TypeConfig) type { + return struct { + const Self = @This(); + const Bus = cfg.bus; + const Z80CTC = cfg.ctc; + const UI_Chip = ui_chip.Type(.{ .bus = cfg.bus }); + + pub const Options = struct { + title: []const u8, + ctc: *Z80CTC, + origin: ig.ImVec2, + size: ig.ImVec2 = .{}, + open: bool = false, + chip: UI_Chip, + }; + + title: []const u8, + ctc: *Z80CTC, + origin: ig.ImVec2, + size: ig.ImVec2, + open: bool, + last_open: bool, + valid: bool, + chip: UI_Chip, + + pub fn initInPlace(self: *Self, opts: Options) void { + self.* = .{ + .title = opts.title, + .ctc = opts.ctc, + .origin = opts.origin, + .size = .{ .x = if (opts.size.x == 0) 460 else opts.size.x, .y = if (opts.size.y == 0) 300 else opts.size.y }, + .open = opts.open, + .last_open = opts.open, + .valid = true, + .chip = opts.chip.init(), + }; + } + + pub fn discard(self: *Self) void { + self.valid = false; + } + + fn drawChannels(self: *Self) void { + const ctc = self.ctc; + if (ig.igBeginTable("##ctc_columns", 5, ig.ImGuiTableFlags_None)) { + ig.igTableSetupColumnEx("", ig.ImGuiTableColumnFlags_WidthFixed, 72, 0); + ig.igTableSetupColumnEx("Chn1", ig.ImGuiTableColumnFlags_WidthFixed, 32, 0); + ig.igTableSetupColumnEx("Chn2", ig.ImGuiTableColumnFlags_WidthFixed, 32, 0); + ig.igTableSetupColumnEx("Chn3", ig.ImGuiTableColumnFlags_WidthFixed, 32, 0); + ig.igTableSetupColumnEx("Chn4", ig.ImGuiTableColumnFlags_WidthFixed, 32, 0); + ig.igTableHeadersRow(); + _ = ig.igTableNextColumn(); + ig.igText("Constant"); + _ = ig.igTableNextColumn(); + for (0..4) |index| { + ig.igText("%02X", ctc.chn[index].constant); + _ = ig.igTableNextColumn(); + } + ig.igText("Counter"); + _ = ig.igTableNextColumn(); + for (0..4) |index| { + ig.igText("%02X", ctc.chn[index].down_counter); + _ = ig.igTableNextColumn(); + } + ig.igText("INT Vec"); + _ = ig.igTableNextColumn(); + for (0..4) |index| { + ig.igText("%02X", ctc.chn[index].irq.vector); + _ = ig.igTableNextColumn(); + } + ig.igText("Control"); + _ = ig.igTableNextColumn(); + for (0..4) |index| { + ig.igText("%02X", ctc.chn[index].control); + _ = ig.igTableNextColumn(); + } + ig.igText(" INT"); + _ = ig.igTableNextColumn(); + for (0..4) |index| { + const control = ctc.chn[index].control; + if ((control & Z80CTC.CTRL.EI) != 0) { + ig.igText("EI"); + } else { + ig.igText("DI"); + } + _ = ig.igTableNextColumn(); + } + ig.igText(" MODE"); + _ = ig.igTableNextColumn(); + for (0..4) |index| { + const control = ctc.chn[index].control; + if ((control & Z80CTC.CTRL.MODE) != 0) { + ig.igText("CTR"); + } else { + ig.igText("TMR"); + } + _ = ig.igTableNextColumn(); + } + ig.igText(" PRESCALE"); + _ = ig.igTableNextColumn(); + for (0..4) |index| { + const control = ctc.chn[index].control; + if ((control & Z80CTC.CTRL.PRESCALER) != 0) { + ig.igText("256"); + } else { + ig.igText("16"); + } + _ = ig.igTableNextColumn(); + } + ig.igText(" EDGE"); + _ = ig.igTableNextColumn(); + for (0..4) |index| { + const control = ctc.chn[index].control; + if ((control & Z80CTC.CTRL.EDGE) != 0) { + ig.igText("RISE"); + } else { + ig.igText("FALL"); + } + _ = ig.igTableNextColumn(); + } + ig.igText(" TRIGGER"); + _ = ig.igTableNextColumn(); + for (0..4) |index| { + const control = ctc.chn[index].control; + if ((control & Z80CTC.CTRL.TRIGGER) != 0) { + ig.igText("WAIT"); + } else { + ig.igText("AUTO"); + } + _ = ig.igTableNextColumn(); + } + ig.igText(" CONSTANT"); + _ = ig.igTableNextColumn(); + for (0..4) |index| { + const control = ctc.chn[index].control; + if ((control & Z80CTC.CTRL.CONST_FOLLOWS) != 0) { + ig.igText("FLWS"); + } else { + ig.igText("NONE"); + } + _ = ig.igTableNextColumn(); + } + ig.igText(" RESET"); + _ = ig.igTableNextColumn(); + for (0..4) |index| { + const control = ctc.chn[index].control; + if ((control & Z80CTC.CTRL.RESET) != 0) { + ig.igText("ON"); + } else { + ig.igText("OFF"); + } + _ = ig.igTableNextColumn(); + } + ig.igText(" CONTROL"); + _ = ig.igTableNextColumn(); + for (0..4) |index| { + const control = ctc.chn[index].control; + if ((control & Z80CTC.CTRL.CONTROL) != 0) { + ig.igText("WRD"); + } else { + ig.igText("VEC"); + } + _ = ig.igTableNextColumn(); + } + ig.igEndTable(); + } + } + + pub fn draw(self: *Self, in_bus: Bus) void { + if (self.open != self.last_open) { + self.last_open = self.open; + } + if (!self.open) return; + ig.igSetNextWindowPos(self.origin, ig.ImGuiCond_FirstUseEver); + ig.igSetNextWindowSize(self.size, ig.ImGuiCond_FirstUseEver); + if (ig.igBegin(self.title.ptr, &self.open, ig.ImGuiWindowFlags_None)) { + if (ig.igBeginChild("##ctc_chip", .{ .x = 176, .y = 0 }, ig.ImGuiChildFlags_None, ig.ImGuiWindowFlags_None)) { + self.chip.draw(in_bus); + } + ig.igEndChild(); + ig.igSameLine(); + if (ig.igBeginChild("##ctc_vals", .{}, ig.ImGuiChildFlags_None, ig.ImGuiWindowFlags_None)) { + self.drawChannels(); + } + ig.igEndChild(); + } + ig.igEnd(); + } + }; +} From b30c9a713836fc2ae1ca5723d13c5a1b80075676 Mon Sep 17 00:00:00 2001 From: Gunter Hager Date: Sat, 22 Mar 2025 15:29:41 +0100 Subject: [PATCH 09/20] Added "(TODO)" menu item to all items that aren't working yet --- emus/kc85/kc85.zig | 20 ++++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/emus/kc85/kc85.zig b/emus/kc85/kc85.zig index 203490d..c797323 100644 --- a/emus/kc85/kc85.zig +++ b/emus/kc85/kc85.zig @@ -220,25 +220,25 @@ fn renderGUI() void { fn uiDrawMenu() void { if (ig.igBeginMainMenuBar()) { if (ig.igBeginMenu("System")) { - if (ig.igMenuItem("Reset")) { + if (ig.igMenuItem("Reset (TODO)")) { // TODO: implement reset } - if (ig.igMenuItem("Cold Boot")) { + if (ig.igMenuItem("Cold Boot (TODO)")) { // TODO: implement cold boot } ig.igEndMenu(); } if (ig.igBeginMenu("Hardware")) { - if (ig.igMenuItem("Memory Map")) { + if (ig.igMenuItem("Memory Map (TODO)")) { // TODO: open window } - if (ig.igMenuItem("System State")) { + if (ig.igMenuItem("System State (TODO)")) { // TODO: open window } - if (ig.igMenuItem("Audio Output")) { + if (ig.igMenuItem("Audio Output (TODO)")) { // TODO: open window } - if (ig.igMenuItem("Display")) { + if (ig.igMenuItem("Display (TODO)")) { // TODO: open window } ig.igSeparator(); @@ -254,13 +254,13 @@ fn uiDrawMenu() void { ig.igEndMenu(); } if (ig.igBeginMenu("Debug")) { - if (ig.igMenuItem("CPU Debugger")) { + if (ig.igMenuItem("CPU Debugger (TODO)")) { // TODO: open window } - if (ig.igMenuItem("Breakpoints")) { + if (ig.igMenuItem("Breakpoints (TODO)")) { // TODO: open window } - if (ig.igBeginMenu("Memory Editor")) { + if (ig.igBeginMenu("Memory Editor (TODO)")) { if (ig.igMenuItem("Window #1")) { // TODO: open window } @@ -275,7 +275,7 @@ fn uiDrawMenu() void { } ig.igEndMenu(); } - if (ig.igBeginMenu("Disassembler")) { + if (ig.igBeginMenu("Disassembler (TODO)")) { if (ig.igMenuItem("Window #1")) { // TODO: open window } From cb5000a95d9e0ebbe59ffae4482fde34e948697f Mon Sep 17 00:00:00 2001 From: Gunter Hager Date: Sun, 23 Mar 2025 12:06:44 +0100 Subject: [PATCH 10/20] Implemented debug UI for intel8255 PPI --- src/ui/ui.zig | 1 + src/ui/ui_intel8255.zig | 136 ++++++++++++++++++++++++++++++++++++++++ 2 files changed, 137 insertions(+) create mode 100644 src/ui/ui_intel8255.zig diff --git a/src/ui/ui.zig b/src/ui/ui.zig index 34c5d80..cde4f35 100644 --- a/src/ui/ui.zig +++ b/src/ui/ui.zig @@ -3,3 +3,4 @@ pub const ui_util = @import("ui_util.zig"); pub const ui_z80 = @import("ui_z80.zig"); pub const ui_z80pio = @import("ui_z80pio.zig"); pub const ui_z80ctc = @import("ui_z80ctc.zig"); +pub const ui_intel8255 = @import("ui_intel8255.zig"); diff --git a/src/ui/ui_intel8255.zig b/src/ui/ui_intel8255.zig new file mode 100644 index 0000000..8f2e0b3 --- /dev/null +++ b/src/ui/ui_intel8255.zig @@ -0,0 +1,136 @@ +const chips = @import("chips"); +const intel8255 = chips.intel8255; +const ui_chip = @import("ui_chip.zig"); +const ig = @import("cimgui"); + +pub const TypeConfig = struct { + bus: type, + ppi: type, +}; + +pub fn Type(comptime cfg: TypeConfig) type { + return struct { + const Self = @This(); + const Bus = cfg.bus; + const INTEL8255 = cfg.ppi; + const UI_Chip = ui_chip.Type(.{ .bus = cfg.bus }); + + pub const Options = struct { + title: []const u8, + ppi: *INTEL8255, + origin: ig.ImVec2, + size: ig.ImVec2 = .{}, + open: bool = false, + chip: UI_Chip, + }; + + title: []const u8, + ppi: *INTEL8255, + origin: ig.ImVec2, + size: ig.ImVec2, + open: bool, + last_open: bool, + valid: bool, + chip: UI_Chip, + + pub fn initInPlace(self: *Self, opts: Options) void { + self.* = .{ + .title = opts.title, + .ctc = opts.ctc, + .origin = opts.origin, + .size = .{ .x = if (opts.size.x == 0) 440 else opts.size.x, .y = if (opts.size.y == 0) 370 else opts.size.y }, + .open = opts.open, + .last_open = opts.open, + .valid = true, + .chip = opts.chip.init(), + }; + } + + pub fn discard(self: *Self) void { + self.valid = false; + } + + fn drawState(self: *Self) void { + const ppi = self.ppi; + if (ig.igBeginTable("##ppi_ports", 5, ig.ImGuiTableFlags_None)) { + ig.igTableSetupColumnEx("", ig.ImGuiTableColumnFlags_WidthFixed, 56, 0); + ig.igTableSetupColumnEx("A", ig.ImGuiTableColumnFlags_WidthFixed, 32, 0); + ig.igTableSetupColumnEx("B", ig.ImGuiTableColumnFlags_WidthFixed, 32, 0); + ig.igTableSetupColumnEx("CHI", ig.ImGuiTableColumnFlags_WidthFixed, 32, 0); + ig.igTableSetupColumnEx("CLO", ig.ImGuiTableColumnFlags_WidthFixed, 32, 0); + ig.igTableHeadersRow(); + _ = ig.igTableNextColumn(); + ig.igText("Mode"); + _ = ig.igTableNextColumn(); + ig.igText("%d", (ppi.control & INTEL8255.CTRL.A_CHI_MODE) >> 5); + _ = ig.igTableNextColumn(); + ig.igText("%d", (ppi.control & INTEL8255.CTRL.B_CLO_MODE) >> 2); + _ = ig.igTableNextColumn(); + ig.igText("%d", (ppi.control & INTEL8255.CTRL.A_CHI_MODE) >> 5); + _ = ig.igTableNextColumn(); + ig.igText("%d", (ppi.control & INTEL8255.CTRL.B_CLO_MODE) >> 2); + _ = ig.igTableNextColumn(); + ig.igText("In/Out"); + _ = ig.igTableNextColumn(); + if ((ppi.control & INTEL8255.CTRL.A) == INTEL8255.CTRL.A_INPUT) { + ig.igText("IN"); + } else { + ig.igText("OUT"); + } + _ = ig.igTableNextColumn(); + if ((ppi.control & INTEL8255.CTRL.B) == INTEL8255.CTRL.B_INPUT) { + ig.igText("IN"); + } else { + ig.igText("OUT"); + } + _ = ig.igTableNextColumn(); + if ((ppi.control & INTEL8255.CTRL.CHI) == INTEL8255.CTRL.CHI_INPUT) { + ig.igText("IN"); + } else { + ig.igText("OUT"); + } + _ = ig.igTableNextColumn(); + if ((ppi.control & INTEL8255.CTRL.CLO) == INTEL8255.CTRL.CLO_INPUT) { + ig.igText("IN"); + } else { + ig.igText("OUT"); + } + _ = ig.igTableNextColumn(); + ig.igText("Output"); + _ = ig.igTableNextColumn(); + ig.igText("%02X", ppi.ports[INTEL8255.PORT.A].output); + _ = ig.igTableNextColumn(); + ig.igText("%02X", ppi.ports[INTEL8255.PORT.B].output); + _ = ig.igTableNextColumn(); + ig.igText("%X", ppi.ports[INTEL8255.PORT.C].output >> 4); + _ = ig.igTableNextColumn(); + ig.igText("%X", ppi.ports[INTEL8255.PORT.C].output & 0x0f); + _ = ig.igTableNextColumn(); + ig.igEndTable(); + } + ig.igSeparator(); + ig.igText("Control %02X", ppi.control); + } + + pub fn draw(self: *Self, in_bus: Bus) void { + if (self.open != self.last_open) { + self.last_open = self.open; + } + if (!self.open) return; + ig.igSetNextWindowPos(self.origin, ig.ImGuiCond_FirstUseEver); + ig.igSetNextWindowSize(self.size, ig.ImGuiCond_FirstUseEver); + if (ig.igBegin(self.title.ptr, &self.open, ig.ImGuiWindowFlags_None)) { + if (ig.igBeginChild("##ppi_chip", .{ .x = 176, .y = 0 }, ig.ImGuiChildFlags_None, ig.ImGuiWindowFlags_None)) { + self.chip.draw(in_bus); + } + ig.igEndChild(); + ig.igSameLine(); + if (ig.igBeginChild("##ppi_vals", .{}, ig.ImGuiChildFlags_None, ig.ImGuiWindowFlags_None)) { + self.drawState(); + } + ig.igEndChild(); + } + ig.igEnd(); + } + }; +} From 8b6ade7f6f1f61f4c1f4400a881618c7807d9200 Mon Sep 17 00:00:00 2001 From: Gunter Hager Date: Sun, 23 Mar 2025 12:33:09 +0100 Subject: [PATCH 11/20] Fixed build error in init --- src/ui/ui_intel8255.zig | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/ui/ui_intel8255.zig b/src/ui/ui_intel8255.zig index 8f2e0b3..aa75acb 100644 --- a/src/ui/ui_intel8255.zig +++ b/src/ui/ui_intel8255.zig @@ -36,7 +36,7 @@ pub fn Type(comptime cfg: TypeConfig) type { pub fn initInPlace(self: *Self, opts: Options) void { self.* = .{ .title = opts.title, - .ctc = opts.ctc, + .ppi = opts.ppi, .origin = opts.origin, .size = .{ .x = if (opts.size.x == 0) 440 else opts.size.x, .y = if (opts.size.y == 0) 370 else opts.size.y }, .open = opts.open, From 9ba775b0f17daf986192c08b596cfb1f8c5f7e3e Mon Sep 17 00:00:00 2001 From: Gunter Hager Date: Thu, 27 Mar 2025 10:13:12 +0100 Subject: [PATCH 12/20] Update .gitignore --- .gitignore | 1 + 1 file changed, 1 insertion(+) diff --git a/.gitignore b/.gitignore index c4e37ff..b4809ee 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,4 @@ zig-*/ .zig-*/ .vscode/ +.cursor From 5480f4bd5f38e6b845eeaf035fc3d18645f35aa7 Mon Sep 17 00:00:00 2001 From: Gunter Hager Date: Tue, 24 Jun 2025 13:22:03 +0200 Subject: [PATCH 13/20] Started to implement memory map debug view --- build.zig.zon | 8 +- emus/kc85/kc85.zig | 12 ++- src/ui/ui.zig | 2 + src/ui/ui_intel8255.zig | 9 +- src/ui/ui_memmap.zig | 228 ++++++++++++++++++++++++++++++++++++++++ src/ui/ui_settings.zig | 71 +++++++++++++ src/ui/ui_z80.zig | 10 +- src/ui/ui_z80ctc.zig | 9 ++ src/ui/ui_z80pio.zig | 10 +- 9 files changed, 349 insertions(+), 10 deletions(-) create mode 100644 src/ui/ui_memmap.zig create mode 100644 src/ui/ui_settings.zig diff --git a/build.zig.zon b/build.zig.zon index 368146b..6454564 100644 --- a/build.zig.zon +++ b/build.zig.zon @@ -4,12 +4,12 @@ .fingerprint = 0x3ecb34175baf6f81, .dependencies = .{ .sokol = .{ - .url = "git+https://github.com/floooh/sokol-zig.git#57fa55c9c6e2f02259680f8036592eb0fbf45b78", - .hash = "sokol-0.1.0-pb1HKxeCLQAKj7VzwB2eVCygr-xCfiFPFGD_3Cqs1IFX", + .url = "git+https://github.com/floooh/sokol-zig.git#d41da3bc951a15466968ecdc2152efa5d2f0f107", + .hash = "sokol-0.1.0-pb1HKwaCLQBSQG4RPJ0YjE6tYhkSIPTotgioIpOIo8Zr", }, .cimgui = .{ - .url = "git+https://github.com/floooh/dcimgui.git#3969c14f7c7abda0e4b59d2616b17b7fb9eb0827", - .hash = "cimgui-0.1.0-44ClkTt5hgBU8BelH8W_G8mso3ys_hrqNUWwJvaxXDs5", + .url = "git+https://github.com/floooh/dcimgui.git#716dae9c6a03eec642a4fb2ac28ebcd2f52f76ba", + .hash = "cimgui-0.1.0-44ClkYmBhgC-Q3NWFyYqwThmmTdmkoIlAjF4ABfyqIAZ", }, }, .paths = .{ diff --git a/emus/kc85/kc85.zig b/emus/kc85/kc85.zig index c797323..aecbeee 100644 --- a/emus/kc85/kc85.zig +++ b/emus/kc85/kc85.zig @@ -126,7 +126,7 @@ const UI_Z80CTC_Pins = [_]UI_CHIP.Pin{ .{ .name = "ZT2", .slot = 23, .mask = kc85.Z80CTC.ZCTO2 }, .{ .name = "CT3", .slot = 25, .mask = kc85.Z80CTC.CLKTRG3 }, }; - +const UI_MEMMAP = ui.ui_memmap.MemMap; // a once-trigger for loading a file after booting has finished var file_loaded = host.time.Once.init(switch (model) { .KC852, .KC853 => 8 * 1000 * 1000, @@ -139,6 +139,7 @@ var args: Args = undefined; var ui_z80: UI_Z80 = undefined; var ui_z80pio: UI_Z80PIO = undefined; var ui_z80ctc: UI_Z80CTC = undefined; +var ui_memmap: UI_MEMMAP = undefined; export fn init() void { host.audio.init(.{}); @@ -193,6 +194,10 @@ export fn init() void { }); start.x += d.x; start.y += d.y; + ui_memmap.initInPlace(.{ + .title = "Memory Map", + .origin = start, + }); host.gfx.init(.{ .display = sys.displayInfo() }); @@ -229,8 +234,8 @@ fn uiDrawMenu() void { ig.igEndMenu(); } if (ig.igBeginMenu("Hardware")) { - if (ig.igMenuItem("Memory Map (TODO)")) { - // TODO: open window + if (ig.igMenuItem("Memory Map")) { + ui_memmap.open = true; } if (ig.igMenuItem("System State (TODO)")) { // TODO: open window @@ -315,6 +320,7 @@ export fn frame() void { ui_z80.draw(sys.bus); ui_z80pio.draw(sys.bus); ui_z80ctc.draw(sys.bus); + ui_memmap.draw(); host.gfx.draw(.{ .display = sys.displayInfo(), diff --git a/src/ui/ui.zig b/src/ui/ui.zig index cde4f35..56f2403 100644 --- a/src/ui/ui.zig +++ b/src/ui/ui.zig @@ -1,6 +1,8 @@ pub const ui_chip = @import("ui_chip.zig"); pub const ui_util = @import("ui_util.zig"); +pub const ui_settings = @import("ui_settings.zig"); pub const ui_z80 = @import("ui_z80.zig"); pub const ui_z80pio = @import("ui_z80pio.zig"); pub const ui_z80ctc = @import("ui_z80ctc.zig"); pub const ui_intel8255 = @import("ui_intel8255.zig"); +pub const ui_memmap = @import("ui_memmap.zig"); diff --git a/src/ui/ui_intel8255.zig b/src/ui/ui_intel8255.zig index aa75acb..42b73e9 100644 --- a/src/ui/ui_intel8255.zig +++ b/src/ui/ui_intel8255.zig @@ -2,7 +2,7 @@ const chips = @import("chips"); const intel8255 = chips.intel8255; const ui_chip = @import("ui_chip.zig"); const ig = @import("cimgui"); - +const ui_settings = @import("ui_settings.zig"); pub const TypeConfig = struct { bus: type, ppi: type, @@ -132,5 +132,12 @@ pub fn Type(comptime cfg: TypeConfig) type { } ig.igEnd(); } + pub fn saveSettings(self: *Self, settings: *ui_settings.Settings) void { + _ = settings.add(self.title, self.open); + } + + pub fn loadSettings(self: *Self, settings: *const ui_settings.Settings) void { + self.open = settings.isOpen(self.title); + } }; } diff --git a/src/ui/ui_memmap.zig b/src/ui/ui_memmap.zig new file mode 100644 index 0000000..ee1fd7e --- /dev/null +++ b/src/ui/ui_memmap.zig @@ -0,0 +1,228 @@ +const std = @import("std"); +const ig = @import("cimgui"); +const ui_util = @import("ui_util.zig"); +const ui_settings = @import("ui_settings.zig"); + +pub const MAX_LAYERS = 16; +pub const MAX_REGIONS = 16; + +pub const Region = struct { + name: []const u8, + addr: u16, + len: u16, + on: bool, +}; + +pub const Layer = struct { + name: []const u8, + num_regions: usize, + regions: [MAX_REGIONS]Region, +}; + +pub const MemMap = struct { + const Self = @This(); + + pub const Options = struct { + title: []const u8, + origin: ig.ImVec2, + size: ig.ImVec2 = .{}, + open: bool = false, + }; + + title: []const u8, + origin: ig.ImVec2, + size: ig.ImVec2, + layer_height: f32, + left_padding: f32, + open: bool, + last_open: bool, + valid: bool, + num_layers: usize, + layers: [MAX_LAYERS]Layer, + + pub fn initInPlace(self: *Self, opts: Options) void { + self.* = .{ + .title = opts.title, + .origin = opts.origin, + .size = .{ + .x = if (opts.size.x == 0) 400 else opts.size.x, + .y = if (opts.size.y == 0) 40 else opts.size.y, + }, + .open = opts.open, + .last_open = opts.open, + .left_padding = 80.0, + .layer_height = 20.0, + .valid = true, + .num_layers = 0, + .layers = undefined, + }; + } + + pub fn discard(self: *Self) void { + self.valid = false; + } + + fn drawGrid(self: *Self, canvas_pos: ig.ImVec2, canvas_area: ig.ImVec2) void { + const dl = ig.igGetWindowDrawList(); + const grid_color = ui_util.color(ig.ImGuiCol_Text); + const y = canvas_pos.y + canvas_area.y - self.layer_height; + + // Line rulers + if (canvas_area.x > self.left_padding) { + const addr = [_][]const u8{ "0000", "4000", "8000", "C000", "FFFF" }; + const glyph_width = ig.igCalcTextSize("X").x; + const x0 = canvas_pos.x + self.left_padding; + const dx = (canvas_area.x - self.left_padding) / 4.0; + const y0 = canvas_pos.y; + const y1 = canvas_pos.y + canvas_area.y + 4.0 - self.layer_height; + + var x = x0; + for (addr, 0..) |addr_str, index| { + const pos = ig.ImVec2{ .x = x, .y = y0 }; + const pos2 = ig.ImVec2{ .x = x, .y = y1 }; + ig.ImDrawList_AddLine(dl, pos, pos2, grid_color); + + const addr_x = if (index == 4) x - 4.0 * glyph_width else x; + const text_pos = ig.ImVec2{ .x = addr_x, .y = y1 }; + ig.ImDrawList_AddText(dl, text_pos, grid_color, addr_str.ptr); + x += dx; + } + + const p0 = ig.ImVec2{ .x = canvas_pos.x + self.left_padding, .y = y1 }; + const p1 = ig.ImVec2{ .x = x0 + 4 * dx, .y = p0.y }; + ig.ImDrawList_AddLine(dl, p0, p1, grid_color); + } + + // Layer names to the left + var text_pos = ig.ImVec2{ .x = canvas_pos.x, .y = y - self.layer_height + 6 }; + var i: usize = 0; + while (i < self.num_layers) : (i += 1) { + ig.ImDrawList_AddText(dl, text_pos, grid_color, self.layers[i].name.ptr); + text_pos.y -= self.layer_height; + } + } + + fn drawRegion(self: *Self, pos: ig.ImVec2, width: f32, reg: Region) void { + const dl = ig.igGetWindowDrawList(); + const style = ig.igGetStyle(); + const alpha = style.*.Alpha; + const on_color = ig.igGetColorU32ImVec4(.{ .x = 0.0, .y = 0.75, .z = 0.0, .w = alpha }); + const off_color = ig.igGetColorU32ImVec4(.{ .x = 0.0, .y = 0.25, .z = 0.0, .w = alpha }); + const color = if (reg.on) on_color else off_color; + + const addr = reg.addr; + const end_addr = reg.addr + reg.len; + if (end_addr > 0x10000) { + // Wraparound + const a = pos; + const b = ig.ImVec2{ + .x = pos.x + (((end_addr & 0xFFFF) * width) / 0x10000) - 2, + .y = pos.y + self.layer_height - 2, + }; + ig.ImDrawList_AddRectFilled(dl, a, b, color); + if (ig.igIsMouseHoveringRect(a, b)) { + var tooltip: [100]u8 = undefined; + if (std.fmt.bufPrint(&tooltip, "{s} (0000..{X:0>4})", .{ reg.name, (end_addr & 0xFFFF) - 1 })) |tooltip_slice| { + ig.igSetTooltip(tooltip_slice.ptr); + } else |_| {} + } + end_addr = 0x10000; + } + const a = ig.ImVec2{ + .x = pos.x + ((@as(f32, @floatFromInt(addr)) * width) / 0x10000), + .y = pos.y, + }; + const b = ig.ImVec2{ + .x = pos.x + ((@as(f32, @floatFromInt(end_addr)) * width) / 0x10000) - 2, + .y = pos.y + self.layer_height - 2, + }; + ig.ImDrawList_AddRectFilled(dl, a, b, color); + if (ig.igIsMouseHoveringRect(a, b)) { + var tooltip: [100]u8 = undefined; + if (std.fmt.bufPrint(&tooltip, "{s} ({X:0>4}..{X:0>4})", .{ reg.name, addr, end_addr - 1 })) |tooltip_slice| { + ig.igSetTooltip(tooltip_slice.ptr); + } else |_| {} + } + } + + fn drawRegions(self: *Self, canvas_pos: ig.ImVec2, canvas_area: ig.ImVec2) void { + var pos = ig.ImVec2{ + .x = canvas_pos.x + self.left_padding, + .y = canvas_pos.y + canvas_area.y + 4 - 2 * self.layer_height, + }; + + for (0..self.num_layers) |li| { + for (0..self.layers[li].num_regions) |ri| { + const reg = self.layers[li].regions[ri]; + if (reg.name.len > 0) { + self.drawRegion(pos, canvas_area.x - self.left_padding, reg); + } + } + pos.y -= self.layer_height; + } + } + + pub fn draw(self: *Self) void { + if (self.open != self.last_open) { + self.last_open = self.open; + } + if (!self.open) return; + + const min_height = 40.0 + ((@as(f32, @floatFromInt(self.num_layers)) + 1.0) * self.layer_height); + ig.igSetNextWindowPos(self.origin, ig.ImGuiCond_FirstUseEver); + ig.igSetNextWindowSize(self.size, ig.ImGuiCond_FirstUseEver); + ig.igSetNextWindowSizeConstraints( + ig.ImVec2{ .x = 120.0, .y = min_height }, + ig.ImVec2{ .x = std.math.inf(f32), .y = std.math.inf(f32) }, + null, + null, + ); + + if (ig.igBegin(self.title.ptr, &self.open, ig.ImGuiWindowFlags_None)) { + const canvas_pos = ig.igGetCursorScreenPos(); + const canvas_area = ig.igGetContentRegionAvail(); + self.drawRegions(canvas_pos, canvas_area); + self.drawGrid(canvas_pos, canvas_area); + } + ig.igEnd(); + } + + pub fn reset(self: *Self) void { + self.num_layers = 0; + @memset(&self.layers, undefined); + } + + pub fn addLayer(self: *Self, name: []const u8) void { + if (self.num_layers >= MAX_LAYERS) return; + self.layers[self.num_layers] = .{ + .name = name, + .num_regions = 0, + .regions = undefined, + }; + self.num_layers += 1; + } + + pub fn addRegion(self: *Self, name: []const u8, addr: u16, len: u16, on: bool) void { + if (self.num_layers == 0) return; + if (len > 0x10000) return; + + const layer = &self.layers[self.num_layers - 1]; + if (layer.num_regions >= MAX_REGIONS) return; + + layer.regions[layer.num_regions] = .{ + .name = name, + .addr = addr, + .len = len, + .on = on, + }; + layer.num_regions += 1; + } + + pub fn saveSettings(self: *Self, settings: *ui_settings.Settings) void { + _ = settings.add(self.title, self.open); + } + + pub fn loadSettings(self: *Self, settings: *const ui_settings.Settings) void { + self.open = settings.isOpen(self.title); + } +}; diff --git a/src/ui/ui_settings.zig b/src/ui/ui_settings.zig new file mode 100644 index 0000000..7d72a40 --- /dev/null +++ b/src/ui/ui_settings.zig @@ -0,0 +1,71 @@ +const std = @import("std"); +const ig = @import("cimgui"); + +pub const MAX_SLOTS = 32; +pub const MAX_STRING_LENGTH = 128; + +pub const SettingsStr = struct { + buf: [MAX_STRING_LENGTH]u8, + + pub fn init(str: []const u8) SettingsStr { + var self = SettingsStr{ + .buf = undefined, + }; + std.mem.copy(u8, &self.buf, str); + return self; + } + + pub fn asSlice(self: *const SettingsStr) []const u8 { + return std.mem.span(&self.buf); + } +}; + +pub const SettingsSlot = struct { + window_title: SettingsStr, + open: bool, +}; + +pub const Settings = struct { + num_slots: usize, + slots: [MAX_SLOTS]SettingsSlot, + + pub fn init() Settings { + return .{ + .num_slots = 0, + .slots = undefined, + }; + } + + pub fn add(self: *Settings, window_title: []const u8, open: bool) bool { + if (self.num_slots >= MAX_SLOTS) { + return false; + } + if (window_title.len >= MAX_STRING_LENGTH) { + return false; + } + + self.slots[self.num_slots] = .{ + .window_title = SettingsStr.init(window_title), + .open = open, + }; + self.num_slots += 1; + return true; + } + + pub fn findSlotIndex(self: *const Settings, window_title: []const u8) ?usize { + for (0..self.num_slots) |i| { + const slot = &self.slots[i]; + if (std.mem.eql(u8, slot.window_title.asSlice(), window_title)) { + return i; + } + } + return null; + } + + pub fn isOpen(self: *const Settings, window_title: []const u8) bool { + if (self.findSlotIndex(window_title)) |index| { + return self.slots[index].open; + } + return false; + } +}; diff --git a/src/ui/ui_z80.zig b/src/ui/ui_z80.zig index 2f0ca92..cb4a932 100644 --- a/src/ui/ui_z80.zig +++ b/src/ui/ui_z80.zig @@ -2,7 +2,7 @@ const chips = @import("chips"); const z80 = chips.z80; const ui_chip = @import("ui_chip.zig"); const ig = @import("cimgui"); - +const ui_settings = @import("ui_settings.zig"); pub const TypeConfig = struct { bus: type, cpu: type, @@ -109,5 +109,13 @@ pub fn Type(comptime cfg: TypeConfig) type { } ig.igEnd(); } + + pub fn saveSettings(self: *Self, settings: *ui_settings.Settings) void { + _ = settings.add(self.title, self.open); + } + + pub fn loadSettings(self: *Self, settings: *const ui_settings.Settings) void { + self.open = settings.isOpen(self.title); + } }; } diff --git a/src/ui/ui_z80ctc.zig b/src/ui/ui_z80ctc.zig index 2ed890a..1c2e981 100644 --- a/src/ui/ui_z80ctc.zig +++ b/src/ui/ui_z80ctc.zig @@ -2,6 +2,7 @@ const chips = @import("chips"); const z80ctc = chips.z80ctc; const ui_chip = @import("ui_chip.zig"); const ig = @import("cimgui"); +const ui_settings = @import("ui_settings.zig"); pub const TypeConfig = struct { bus: type, @@ -196,5 +197,13 @@ pub fn Type(comptime cfg: TypeConfig) type { } ig.igEnd(); } + + pub fn saveSettings(self: *Self, settings: *ui_settings.Settings) void { + _ = settings.add(self.title, self.open); + } + + pub fn loadSettings(self: *Self, settings: *const ui_settings.Settings) void { + self.open = settings.isOpen(self.title); + } }; } diff --git a/src/ui/ui_z80pio.zig b/src/ui/ui_z80pio.zig index c5ad833..e9456f8 100644 --- a/src/ui/ui_z80pio.zig +++ b/src/ui/ui_z80pio.zig @@ -2,7 +2,7 @@ const chips = @import("chips"); const z80pio = chips.z80pio; const ui_chip = @import("ui_chip.zig"); const ig = @import("cimgui"); - +const ui_settings = @import("ui_settings.zig"); pub const TypeConfig = struct { bus: type, pio: type, @@ -167,5 +167,13 @@ pub fn Type(comptime cfg: TypeConfig) type { } ig.igEnd(); } + + pub fn saveSettings(self: *Self, settings: *ui_settings.Settings) void { + _ = settings.add(self.title, self.open); + } + + pub fn loadSettings(self: *Self, settings: *const ui_settings.Settings) void { + self.open = settings.isOpen(self.title); + } }; } From 3d24be56f7bf4542dcf30a02e2bbf47ccc38e693 Mon Sep 17 00:00:00 2001 From: Gunter Hager Date: Fri, 12 Sep 2025 09:13:25 +0200 Subject: [PATCH 14/20] Fixed build issues --- build.zig | 2 ++ build.zig.zon | 8 ++++---- 2 files changed, 6 insertions(+), 4 deletions(-) diff --git a/build.zig b/build.zig index 4e4aa98..d6bb3e7 100644 --- a/build.zig +++ b/build.zig @@ -23,6 +23,7 @@ pub fn build(b: *Build) !void { dep_sokol.artifact("sokol_clib").addIncludePath(dep_cimgui.path("src")); const dep_shdc = dep_sokol.builder.dependency("shdc", .{}); const mod_sokol = dep_sokol.module("sokol"); + const mod_cimgui = dep_cimgui.module("cimgui"); // shader module const mod_shaders = try sokol.shdc.createModule(b, "shaders", mod_sokol, .{ @@ -115,5 +116,6 @@ pub fn build(b: *Build) !void { .optimize = optimize, .mod_chipz = mod_chipz, .mod_sokol = mod_sokol, + .mod_cimgui = mod_cimgui, }); } diff --git a/build.zig.zon b/build.zig.zon index 6454564..8c54746 100644 --- a/build.zig.zon +++ b/build.zig.zon @@ -4,12 +4,12 @@ .fingerprint = 0x3ecb34175baf6f81, .dependencies = .{ .sokol = .{ - .url = "git+https://github.com/floooh/sokol-zig.git#d41da3bc951a15466968ecdc2152efa5d2f0f107", - .hash = "sokol-0.1.0-pb1HKwaCLQBSQG4RPJ0YjE6tYhkSIPTotgioIpOIo8Zr", + .url = "git+https://github.com/floooh/sokol-zig.git#1d6a902f007fc32501ef26676f7cb73dcd81b047", + .hash = "sokol-0.1.0-pb1HK_ZPLgC-2uMA63u1iiB2BqPcibWOEZVH0Hz1ursz", }, .cimgui = .{ - .url = "git+https://github.com/floooh/dcimgui.git#716dae9c6a03eec642a4fb2ac28ebcd2f52f76ba", - .hash = "cimgui-0.1.0-44ClkYmBhgC-Q3NWFyYqwThmmTdmkoIlAjF4ABfyqIAZ", + .url = "git+https://github.com/floooh/dcimgui.git#581c2e909c899c21923c779d4c41ea56ab93bbb4", + .hash = "cimgui-0.1.0-44ClkQ14kwAiwcr3ioDjxvPuiWQctWrt-tIFtsfP6xmU", }, }, .paths = .{ From 1fd248b6c3f94b2f88a9dc6b2a4769da2efb441b Mon Sep 17 00:00:00 2001 From: Gunter Hager Date: Tue, 2 Dec 2025 07:29:01 +0100 Subject: [PATCH 15/20] Create copilot-instructions.md --- .github/copilot-instructions.md | 53 +++++++++++++++++++++++++++++++++ 1 file changed, 53 insertions(+) create mode 100644 .github/copilot-instructions.md diff --git a/.github/copilot-instructions.md b/.github/copilot-instructions.md new file mode 100644 index 0000000..5c2a96a --- /dev/null +++ b/.github/copilot-instructions.md @@ -0,0 +1,53 @@ +## Quick orientation for AI coding agents + +This repository implements several 8-bit emulator experiments written in Zig. The codebase is organized into emulated "systems" (platforms/arcade machines), reusable "chips" (CPU, sound, IO), a small host layer, and UI helpers. + +Key directories and files to inspect first: +- `src/chipz.zig` — top-level module imports and entry points for the emulator. +- `src/systems/` — each system (eg. `namco.zig`, `bombjack.zig`, `kc85.zig`) defines machine composition and wiring of chips and memory. +- `src/chips/` — chip implementations, notably `z80.zig` (cycle-stepped Z80), `ay3891.zig` (audio), Intel 8255, CTC, PIO, etc. +- `src/common/` — shared utilities: `memory.zig` (paged memory model), `audio.zig`, `clock.zig`, and small helpers used broadly. +- `src/host/` — host abstraction (gfx, audio, timing, profiling) that separates platform I/O from machine logic. +- `src/ui/` — realtime debugging and UI panels (Z80 state, memory map, chip helpers). +- `emus/` and `media/` — ROMs and media assets used by systems. +- `tests/` — unit tests for many chips (use these as canonical examples of expected behavior). + +Important architecture & conventions +- Systems are composed by wiring together chips and mapping memory explicitly. See `src/systems/*.zig` for examples of how ROMs, RAM and IO are mapped. +- The Z80 implementation is a cycle-stepped emulator that exposes a Type(comptime cfg) API (see `src/chips/z80.zig`). Many chips in this codebase use Zig `comptime`-driven types; prefer adding specialized types via the existing Type(...) patterns rather than duplicating logic. +- Memory uses a paged model (`src/common/memory.zig`) with separate read/write pointers per page. Mapping functions include `mapRAM`, `mapROM`, `mapRW`, and `unmap`. All slices passed to Memory must outlive the Memory object — keep host lifetime in mind when writing tests or wiring memory. +- Host vs machine separation: device logic (chips, memory, systems) is pure emulation code; `src/host` contains platform glue for graphics/audio/timing. Avoid placing platform-specific code into `src/chips` or `src/systems`. + +Build / run / test workflows (concrete): +- Run arcade or system targets (examples from `README.md`): + - `zig build --release=fast run-pacman` + - `zig build --release=fast run-pengo` + - `zig build --release=fast run-kc853 -- -slot8 m022` (KC85 example with a slot) +- Unit tests are provided under `tests/`. Run a test file directly with Zig or use the repository build targets: + - `zig test tests/memory.test.zig` + - or `zig build ` when available in `build.zig` (inspect `build.zig` for provided tasks). +- Build artifacts are placed in `zig-out/bin/` (executables like `pacman`, `z80test`, etc.). + +Project-specific patterns to follow +- Prefer small, well-scoped files: chips implement a single device; systems assemble devices. +- Use the provided Memory Type for mapping. Example mapping pattern from systems: + - `mem.mapROM(0x0000, rom.len, rom_slice)` + - `mem.mapRAM(0x4000, ram_size, ram_slice)` +- Chips expose low-level, cycle-accurate interfaces: for the Z80, code inspects and manipulates pin masks and steps the CPU by cycles — avoid attempts at high-level shortcuts that skip pins or the step model. +- Use comptime Type stamps already present (e.g., `Z80.Type(TypeConfig{...})`) when creating specialized chip instances. + +Integration & extension notes +- Adding a new system: implement `src/systems/.zig`, wire chips and memory using existing APIs, add a `run-` target to `build.zig` mirroring existing runs. +- Adding a new chip: follow shape in `src/chips/*` — provide a Type(comptime cfg) when relevant, keep device logic free of host I/O, and add focused unit tests under `tests/`. +- ROM and media files live under `emus/` and `media/` — systems load these directly; keep naming and offsets consistent with existing systems. + +Examples to reference while coding +- See `src/systems/namco.zig` and `emus/namco/roms/pacman` for an arcade wiring example. +- See `src/chips/z80.zig` for cycle-stepped CPU semantics and `src/common/memory.zig` for mapping behavior. +- See tests like `tests/memory.test.zig` and `tests/intel8255.test.zig` for small, focused test patterns. + +When you need clarification from a human +- If a requested change touches host APIs (`src/host/*`) or build targets (`build.zig`), ask for which platforms to target (macOS / Linux) and whether CI needs updating. +- If you must change memory ownership/lifetimes, request guidance — many memory APIs rely on host-owned slices that must outlive the Memory object. + +If this snapshot is missing something you expect, tell me what to add (target, device, or workflow) and I'll update this file. From 2f4e6cb132e8c6f0d1ed239d1bdb43e4761a0be3 Mon Sep 17 00:00:00 2001 From: Gunter Hager Date: Tue, 2 Dec 2025 21:26:39 +0100 Subject: [PATCH 16/20] Update readFileAlloc usage for Zig API changes --- emus/kc85/kc85.zig | 4 ++-- tools/z80gen/generate.zig | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/emus/kc85/kc85.zig b/emus/kc85/kc85.zig index e8e0c92..5290df7 100644 --- a/emus/kc85/kc85.zig +++ b/emus/kc85/kc85.zig @@ -515,7 +515,7 @@ const Args = struct { print("Expected path to .KCC or .TAP file after '{s}'\n", .{arg}); return error.InvalidArgs; }; - res.file_data = fs.cwd().readFileAlloc(next, allocator, .limited(64 * 1024)) catch |err| { + res.file_data = fs.cwd().readFileAlloc(allocator, next, 64 * 1024) catch |err| { print("Failed to load file '{s}'\n", .{next}); return err; }; @@ -583,7 +583,7 @@ const Args = struct { print("Expect ROM dump file path after '{s} {s}'\n", .{ arg, mod_name }); return error.InvalidArgs; }; - mod.rom_dump = fs.cwd().readFileAlloc(rom_dump_path, allocator, .limited(64 * 1024)) catch |err| { + mod.rom_dump = fs.cwd().readFileAlloc(allocator, rom_dump_path, 64 * 1024) catch |err| { print("Failed to load module rom dump file '{s}'\n", .{rom_dump_path}); return err; }; diff --git a/tools/z80gen/generate.zig b/tools/z80gen/generate.zig index 9004f25..99d60fe 100644 --- a/tools/z80gen/generate.zig +++ b/tools/z80gen/generate.zig @@ -111,7 +111,7 @@ fn checkBeginEnd(line: []const u8, file: std.fs.File, comptime key: []const u8, pub fn write(allocator: std.mem.Allocator, path: []const u8) !void { const max_size = 5 * 1024 * 1024; - const src = try std.fs.cwd().readFileAlloc(path, allocator, .limited(max_size)); + const src = try std.fs.cwd().readFileAlloc(allocator, path, max_size); const dst = try std.fs.cwd().createFile(path, .{ .truncate = true, .lock = .exclusive }); defer dst.close(); From 41acbb957502cfeaa3a4b32ff49f712054ec2a06 Mon Sep 17 00:00:00 2001 From: Gunter Hager Date: Tue, 10 Mar 2026 13:54:18 +0100 Subject: [PATCH 17/20] fix build: use root_module.addIncludePath for Zig 0.14+ API, update deps In Zig 0.14+, addIncludePath was moved from Build.Step.Compile to Build.Module. Update the sokol/cimgui dependency hashes to latest. Co-Authored-By: Claude Sonnet 4.6 --- build.zig | 2 +- build.zig.zon | 8 ++++---- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/build.zig b/build.zig index d6bb3e7..882da12 100644 --- a/build.zig +++ b/build.zig @@ -20,7 +20,7 @@ pub fn build(b: *Build) !void { }); // inject the cimgui header search path into the sokol C library compile step - dep_sokol.artifact("sokol_clib").addIncludePath(dep_cimgui.path("src")); + dep_sokol.artifact("sokol_clib").root_module.addIncludePath(dep_cimgui.path("src")); const dep_shdc = dep_sokol.builder.dependency("shdc", .{}); const mod_sokol = dep_sokol.module("sokol"); const mod_cimgui = dep_cimgui.module("cimgui"); diff --git a/build.zig.zon b/build.zig.zon index a94f1e0..a23abc3 100644 --- a/build.zig.zon +++ b/build.zig.zon @@ -4,12 +4,12 @@ .fingerprint = 0x3ecb34175baf6f81, .dependencies = .{ .sokol = .{ - .url = "git+https://github.com/floooh/sokol-zig.git#9dacdb9ecdbd124b99c828d8891363236f0a56de", - .hash = "sokol-0.1.0-pb1HK6VgNgBI8wSklN7Gk_SV_sgIKAa-p3042rJ6HQH1", + .url = "git+https://github.com/floooh/sokol-zig.git#de5906fb0f81f96e92c1c67a2b8a24dc50872207", + .hash = "sokol-0.1.0-pb1HK9ngNgD_m7L3HpbsyJ-jHRGVzcd672pjJDEPBQWh", }, .cimgui = .{ - .url = "git+https://github.com/floooh/dcimgui.git#a89e0594214cf315de033c7cb6c144c7b54da09b", - .hash = "cimgui-0.1.0-44Clkd6YlAAYULKHDwsDX9EPmka-VAVEjUl-o6ve307E", + .url = "git+https://github.com/floooh/dcimgui.git#4557d7526fdd977f46ca10c2bbdd63532254f8d6", + .hash = "cimgui-0.1.0-44ClkXnYlwBKnil7TfCWL9z1Zo_2YJCJREzrpdFiGvA1", }, }, .paths = .{ From 879e6c50f9239a784efc7a967e11ecf812152c89 Mon Sep 17 00:00:00 2001 From: Gunter Hager Date: Tue, 10 Mar 2026 14:03:12 +0100 Subject: [PATCH 18/20] Update .gitignore --- .gitignore | 1 + 1 file changed, 1 insertion(+) diff --git a/.gitignore b/.gitignore index b4809ee..cf77ea9 100644 --- a/.gitignore +++ b/.gitignore @@ -2,3 +2,4 @@ zig-*/ .zig-*/ .vscode/ .cursor +.claude/settings.local.json From d1cd01f86629ad57ff59b4824a536150126538df Mon Sep 17 00:00:00 2001 From: Gunter Hager Date: Wed, 18 Mar 2026 18:34:58 +0100 Subject: [PATCH 19/20] add Z80 debugger UI: disassembler, CPU debugger, breakpoints - src/chips/z80dasm.zig: stateless Z80 disassembler (port of z80dasm.h); callback-based memory access, handles all prefixes (CB/DD/ED/FD) - src/ui/ui_dasm.zig: standalone scrollable disassembler window with navigation back-stack and jump-target buttons for CALL/JP/JR/RST/DJNZ - src/ui/ui_dbg.zig: comptime-parameterized CPU debugger (bus+cpu types); step-into/over, breakpoints, execution history, F5/F6/F7/F9 hotkeys - Integrated into KC85: per-tick execWithDebug() loop, Debug menu wired up All UI is reusable for other emulators (Namco, Bombjack) via runtime read_cb and comptime Type(.{.bus=..., .cpu=...}) instantiation. Co-Authored-By: Claude Sonnet 4.6 --- emus/kc85/kc85.zig | 93 +++++--- src/chips/chips.zig | 1 + src/chips/z80dasm.zig | 517 ++++++++++++++++++++++++++++++++++++++++++ src/ui/ui.zig | 2 + src/ui/ui_dasm.zig | 280 +++++++++++++++++++++++ src/ui/ui_dbg.zig | 405 +++++++++++++++++++++++++++++++++ 6 files changed, 1269 insertions(+), 29 deletions(-) create mode 100644 src/chips/z80dasm.zig create mode 100644 src/ui/ui_dasm.zig create mode 100644 src/ui/ui_dbg.zig diff --git a/emus/kc85/kc85.zig b/emus/kc85/kc85.zig index 2616f44..0e11340 100644 --- a/emus/kc85/kc85.zig +++ b/emus/kc85/kc85.zig @@ -7,6 +7,7 @@ const slog = sokol.log; const host = @import("chipz").host; const kc85 = @import("chipz").systems.kc85; const ui = @import("chipz").ui; +const clock = @import("chipz").common.clock; const ig = @import("cimgui"); const simgui = sokol.imgui; @@ -125,6 +126,8 @@ const UI_Z80CTC_Pins = [_]UI_CHIP.Pin{ .{ .name = "CT3", .slot = 25, .mask = kc85.Z80CTC.CLKTRG3 }, }; const UI_MEMMAP = ui.ui_memmap.MemMap; +const UI_DASM = ui.ui_dasm.Dasm; +const UI_DBG = ui.ui_dbg.Type(.{ .bus = kc85.Bus, .cpu = kc85.Z80 }); // a once-trigger for loading a file after booting has finished var file_loaded = host.time.Once.init(switch (model) { .KC852, .KC853 => 8 * 1000 * 1000, @@ -137,6 +140,13 @@ var ui_z80: UI_Z80 = undefined; var ui_z80pio: UI_Z80PIO = undefined; var ui_z80ctc: UI_Z80CTC = undefined; var ui_memmap: UI_MEMMAP = undefined; +var ui_dasm: [2]UI_DASM = undefined; +var ui_dbg_win: UI_DBG = undefined; + +fn memRead(addr: u16, userdata: ?*anyopaque) u8 { + const s: *KC85 = @ptrCast(@alignCast(userdata)); + return s.mem.rd(addr); +} export fn init() void { host.audio.init(.{}); @@ -195,6 +205,31 @@ export fn init() void { .title = "Memory Map", .origin = start, }); + start.x += d.x; + start.y += d.y; + ui_dasm[0].initInPlace(.{ + .title = "Disassembler #1", + .read_cb = memRead, + .userdata = &sys, + .origin = start, + }); + start.x += d.x; + start.y += d.y; + ui_dasm[1].initInPlace(.{ + .title = "Disassembler #2", + .read_cb = memRead, + .userdata = &sys, + .origin = start, + }); + start.x += d.x; + start.y += d.y; + ui_dbg_win.initInPlace(.{ + .title = "CPU Debugger", + .cpu = &sys.cpu, + .read_cb = memRead, + .userdata = &sys, + .origin = start, + }); host.gfx.init(.{ .display = sys.displayInfo() }); @@ -219,6 +254,24 @@ fn renderGUI() void { simgui.render(); } +fn execWithDebug(frame_time_us: u32) u32 { + if (ui_dbg_win.isStopped()) { + // Paused: don't advance emulation + return 0; + } else if (ui_dbg_win.needsTickDebug()) { + // Tick-by-tick for step mode or active breakpoints + const max_ticks = clock.microSecondsToTicks(KC85.FREQUENCY, frame_time_us); + var ticks: u32 = 0; + while (ticks < max_ticks) : (ticks += 1) { + sys.bus = sys.tick(sys.bus); + if (ui_dbg_win.tick(sys.bus)) break; + } + return ticks; + } else { + return sys.exec(frame_time_us); + } +} + fn uiDrawMenu() void { if (ig.igBeginMainMenuBar()) { if (ig.igBeginMenu("System")) { @@ -256,41 +309,20 @@ fn uiDrawMenu() void { ig.igEndMenu(); } if (ig.igBeginMenu("Debug")) { - if (ig.igMenuItem("CPU Debugger (TODO)")) { - // TODO: open window + if (ig.igMenuItem("CPU Debugger")) { + ui_dbg_win.open = true; } - if (ig.igMenuItem("Breakpoints (TODO)")) { - // TODO: open window - } - if (ig.igBeginMenu("Memory Editor (TODO)")) { + if (ig.igBeginMenu("Disassembler")) { if (ig.igMenuItem("Window #1")) { - // TODO: open window + ui_dasm[0].open = true; } if (ig.igMenuItem("Window #2")) { - // TODO: open window - } - if (ig.igMenuItem("Window #3")) { - // TODO: open window - } - if (ig.igMenuItem("Window #4")) { - // TODO: open window + ui_dasm[1].open = true; } ig.igEndMenu(); } - if (ig.igBeginMenu("Disassembler (TODO)")) { - if (ig.igMenuItem("Window #1")) { - // TODO: open window - } - if (ig.igMenuItem("Window #2")) { - // TODO: open window - } - if (ig.igMenuItem("Window #3")) { - // TODO: open window - } - if (ig.igMenuItem("Window #4")) { - // TODO: open window - } - ig.igEndMenu(); + if (ig.igMenuItem("Memory Editor (TODO)")) { + // TODO: open memory editor window } ig.igEndMenu(); } @@ -302,7 +334,7 @@ export fn frame() void { const frame_time_us = host.time.frameTime(); host.prof.pushMicroSeconds(.FRAME, frame_time_us); host.time.emuStart(); - const num_ticks = sys.exec(frame_time_us); + const num_ticks = execWithDebug(frame_time_us); host.prof.pushMicroSeconds(.EMU, host.time.emuEnd()); // call simgui.newFrame() before any ImGui calls @@ -318,6 +350,9 @@ export fn frame() void { ui_z80pio.draw(sys.bus); ui_z80ctc.draw(sys.bus); ui_memmap.draw(); + ui_dasm[0].draw(); + ui_dasm[1].draw(); + ui_dbg_win.draw(sys.bus); host.gfx.draw(.{ .display = sys.displayInfo(), diff --git a/src/chips/chips.zig b/src/chips/chips.zig index 31e33b0..cfb00d4 100644 --- a/src/chips/chips.zig +++ b/src/chips/chips.zig @@ -3,3 +3,4 @@ pub const z80ctc = @import("z80ctc.zig"); pub const z80pio = @import("z80pio.zig"); pub const ay3891 = @import("ay3891.zig"); pub const intel8255 = @import("intel8255.zig"); +pub const z80dasm = @import("z80dasm.zig"); diff --git a/src/chips/z80dasm.zig b/src/chips/z80dasm.zig new file mode 100644 index 0000000..a0fbbd3 --- /dev/null +++ b/src/chips/z80dasm.zig @@ -0,0 +1,517 @@ +//! Z80 disassembler. +//! +//! Zig port of the z80dasm.h from the chips project by Andre Weissflog. +//! Decoding strategy: http://www.z80.info/decoding.htm +const std = @import("std"); + +pub const MAX_MNEMONIC_LEN = 32; +pub const MAX_BYTES = 4; + +pub const Result = struct { + next_pc: u16, + num_bytes: u8, + mnemonic: [MAX_MNEMONIC_LEN]u8 = [_]u8{0} ** MAX_MNEMONIC_LEN, + mnemonic_len: u8, + + pub fn mnemonicSlice(self: *const Result) []const u8 { + return self.mnemonic[0..self.mnemonic_len]; + } +}; + +// Register name tables +const r_hl = [_][]const u8{ "B", "C", "D", "E", "H", "L", "(HL)", "A" }; +const r_ix = [_][]const u8{ "B", "C", "D", "E", "IXH", "IXL", "(IX", "A" }; +const r_iy = [_][]const u8{ "B", "C", "D", "E", "IYH", "IYL", "(IY", "A" }; +const rp_hl = [_][]const u8{ "BC", "DE", "HL", "SP" }; +const rp_ix = [_][]const u8{ "BC", "DE", "IX", "SP" }; +const rp_iy = [_][]const u8{ "BC", "DE", "IY", "SP" }; +const rp2_hl = [_][]const u8{ "BC", "DE", "HL", "AF" }; +const rp2_ix = [_][]const u8{ "BC", "DE", "IX", "AF" }; +const rp2_iy = [_][]const u8{ "BC", "DE", "IY", "AF" }; +const cc = [_][]const u8{ "NZ", "Z", "NC", "C", "PO", "PE", "P", "M" }; +const alu = [_][]const u8{ "ADD A,", "ADC A,", "SUB ", "SBC A,", "AND ", "XOR ", "OR ", "CP " }; +const rot = [_][]const u8{ "RLC ", "RRC ", "RL ", "RR ", "SLA ", "SRA ", "SLL ", "SRL " }; +const x0z7 = [_][]const u8{ "RLCA", "RRCA", "RLA", "RRA", "DAA", "CPL", "SCF", "CCF" }; +const edx1z7 = [_][]const u8{ "LD I,A", "LD R,A", "LD A,I", "LD A,R", "RRD", "RLD", "NOP (ED)", "NOP (ED)" }; +const im_modes = [_][]const u8{ "0", "0", "1", "2", "0", "0", "1", "2" }; +const bli = [4][4][]const u8{ + .{ "LDI", "CPI", "INI", "OUTI" }, + .{ "LDD", "CPD", "IND", "OUTD" }, + .{ "LDIR", "CPIR", "INIR", "OTIR" }, + .{ "LDDR", "CPDR", "INDR", "OTDR" }, +}; + +const Ctx = struct { + pc: u16, + num_bytes: u8, + read_fn: *const fn (u16, ?*anyopaque) u8, + user_data: ?*anyopaque, + buf: [MAX_MNEMONIC_LEN]u8, + buf_pos: u8, + pre: u8, // prefix byte: 0, 0xDD, or 0xFD + + fn fetchU8(self: *Ctx) u8 { + const v = self.read_fn(self.pc, self.user_data); + self.pc +%= 1; + self.num_bytes += 1; + return v; + } + + fn fetchI8(self: *Ctx) i8 { + return @bitCast(self.fetchU8()); + } + + fn fetchU16(self: *Ctx) u16 { + const lo: u16 = self.fetchU8(); + const hi: u16 = self.fetchU8(); + return lo | (hi << 8); + } + + fn chr(self: *Ctx, c: u8) void { + if (self.buf_pos < MAX_MNEMONIC_LEN) { + self.buf[self.buf_pos] = c; + self.buf_pos += 1; + } + } + + fn str(self: *Ctx, s: []const u8) void { + for (s) |c| self.chr(c); + } + + // output signed 8-bit offset as decimal (+d or -d) + fn strD8(self: *Ctx, val: i8) void { + if (val < 0) { + self.chr('-'); + const v: u8 = @intCast(-@as(i16, val)); + if (v >= 100) self.chr('1'); + const v2: u8 = if (v >= 100) v - 100 else v; + if (v2 / 10 != 0) self.chr('0' + v2 / 10); + self.chr('0' + v2 % 10); + } else { + self.chr('+'); + const v: u8 = @intCast(val); + if (v >= 100) self.chr('1'); + const v2: u8 = if (v >= 100) v - 100 else v; + if (v2 / 10 != 0) self.chr('0' + v2 / 10); + self.chr('0' + v2 % 10); + } + } + + // output unsigned 8-bit value as hex (e.g. "1Fh") + fn strU8(self: *Ctx, val: u8) void { + const hex = "0123456789ABCDEF"; + self.chr(hex[(val >> 4) & 0xF]); + self.chr(hex[val & 0xF]); + self.chr('h'); + } + + // output unsigned 16-bit value as hex (e.g. "1234h") + fn strU16(self: *Ctx, val: u16) void { + const hex = "0123456789ABCDEF"; + self.chr(hex[(val >> 12) & 0xF]); + self.chr(hex[(val >> 8) & 0xF]); + self.chr(hex[(val >> 4) & 0xF]); + self.chr(hex[val & 0xF]); + self.chr('h'); + } + + // (HL) or (IX+d) or (IY+d) — fetches the displacement byte if prefixed + fn mem(self: *Ctx, regs: []const []const u8) void { + self.str(regs[6]); + if (self.pre != 0) { + const d = self.fetchI8(); + self.strD8(d); + self.chr(')'); + } + } + + // (HL) or (IX+d) or (IY+d) — with an already-fetched displacement byte + fn memD(self: *Ctx, d: i8, regs: []const []const u8) void { + self.str(regs[6]); + if (self.pre != 0) { + self.strD8(d); + self.chr(')'); + } + } + + // register or memory reference + fn memR(self: *Ctx, i: u3, regs: []const []const u8) void { + if (i == 6) { + self.mem(regs); + } else { + self.str(regs[i]); + } + } + + // register or memory reference with pre-fetched displacement + fn memRd(self: *Ctx, i: u3, d: i8, regs: []const []const u8) void { + self.str(regs[i]); + if (i == 6 and self.pre != 0) { + self.strD8(d); + self.chr(')'); + } + } + + fn r(self: *Ctx) []const []const u8 { + return switch (self.pre) { + 0xDD => &r_ix, + 0xFD => &r_iy, + else => &r_hl, + }; + } + + fn rp(self: *Ctx) []const []const u8 { + return switch (self.pre) { + 0xDD => &rp_ix, + 0xFD => &rp_iy, + else => &rp_hl, + }; + } + + fn rp2(self: *Ctx) []const []const u8 { + return switch (self.pre) { + 0xDD => &rp2_ix, + 0xFD => &rp2_iy, + else => &rp2_hl, + }; + } +}; + +/// Disassemble one Z80 instruction starting at `pc`. +/// `read_fn(addr, user_data)` returns the byte at the given address. +/// Returns the next PC and the disassembled mnemonic string. +pub fn op(pc: u16, read_fn: *const fn (u16, ?*anyopaque) u8, user_data: ?*anyopaque) Result { + var ctx = Ctx{ + .pc = pc, + .num_bytes = 0, + .read_fn = read_fn, + .user_data = user_data, + .buf = [_]u8{0} ** MAX_MNEMONIC_LEN, + .buf_pos = 0, + .pre = 0, + }; + disasm(&ctx); + return Result{ + .next_pc = ctx.pc, + .num_bytes = ctx.num_bytes, + .mnemonic = ctx.buf, + .mnemonic_len = ctx.buf_pos, + }; +} + +fn disasm(ctx: *Ctx) void { + var byte = ctx.fetchU8(); + + // handle DD/FD prefix + if (byte == 0xDD or byte == 0xFD) { + ctx.pre = byte; + byte = ctx.fetchU8(); + if (byte == 0xED) { + ctx.pre = 0; // ED after prefix cancels prefix + } + } + + const x: u2 = @truncate(byte >> 6); + const y: u3 = @truncate((byte >> 3) & 7); + const z: u3 = @truncate(byte & 7); + const p: u2 = @truncate(y >> 1); + const q: u1 = @truncate(y & 1); + + if (x == 1) { + // 8-bit load block + if (y == 6) { + if (z == 6) { + ctx.str("HALT"); + } else { + // LD (HL),r / LD (IX+d),r / LD (IY+d),r + ctx.str("LD "); + ctx.mem(ctx.r()); + ctx.chr(','); + if (ctx.pre != 0 and (z == 4 or z == 5)) { + ctx.str(r_hl[z]); + } else { + ctx.str(ctx.r()[z]); + } + } + } else if (z == 6) { + // LD r,(HL) / LD r,(IX+d) / LD r,(IY+d) + ctx.str("LD "); + if (ctx.pre != 0 and (y == 4 or y == 5)) { + ctx.str(r_hl[y]); + } else { + ctx.str(ctx.r()[y]); + } + ctx.chr(','); + ctx.mem(ctx.r()); + } else { + // regular LD r,s + ctx.str("LD "); + ctx.str(ctx.r()[y]); + ctx.chr(','); + ctx.str(ctx.r()[z]); + } + } else if (x == 2) { + // 8-bit ALU block + ctx.str(alu[y]); + ctx.memR(z, ctx.r()); + } else if (x == 0) { + switch (z) { + 0 => switch (y) { + 0 => ctx.str("NOP"), + 1 => ctx.str("EX AF,AF'"), + 2 => { + ctx.str("DJNZ "); + const d = ctx.fetchI8(); + ctx.strU16(ctx.pc +% @as(u16, @bitCast(@as(i16, d)))); + }, + 3 => { + ctx.str("JR "); + const d = ctx.fetchI8(); + ctx.strU16(ctx.pc +% @as(u16, @bitCast(@as(i16, d)))); + }, + else => { + ctx.str("JR "); + ctx.str(cc[y - 4]); + ctx.chr(','); + const d = ctx.fetchI8(); + ctx.strU16(ctx.pc +% @as(u16, @bitCast(@as(i16, d)))); + }, + }, + 1 => { + if (q == 0) { + ctx.str("LD "); + ctx.str(ctx.rp()[p]); + ctx.chr(','); + ctx.strU16(ctx.fetchU16()); + } else { + ctx.str("ADD "); + ctx.str(ctx.rp()[2]); + ctx.chr(','); + ctx.str(ctx.rp()[p]); + } + }, + 2 => { + ctx.str("LD "); + switch (y) { + 0 => ctx.str("(BC),A"), + 1 => ctx.str("A,(BC)"), + 2 => ctx.str("(DE),A"), + 3 => ctx.str("A,(DE)"), + 4 => { + ctx.chr('('); + ctx.strU16(ctx.fetchU16()); + ctx.str("),"); + ctx.str(ctx.rp()[2]); + }, + 5 => { + ctx.str(ctx.rp()[2]); + ctx.str(",("); + ctx.strU16(ctx.fetchU16()); + ctx.chr(')'); + }, + 6 => { + ctx.chr('('); + ctx.strU16(ctx.fetchU16()); + ctx.str("),A"); + }, + 7 => { + ctx.str("A,("); + ctx.strU16(ctx.fetchU16()); + ctx.chr(')'); + }, + } + }, + 3 => { + ctx.str(if (q == 0) "INC " else "DEC "); + ctx.str(ctx.rp()[p]); + }, + 4 => { + ctx.str("INC "); + ctx.memR(y, ctx.r()); + }, + 5 => { + ctx.str("DEC "); + ctx.memR(y, ctx.r()); + }, + 6 => { + ctx.str("LD "); + ctx.memR(y, ctx.r()); + ctx.chr(','); + ctx.strU8(ctx.fetchU8()); + }, + 7 => ctx.str(x0z7[y]), + } + } else { // x == 3 + switch (z) { + 0 => { + ctx.str("RET "); + ctx.str(cc[y]); + }, + 1 => { + if (q == 0) { + ctx.str("POP "); + ctx.str(ctx.rp2()[p]); + } else { + switch (p) { + 0 => ctx.str("RET"), + 1 => ctx.str("EXX"), + 2 => { + ctx.str("JP ("); + ctx.str(ctx.rp()[2]); + ctx.chr(')'); + }, + 3 => { + ctx.str("LD SP,"); + ctx.str(ctx.rp()[2]); + }, + } + } + }, + 2 => { + ctx.str("JP "); + ctx.str(cc[y]); + ctx.chr(','); + ctx.strU16(ctx.fetchU16()); + }, + 3 => { + switch (y) { + 0 => { + ctx.str("JP "); + ctx.strU16(ctx.fetchU16()); + }, + 1 => { // CB prefix + const saved_pre = ctx.pre; + var d: i8 = 0; + if (saved_pre != 0) { + d = ctx.fetchI8(); + } + const cb_op = ctx.fetchU8(); + const cx: u2 = @truncate(cb_op >> 6); + const cy: u3 = @truncate((cb_op >> 3) & 7); + const cz: u3 = @truncate(cb_op & 7); + if (cx == 0) { + ctx.str(rot[cy]); + ctx.memRd(cz, d, ctx.r()); + } else { + if (cx == 1) ctx.str("BIT ") else if (cx == 2) ctx.str("RES ") else ctx.str("SET "); + ctx.chr('0' + @as(u8, cy)); + if (saved_pre != 0) { + ctx.chr(','); + ctx.memD(d, ctx.r()); + } + if (saved_pre == 0 or cz != 6) { + ctx.chr(','); + ctx.str(ctx.r()[cz]); + } + } + }, + 2 => { + ctx.str("OUT ("); + ctx.strU8(ctx.fetchU8()); + ctx.str("),A"); + }, + 3 => { + ctx.str("IN A,("); + ctx.strU8(ctx.fetchU8()); + ctx.chr(')'); + }, + 4 => { + ctx.str("EX (SP),"); + ctx.str(ctx.rp()[2]); + }, + 5 => ctx.str("EX DE,HL"), + 6 => ctx.str("DI"), + 7 => ctx.str("EI"), + } + }, + 4 => { + ctx.str("CALL "); + ctx.str(cc[y]); + ctx.chr(','); + ctx.strU16(ctx.fetchU16()); + }, + 5 => { + if (q == 0) { + ctx.str("PUSH "); + ctx.str(ctx.rp2()[p]); + } else { + switch (p) { + 0 => { + ctx.str("CALL "); + ctx.strU16(ctx.fetchU16()); + }, + 1 => ctx.str("DBL PREFIX"), + 3 => ctx.str("DBL PREFIX"), + 2 => { // ED prefix + const ed_op = ctx.fetchU8(); + const ex: u2 = @truncate(ed_op >> 6); + const ey: u3 = @truncate((ed_op >> 3) & 7); + const ez: u3 = @truncate(ed_op & 7); + const ep: u2 = @truncate(ey >> 1); + const eq: u1 = @truncate(ey & 1); + if (ex == 0 or ex == 3) { + ctx.str("NOP (ED)"); + } else if (ex == 2) { + if (ey >= 4 and ez <= 3) { + ctx.str(bli[ey - 4][ez]); + } else { + ctx.str("NOP (ED)"); + } + } else { // ex == 1 + switch (ez) { + 0 => { + ctx.str("IN "); + if (ey != 6) { + ctx.str(r_hl[ey]); + ctx.chr(','); + } + ctx.str("(C)"); + }, + 1 => { + ctx.str("OUT (C),"); + ctx.str(if (ey == 6) "0" else r_hl[ey]); + }, + 2 => { + ctx.str(if (eq == 0) "SBC" else "ADC"); + ctx.str(" HL,"); + ctx.str(rp_hl[ep]); + }, + 3 => { + ctx.str("LD "); + if (eq == 0) { + ctx.chr('('); + ctx.strU16(ctx.fetchU16()); + ctx.str("),"); + ctx.str(rp_hl[ep]); + } else { + ctx.str(rp_hl[ep]); + ctx.str(",("); + ctx.strU16(ctx.fetchU16()); + ctx.chr(')'); + } + }, + 4 => ctx.str("NEG"), + 5 => ctx.str(if (ey == 1) "RETI" else "RETN"), + 6 => { + ctx.str("IM "); + ctx.str(im_modes[ey]); + }, + 7 => ctx.str(edx1z7[ey]), + } + } + }, + } + } + }, + 6 => { + // ALU n + ctx.str(alu[y]); + ctx.strU8(ctx.fetchU8()); + }, + 7 => { + ctx.str("RST "); + ctx.strU8(@as(u8, y) * 8); + }, + } + } +} diff --git a/src/ui/ui.zig b/src/ui/ui.zig index 56f2403..c7133c0 100644 --- a/src/ui/ui.zig +++ b/src/ui/ui.zig @@ -6,3 +6,5 @@ pub const ui_z80pio = @import("ui_z80pio.zig"); pub const ui_z80ctc = @import("ui_z80ctc.zig"); pub const ui_intel8255 = @import("ui_intel8255.zig"); pub const ui_memmap = @import("ui_memmap.zig"); +pub const ui_dasm = @import("ui_dasm.zig"); +pub const ui_dbg = @import("ui_dbg.zig"); diff --git a/src/ui/ui_dasm.zig b/src/ui/ui_dasm.zig new file mode 100644 index 0000000..46ec0c4 --- /dev/null +++ b/src/ui/ui_dasm.zig @@ -0,0 +1,280 @@ +//! Z80 disassembler UI window. +const std = @import("std"); +const z80dasm = @import("chips").z80dasm; +const ig = @import("cimgui"); +const ui_settings = @import("ui_settings.zig"); + +pub const NUM_LINES = 512; +pub const STACK_MAX = 128; + +pub const Dasm = struct { + const Self = @This(); + + pub const Options = struct { + title: []const u8, + read_cb: *const fn (addr: u16, userdata: ?*anyopaque) u8, + userdata: ?*anyopaque, + origin: ig.ImVec2, + size: ig.ImVec2 = .{}, + open: bool = false, + }; + + title: []const u8, + read_cb: *const fn (addr: u16, userdata: ?*anyopaque) u8, + userdata: ?*anyopaque, + origin: ig.ImVec2, + size: ig.ImVec2, + open: bool, + last_open: bool, + valid: bool, + start_addr: u16, + highlight_addr: u16, + highlight_valid: bool, + stack: [STACK_MAX]u16, + stack_num: u8, + stack_pos: u8, + + pub fn initInPlace(self: *Self, opts: Options) void { + self.* = .{ + .title = opts.title, + .read_cb = opts.read_cb, + .userdata = opts.userdata, + .origin = opts.origin, + .size = .{ + .x = if (opts.size.x == 0) 400 else opts.size.x, + .y = if (opts.size.y == 0) 256 else opts.size.y, + }, + .open = opts.open, + .last_open = opts.open, + .valid = true, + .start_addr = 0, + .highlight_addr = 0, + .highlight_valid = false, + .stack = [_]u16{0} ** STACK_MAX, + .stack_num = 0, + .stack_pos = 0, + }; + } + + pub fn discard(self: *Self) void { + self.valid = false; + } + + fn goto(self: *Self, addr: u16) void { + self.start_addr = addr; + } + + fn stackPush(self: *Self, addr: u16) void { + if (self.stack_num < STACK_MAX) { + if (self.stack_num > 0 and addr == self.stack[self.stack_num - 1]) return; + self.stack_pos = self.stack_num; + self.stack[self.stack_num] = addr; + self.stack_num += 1; + } + } + + fn stackBack(self: *Self) ?u16 { + if (self.stack_num == 0) return null; + const addr = self.stack[self.stack_pos]; + if (self.stack_pos > 0) self.stack_pos -= 1; + return addr; + } + + fn jumpTarget(next_addr: u16, bytes: []const u8) ?u16 { + if (bytes.len == 3) { + switch (bytes[0]) { + 0xCD, 0xDC, 0xFC, 0xD4, 0xC4, 0xF4, 0xEC, 0xE4, 0xCC, + 0xC3, 0xDA, 0xFA, 0xD2, 0xC2, 0xF2, 0xEA, 0xE2, 0xCA, + => return (@as(u16, bytes[2]) << 8) | bytes[1], + else => {}, + } + } + if (bytes.len == 2) { + switch (bytes[0]) { + 0x10, 0x18, 0x38, 0x30, 0x20, 0x28 => { + const off: i16 = @as(i8, @bitCast(bytes[1])); + return next_addr +% @as(u16, @bitCast(off)); + }, + else => {}, + } + } + if (bytes.len == 1) { + return switch (bytes[0]) { + 0xC7 => 0x00, + 0xCF => 0x08, + 0xD7 => 0x10, + 0xDF => 0x18, + 0xE7 => 0x20, + 0xEF => 0x28, + 0xF7 => 0x30, + 0xFF => 0x38, + else => null, + }; + } + return null; + } + + fn drawStack(self: *Self) void { + _ = ig.igBeginChild("##dasm_stack", .{ .x = 72, .y = 0 }, ig.ImGuiChildFlags_Borders, ig.ImGuiWindowFlags_None); + if (ig.igButton("Clear")) { + self.stack_num = 0; + } + if (ig.igBeginListBox("##stack", .{ .x = -1, .y = -1 })) { + var i: usize = 0; + while (i < self.stack_num) : (i += 1) { + var buf: [6]u8 = undefined; + const s = std.fmt.bufPrint(&buf, "{X:0>4}", .{self.stack[i]}) catch unreachable; + buf[s.len] = 0; + ig.igPushIDInt(@intCast(i)); + if (ig.igSelectableEx(&buf, i == self.stack_pos, ig.ImGuiSelectableFlags_None, .{})) { + self.stack_pos = @intCast(i); + self.goto(self.stack[i]); + } + if (ig.igIsItemHovered(ig.ImGuiHoveredFlags_None)) { + var tip: [16]u8 = undefined; + const ts = std.fmt.bufPrint(&tip, "Goto {X:0>4}\x00", .{self.stack[i]}) catch unreachable; + _ = ts; + ig.igSetTooltip(&tip); + self.highlight_addr = self.stack[i]; + self.highlight_valid = true; + } + ig.igPopID(); + } + ig.igEndListBox(); + } + ig.igEndChild(); + } + + fn drawDisasm(self: *Self) void { + _ = ig.igBeginChild("##dasm_box", .{ .x = 0, .y = 0 }, ig.ImGuiChildFlags_Borders, ig.ImGuiWindowFlags_None); + + // Address input + var addr_val: u16 = self.start_addr; + ig.igSetNextItemWidth(60); + if (ig.igInputScalarEx("##addr", ig.ImGuiDataType_U16, &addr_val, null, null, "%04X", ig.ImGuiInputTextFlags_CharsHexadecimal)) { + self.start_addr = addr_val; + } + ig.igSameLine(); + if (ig.igArrowButton("##back", ig.ImGuiDir_Left)) { + if (self.stackBack()) |a| self.goto(a); + } + if (ig.igIsItemHovered(ig.ImGuiHoveredFlags_None) and self.stack_num > 0) { + var tip: [16]u8 = undefined; + const ts = std.fmt.bufPrint(&tip, "Goto {X:0>4}\x00", .{self.stack[self.stack_pos]}) catch unreachable; + _ = ts; + ig.igSetTooltip(&tip); + } + + _ = ig.igBeginChild("##dasm_inner", .{ .x = 0, .y = 0 }, ig.ImGuiChildFlags_None, ig.ImGuiWindowFlags_None); + ig.igPushStyleVarImVec2(ig.ImGuiStyleVar_FramePadding, .{ .x = 0, .y = 0 }); + ig.igPushStyleVarImVec2(ig.ImGuiStyleVar_ItemSpacing, .{ .x = 0, .y = 0 }); + + const line_height = ig.igGetTextLineHeight(); + const glyph_width = ig.igCalcTextSize("F").x; + const cell_width = 3 * glyph_width; + + var clipper: ig.ImGuiListClipper = .{}; + ig.ImGuiListClipper_Begin(&clipper, NUM_LINES, line_height); + _ = ig.ImGuiListClipper_Step(&clipper); + + // Advance to DisplayStart + var cur_addr: u16 = self.start_addr; + var line_i: i32 = 0; + while (line_i < clipper.DisplayStart) : (line_i += 1) { + const res = z80dasm.op(cur_addr, self.read_cb, self.userdata); + cur_addr = res.next_pc; + } + + // Draw visible lines + while (line_i < clipper.DisplayEnd) : (line_i += 1) { + const op_addr = cur_addr; + const res = z80dasm.op(cur_addr, self.read_cb, self.userdata); + cur_addr = res.next_pc; + + var highlight = false; + if (self.highlight_valid and self.highlight_addr == op_addr) { + ig.igPushStyleColor(ig.ImGuiCol_Text, 0xFF30FF30); + highlight = true; + } + + // Address + var addr_buf: [8]u8 = undefined; + const addr_s = std.fmt.bufPrint(&addr_buf, "{X:0>4}: \x00", .{op_addr}) catch unreachable; + _ = addr_s; + ig.igTextUnformatted(&addr_buf); + ig.igSameLine(); + + // Instruction bytes + const line_start_x = ig.igGetCursorPosX(); + for (0..res.num_bytes) |bi| { + ig.igSameLineEx(line_start_x + cell_width * @as(f32, @floatFromInt(bi)), 0); + var byte_buf: [4]u8 = undefined; + const b = self.read_cb(op_addr +% @as(u16, @intCast(bi)), self.userdata); + const bs = std.fmt.bufPrint(&byte_buf, "{X:0>2} \x00", .{b}) catch unreachable; + _ = bs; + ig.igTextUnformatted(&byte_buf); + } + + // Mnemonic + ig.igSameLineEx(line_start_x + cell_width * 4 + glyph_width * 2, 0); + var mn_buf: [z80dasm.MAX_MNEMONIC_LEN + 1]u8 = undefined; + @memcpy(mn_buf[0..res.mnemonic_len], res.mnemonic[0..res.mnemonic_len]); + mn_buf[res.mnemonic_len] = 0; + ig.igTextUnformatted(&mn_buf); + + if (highlight) ig.igPopStyleColor(); + + // Collect bytes for jump-target detection + var bytes_buf: [z80dasm.MAX_BYTES]u8 = undefined; + for (0..res.num_bytes) |bi| { + bytes_buf[bi] = self.read_cb(op_addr +% @as(u16, @intCast(bi)), self.userdata); + } + if (jumpTarget(cur_addr, bytes_buf[0..res.num_bytes])) |jt| { + ig.igSameLineEx(line_start_x + cell_width * 4 + glyph_width * 2 + glyph_width * 20, 0); + ig.igPushIDInt(line_i); + if (ig.igArrowButton("##jmp", ig.ImGuiDir_Right)) { + ig.igSetScrollY(0); + self.stackPush(op_addr); + self.goto(jt); + } + if (ig.igIsItemHovered(ig.ImGuiHoveredFlags_None)) { + var tip: [16]u8 = undefined; + const ts = std.fmt.bufPrint(&tip, "Goto {X:0>4}\x00", .{jt}) catch unreachable; + _ = ts; + ig.igSetTooltip(&tip); + self.highlight_addr = jt; + self.highlight_valid = true; + } + ig.igPopID(); + } + } + ig.ImGuiListClipper_End(&clipper); + ig.igPopStyleVarEx(2); + ig.igEndChild(); // ##dasm_inner + ig.igEndChild(); // ##dasm_box + } + + pub fn draw(self: *Self) void { + if (self.open != self.last_open) self.last_open = self.open; + if (!self.open) return; + + self.highlight_valid = false; + + ig.igSetNextWindowPos(self.origin, ig.ImGuiCond_FirstUseEver); + ig.igSetNextWindowSize(self.size, ig.ImGuiCond_FirstUseEver); + if (ig.igBegin(self.title.ptr, &self.open, ig.ImGuiWindowFlags_None)) { + self.drawStack(); + ig.igSameLine(); + self.drawDisasm(); + } + ig.igEnd(); + } + + pub fn saveSettings(self: *Self, settings: *ui_settings.Settings) void { + _ = settings.add(self.title, self.open); + } + + pub fn loadSettings(self: *Self, settings: *const ui_settings.Settings) void { + self.open = settings.isOpen(self.title); + } +}; diff --git a/src/ui/ui_dbg.zig b/src/ui/ui_dbg.zig new file mode 100644 index 0000000..69244fa --- /dev/null +++ b/src/ui/ui_dbg.zig @@ -0,0 +1,405 @@ +//! Z80 CPU debugger UI. +//! +//! Shows a disassembly view centered on the current PC, with step/continue +//! controls, breakpoints, and execution history. +const std = @import("std"); +const z80dasm = @import("chips").z80dasm; +const ig = @import("cimgui"); +const ui_settings = @import("ui_settings.zig"); + +pub const MAX_BREAKPOINTS = 32; +pub const NUM_HISTORY = 256; +pub const NUM_DBG_LINES = 48; + +pub const TypeConfig = struct { + bus: type, + cpu: type, +}; + +pub const StepMode = enum { none, into, over }; + +pub const Breakpoint = struct { + addr: u16, + enabled: bool, +}; + +pub fn Type(comptime cfg: TypeConfig) type { + return struct { + const Self = @This(); + const Bus = cfg.bus; + const Z80 = cfg.cpu; + + const M1_MASK = Z80.M1 | Z80.MREQ | Z80.RD; + + pub const Options = struct { + title: []const u8, + cpu: *Z80, + read_cb: *const fn (addr: u16, userdata: ?*anyopaque) u8, + userdata: ?*anyopaque, + origin: ig.ImVec2, + size: ig.ImVec2 = .{}, + open: bool = false, + }; + + const DasmLine = struct { + addr: u16, + num_bytes: u8, + bytes: [4]u8, + mnemonic: [z80dasm.MAX_MNEMONIC_LEN]u8, + mnemonic_len: u8, + }; + + title: []const u8, + cpu: *Z80, + read_cb: *const fn (addr: u16, userdata: ?*anyopaque) u8, + userdata: ?*anyopaque, + origin: ig.ImVec2, + size: ig.ImVec2, + open: bool, + last_open: bool, + valid: bool, + + stopped: bool, + step_mode: StepMode, + cur_op_pc: u16, + stepover_pc: u16, + + num_breakpoints: u8, + breakpoints: [MAX_BREAKPOINTS]Breakpoint, + + history: [NUM_HISTORY]u16, + history_pos: u8, + show_history: bool, + + dasm_lines: [NUM_DBG_LINES]DasmLine, + dasm_num_lines: u8, + + pub fn initInPlace(self: *Self, opts: Options) void { + self.* = .{ + .title = opts.title, + .cpu = opts.cpu, + .read_cb = opts.read_cb, + .userdata = opts.userdata, + .origin = opts.origin, + .size = .{ + .x = if (opts.size.x == 0) 460 else opts.size.x, + .y = if (opts.size.y == 0) 420 else opts.size.y, + }, + .open = opts.open, + .last_open = opts.open, + .valid = true, + .stopped = false, + .step_mode = .none, + .cur_op_pc = 0, + .stepover_pc = 0, + .num_breakpoints = 0, + .breakpoints = [_]Breakpoint{.{ .addr = 0, .enabled = false }} ** MAX_BREAKPOINTS, + .history = [_]u16{0} ** NUM_HISTORY, + .history_pos = 0, + .show_history = false, + .dasm_lines = undefined, + .dasm_num_lines = 0, + }; + } + + pub fn discard(self: *Self) void { + self.valid = false; + } + + pub fn isStopped(self: *const Self) bool { + return self.stopped; + } + + /// Returns true if tick-by-tick execution is needed + /// (step mode active or breakpoints enabled). + pub fn needsTickDebug(self: *const Self) bool { + if (self.step_mode != .none) return true; + for (self.breakpoints[0..self.num_breakpoints]) |bp| { + if (bp.enabled) return true; + } + return false; + } + + /// Call after each individual CPU tick. + /// Returns true if execution should stop. + pub fn tick(self: *Self, pins: Bus) bool { + if (pins & M1_MASK == M1_MASK) { + const pc = Z80.getAddr(pins); + self.cur_op_pc = pc; + self.history[self.history_pos] = pc; + self.history_pos +%= 1; + for (self.breakpoints[0..self.num_breakpoints]) |bp| { + if (bp.enabled and bp.addr == pc) { + self.stopped = true; + self.step_mode = .none; + return true; + } + } + if (self.step_mode == .into) { + self.stopped = true; + self.step_mode = .none; + return true; + } + if (self.step_mode == .over and pc == self.stepover_pc) { + self.stopped = true; + self.step_mode = .none; + return true; + } + } + return false; + } + + pub fn breakExec(self: *Self) void { + self.stopped = true; + self.step_mode = .none; + } + + pub fn continueExec(self: *Self) void { + self.stopped = false; + self.step_mode = .none; + } + + pub fn stepInto(self: *Self) void { + self.stopped = false; + self.step_mode = .into; + } + + pub fn stepOver(self: *Self) void { + const opcode = self.read_cb(self.cur_op_pc, self.userdata); + const is_stepover_op = switch (opcode) { + 0xCD, 0xDC, 0xFC, 0xD4, 0xC4, 0xF4, 0xEC, 0xE4, 0xCC, 0x10 => true, + else => false, + }; + if (is_stepover_op) { + const res = z80dasm.op(self.cur_op_pc, self.read_cb, self.userdata); + self.stepover_pc = res.next_pc; + self.stopped = false; + self.step_mode = .over; + } else { + self.stepInto(); + } + } + + fn hasBreakpointAt(self: *const Self, addr: u16) bool { + for (self.breakpoints[0..self.num_breakpoints]) |bp| { + if (bp.addr == addr) return true; + } + return false; + } + + fn isBreakpointEnabled(self: *const Self, addr: u16) bool { + for (self.breakpoints[0..self.num_breakpoints]) |bp| { + if (bp.addr == addr and bp.enabled) return true; + } + return false; + } + + pub fn addBreakpoint(self: *Self, addr: u16) void { + if (self.num_breakpoints >= MAX_BREAKPOINTS) return; + for (self.breakpoints[0..self.num_breakpoints]) |*bp| { + if (bp.addr == addr) { + bp.enabled = true; + return; + } + } + self.breakpoints[self.num_breakpoints] = .{ .addr = addr, .enabled = true }; + self.num_breakpoints += 1; + } + + pub fn removeBreakpoint(self: *Self, addr: u16) void { + for (self.breakpoints[0..self.num_breakpoints], 0..) |bp, i| { + if (bp.addr == addr) { + var j = i; + while (j + 1 < self.num_breakpoints) : (j += 1) { + self.breakpoints[j] = self.breakpoints[j + 1]; + } + self.num_breakpoints -= 1; + return; + } + } + } + + pub fn toggleBreakpoint(self: *Self, addr: u16) void { + if (self.hasBreakpointAt(addr)) { + self.removeBreakpoint(addr); + } else { + self.addBreakpoint(addr); + } + } + + fn rebuildDasm(self: *Self) void { + const look_back: u16 = 5 * 4; + const start = self.cur_op_pc -% look_back; + var pc: u16 = start; + var n: u8 = 0; + while (n < NUM_DBG_LINES) { + const op_addr = pc; + const res = z80dasm.op(pc, self.read_cb, self.userdata); + var line = DasmLine{ + .addr = op_addr, + .num_bytes = res.num_bytes, + .bytes = undefined, + .mnemonic = res.mnemonic, + .mnemonic_len = res.mnemonic_len, + }; + for (0..res.num_bytes) |bi| { + line.bytes[bi] = self.read_cb(op_addr +% @as(u16, @intCast(bi)), self.userdata); + } + self.dasm_lines[n] = line; + n += 1; + pc = res.next_pc; + } + self.dasm_num_lines = n; + } + + fn drawButtons(self: *Self) void { + const stopped = self.stopped; + if (stopped) { + if (ig.igButton("Continue [F5]")) self.continueExec(); + } else { + if (ig.igButton("Break [F5]")) self.breakExec(); + } + ig.igSameLine(); + if (!stopped) ig.igBeginDisabled(true); + if (ig.igButton("Step Over [F6]")) self.stepOver(); + ig.igSameLine(); + if (ig.igButton("Step Into [F7]")) self.stepInto(); + if (!stopped) ig.igEndDisabled(); + } + + fn drawDisasm(self: *Self) void { + self.rebuildDasm(); + + const glyph_width = ig.igCalcTextSize("F").x; + const cell_width = 3.0 * glyph_width; + + ig.igPushStyleVarImVec2(ig.ImGuiStyleVar_FramePadding, .{ .x = 0, .y = 0 }); + ig.igPushStyleVarImVec2(ig.ImGuiStyleVar_ItemSpacing, .{ .x = 0, .y = 0 }); + + const avail = ig.igGetContentRegionAvail(); + _ = ig.igBeginChild("##dbg_dasm", .{ .x = avail.x, .y = avail.y - 40 }, ig.ImGuiChildFlags_None, ig.ImGuiWindowFlags_None); + + for (self.dasm_lines[0..self.dasm_num_lines]) |line| { + const is_cur = line.addr == self.cur_op_pc; + const has_bp = self.isBreakpointEnabled(line.addr); + + if (is_cur) { + ig.igPushStyleColor(ig.ImGuiCol_Text, 0xFF00FFFF); + } else if (has_bp) { + ig.igPushStyleColor(ig.ImGuiCol_Text, 0xFF4040FF); + } + + ig.igPushIDInt(@as(c_int, @intCast(line.addr))); + + // Clickable indicator + address column + const indicator: []const u8 = if (has_bp) (if (is_cur) ">*" else " *") else (if (is_cur) "> " else " "); + var row_buf: [16]u8 = undefined; + const row_s = std.fmt.bufPrint(&row_buf, "{s}{X:0>4}: \x00", .{ indicator, line.addr }) catch unreachable; + _ = row_s; + if (ig.igSelectable(&row_buf)) { + self.toggleBreakpoint(line.addr); + } + ig.igSameLine(); + + // Bytes + const line_start_x = ig.igGetCursorPosX(); + for (0..line.num_bytes) |bi| { + ig.igSameLineEx(line_start_x + cell_width * @as(f32, @floatFromInt(bi)), 0); + var byte_buf: [4]u8 = undefined; + const bs = std.fmt.bufPrint(&byte_buf, "{X:0>2} \x00", .{line.bytes[bi]}) catch unreachable; + _ = bs; + ig.igTextUnformatted(&byte_buf); + } + + // Mnemonic + ig.igSameLineEx(line_start_x + cell_width * 4 + glyph_width, 0); + var mn_buf: [z80dasm.MAX_MNEMONIC_LEN + 1]u8 = undefined; + @memcpy(mn_buf[0..line.mnemonic_len], line.mnemonic[0..line.mnemonic_len]); + mn_buf[line.mnemonic_len] = 0; + ig.igTextUnformatted(&mn_buf); + + ig.igPopID(); + + if (is_cur or has_bp) ig.igPopStyleColor(); + + if (is_cur) ig.igSetScrollHereY(0.3); + } + + ig.igEndChild(); + ig.igPopStyleVarEx(2); + } + + fn drawHistory(self: *Self) void { + if (!ig.igBegin("Execution History", &self.show_history, ig.ImGuiWindowFlags_None)) { + ig.igEnd(); + return; + } + if (ig.igBeginListBox("##history", .{ .x = -1, .y = -1 })) { + const show: u8 = @min(64, self.history_pos); + var i: usize = @as(usize, self.history_pos); + var cnt: u8 = show; + while (cnt > 0) { + cnt -= 1; + if (i == 0) i = NUM_HISTORY; + i -= 1; + var buf: [8]u8 = undefined; + const s = std.fmt.bufPrint(&buf, "{X:0>4}\x00", .{self.history[i]}) catch unreachable; + _ = s; + ig.igTextUnformatted(&buf); + } + ig.igEndListBox(); + } + ig.igEnd(); + } + + fn handleKeys(self: *Self) void { + if (ig.igIsKeyPressedEx(ig.ImGuiKey_F5, false)) { + if (self.stopped) self.continueExec() else self.breakExec(); + } + if (self.stopped) { + if (ig.igIsKeyPressedEx(ig.ImGuiKey_F6, false)) self.stepOver(); + if (ig.igIsKeyPressedEx(ig.ImGuiKey_F7, false)) self.stepInto(); + if (ig.igIsKeyPressedEx(ig.ImGuiKey_F9, false)) self.toggleBreakpoint(self.cur_op_pc); + } + } + + pub fn draw(self: *Self, bus: Bus) void { + _ = bus; + if (self.open != self.last_open) self.last_open = self.open; + if (!self.open) return; + + self.handleKeys(); + if (self.show_history) self.drawHistory(); + + ig.igSetNextWindowPos(self.origin, ig.ImGuiCond_FirstUseEver); + ig.igSetNextWindowSize(self.size, ig.ImGuiCond_FirstUseEver); + if (ig.igBegin(self.title.ptr, &self.open, ig.ImGuiWindowFlags_None)) { + self.drawButtons(); + ig.igSeparator(); + self.drawDisasm(); + ig.igSeparator(); + var status_buf: [64]u8 = undefined; + const ss = std.fmt.bufPrint(&status_buf, "PC:{X:0>4} {s} BPs:{}\x00", .{ + self.cur_op_pc, + if (self.stopped) @as([]const u8, "STOPPED") else @as([]const u8, "RUNNING"), + self.num_breakpoints, + }) catch unreachable; + _ = ss; + ig.igTextUnformatted(&status_buf); + ig.igSameLine(); + if (ig.igButton("History")) self.show_history = !self.show_history; + ig.igSameLine(); + if (ig.igButton("Clear BPs")) self.num_breakpoints = 0; + } + ig.igEnd(); + } + + pub fn saveSettings(self: *Self, settings: *ui_settings.Settings) void { + _ = settings.add(self.title, self.open); + } + + pub fn loadSettings(self: *Self, settings: *const ui_settings.Settings) void { + self.open = settings.isOpen(self.title); + } + }; +} From b2ad767f02368621a1f30a72689bf24255720e7c Mon Sep 17 00:00:00 2001 From: Gunter Hager Date: Wed, 18 Mar 2026 18:43:14 +0100 Subject: [PATCH 20/20] Update .gitignore --- .gitignore | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.gitignore b/.gitignore index cf77ea9..6b18d97 100644 --- a/.gitignore +++ b/.gitignore @@ -2,4 +2,4 @@ zig-*/ .zig-*/ .vscode/ .cursor -.claude/settings.local.json +/.claude