From 4e13a5e0d6369549d473bcd8b3635e2ddfc7045d Mon Sep 17 00:00:00 2001 From: mattgodbolt-molty Date: Sun, 1 Mar 2026 14:13:09 -0600 Subject: [PATCH 1/2] Wrap z80_ops.js in factory closure (Phase 2) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - makeZ80Runner(z80) factory closes over bus shims and timing - Removes z80_ops.js imports of readbyte/writebyte/z80 singleton - tstates and eventNextEvent moved to Z80 instance - Zero changes to z80_generator.mjs or generated opcode code 🤖 Generated by LLM (Claude, via OpenClaw) --- src/debug.js | 14 ++- src/miracle.js | 15 ++- src/z80/z80.js | 145 +++++++++++++++-------------- src/z80/z80_ops.js | 190 +++++++++++++++++++------------------- tests/disassemble.test.js | 25 +++-- tests/fuse.test.js | 40 ++++---- 6 files changed, 226 insertions(+), 203 deletions(-) diff --git a/src/debug.js b/src/debug.js index 8c0f19e..b42622d 100644 --- a/src/debug.js +++ b/src/debug.js @@ -1,11 +1,15 @@ import { hexbyte, hexword } from "./utils"; import { bus, readbyte, virtualAddress } from "./bus"; -import { clearBreakpoint, audio_enable, cycleCallback, start } from "./miracle"; +import { + clearBreakpoint, + audio_enable, + cycleCallback, + start, + z80_do_opcodes, +} from "./miracle"; import { z80 } from "./z80/z80.js"; -import { z80_do_opcodes } from "./z80/z80_ops"; import { disassemble } from "./z80/z80_dis"; import { vdp } from "./vdp"; -import { setEventNextEvent, setTstates } from "./z80/z80_ops"; let debugSerial = 0; let annotations = null; @@ -189,8 +193,8 @@ export function stepUntil(f) { audio_enable(true); clearBreakpoint(); for (let i = 0; i < 65536; i++) { - setTstates(0); - setEventNextEvent(1); + z80.tstates = 0; + z80.eventNextEvent = 1; z80_do_opcodes(cycleCallback); if (f()) break; } diff --git a/src/miracle.js b/src/miracle.js index 3e03182..af30624 100644 --- a/src/miracle.js +++ b/src/miracle.js @@ -1,12 +1,9 @@ import { vdp } from "./vdp"; import { SoundChip } from "./soundchip"; import { z80, z80_reset, z80_set_irq, z80_nmi } from "./z80/z80.js"; -import { - tstates, - setEventNextEvent, - setTstates, - z80_do_opcodes, -} from "./z80/z80_ops"; +import { makeZ80Runner } from "./z80/z80_ops"; + +export const { z80_do_opcodes } = makeZ80Runner(z80); import { showDebug, debugKeyPress } from "./debug"; import { bus } from "./bus"; @@ -36,8 +33,8 @@ export function cycleCallback(tstates) { } function line() { - setEventNextEvent(tstatesPerHblank); - setTstates(tstates - tstatesPerHblank); + z80.eventNextEvent = tstatesPerHblank; + z80.tstates -= tstatesPerHblank; z80_do_opcodes(cycleCallback); const vdp_status = vdp.hblank(); z80_set_irq(!!(vdp_status & 3)); @@ -343,7 +340,7 @@ export function paintScreen() { } export function breakpoint() { - setEventNextEvent(0); + z80.eventNextEvent = 0; breakpointHit = true; audio_enable(false); } diff --git a/src/z80/z80.js b/src/z80/z80.js index 2a7f4b2..1f84c18 100644 --- a/src/z80/z80.js +++ b/src/z80/z80.js @@ -1,7 +1,6 @@ // Z80 state, flag tables, lifecycle functions, and micro-op methods. -import { readbyte, writebyte, readport, writeport } from "../bus"; -import { addTstates } from "./z80_ops.js"; +import { bus } from "../bus"; import { FLAG_C, FLAG_N, @@ -108,6 +107,11 @@ class Z80 { this.halted = false; this.irq_pending = false; this.irq_suppress = false; + // Timing state (used by z80_ops.js factory closure) + this.tstates = 0; + this.eventNextEvent = 0; + // Bus reference (set after construction) + this.bus = null; } // ------------------------------------------------------------------------- @@ -519,17 +523,17 @@ class Z80 { // ------------------------------------------------------------------------- rld() { - const mem = readbyte(this.hl()); - addTstates(10); - writebyte(this.hl(), ((mem & 0x0f) << 4) | (this.a & 0x0f)); + const mem = this.bus.readbyte(this.hl()); + this.tstates += 10; + this.bus.writebyte(this.hl(), ((mem & 0x0f) << 4) | (this.a & 0x0f)); this.a = (this.a & 0xf0) | (mem >> 4); this.f = (this.f & FLAG_C) | sz53p_table[this.a]; } rrd() { - const mem = readbyte(this.hl()); - addTstates(10); - writebyte(this.hl(), ((this.a & 0x0f) << 4) | (mem >> 4)); + const mem = this.bus.readbyte(this.hl()); + this.tstates += 10; + this.bus.writebyte(this.hl(), ((this.a & 0x0f) << 4) | (mem >> 4)); this.a = (this.a & 0xf0) | (mem & 0x0f); this.f = (this.f & FLAG_C) | sz53p_table[this.a]; } @@ -583,11 +587,11 @@ class Z80 { exSPHL() { const sp0 = this.sp, sp1 = (this.sp + 1) & 0xffff; - const lo = readbyte(sp0), - hi = readbyte(sp1); - addTstates(15); - writebyte(sp1, this.h); - writebyte(sp0, this.l); + const lo = this.bus.readbyte(sp0), + hi = this.bus.readbyte(sp1); + this.tstates += 15; + this.bus.writebyte(sp1, this.h); + this.bus.writebyte(sp0, this.l); this.l = lo; this.h = hi; } @@ -595,11 +599,11 @@ class Z80 { exSPIX() { const sp0 = this.sp, sp1 = (this.sp + 1) & 0xffff; - const lo = readbyte(sp0), - hi = readbyte(sp1); - addTstates(15); - writebyte(sp1, this.ixh); - writebyte(sp0, this.ixl); + const lo = this.bus.readbyte(sp0), + hi = this.bus.readbyte(sp1); + this.tstates += 15; + this.bus.writebyte(sp1, this.ixh); + this.bus.writebyte(sp0, this.ixl); this.ixl = lo; this.ixh = hi; } @@ -607,11 +611,11 @@ class Z80 { exSPIY() { const sp0 = this.sp, sp1 = (this.sp + 1) & 0xffff; - const lo = readbyte(sp0), - hi = readbyte(sp1); - addTstates(15); - writebyte(sp1, this.iyh); - writebyte(sp0, this.iyl); + const lo = this.bus.readbyte(sp0), + hi = this.bus.readbyte(sp1); + this.tstates += 15; + this.bus.writebyte(sp1, this.iyh); + this.bus.writebyte(sp0, this.iyl); this.iyl = lo; this.iyh = hi; } @@ -636,10 +640,10 @@ class Z80 { // Reads the displacement byte unconditionally (matches real Z80 fetch behaviour); // PC always advances past it. When taken, the signed displacement is applied first. jr(taken) { - const disp = readbyte(this.pc); - addTstates(3); + const disp = this.bus.readbyte(this.pc); + this.tstates += 3; if (taken) { - addTstates(5); + this.tstates += 5; this.pc = (this.pc + sign_extend(disp) + 1) & 0xffff; } else { this.pc = (this.pc + 1) & 0xffff; @@ -649,11 +653,11 @@ class Z80 { // DJNZ — decrement B; if non-zero, take relative branch. // Timing differs from JR: 8 t-states not-taken, 13 taken (no 3-cycle "JR fetch" overhead). djnz() { - addTstates(4); + this.tstates += 4; this.b = (this.b - 1) & 0xff; if (this.b) { - addTstates(5); - this.pc += sign_extend(readbyte(this.pc)); + this.tstates += 5; + this.pc += sign_extend(this.bus.readbyte(this.pc)); this.pc &= 0xffff; } this.pc = (this.pc + 1) & 0xffff; @@ -666,15 +670,15 @@ class Z80 { // IN A,(nn) — port address is nn + (A << 8) inAN() { const port = this.fetchByte() + (this.a << 8); - addTstates(7); - this.a = readport(port); + this.tstates += 7; + this.a = this.bus.readport(port); } // IN r,(C) — reads from BC port, updates flags, returns value for assignment. // Caller assigns: z80.b = z80.inC() (or discards for IN F,(C)) inC() { - addTstates(4); - const v = readport(this.bc()); + this.tstates += 4; + const v = this.bus.readport(this.bc()); this.f = (this.f & FLAG_C) | sz53p_table[v]; return v; } @@ -682,14 +686,14 @@ class Z80 { // OUT (nn),A — port address is nn + (A << 8) outAN() { const port = this.fetchByte() + (this.a << 8); - addTstates(7); - writeport(port, this.a); + this.tstates += 7; + this.bus.writeport(port, this.a); } // OUT (C),value — write value to BC port outC(value) { - addTstates(4); - writeport(this.bc(), value); + this.tstates += 4; + this.bus.writeport(this.bc(), value); } // ------------------------------------------------------------------------- @@ -697,7 +701,7 @@ class Z80 { // ------------------------------------------------------------------------- fetchByte() { - const b = readbyte(this.pc++); + const b = this.bus.readbyte(this.pc++); this.pc &= 0xffff; return b; } @@ -708,15 +712,15 @@ class Z80 { push16(val) { this.sp = (this.sp - 1) & 0xffff; - writebyte(this.sp, val >> 8); + this.bus.writebyte(this.sp, val >> 8); this.sp = (this.sp - 1) & 0xffff; - writebyte(this.sp, val & 0xff); + this.bus.writebyte(this.sp, val & 0xff); } pop16() { - const lo = readbyte(this.sp++); + const lo = this.bus.readbyte(this.sp++); this.sp &= 0xffff; - const hi = readbyte(this.sp++); + const hi = this.bus.readbyte(this.sp++); this.sp &= 0xffff; return lo | (hi << 8); } @@ -727,9 +731,9 @@ class Z80 { // Shared core for LDI/LDD: copy one byte from (HL) to (DE), step both and decrement BC. _ldx(dir) { - let byte = readbyte(this.hl()); - addTstates(8); - writebyte(this.de(), byte); + let byte = this.bus.readbyte(this.hl()); + this.tstates += 8; + this.bus.writebyte(this.de(), byte); this.setHL(this.hl() + dir); this.setDE(this.de() + dir); this.setBC(this.bc() - 1); @@ -750,25 +754,25 @@ class Z80 { ldir() { this._ldx(1); if (this.bc()) { - addTstates(5); + this.tstates += 5; this.pc = (this.pc - 2) & 0xffff; } } lddr() { this._ldx(-1); if (this.bc()) { - addTstates(5); + this.tstates += 5; this.pc = (this.pc - 2) & 0xffff; } } // Shared core for CPI/CPD: compare A with (HL), step HL, decrement BC. _cpx(dir) { - const mem = readbyte(this.hl()); + const mem = this.bus.readbyte(this.hl()); let diff = (this.a - mem) & 0xff; const lookup = ((this.a & 0x08) >> 3) | ((mem & 0x08) >> 2) | ((diff & 0x08) >> 1); - addTstates(8); + this.tstates += 8; this.setHL(this.hl() + dir); this.setBC(this.bc() - 1); this.f = @@ -790,23 +794,23 @@ class Z80 { cpir() { this._cpx(1); if ((this.f & (FLAG_V | FLAG_Z)) === FLAG_V) { - addTstates(5); + this.tstates += 5; this.pc = (this.pc - 2) & 0xffff; } } cpdr() { this._cpx(-1); if ((this.f & (FLAG_V | FLAG_Z)) === FLAG_V) { - addTstates(5); + this.tstates += 5; this.pc = (this.pc - 2) & 0xffff; } } // Shared core for INI/IND: read one byte from port BC into (HL), step HL, decrement B. _inx(dir) { - const byte = readport(this.bc()); - addTstates(8); - writebyte(this.hl(), byte); + const byte = this.bus.readport(this.bc()); + this.tstates += 8; + this.bus.writebyte(this.hl(), byte); this.b = (this.b - 1) & 0xff; this.setHL(this.hl() + dir); this.f = (byte & 0x80 ? FLAG_N : 0) | sz53_table[this.b]; @@ -822,25 +826,25 @@ class Z80 { inir() { this._inx(1); if (this.b) { - addTstates(5); + this.tstates += 5; this.pc = (this.pc - 2) & 0xffff; } } indr() { this._inx(-1); if (this.b) { - addTstates(5); + this.tstates += 5; this.pc = (this.pc - 2) & 0xffff; } } // Shared core for OUTI/OUTD: read (HL), decrement B (happens first!), step HL, write to port. _outx(dir) { - const byte = readbyte(this.hl()); + const byte = this.bus.readbyte(this.hl()); this.b = (this.b - 1) & 0xff; /* B decremented before the write, per spec */ - addTstates(8); + this.tstates += 8; this.setHL(this.hl() + dir); - writeport(this.bc(), byte); + this.bus.writeport(this.bc(), byte); this.f = (byte & 0x80 ? FLAG_N : 0) | sz53_table[this.b]; /* C,H and P/V flags not implemented */ } @@ -854,18 +858,18 @@ class Z80 { // OTIR/OTDR have different conditional timing from OUTI/OUTD _otxr(dir) { - const byte = readbyte(this.hl()); - addTstates(5); + const byte = this.bus.readbyte(this.hl()); + this.tstates += 5; this.b = (this.b - 1) & 0xff; this.setHL(this.hl() + dir); - writeport(this.bc(), byte); + this.bus.writeport(this.bc(), byte); this.f = (byte & 0x80 ? FLAG_N : 0) | sz53_table[this.b]; /* C,H and P/V flags not implemented */ if (this.b) { - addTstates(8); + this.tstates += 8; this.pc = (this.pc - 2) & 0xffff; } else { - addTstates(3); + this.tstates += 3; } } @@ -878,6 +882,7 @@ class Z80 { } export const z80 = new Z80(); +z80.bus = bus; // --------------------------------------------------------------------------- // Lifecycle functions (standalone exports; external API unchanged) @@ -932,16 +937,18 @@ export function z80_interrupt() { switch (z80.im) { case 0: z80.pc = 0x0038; - addTstates(12); + z80.tstates += 12; break; case 1: z80.pc = 0x0038; - addTstates(13); + z80.tstates += 13; break; case 2: { const inttemp = 0x100 * z80.i + 0xff; - z80.pc = readbyte(inttemp) | (readbyte((inttemp + 1) & 0xffff) << 8); - addTstates(19); + z80.pc = + z80.bus.readbyte(inttemp) | + (z80.bus.readbyte((inttemp + 1) & 0xffff) << 8); + z80.tstates += 19; break; } } @@ -953,6 +960,6 @@ export function z80_instruction_hook() {} export function z80_nmi() { z80.iff1 = 0; z80.push16(z80.pc); - addTstates(11); + z80.tstates += 11; z80.pc = 0x0066; } diff --git a/src/z80/z80_ops.js b/src/z80/z80_ops.js index 20a34fa..1a9ecfc 100644 --- a/src/z80/z80_ops.js +++ b/src/z80/z80_ops.js @@ -2,7 +2,6 @@ // referenced from the opcode code inlined by the @z80-generate transform. /* eslint-disable no-unused-vars */ import { - z80, z80_instruction_hook, z80_set_irq, z80_interrupt, @@ -14,7 +13,6 @@ import { parity_table, sz53p_table, } from "./z80.js"; -import { readbyte, readport, writebyte, writeport } from "../bus"; import { FLAG_C, FLAG_N, @@ -27,115 +25,117 @@ import { FLAG_S, } from "./flags.js"; -export let tstates = 0; -let event_next_event = 0; -export function setTstates(ts) { - tstates = ts; -} -export function addTstates(ts) { - tstates += ts; -} -export function setEventNextEvent(e) { - event_next_event = e; -} - export function sign_extend(v) { return v < 128 ? v : v - 256; } -function z80_defaults(ops) { - for (let i = 0; i < 256; ++i) { - if (!ops[i]) ops[i] = ops[256]; +export function makeZ80Runner(z80) { + const bus = z80.bus; + const readbyte = (a) => bus.readbyte(a); + const writebyte = (a, v) => bus.writebyte(a, v); + const readport = (a) => bus.readport(a); + const writeport = (a, v) => bus.writeport(a, v); + + function addTstates(ts) { + z80.tstates += ts; } -} -const z80BaseOps = (() => { - const ops = []; - /* @z80-generate opcodes_base.dat */ - z80_defaults(ops); - return ops; -})(); + function z80_defaults(ops) { + for (let i = 0; i < 256; ++i) { + if (!ops[i]) ops[i] = ops[256]; + } + } -const z80EdOps = (() => { - const ops = []; - /* @z80-generate opcodes_ed.dat */ - z80_defaults(ops); - return ops; -})(); -function z80_edxx(opcode) { - z80EdOps[opcode](); -} + const z80BaseOps = (() => { + const ops = []; + /* @z80-generate opcodes_base.dat */ + z80_defaults(ops); + return ops; + })(); -const z80CbOps = (() => { - const ops = []; - /* @z80-generate opcodes_cb.dat */ - z80_defaults(ops); - return ops; -})(); -function z80_cbxx(opcode) { - z80CbOps[opcode](); -} + const z80EdOps = (() => { + const ops = []; + /* @z80-generate opcodes_ed.dat */ + z80_defaults(ops); + return ops; + })(); + function z80_edxx(opcode) { + z80EdOps[opcode](); + } -const z80DdOps = (() => { - const ops = []; - /* @z80-generate opcodes_ddfd.dat ix */ - z80_defaults(ops); - return ops; -})(); -function z80_ddxx(opcode) { - // If this opcode has no DD-specific override, fall through to the base opcode. - if (z80DdOps[opcode] !== z80DdOps[256]) { - z80DdOps[opcode](); - } else { - z80BaseOps[opcode](); + const z80CbOps = (() => { + const ops = []; + /* @z80-generate opcodes_cb.dat */ + z80_defaults(ops); + return ops; + })(); + function z80_cbxx(opcode) { + z80CbOps[opcode](); } -} -const z80FdOps = (() => { - const ops = []; - /* @z80-generate opcodes_ddfd.dat iy */ - z80_defaults(ops); - return ops; -})(); -function z80_fdxx(opcode) { - // Same fallthrough logic as z80_ddxx. - if (z80FdOps[opcode] !== z80FdOps[256]) { - z80FdOps[opcode](); - } else { - z80BaseOps[opcode](); + const z80DdOps = (() => { + const ops = []; + /* @z80-generate opcodes_ddfd.dat ix */ + z80_defaults(ops); + return ops; + })(); + function z80_ddxx(opcode) { + // If this opcode has no DD-specific override, fall through to the base opcode. + if (z80DdOps[opcode] !== z80DdOps[256]) { + z80DdOps[opcode](); + } else { + z80BaseOps[opcode](); + } } -} -const z80DdfdcbOps = (() => { - const ops = []; - /* @z80-generate opcodes_ddfdcb.dat */ - z80_defaults(ops); - return ops; -})(); -function z80_ddfdcbxx(opcode, tempaddr) { - z80DdfdcbOps[opcode](tempaddr); -} + const z80FdOps = (() => { + const ops = []; + /* @z80-generate opcodes_ddfd.dat iy */ + z80_defaults(ops); + return ops; + })(); + function z80_fdxx(opcode) { + // Same fallthrough logic as z80_ddxx. + if (z80FdOps[opcode] !== z80FdOps[256]) { + z80FdOps[opcode](); + } else { + z80BaseOps[opcode](); + } + } -export function z80_do_opcodes(cycleCallback) { - while (tstates < event_next_event) { - if (z80.irq_pending && z80.iff1) { - if (z80.irq_suppress) { - // Prevent triggering an IRQ on the instruction immediately after EI - z80.irq_suppress = false; - } else { - z80.irq_suppress = true; - z80_interrupt(); + const z80DdfdcbOps = (() => { + const ops = []; + /* @z80-generate opcodes_ddfdcb.dat */ + z80_defaults(ops); + return ops; + })(); + function z80_ddfdcbxx(opcode, tempaddr) { + z80DdfdcbOps[opcode](tempaddr); + } + + function z80_do_opcodes(cycleCallback) { + while (z80.tstates < z80.eventNextEvent) { + if (z80.irq_pending && z80.iff1) { + if (z80.irq_suppress) { + // Prevent triggering an IRQ on the instruction immediately after EI + z80.irq_suppress = false; + } else { + z80.irq_suppress = true; + z80_interrupt(); + } } - } - const oldTstates = tstates; - addTstates(4); - z80.r = (z80.r + 1) & 0x7f; - const opcode = readbyte(z80.pc); - z80_instruction_hook(z80.pc, opcode); - z80.pc = (z80.pc + 1) & 0xffff; + const oldTstates = z80.tstates; + addTstates(4); + z80.r = (z80.r + 1) & 0x7f; + const opcode = readbyte(z80.pc); + z80_instruction_hook(z80.pc, opcode); + z80.pc = (z80.pc + 1) & 0xffff; - z80BaseOps[opcode](); - cycleCallback(tstates - oldTstates); + z80BaseOps[opcode](); + cycleCallback(z80.tstates - oldTstates); + } } + + return { z80_do_opcodes }; } diff --git a/tests/disassemble.test.js b/tests/disassemble.test.js index b6a8392..0f73b65 100644 --- a/tests/disassemble.test.js +++ b/tests/disassemble.test.js @@ -13,14 +13,23 @@ import { vi, describe, it, expect, beforeEach } from "vitest"; // --------------------------------------------------------------------------- const mem = new Uint8Array(0x10000); -vi.mock("../src/bus", () => ({ - readbyte: (addr) => mem[addr & 0xffff], - writebyte: (addr, val) => { - mem[addr & 0xffff] = val & 0xff; - }, - writeport: () => {}, - readport: () => 0xff, -})); +vi.mock("../src/bus", () => { + const busObj = { + readbyte: (addr) => mem[addr & 0xffff], + writebyte: (addr, val) => { + mem[addr & 0xffff] = val & 0xff; + }, + writeport: () => {}, + readport: () => 0xff, + }; + return { + bus: busObj, + readbyte: busObj.readbyte, + writebyte: busObj.writebyte, + writeport: busObj.writeport, + readport: busObj.readport, + }; +}); vi.mock("../src/utils", () => ({ hexbyte: (v) => v.toString(16).padStart(2, "0"), diff --git a/tests/fuse.test.js b/tests/fuse.test.js index 74f1647..04f51d3 100644 --- a/tests/fuse.test.js +++ b/tests/fuse.test.js @@ -62,23 +62,29 @@ const __dirname = path.dirname(fileURLToPath(import.meta.url)); // --------------------------------------------------------------------------- const mem = new Uint8Array(0x10000); -vi.mock("../src/bus", () => ({ - readbyte: (addr) => mem[addr & 0xffff], - writebyte: (addr, val) => { - mem[addr & 0xffff] = val & 0xff; - }, - readport: (port) => (port >> 8) & 0xff, // FUSE convention: port returns high byte of address - writeport: () => {}, -})); +vi.mock("../src/bus", () => { + const busObj = { + readbyte: (addr) => mem[addr & 0xffff], + writebyte: (addr, val) => { + mem[addr & 0xffff] = val & 0xff; + }, + readport: (port) => (port >> 8) & 0xff, // FUSE convention: port returns high byte of address + writeport: () => {}, + }; + return { + bus: busObj, + readbyte: busObj.readbyte, + writebyte: busObj.writebyte, + readport: busObj.readport, + writeport: busObj.writeport, + }; +}); // These imports must come *after* vi.mock (vitest hoists vi.mock automatically) import { z80, z80_init } from "../src/z80/z80.js"; -import { - z80_do_opcodes, - tstates, - setTstates, - setEventNextEvent, -} from "../src/z80/z80_ops.js"; +import { makeZ80Runner } from "../src/z80/z80_ops.js"; + +const { z80_do_opcodes } = makeZ80Runner(z80); // --------------------------------------------------------------------------- // FUSE test file parsers @@ -348,8 +354,8 @@ describe("FUSE Z80 tests", () => { setZ80State(test.state); // Run for the specified number of tstates - setTstates(0); - setEventNextEvent(test.state.runTstates); + z80.tstates = 0; + z80.eventNextEvent = test.state.runTstates; z80_do_opcodes(() => {}); // Check final CPU state @@ -376,7 +382,7 @@ describe("FUSE Z80 tests", () => { } // Check tstates - expect(tstates, "tstates").toBe(exp.tstates); + expect(z80.tstates, "tstates").toBe(exp.tstates); // Check memory writes for (const [addr, val] of test.expected.memChanges) { From ea6828706787221bacc53e6ce8150175e5cdf104 Mon Sep 17 00:00:00 2001 From: mattgodbolt-molty Date: Sun, 1 Mar 2026 17:33:49 -0600 Subject: [PATCH 2/2] Add SMS top-level class (Phase 3) Introduce src/sms.js with an SMS class that owns Bus, Z80, VDP, and soundChip, replacing the module-level singletons. Lifecycle functions (z80_reset, z80_set_irq, z80_interrupt, z80_nmi, z80_instruction_hook) become Z80 class methods. VDP receives its setIrq callback via init() instead of importing z80_set_irq. miracle.js becomes a thin DOM/audio shell that delegates to the SMS instance. Co-Authored-By: Claude Opus 4.6 --- src/bus.js | 22 +----- src/debug.js | 12 +--- src/main.js | 8 +-- src/miracle.js | 44 +++++------- src/sms.js | 50 ++++++++++++++ src/vdp.js | 9 ++- src/z80/z80.js | 139 ++++++++++++++++++++------------------ src/z80/z80_dis.js | 2 +- src/z80/z80_ops.js | 7 +- tests/disassemble.test.js | 5 +- tests/fuse.test.js | 35 ++++------ 11 files changed, 168 insertions(+), 165 deletions(-) create mode 100644 src/sms.js diff --git a/src/bus.js b/src/bus.js index 846b9fd..de3873b 100644 --- a/src/bus.js +++ b/src/bus.js @@ -1,6 +1,6 @@ import { hexbyte, hexword } from "./utils"; -class Bus { +export class Bus { #ram = new Uint8Array(0x2000); #cartridgeRam = new Uint8Array(0x8000); #romBanks = []; @@ -248,23 +248,3 @@ class Bus { } } } - -export const bus = new Bus(); - -// Convenience re-exports so callers that import bare functions (z80.js, -// z80_ops.js, z80_dis.js, debug.js) only need to change their import path. -export function readbyte(a) { - return bus.readbyte(a); -} -export function writebyte(a, v) { - bus.writebyte(a, v); -} -export function readport(a) { - return bus.readport(a); -} -export function writeport(a, v) { - bus.writeport(a, v); -} -export function virtualAddress(a) { - return bus.virtualAddress(a); -} diff --git a/src/debug.js b/src/debug.js index b42622d..e429125 100644 --- a/src/debug.js +++ b/src/debug.js @@ -1,15 +1,7 @@ import { hexbyte, hexword } from "./utils"; -import { bus, readbyte, virtualAddress } from "./bus"; -import { - clearBreakpoint, - audio_enable, - cycleCallback, - start, - z80_do_opcodes, -} from "./miracle"; -import { z80 } from "./z80/z80.js"; +import { bus, z80, vdp, z80_do_opcodes, readbyte, virtualAddress } from "./sms"; +import { clearBreakpoint, audio_enable, cycleCallback, start } from "./miracle"; import { disassemble } from "./z80/z80_dis"; -import { vdp } from "./vdp"; let debugSerial = 0; let annotations = null; diff --git a/src/main.js b/src/main.js index 6a7506b..e6ba9da 100644 --- a/src/main.js +++ b/src/main.js @@ -1,7 +1,7 @@ import { RomList } from "./roms"; import { z80_init } from "./z80/z80.js"; import { miracle_init, miracle_reset, start, stop } from "./miracle"; -import { bus } from "./bus"; +import { sms } from "./sms"; import { step, stepOver, stepOut, debug_init } from "./debug"; function loadRomData(name) { @@ -18,7 +18,7 @@ function loadRomData(name) { function resetLoadAndStart(filename, romdata) { miracle_reset(); - bus.loadRom(filename, romdata, debug_init); + sms.loadRom(filename, romdata, debug_init); hideRomChooser(); start(); } @@ -129,10 +129,10 @@ function go() { const parsedQuery = parseQuery(); if (parsedQuery["b64sms"]) { - bus.loadRom("b64.sms", atob(parsedQuery["b64sms"]), debug_init); + sms.loadRom("b64.sms", atob(parsedQuery["b64sms"]), debug_init); } else { const defaultRom = getDefaultRom(); - bus.loadRom(defaultRom, loadRomData(defaultRom), debug_init); + sms.loadRom(defaultRom, loadRomData(defaultRom), debug_init); } start(); diff --git a/src/miracle.js b/src/miracle.js index af30624..70ca13b 100644 --- a/src/miracle.js +++ b/src/miracle.js @@ -1,11 +1,8 @@ -import { vdp } from "./vdp"; import { SoundChip } from "./soundchip"; -import { z80, z80_reset, z80_set_irq, z80_nmi } from "./z80/z80.js"; -import { makeZ80Runner } from "./z80/z80_ops"; - -export const { z80_do_opcodes } = makeZ80Runner(z80); +import { sms } from "./sms"; import { showDebug, debugKeyPress } from "./debug"; -import { bus } from "./bus"; + +export { sms }; let breakpointHit = false; let running = false; @@ -33,14 +30,14 @@ export function cycleCallback(tstates) { } function line() { - z80.eventNextEvent = tstatesPerHblank; - z80.tstates -= tstatesPerHblank; - z80_do_opcodes(cycleCallback); - const vdp_status = vdp.hblank(); - z80_set_irq(!!(vdp_status & 3)); + sms.z80.eventNextEvent = tstatesPerHblank; + sms.z80.tstates -= tstatesPerHblank; + sms.z80_do_opcodes(cycleCallback); + const vdp_status = sms.vdp.hblank(); + sms.z80.setIrq(!!(vdp_status & 3)); if (breakpointHit) { running = false; - showDebug(z80.pc); + showDebug(sms.z80.pc); } else if (vdp_status & 4) { paintScreen(); } @@ -62,7 +59,7 @@ let lastFrame = null; function run() { if (!running) { - showDebug(z80.pc); + showDebug(sms.z80.pc); return; } const now = Date.now(); @@ -212,10 +209,6 @@ export function audio_enable(enable) { if (enable && audioContext) audioContext.resume(); } -function audio_reset() { - soundChip.reset(); -} - export function miracle_init() { canvas = document.getElementById("screen"); ctx = canvas.getContext("2d"); @@ -228,9 +221,8 @@ export function miracle_init() { // Unsupported.... } - vdp.init(canvas, fb32, paintScreen, breakpoint); audio_init(); - bus.connect(vdp, soundChip); + sms.init(canvas, fb32, paintScreen, breakpoint, soundChip); miracle_reset(); // Scale the canvas to fill its container while maintaining the native aspect ratio. @@ -264,11 +256,7 @@ export function miracle_init() { } export function miracle_reset() { - bus.reset(); - //inputMode = 7; - z80_reset(); - vdp.reset(); - audio_reset(); + sms.reset(); } const keys = { @@ -298,7 +286,7 @@ function keyDown(evt) { if (!running) return; const key = keys[keyCode(evt)]; if (key) { - bus.joystick &= ~key; + sms.bus.joystick &= ~key; if (!evt.metaKey) { evt.preventDefault(); return; @@ -306,7 +294,7 @@ function keyDown(evt) { } switch (evt.keyCode) { case 80: // 'P' for pause - z80_nmi(); + sms.z80.nmi(); break; case 8: // 'Backspace' is debug breakpoint(); @@ -319,7 +307,7 @@ function keyUp(evt) { if (!running) return; const key = keys[keyCode(evt)]; if (key) { - bus.joystick |= key; + sms.bus.joystick |= key; if (!evt.metaKey) { evt.preventDefault(); } @@ -340,7 +328,7 @@ export function paintScreen() { } export function breakpoint() { - z80.eventNextEvent = 0; + sms.z80.eventNextEvent = 0; breakpointHit = true; audio_enable(false); } diff --git a/src/sms.js b/src/sms.js new file mode 100644 index 0000000..7212875 --- /dev/null +++ b/src/sms.js @@ -0,0 +1,50 @@ +import { Bus } from "./bus"; +import { Z80 } from "./z80/z80.js"; +import { VDP } from "./vdp"; +import { makeZ80Runner } from "./z80/z80_ops"; + +export class SMS { + constructor() { + this.bus = new Bus(); + this.z80 = new Z80(); + this.vdp = new VDP(); + this.soundChip = null; + this.z80.bus = this.bus; + const { z80_do_opcodes } = makeZ80Runner(this.z80); + this.z80_do_opcodes = z80_do_opcodes; + } + + init(canvas, fb32, paintScreen, breakpoint, soundChip) { + this.soundChip = soundChip; + this.vdp.init(canvas, fb32, paintScreen, breakpoint, (asserted) => + this.z80.setIrq(asserted), + ); + this.bus.connect(this.vdp, soundChip); + } + + reset() { + this.bus.reset(); + this.z80.reset(); + this.vdp.reset(); + this.soundChip?.reset(); + } + + loadRom(name, rom, onLoaded) { + this.bus.loadRom(name, rom, onLoaded); + } +} + +// Default instance +export const sms = new SMS(); + +// Convenience re-exports (alias to default instance members) +export const bus = sms.bus; +export const z80 = sms.z80; +export const vdp = sms.vdp; +export const { z80_do_opcodes } = sms; +export function readbyte(a) { + return sms.bus.readbyte(a); +} +export function virtualAddress(a) { + return sms.bus.virtualAddress(a); +} diff --git a/src/vdp.js b/src/vdp.js index 3eed22c..fd5922c 100644 --- a/src/vdp.js +++ b/src/vdp.js @@ -1,5 +1,4 @@ import { hexbyte, hexword } from "./utils"; -import { z80_set_irq } from "./z80/z80.js"; export class VDP { // Exposed for debug.js (read-only intent). @@ -9,6 +8,7 @@ export class VDP { #fb32; #paintScreen; #breakpoint; + #setIrq; #vram; #vramUntwiddled; #palette; @@ -33,11 +33,12 @@ export class VDP { // Initialisation // ------------------------------------------------------------------------- - init(canvas, fb32, paintScreen, breakpoint) { + init(canvas, fb32, paintScreen, breakpoint, setIrq) { this.#canvas = canvas; this.#fb32 = fb32; this.#paintScreen = paintScreen; this.#breakpoint = breakpoint; + this.#setIrq = setIrq; this.#vram = new Uint8Array(0x4000); this.#vramUntwiddled = new Uint8Array(0x8000); this.#palette = new Uint8Array(32); @@ -218,7 +219,7 @@ export class VDP { // Clear top three here. this.#vdp_status &= 0x1f; this.#vdp_pending_hblank = false; - z80_set_irq(false); + this.#setIrq(false); this.#vdp_addr_state = 0; return res; } @@ -601,5 +602,3 @@ export class VDP { return needIrq; } } - -export const vdp = new VDP(); diff --git a/src/z80/z80.js b/src/z80/z80.js index 1f84c18..ff82a46 100644 --- a/src/z80/z80.js +++ b/src/z80/z80.js @@ -1,6 +1,5 @@ // Z80 state, flag tables, lifecycle functions, and micro-op methods. -import { bus } from "../bus"; import { FLAG_C, FLAG_N, @@ -879,20 +878,83 @@ class Z80 { otdr() { this._otxr(-1); } + + // ------------------------------------------------------------------------- + // Lifecycle methods + // ------------------------------------------------------------------------- + + reset() { + this.a = this.f = this.b = this.c = this.d = this.e = this.h = this.l = 0; + this.a_ = + this.f_ = + this.b_ = + this.c_ = + this.d_ = + this.e_ = + this.h_ = + this.l_ = + 0; + this.ixh = this.ixl = this.iyh = this.iyl = 0; + this.i = this.r = this.r7 = 0; + this.sp = this.pc = 0; + this.iff1 = this.iff2 = this.im = 0; + this.halted = false; + this.irq_pending = false; + this.irq_suppress = true; + } + + setIrq(asserted) { + this.irq_pending = asserted; + if (this.irq_pending && this.iff1) this.interrupt(); + } + + interrupt() { + if (this.iff1) { + if (this.halted) { + this.pc = (this.pc + 1) & 0xffff; + this.halted = false; + } + this.iff1 = this.iff2 = 0; + this.push16(this.pc); + this.r = (this.r + 1) & 0x7f; + switch (this.im) { + case 0: + this.pc = 0x0038; + this.tstates += 12; + break; + case 1: + this.pc = 0x0038; + this.tstates += 13; + break; + case 2: { + const inttemp = 0x100 * this.i + 0xff; + this.pc = + this.bus.readbyte(inttemp) | + (this.bus.readbyte((inttemp + 1) & 0xffff) << 8); + this.tstates += 19; + break; + } + } + } + } + + instructionHook() {} + + nmi() { + this.iff1 = 0; + this.push16(this.pc); + this.tstates += 11; + this.pc = 0x0066; + } } -export const z80 = new Z80(); -z80.bus = bus; +export { Z80 }; // --------------------------------------------------------------------------- -// Lifecycle functions (standalone exports; external API unchanged) +// Self-initialise lookup tables at module load time // --------------------------------------------------------------------------- -export function z80_init() { - z80_init_tables(); -} - -function z80_init_tables() { +(() => { for (let i = 0; i < 0x100; i++) { sz53_table[i] = i & (FLAG_3 | FLAG_5 | FLAG_S); let j = i; @@ -906,60 +968,7 @@ function z80_init_tables() { } sz53_table[0] |= FLAG_Z; sz53p_table[0] |= FLAG_Z; -} - -export function z80_reset() { - z80.a = z80.f = z80.b = z80.c = z80.d = z80.e = z80.h = z80.l = 0; - z80.a_ = z80.f_ = z80.b_ = z80.c_ = z80.d_ = z80.e_ = z80.h_ = z80.l_ = 0; - z80.ixh = z80.ixl = z80.iyh = z80.iyl = 0; - z80.i = z80.r = z80.r7 = 0; - z80.sp = z80.pc = 0; - z80.iff1 = z80.iff2 = z80.im = 0; - z80.halted = false; - z80.irq_pending = false; - z80.irq_suppress = true; -} - -export function z80_set_irq(asserted) { - z80.irq_pending = asserted; - if (z80.irq_pending && z80.iff1) z80_interrupt(); -} +})(); -export function z80_interrupt() { - if (z80.iff1) { - if (z80.halted) { - z80.pc = (z80.pc + 1) & 0xffff; - z80.halted = false; - } - z80.iff1 = z80.iff2 = 0; - z80.push16(z80.pc); - z80.r = (z80.r + 1) & 0x7f; - switch (z80.im) { - case 0: - z80.pc = 0x0038; - z80.tstates += 12; - break; - case 1: - z80.pc = 0x0038; - z80.tstates += 13; - break; - case 2: { - const inttemp = 0x100 * z80.i + 0xff; - z80.pc = - z80.bus.readbyte(inttemp) | - (z80.bus.readbyte((inttemp + 1) & 0xffff) << 8); - z80.tstates += 19; - break; - } - } - } -} - -export function z80_instruction_hook() {} - -export function z80_nmi() { - z80.iff1 = 0; - z80.push16(z80.pc); - z80.tstates += 11; - z80.pc = 0x0066; -} +// Backward-compatible no-op (tables are now self-initialised above). +export function z80_init() {} diff --git a/src/z80/z80_dis.js b/src/z80/z80_dis.js index e507e9d..7a613e7 100644 --- a/src/z80/z80_dis.js +++ b/src/z80/z80_dis.js @@ -1,6 +1,6 @@ /* eslint-disable */ import { hexbyte } from "../utils"; -import { readbyte } from "../bus"; +import { readbyte } from "../sms"; import { addressHtml } from "../debug"; import { sign_extend } from "./z80_ops.js"; diff --git a/src/z80/z80_ops.js b/src/z80/z80_ops.js index 1a9ecfc..fc6e07f 100644 --- a/src/z80/z80_ops.js +++ b/src/z80/z80_ops.js @@ -2,9 +2,6 @@ // referenced from the opcode code inlined by the @z80-generate transform. /* eslint-disable no-unused-vars */ import { - z80_instruction_hook, - z80_set_irq, - z80_interrupt, halfcarry_add_table, halfcarry_sub_table, overflow_add_table, @@ -121,7 +118,7 @@ export function makeZ80Runner(z80) { z80.irq_suppress = false; } else { z80.irq_suppress = true; - z80_interrupt(); + z80.interrupt(); } } @@ -129,7 +126,7 @@ export function makeZ80Runner(z80) { addTstates(4); z80.r = (z80.r + 1) & 0x7f; const opcode = readbyte(z80.pc); - z80_instruction_hook(z80.pc, opcode); + z80.instructionHook(z80.pc, opcode); z80.pc = (z80.pc + 1) & 0xffff; z80BaseOps[opcode](); diff --git a/tests/disassemble.test.js b/tests/disassemble.test.js index 0f73b65..df1486f 100644 --- a/tests/disassemble.test.js +++ b/tests/disassemble.test.js @@ -13,7 +13,7 @@ import { vi, describe, it, expect, beforeEach } from "vitest"; // --------------------------------------------------------------------------- const mem = new Uint8Array(0x10000); -vi.mock("../src/bus", () => { +vi.mock("../src/sms", () => { const busObj = { readbyte: (addr) => mem[addr & 0xffff], writebyte: (addr, val) => { @@ -25,9 +25,6 @@ vi.mock("../src/bus", () => { return { bus: busObj, readbyte: busObj.readbyte, - writebyte: busObj.writebyte, - writeport: busObj.writeport, - readport: busObj.readport, }; }); diff --git a/tests/fuse.test.js b/tests/fuse.test.js index 04f51d3..dc28215 100644 --- a/tests/fuse.test.js +++ b/tests/fuse.test.js @@ -49,7 +49,7 @@ const KNOWN_FAILURES = new Set([ "edbb_1", ]); -import { vi, describe, it, expect, beforeAll, beforeEach } from "vitest"; +import { describe, it, expect, beforeAll, beforeEach } from "vitest"; import { readFileSync } from "fs"; import { fileURLToPath } from "url"; import path from "path"; @@ -57,33 +57,24 @@ import path from "path"; const __dirname = path.dirname(fileURLToPath(import.meta.url)); // --------------------------------------------------------------------------- -// Memory mock — a flat 64K array used by z80.js and z80_ops.js -// via the ../miracle module. We mutate it between tests; never replace it. +// Memory mock — a flat 64K array; wired directly to a fresh Z80 instance. // --------------------------------------------------------------------------- const mem = new Uint8Array(0x10000); -vi.mock("../src/bus", () => { - const busObj = { - readbyte: (addr) => mem[addr & 0xffff], - writebyte: (addr, val) => { - mem[addr & 0xffff] = val & 0xff; - }, - readport: (port) => (port >> 8) & 0xff, // FUSE convention: port returns high byte of address - writeport: () => {}, - }; - return { - bus: busObj, - readbyte: busObj.readbyte, - writebyte: busObj.writebyte, - readport: busObj.readport, - writeport: busObj.writeport, - }; -}); +const mockBus = { + readbyte: (addr) => mem[addr & 0xffff], + writebyte: (addr, val) => { + mem[addr & 0xffff] = val & 0xff; + }, + readport: (port) => (port >> 8) & 0xff, // FUSE convention: port returns high byte of address + writeport: () => {}, +}; -// These imports must come *after* vi.mock (vitest hoists vi.mock automatically) -import { z80, z80_init } from "../src/z80/z80.js"; +import { Z80, z80_init } from "../src/z80/z80.js"; import { makeZ80Runner } from "../src/z80/z80_ops.js"; +const z80 = new Z80(); +z80.bus = mockBus; const { z80_do_opcodes } = makeZ80Runner(z80); // ---------------------------------------------------------------------------