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. diff --git a/.gitignore b/.gitignore index c4e37ff..6b18d97 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,5 @@ zig-*/ .zig-*/ .vscode/ +.cursor +/.claude diff --git a/build.zig b/build.zig index e7c459f..882da12 100644 --- a/build.zig +++ b/build.zig @@ -12,9 +12,18 @@ 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").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"); // shader module const mod_shaders = try sokol.shdc.createModule(b, "shaders", mod_sokol, .{ @@ -58,10 +67,22 @@ 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 }, .{ .name = "shaders", .module = mod_shaders }, }, }); + 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", .{ @@ -73,6 +94,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 }, }, }); @@ -94,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 edf05c6..a23abc3 100644 --- a/build.zig.zon +++ b/build.zig.zon @@ -4,8 +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#4557d7526fdd977f46ca10c2bbdd63532254f8d6", + .hash = "cimgui-0.1.0-44ClkXnYlwBKnil7TfCWL9z1Zo_2YJCJREzrpdFiGvA1", }, }, .paths = .{ diff --git a/emus/build.zig b/emus/build.zig index 41a4c96..1c45c4d 100644 --- a/emus/build.zig +++ b/emus/build.zig @@ -31,6 +31,7 @@ pub const Options = struct { optimize: OptimizeMode, mod_chipz: *Build.Module, mod_sokol: *Build.Module, + mod_cimgui: *Build.Module, }; pub fn build(b: *Build, opts: Options) void { @@ -43,6 +44,7 @@ pub fn build(b: *Build, opts: Options) void { .optimize = opts.optimize, .mod_chipz = opts.mod_chipz, .mod_sokol = opts.mod_sokol, + .mod_cimgui = opts.mod_cimgui, }); } } @@ -55,6 +57,7 @@ const EmuOptions = struct { optimize: OptimizeMode, mod_chipz: *Build.Module, mod_sokol: *Build.Module, + mod_cimgui: *Build.Module, }; fn addEmulator(b: *Build, opts: EmuOptions) void { @@ -65,6 +68,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 92755bb..0e11340 100644 --- a/emus/kc85/kc85.zig +++ b/emus/kc85/kc85.zig @@ -6,6 +6,11 @@ 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 clock = @import("chipz").common.clock; + +const ig = @import("cimgui"); +const simgui = sokol.imgui; const model: kc85.Model = switch (build_options.model) { .KC852 => .KC852, @@ -19,7 +24,110 @@ 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 }, +}; +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 }, +}; +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 }, +}; +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, @@ -28,6 +136,17 @@ var file_loaded = host.time.Once.init(switch (model) { var sys: KC85 = undefined; 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; +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(.{}); @@ -54,8 +173,72 @@ 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 = 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_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; + ui_memmap.initInPlace(.{ + .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() }); + // 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| { if (slot.mod_type != .NONE) { @@ -67,12 +250,110 @@ export fn init() void { } } +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")) { + if (ig.igMenuItem("Reset (TODO)")) { + // TODO: implement reset + } + if (ig.igMenuItem("Cold Boot (TODO)")) { + // TODO: implement cold boot + } + ig.igEndMenu(); + } + if (ig.igBeginMenu("Hardware")) { + if (ig.igMenuItem("Memory Map")) { + ui_memmap.open = true; + } + if (ig.igMenuItem("System State (TODO)")) { + // TODO: open window + } + if (ig.igMenuItem("Audio Output (TODO)")) { + // TODO: open window + } + if (ig.igMenuItem("Display (TODO)")) { + // TODO: open window + } + ig.igSeparator(); + if (ig.igMenuItem("Z80")) { + ui_z80.open = true; + } + if (ig.igMenuItem("Z80 PIO")) { + ui_z80pio.open = true; + } + if (ig.igMenuItem("Z80 CTC")) { + ui_z80ctc.open = true; + } + ig.igEndMenu(); + } + if (ig.igBeginMenu("Debug")) { + if (ig.igMenuItem("CPU Debugger")) { + ui_dbg_win.open = true; + } + if (ig.igBeginMenu("Disassembler")) { + if (ig.igMenuItem("Window #1")) { + ui_dasm[0].open = true; + } + if (ig.igMenuItem("Window #2")) { + ui_dasm[1].open = true; + } + ig.igEndMenu(); + } + if (ig.igMenuItem("Memory Editor (TODO)")) { + // TODO: open memory editor window + } + 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); + const num_ticks = execWithDebug(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(); + ui_z80.draw(sys.bus); + 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(), .status = .{ @@ -92,6 +373,7 @@ export fn frame() void { } export fn cleanup() void { + simgui.shutdown(); host.gfx.shutdown(); host.prof.shutdown(); host.audio.shutdown(); @@ -101,6 +383,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/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/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/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/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 7765787..9e9c76b 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..c7133c0 --- /dev/null +++ b/src/ui/ui.zig @@ -0,0 +1,10 @@ +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"); +pub const ui_dasm = @import("ui_dasm.zig"); +pub const ui_dbg = @import("ui_dbg.zig"); diff --git a/src/ui/ui_chip.zig b/src/ui/ui_chip.zig new file mode 100644 index 0000000..3a9424e --- /dev/null +++ b/src/ui/ui_chip.zig @@ -0,0 +1,194 @@ +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 { + return struct { + const Self = @This(); + const Bus = cfg.bus; + + pub const Pin = struct { + name: []const u8, + slot: usize, + mask: Bus, + }; + + 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: []const Pin, + + 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: 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: 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: 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; + 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]; + 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_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_f - l_f) * slot_dist; + } else if (pin.slot < (l + r + t)) { + // top side + 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_f - (l_f + r_f + t_f)) * 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.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 }; + 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 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 }); + + ig.ImDrawList_AddRect(dl, p0, p1, line_color); + const chip_ts = ig.igCalcTextSize(self.name.ptr); + if (self.is_name_outside) { + 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 { + 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{}; + for (0..self.pins.len) |index| { + const pin = self.pins[index]; + const pin_pos = self.pinPos(index, c); + p.x = pin_pos.x - pw / 2; + p.y = pin_pos.y - ph / 2; + const ts = ig.igCalcTextSize(pin.name.ptr); + 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 }; + 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); + } + } + + /// 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_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); + } + }; +} diff --git a/src/ui/ui_intel8255.zig b/src/ui/ui_intel8255.zig new file mode 100644 index 0000000..42b73e9 --- /dev/null +++ b/src/ui/ui_intel8255.zig @@ -0,0 +1,143 @@ +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, +}; + +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, + .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, + .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(); + } + 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_util.zig b/src/ui/ui_util.zig new file mode 100644 index 0000000..a042ad9 --- /dev/null +++ b/src/ui/ui_util.zig @@ -0,0 +1,7 @@ +//! UI helper functions +const ig = @import("cimgui"); + +pub fn color(imgui_color: usize) ig.ImU32 { + const style = ig.igGetStyle(); + return ig.igGetColorU32Ex(@intCast(imgui_color), style.*.Alpha); +} diff --git a/src/ui/ui_z80.zig b/src/ui/ui_z80.zig new file mode 100644 index 0000000..cb4a932 --- /dev/null +++ b/src/ui/ui_z80.zig @@ -0,0 +1,121 @@ +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, +}; + +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, 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.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("##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(in_bus); + } + ig.igEndChild(); + } + 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 new file mode 100644 index 0000000..1c2e981 --- /dev/null +++ b/src/ui/ui_z80ctc.zig @@ -0,0 +1,209 @@ +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, + 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(); + } + + 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 new file mode 100644 index 0000000..e9456f8 --- /dev/null +++ b/src/ui/ui_z80pio.zig @@ -0,0 +1,179 @@ +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, +}; + +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(); + } + + 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); + } + }; +}