UNPKG

zx-generation

Version:

A high-fidelity ZX Spectrum emulator in JavaScript — fully generated by a large language model (LLM) to explore the boundaries of AI in systems programming.

1,615 lines (1,476 loc) 276 kB
/** * ZXGeneration - ZX Spectrum Emulator * @version 1.0.1 * @license MIT */ (function (global, factory) { typeof exports === 'object' && typeof module !== 'undefined' ? factory(exports) : typeof define === 'function' && define.amd ? define(['exports'], factory) : (global = typeof globalThis !== 'undefined' ? globalThis : global || self, factory(global.ZXGeneration = {})); })(this, (function (exports) { 'use strict'; /** * Z80 Registers Manager * Handles all register operations and 16-bit register pairs */ class Registers { constructor() { this.reset(); } reset() { this.data = { A: 0, F: 0, B: 0, C: 0, D: 0, E: 0, H: 0, L: 0, A_: 0, F_: 0, B_: 0, C_: 0, D_: 0, E_: 0, H_: 0, L_: 0, I: 0, R: 0, IX: 0, IY: 0, SP: 0xffff, PC: 0x0000 }; } // 8-bit register access get(name) { return this.data[name] & 0xff; } set(name, value) { this.data[name] = value & 0xff; } // 16-bit register pair getters getBC() { return this.data.B << 8 | this.data.C; } getDE() { return this.data.D << 8 | this.data.E; } getHL() { return this.data.H << 8 | this.data.L; } getAF() { return this.data.A << 8 | this.data.F; } // 16-bit register pair setters setBC(value) { const val = value & 0xffff; this.data.B = val >> 8 & 0xff; this.data.C = val & 0xff; } setDE(value) { const val = value & 0xffff; this.data.D = val >> 8 & 0xff; this.data.E = val & 0xff; } setHL(value) { const val = value & 0xffff; this.data.H = val >> 8 & 0xff; this.data.L = val & 0xff; } setAF(value) { const val = value & 0xffff; this.data.A = val >> 8 & 0xff; this.data.F = val & 0xff; } // 16-bit register access get16(name) { switch (name) { case 'BC': return this.getBC(); case 'DE': return this.getDE(); case 'HL': return this.getHL(); case 'AF': return this.getAF(); case 'SP': return this.data.SP & 0xffff; case 'PC': return this.data.PC & 0xffff; case 'IX': return this.data.IX & 0xffff; case 'IY': return this.data.IY & 0xffff; default: throw new Error(`Unknown 16-bit register: ${name}`); } } set16(name, value) { const val = value & 0xffff; switch (name) { case 'BC': this.setBC(val); break; case 'DE': this.setDE(val); break; case 'HL': this.setHL(val); break; case 'AF': this.setAF(val); break; case 'SP': this.data.SP = val; break; case 'PC': this.data.PC = val; break; case 'IX': this.data.IX = val; break; case 'IY': this.data.IY = val; break; default: throw new Error(`Unknown 16-bit register: ${name}`); } } // Increment/Decrement 16-bit registers inc16(name) { const value = this.get16(name); this.set16(name, value + 1 & 0xffff); } dec16(name) { const value = this.get16(name); this.set16(name, value - 1 & 0xffff); } // Program counter operations incrementPC(amount = 1) { this.data.PC = this.data.PC + amount & 0xffff; } setPC(address) { this.data.PC = address & 0xffff; } getPC() { return this.data.PC & 0xffff; } // R register operations (7-bit counter with bit 7 unchanged) incrementR() { this.data.R = this.data.R + 1 & 0x7f | this.data.R & 0x80; } // Exchange operations exchangeAF() { const tempA = this.data.A; const tempF = this.data.F; this.data.A = this.data.A_; this.data.F = this.data.F_; this.data.A_ = tempA; this.data.F_ = tempF; } exchangeAll() { // EXX - Exchange BC, DE, HL with their shadow registers const tempB = this.data.B; const tempC = this.data.C; const tempD = this.data.D; const tempE = this.data.E; const tempH = this.data.H; const tempL = this.data.L; this.data.B = this.data.B_; this.data.C = this.data.C_; this.data.D = this.data.D_; this.data.E = this.data.E_; this.data.H = this.data.H_; this.data.L = this.data.L_; this.data.B_ = tempB; this.data.C_ = tempC; this.data.D_ = tempD; this.data.E_ = tempE; this.data.H_ = tempH; this.data.L_ = tempL; } exchangeDE_HL() { const tempDE = this.getDE(); this.setDE(this.getHL()); this.setHL(tempDE); } // Debug helper dump() { return { A: this.data.A.toString(16).padStart(2, '0'), F: this.data.F.toString(16).padStart(2, '0'), BC: this.getBC().toString(16).padStart(4, '0'), DE: this.getDE().toString(16).padStart(4, '0'), HL: this.getHL().toString(16).padStart(4, '0'), SP: this.data.SP.toString(16).padStart(4, '0'), PC: this.data.PC.toString(16).padStart(4, '0'), IX: this.data.IX.toString(16).padStart(4, '0'), IY: this.data.IY.toString(16).padStart(4, '0'), I: this.data.I.toString(16).padStart(2, '0'), R: this.data.R.toString(16).padStart(2, '0') }; } // Undocumented IX/IY half registers getIXH() { return this.data.IX >> 8 & 0xff; } setIXH(value) { this.data.IX = this.data.IX & 0x00ff | (value & 0xff) << 8; } getIXL() { return this.data.IX & 0xff; } setIXL(value) { this.data.IX = this.data.IX & 0xff00 | value & 0xff; } getIYH() { return this.data.IY >> 8 & 0xff; } setIYH(value) { this.data.IY = this.data.IY & 0x00ff | (value & 0xff) << 8; } getIYL() { return this.data.IY & 0xff; } setIYL(value) { this.data.IY = this.data.IY & 0xff00 | value & 0xff; } // Property accessors for test compatibility get a() { return this.get('A'); } set a(value) { this.set('A', value); } get f() { return this.get('F'); } set f(value) { this.set('F', value); } get b() { return this.get('B'); } set b(value) { this.set('B', value); } get c() { return this.get('C'); } set c(value) { this.set('C', value); } get d() { return this.get('D'); } set d(value) { this.set('D', value); } get e() { return this.get('E'); } set e(value) { this.set('E', value); } get h() { return this.get('H'); } set h(value) { this.set('H', value); } get l() { return this.get('L'); } set l(value) { this.set('L', value); } get i() { return this.get('I'); } set i(value) { this.set('I', value); } get r() { return this.get('R'); } set r(value) { this.set('R', value); } get pc() { return this.getPC(); } set pc(value) { this.setPC(value); } get sp() { return this.data.SP & 0xffff; } set sp(value) { this.data.SP = value & 0xffff; } get ix() { return this.data.IX & 0xffff; } set ix(value) { this.data.IX = value & 0xffff; } get iy() { return this.data.IY & 0xffff; } set iy(value) { this.data.IY = value & 0xffff; } get bc() { return this.getBC(); } set bc(value) { this.setBC(value); } get de() { return this.getDE(); } set de(value) { this.setDE(value); } get hl() { return this.getHL(); } set hl(value) { this.setHL(value); } get af() { return this.getAF(); } set af(value) { this.setAF(value); } } /** * Z80 Flags Manager * Handles all flag operations including undocumented F3/F5 flags */ class Flags { constructor() { this.masks = { S: 0x80, // Sign Z: 0x40, // Zero F5: 0x20, // Undocumented - copy of bit 5 H: 0x10, // Half carry F3: 0x08, // Undocumented - copy of bit 3 PV: 0x04, // Parity/Overflow N: 0x02, // Add/Subtract C: 0x01 // Carry }; } /** * Get flag value from F register */ getFlag(fRegister, flagMask) { return (fRegister & flagMask) !== 0; } /** * Set or clear a flag in F register */ setFlag(fRegister, flagMask, value) { if (value) { return fRegister | flagMask; } else { return fRegister & ~flagMask; } } /** * Update flags after arithmetic/logical operations */ updateFlags(fRegister, result, operation = 'arithmetic') { const result8 = result & 0xff; let newF = fRegister; // Always update undocumented flags F3 and F5 newF = this.setFlag(newF, this.masks.F5, (result8 & 0x20) !== 0); newF = this.setFlag(newF, this.masks.F3, (result8 & 0x08) !== 0); newF = this.setFlag(newF, this.masks.S, (result8 & 0x80) !== 0); newF = this.setFlag(newF, this.masks.Z, result8 === 0); if (operation === 'subtract') { newF = this.setFlag(newF, this.masks.N, true); } else if (operation === 'arithmetic') { newF = this.setFlag(newF, this.masks.N, false); } else if (operation === 'logical') { newF = this.setFlag(newF, this.masks.N, false); // For logical operations, set parity flag newF = this.setFlag(newF, this.masks.PV, this.calculateParity(result8)); } return newF; } /** * Calculate parity of an 8-bit value */ calculateParity(value) { let parity = 0; let temp = value & 0xff; for (let i = 0; i < 8; i++) { if (temp & 1) { parity++; } temp >>= 1; } return (parity & 1) === 0; } /** * Update flags for IN instruction */ updateInFlags(fRegister, value) { let newF = fRegister; newF = this.setFlag(newF, this.masks.S, (value & 0x80) !== 0); newF = this.setFlag(newF, this.masks.Z, value === 0); newF = this.setFlag(newF, this.masks.H, false); newF = this.setFlag(newF, this.masks.N, false); newF = this.setFlag(newF, this.masks.PV, this.calculateParity(value)); // Undocumented flags newF = this.setFlag(newF, this.masks.F5, (value & 0x20) !== 0); newF = this.setFlag(newF, this.masks.F3, (value & 0x08) !== 0); return newF; } /** * Update flags for increment operation */ updateIncFlags(fRegister, originalValue, result) { let newF = fRegister; newF = this.setFlag(newF, this.masks.S, (result & 0x80) !== 0); newF = this.setFlag(newF, this.masks.Z, result === 0); newF = this.setFlag(newF, this.masks.H, (originalValue & 0x0f) === 0x0f); newF = this.setFlag(newF, this.masks.PV, originalValue === 0x7f); newF = this.setFlag(newF, this.masks.N, false); // Undocumented flags newF = this.setFlag(newF, this.masks.F5, (result & 0x20) !== 0); newF = this.setFlag(newF, this.masks.F3, (result & 0x08) !== 0); return newF; } /** * Update flags for decrement operation */ updateDecFlags(fRegister, originalValue, result) { let newF = fRegister; newF = this.setFlag(newF, this.masks.S, (result & 0x80) !== 0); newF = this.setFlag(newF, this.masks.Z, result === 0); newF = this.setFlag(newF, this.masks.H, (originalValue & 0x0f) === 0); newF = this.setFlag(newF, this.masks.PV, originalValue === 0x80); newF = this.setFlag(newF, this.masks.N, true); // Undocumented flags - FIXED: was this.flags.F3, now this.masks.F3 newF = this.setFlag(newF, this.masks.F5, (result & 0x20) !== 0); newF = this.setFlag(newF, this.masks.F3, (result & 0x08) !== 0); return newF; } /** * Update flags for BIT test operation */ updateBitTestFlags(fRegister, bit, value) { const mask = 1 << bit; const result = value & mask; let newF = fRegister; newF = this.setFlag(newF, this.masks.Z, result === 0); newF = this.setFlag(newF, this.masks.H, true); newF = this.setFlag(newF, this.masks.N, false); newF = this.setFlag(newF, this.masks.S, bit === 7 && result !== 0); newF = this.setFlag(newF, this.masks.PV, result === 0); // PV acts as Z for BIT // Undocumented flags: F3 and F5 are set from the value being tested newF = this.setFlag(newF, this.masks.F5, (value & 0x20) !== 0); newF = this.setFlag(newF, this.masks.F3, (value & 0x08) !== 0); return newF; } } /** * Memory Interface * Provides abstraction layer for memory access */ class MemoryInterface { constructor(memory) { this.memory = memory; } /** * Read a byte from memory */ readByte(address) { return this.memory.read(address & 0xffff); } /** * Write a byte to memory */ writeByte(address, value) { this.memory.write(address & 0xffff, value & 0xff); } /** * Read a 16-bit word from memory (little-endian) */ readWord(address) { const addr = address & 0xffff; const low = this.memory.read(addr); const high = this.memory.read(addr + 1 & 0xffff); return low | high << 8; } /** * Write a 16-bit word to memory (little-endian) */ writeWord(address, value) { const addr = address & 0xffff; const val = value & 0xffff; this.memory.write(addr, val & 0xff); this.memory.write(addr + 1 & 0xffff, val >> 8 & 0xff); } /** * Read byte and increment PC */ fetchByte(registers) { const byte = this.readByte(registers.getPC()); registers.incrementPC(); return byte; } /** * Read word and increment PC by 2 */ fetchWord(registers) { const low = this.fetchByte(registers); const high = this.fetchByte(registers); return low | high << 8; } /** * Push byte to stack */ pushByte(registers, value) { const sp = registers.get16('SP') - 1 & 0xffff; registers.set16('SP', sp); this.writeByte(sp, value); } /** * Pop byte from stack */ popByte(registers) { const sp = registers.get16('SP'); const value = this.readByte(sp); registers.set16('SP', sp + 1 & 0xffff); return value; } /** * Push word to stack */ pushWord(registers, value) { this.pushByte(registers, value >> 8 & 0xff); this.pushByte(registers, value & 0xff); } /** * Pop word from stack */ popWord(registers) { const low = this.popByte(registers); const high = this.popByte(registers); return low | high << 8; } } /** * I/O Interface * Provides abstraction layer for I/O port access */ class IOInterface { constructor(ula) { this.ula = ula; } /** * Read from I/O port */ readPort(port) { return this.ula.readPort(port & 0xffff); } /** * Write to I/O port */ writePort(port, value) { this.ula.writePort(port & 0xffff, value & 0xff); } } /** * ArithmeticInstructions – fixed version * Implements ADD, SUB, ADC, SBC, INC, DEC, CP, ADD HL, NEG * ------------------------------------------------------------------ * Fixes applied : * 1. **Overflow flag preservation** – `updateFlags()` is called, then * the pre-computed overflow bit is restored so parity logic inside * `updateFlags()` cannot overwrite it. * 2. **Accurate timing** – every public method now accepts an optional * `cycles` argument. It defaults to 4 T (the register-to-register * form) but the caller can supply **7 T** or **11 T** for immediate * or memory forms, IX/IY variants, etc. This keeps the interface * simple while allowing cycle-exact emulation at call-site level. * ------------------------------------------------------------------ */ class ArithmeticInstructions { /** @param {Registers} registers * @param {Flags} flags * @param {Memory} memory */ constructor(registers, flags, memory) { this.registers = registers; this.flags = flags; this.memory = memory; } /* ------------------------------------------------------------- * 8-bit arithmetic * ----------------------------------------------------------- */ /** * ADD A, value * @param {number} value 8-bit operand * @param {number} cycles T-states to report (default 4) */ addA(value, cycles = 4) { const a = this.registers.get('A'); const result = a + value; const halfCarry = (a & 0x0f) + (value & 0x0f) > 0x0f; const carry = result > 0xff; let f = this.registers.get('F'); f = this.flags.setFlag(f, this.flags.masks.C, carry); f = this.flags.setFlag(f, this.flags.masks.H, halfCarry); f = this.flags.setFlag(f, this.flags.masks.N, false); // overflow when operands have same sign, result has opposite const overflow = ((a ^ value) & 0x80) === 0 && ((a ^ result) & 0x80) !== 0; // Update S,Z,F5,F3 (and possibly PV) from result f = this.flags.updateFlags(f, result & 0xff); // restore correct overflow f = this.flags.setFlag(f, this.flags.masks.PV, overflow); this.registers.set('A', result & 0xff); this.registers.set('F', f); return cycles; } /** * SUB A, value */ subA(value, cycles = 4) { const a = this.registers.get('A'); const result = a - value; const halfBorrow = (a & 0x0f) - (value & 0x0f) < 0; const borrow = result < 0; let f = this.registers.get('F'); f = this.flags.setFlag(f, this.flags.masks.C, borrow); f = this.flags.setFlag(f, this.flags.masks.H, halfBorrow); f = this.flags.setFlag(f, this.flags.masks.N, true); const overflow = ((a ^ value) & 0x80) !== 0 && ((a ^ result) & 0x80) !== 0; f = this.flags.updateFlags(f, result & 0xff, 'subtract'); f = this.flags.setFlag(f, this.flags.masks.PV, overflow); this.registers.set('A', result & 0xff); this.registers.set('F', f); return cycles; } /** * ADC A, value (Add with Carry) */ adcA(value, cycles = 4) { const a = this.registers.get('A'); const carry = this.flags.getFlag(this.registers.get('F'), this.flags.masks.C) ? 1 : 0; const result = a + value + carry; const halfCarry = (a & 0x0f) + (value & 0x0f) + carry > 0x0f; const carryOut = result > 0xff; let f = this.registers.get('F'); f = this.flags.setFlag(f, this.flags.masks.C, carryOut); f = this.flags.setFlag(f, this.flags.masks.H, halfCarry); f = this.flags.setFlag(f, this.flags.masks.N, false); const overflow = ((a ^ value) & 0x80) === 0 && ((a ^ result) & 0x80) !== 0; f = this.flags.updateFlags(f, result & 0xff); f = this.flags.setFlag(f, this.flags.masks.PV, overflow); this.registers.set('A', result & 0xff); this.registers.set('F', f); return cycles; } /** * SBC A, value (Subtract with Carry) */ sbcA(value, cycles = 4) { const a = this.registers.get('A'); const carry = this.flags.getFlag(this.registers.get('F'), this.flags.masks.C) ? 1 : 0; const result = a - value - carry; const halfBorrow = (a & 0x0f) - (value & 0x0f) - carry < 0; const borrow = result < 0; let f = this.registers.get('F'); f = this.flags.setFlag(f, this.flags.masks.C, borrow); f = this.flags.setFlag(f, this.flags.masks.H, halfBorrow); f = this.flags.setFlag(f, this.flags.masks.N, true); const overflow = ((a ^ value) & 0x80) !== 0 && ((a ^ result) & 0x80) !== 0; f = this.flags.updateFlags(f, result & 0xff, 'subtract'); f = this.flags.setFlag(f, this.flags.masks.PV, overflow); this.registers.set('A', result & 0xff); this.registers.set('F', f); return cycles; } /** * CP value (Compare with A) */ cpA(value, cycles = 4) { const a = this.registers.get('A'); const result = a - value; const halfBorrow = (a & 0x0f) - (value & 0x0f) < 0; const borrow = result < 0; const temp = result & 0xff; let f = this.registers.get('F'); f = this.flags.setFlag(f, this.flags.masks.C, borrow); f = this.flags.setFlag(f, this.flags.masks.H, halfBorrow); f = this.flags.setFlag(f, this.flags.masks.N, true); f = this.flags.setFlag(f, this.flags.masks.S, (temp & 0x80) !== 0); f = this.flags.setFlag(f, this.flags.masks.Z, temp === 0); const overflow = ((a ^ value) & 0x80) !== 0 && ((a ^ temp) & 0x80) !== 0; f = this.flags.setFlag(f, this.flags.masks.PV, overflow); // undocumented bits from *operand* f = this.flags.setFlag(f, this.flags.masks.F5, (value & 0x20) !== 0); f = this.flags.setFlag(f, this.flags.masks.F3, (value & 0x08) !== 0); this.registers.set('F', f); return cycles; } /* ------------------------------------------------------------- * INC / DEC single-register helpers * ----------------------------------------------------------- */ incReg(regName, cycles = 4) { const before = this.registers.get(regName); const after = before + 1 & 0xff; this.registers.set(regName, after); const f = this.flags.updateIncFlags(this.registers.get('F'), before, after); this.registers.set('F', f); return cycles; } decReg(regName, cycles = 4) { const before = this.registers.get(regName); const after = before - 1 & 0xff; this.registers.set(regName, after); const f = this.flags.updateDecFlags(this.registers.get('F'), before, after); this.registers.set('F', f); return cycles; } /* ------------------------------------------------------------- * INC / DEC (HL) memory cell * ----------------------------------------------------------- */ incHL(cycles = 11) { const addr = this.registers.getHL(); const before = this.memory.readByte(addr); const after = before + 1 & 0xff; this.memory.writeByte(addr, after); const f = this.flags.updateIncFlags(this.registers.get('F'), before, after); this.registers.set('F', f); return cycles; } decHL(cycles = 11) { const addr = this.registers.getHL(); const before = this.memory.readByte(addr); const after = before - 1 & 0xff; this.memory.writeByte(addr, after); const f = this.flags.updateDecFlags(this.registers.get('F'), before, after); this.registers.set('F', f); return cycles; } /* ------------------------------------------------------------- * 16-bit arithmetic * ----------------------------------------------------------- */ /** * ADD HL, rr * @param {number} value 16-bit source register-pair */ addHL(value, cycles = 11) { const hl = this.registers.getHL(); const result = hl + value; let f = this.registers.get('F'); f = this.flags.setFlag(f, this.flags.masks.C, result > 0xffff); f = this.flags.setFlag(f, this.flags.masks.H, (hl & 0x0fff) + (value & 0x0fff) > 0x0fff); f = this.flags.setFlag(f, this.flags.masks.N, false); this.registers.setHL(result & 0xffff); // undocumented bits from high byte of result const high = result >> 8 & 0xff; f = this.flags.setFlag(f, this.flags.masks.F5, (high & 0x20) !== 0); f = this.flags.setFlag(f, this.flags.masks.F3, (high & 0x08) !== 0); this.registers.set('F', f); return cycles; } /* ------------------------------------------------------------- * NEG * ----------------------------------------------------------- */ neg(cycles = 8) { const a = this.registers.get('A'); const result = -a & 0xff; let f = this.registers.get('F'); f = this.flags.setFlag(f, this.flags.masks.C, a !== 0); f = this.flags.setFlag(f, this.flags.masks.H, (a & 0x0f) !== 0); f = this.flags.setFlag(f, this.flags.masks.PV, a === 0x80); f = this.flags.setFlag(f, this.flags.masks.N, true); f = this.flags.setFlag(f, this.flags.masks.S, (result & 0x80) !== 0); f = this.flags.setFlag(f, this.flags.masks.Z, result === 0); f = this.flags.setFlag(f, this.flags.masks.F5, (result & 0x20) !== 0); f = this.flags.setFlag(f, this.flags.masks.F3, (result & 0x08) !== 0); this.registers.set('A', result); this.registers.set('F', f); return cycles; } } /** * Logical Instructions * Handles AND, OR, XOR, CPL, SCF, CCF, DAA operations */ class LogicalInstructions { constructor(registers, flags) { this.registers = registers; this.flags = flags; } /** * AND A, value */ andA(value) { const result = this.registers.get('A') & value; this.registers.set('A', result); let newF = this.registers.get('F'); newF = this.flags.setFlag(newF, this.flags.masks.C, false); newF = this.flags.setFlag(newF, this.flags.masks.H, true); newF = this.flags.updateFlags(newF, result, 'logical'); this.registers.set('F', newF); return 4; // cycles } /** * OR A, value */ orA(value) { const result = this.registers.get('A') | value; this.registers.set('A', result); let newF = this.registers.get('F'); newF = this.flags.setFlag(newF, this.flags.masks.C, false); newF = this.flags.setFlag(newF, this.flags.masks.H, false); newF = this.flags.updateFlags(newF, result, 'logical'); this.registers.set('F', newF); return 4; // cycles } /** * XOR A, value */ xorA(value) { const result = this.registers.get('A') ^ value; this.registers.set('A', result); let newF = this.registers.get('F'); newF = this.flags.setFlag(newF, this.flags.masks.C, false); newF = this.flags.setFlag(newF, this.flags.masks.H, false); newF = this.flags.updateFlags(newF, result, 'logical'); this.registers.set('F', newF); return 4; // cycles } /** * CPL (Complement A) */ cpl() { this.registers.set('A', ~this.registers.get('A') & 0xff); let newF = this.registers.get('F'); newF = this.flags.setFlag(newF, this.flags.masks.H, true); newF = this.flags.setFlag(newF, this.flags.masks.N, true); // Undocumented flags from A const a = this.registers.get('A'); newF = this.flags.setFlag(newF, this.flags.masks.F5, (a & 0x20) !== 0); newF = this.flags.setFlag(newF, this.flags.masks.F3, (a & 0x08) !== 0); this.registers.set('F', newF); return 4; // cycles } /** * SCF (Set Carry Flag) */ scf() { let newF = this.registers.get('F'); newF = this.flags.setFlag(newF, this.flags.masks.C, true); newF = this.flags.setFlag(newF, this.flags.masks.H, false); newF = this.flags.setFlag(newF, this.flags.masks.N, false); // Undocumented flags from A const a = this.registers.get('A'); newF = this.flags.setFlag(newF, this.flags.masks.F5, (a & 0x20) !== 0); newF = this.flags.setFlag(newF, this.flags.masks.F3, (a & 0x08) !== 0); this.registers.set('F', newF); return 4; // cycles } /** * CCF (Complement Carry Flag) */ ccf() { const oldCarry = this.flags.getFlag(this.registers.get('F'), this.flags.masks.C); let newF = this.registers.get('F'); newF = this.flags.setFlag(newF, this.flags.masks.H, oldCarry); newF = this.flags.setFlag(newF, this.flags.masks.C, !oldCarry); newF = this.flags.setFlag(newF, this.flags.masks.N, false); // Undocumented flags from A const a = this.registers.get('A'); newF = this.flags.setFlag(newF, this.flags.masks.F5, (a & 0x20) !== 0); newF = this.flags.setFlag(newF, this.flags.masks.F3, (a & 0x08) !== 0); this.registers.set('F', newF); return 4; // cycles } /** * DAA (Decimal Adjust Accumulator) * * Flags affected: S Z H PV N C F5 F3 * This is the official Z80 logic for DAA. * - H is set if there is a half-carry out of bit 3 of the result (after correction) * - C is set if correction was >= 0x60, or if C was already set and a correction was done * - N is not changed */ daa() { let a = this.registers.get('A'); let f = this.registers.get('F'); const c = this.flags.getFlag(f, this.flags.masks.C); const h = this.flags.getFlag(f, this.flags.masks.H); const n = this.flags.getFlag(f, this.flags.masks.N); let correction = 0; let setC = false; // DAA algorithm if (!n) { // After addition if (h || (a & 0x0f) > 9) { correction |= 0x06; } if (c || a > 0x99) { correction |= 0x60; setC = true; } const newA = a + correction & 0xff; // Set or clear H: was there a half-carry? const halfCarry = (a & 0x0f) + (correction & 0x0f) > 0x0f; f = this.flags.setFlag(f, this.flags.masks.H, halfCarry); a = newA; } else { // After subtraction - DAA adjusts based on invalid BCD digits if (h) { correction |= 0x06; } if (c) { correction |= 0x60; } const newA = a - correction & 0xff; // H flag behavior after subtraction DAA is complex: // Set if there was a borrow from bit 4 during the correction const halfBorrow = (a & 0x0f) < (correction & 0x0f); f = this.flags.setFlag(f, this.flags.masks.H, halfBorrow); a = newA; // C flag remains as it was (set by the previous SUB/SBC) setC = c; } // Set S, Z, PV f = this.flags.setFlag(f, this.flags.masks.S, (a & 0x80) !== 0); f = this.flags.setFlag(f, this.flags.masks.Z, a === 0); f = this.flags.setFlag(f, this.flags.masks.PV, this.flags.calculateParity(a)); // Set/clear carry f = this.flags.setFlag(f, this.flags.masks.C, setC); // F5/F3 undocumented: from result f = this.flags.setFlag(f, this.flags.masks.F5, (a & 0x20) !== 0); f = this.flags.setFlag(f, this.flags.masks.F3, (a & 0x08) !== 0); this.registers.set('A', a); this.registers.set('F', f); return 4; // cycles } } /** * Common utility functions for the Z80 emulator */ /** * Sign-extend an 8-bit value to a signed integer * @param {number} value - 8-bit unsigned value * @returns {number} Sign-extended value (-128 to 127) */ function sign8(value) { return value & 0x80 ? value - 256 : value; } /** * Memory interface recommendation for performance * @example * // For best performance, back your memory with TypedArray: * class Memory { * constructor(size = 65536) { * this.ram = new Uint8Array(size); * } * * read(address) { * return this.ram[address & 0xFFFF]; * } * * write(address, value) { * this.ram[address & 0xFFFF] = value & 0xFF; * } * } */ /** * Load Instructions * Handles all LD operations for 8‐bit and 16‐bit loads, including special * cases LD A,I and LD A,R with correct flag behaviour (PV = IFF2). */ class LoadInstructions { constructor(registers, memory, io, flags, cpu = null) { this.registers = registers; this.memory = memory; this.io = io; this.flags = flags; this.cpu = cpu; // optional, only needed for IFF2 in LD A,I and LD A,R } /* LD reg, value */ loadRegImmediate(regName, value) { this.registers.set(regName, value & 0xff); return 7; } /* LD reg16, value */ loadReg16Immediate(regName, value) { this.registers.set16(regName, value & 0xffff); return regName === 'IX' || regName === 'IY' ? 14 : 10; } /* LD reg, reg */ loadRegReg(destReg, srcReg) { this.registers.set(destReg, this.registers.get(srcReg)); return 4; } /* LD reg, (HL) */ loadRegFromHL(regName) { const addr = this.registers.getHL(); this.registers.set(regName, this.memory.readByte(addr)); return 7; } /* LD (HL), reg */ loadHLFromReg(regName) { const addr = this.registers.getHL(); this.memory.writeByte(addr, this.registers.get(regName)); return 7; } /* LD (HL), n */ loadHLImmediate(value) { const addr = this.registers.getHL(); this.memory.writeByte(addr, value & 0xff); return 10; } /* LD A,(BC) */ loadAFromBC() { const addr = this.registers.getBC(); this.registers.set('A', this.memory.readByte(addr)); return 7; } /* LD A,(DE) */ loadAFromDE() { const addr = this.registers.getDE(); this.registers.set('A', this.memory.readByte(addr)); return 7; } /* LD (BC),A */ loadBCFromA() { const addr = this.registers.getBC(); this.memory.writeByte(addr, this.registers.get('A')); return 7; } /* LD (DE),A */ loadDEFromA() { const addr = this.registers.getDE(); this.memory.writeByte(addr, this.registers.get('A')); return 7; } /* LD A,(nn) */ loadAFromAddress(address) { this.registers.set('A', this.memory.readByte(address & 0xffff)); return 13; } /* LD (nn),A */ loadAddressFromA(address) { this.memory.writeByte(address & 0xffff, this.registers.get('A')); return 13; } /* LD HL,(nn) */ loadHLFromAddress(address) { this.registers.setHL(this.memory.readWord(address & 0xffff)); return 16; } /* LD (nn),HL */ loadAddressFromHL(address) { this.memory.writeWord(address & 0xffff, this.registers.getHL()); return 16; } /* LD reg16,(nn) (ED) */ loadReg16FromAddress(regName, address) { this.registers.set16(regName, this.memory.readWord(address & 0xffff)); return 20; } /* LD (nn),reg16 (ED) */ loadAddressFromReg16(address, regName) { this.memory.writeWord(address & 0xffff, this.registers.get16(regName)); return 20; } /* LD SP,HL */ loadSPFromHL() { this.registers.set16('SP', this.registers.getHL()); return 6; } /* LD I,A */ loadIFromA() { this.registers.set('I', this.registers.get('A')); return 9; } /* LD R,A */ loadRFromA() { this.registers.set('R', this.registers.get('A')); return 9; } /** * Helper used by LD A,I and LD A,R to compute flags. * @param {number} value The value loaded into A (either I or R) */ _updateFlagsAfterLoadAIorAR(value) { let f = this.registers.get('F'); const carryState = this.flags.getFlag(f, this.flags.masks.C); // preserve C f = this.flags.setFlag(f, this.flags.masks.S, (value & 0x80) !== 0); f = this.flags.setFlag(f, this.flags.masks.Z, value === 0); f = this.flags.setFlag(f, this.flags.masks.H, false); f = this.flags.setFlag(f, this.flags.masks.N, false); // PV == IFF2 when CPU context is provided if (this.cpu) { f = this.flags.setFlag(f, this.flags.masks.PV, !!this.cpu.iff2); } // Undocumented flags f = this.flags.setFlag(f, this.flags.masks.F5, (value & 0x20) !== 0); f = this.flags.setFlag(f, this.flags.masks.F3, (value & 0x08) !== 0); // Restore original carry f = this.flags.setFlag(f, this.flags.masks.C, carryState); this.registers.set('F', f); } /* LD A,I */ loadAFromI() { const i = this.registers.get('I') & 0xff; this.registers.set('A', i); this._updateFlagsAfterLoadAIorAR(i); return 9; } /* LD A,R */ loadAFromR() { const r = this.registers.get('R') & 0xff; this.registers.set('A', r); this._updateFlagsAfterLoadAIorAR(r); return 9; } /* LD reg,(IX/IY+d) */ loadRegFromIndexed(regName, indexReg, displacement) { const signedDisp = sign8(displacement); const addr = this.registers.get16(indexReg) + signedDisp & 0xffff; this.registers.set(regName, this.memory.readByte(addr)); return 19; } /* LD (IX/IY+d),reg */ loadIndexedFromReg(indexReg, displacement, regName) { const signedDisp = sign8(displacement); const addr = this.registers.get16(indexReg) + signedDisp & 0xffff; this.memory.writeByte(addr, this.registers.get(regName)); return 19; } /* LD (IX/IY+d),n */ loadIndexedImmediate(indexReg, displacement, value) { const signedDisp = sign8(displacement); const addr = this.registers.get16(indexReg) + signedDisp & 0xffff; this.memory.writeByte(addr, value & 0xff); return 19; } } /** * Jump and Flow Control Instructions * Handles JP, JR, CALL, RET, RST, DJNZ operations */ class JumpInstructions { constructor(registers, flags, memory) { this.registers = registers; this.flags = flags; this.memory = memory; } /** * JP nn (Unconditional jump) */ jump(address) { this.registers.setPC(address); return 10; // cycles } /** * JP cc, nn (Conditional jump) */ jumpConditional(condition, address) { if (this.checkCondition(condition)) { this.registers.setPC(address); } return 10; // cycles } /** * JP (HL) */ jumpHL() { this.registers.setPC(this.registers.getHL()); return 4; // cycles } /** * JP (IX) / JP (IY) */ jumpIndexed(indexReg) { this.registers.setPC(this.registers.get16(indexReg)); return 8; // cycles } /** * JR e (Relative jump) */ jumpRelative(offset) { const signedOffset = offset > 127 ? offset - 256 : offset; const newPC = this.registers.getPC() + signedOffset & 0xffff; this.registers.setPC(newPC); return 12; // cycles } /** * JR cc, e (Conditional relative jump) */ jumpRelativeConditional(condition, offset) { if (this.checkCondition(condition)) { const signedOffset = offset > 127 ? offset - 256 : offset; const newPC = this.registers.getPC() + signedOffset & 0xffff; this.registers.setPC(newPC); return 12; // cycles } return 7; // cycles } /** * CALL nn (Unconditional call) */ call(address) { this.memory.pushWord(this.registers, this.registers.getPC()); this.registers.setPC(address); return 17; // cycles } /** * CALL cc, nn (Conditional call) */ callConditional(condition, address) { if (this.checkCondition(condition)) { this.memory.pushWord(this.registers, this.registers.getPC()); this.registers.setPC(address); return 17; // cycles } return 10; // cycles } /** * RET (Unconditional return) */ ret() { const address = this.memory.popWord(this.registers); this.registers.setPC(address); return 10; // cycles } /** * RET cc (Conditional return) */ retConditional(condition) { if (this.checkCondition(condition)) { const address = this.memory.popWord(this.registers); this.registers.setPC(address); return 11; // cycles } return 5; // cycles } /** * RETI (Return from interrupt) */ reti() { const address = this.memory.popWord(this.registers); this.registers.setPC(address); // RETI also signals to peripherals that interrupt routine is complete return 14; // cycles } /** * RETN (Return from non-maskable interrupt) */ retn(cpu) { const address = this.memory.popWord(this.registers); this.registers.setPC(address); // Restore interrupt state: IFF1 = IFF2 if (cpu) { cpu.iff1 = cpu.iff2; } return 14; // cycles } /** * RST p (Restart) */ rst(address) { this.memory.pushWord(this.registers, this.registers.getPC()); this.registers.setPC(address); return 11; // cycles } /** * DJNZ e (Decrement B and jump if not zero) */ djnz(offset) { const b = this.registers.get('B') - 1 & 0xff; this.registers.set('B', b); if (b !== 0) { const signedOffset = offset > 127 ? offset - 256 : offset; const newPC = this.registers.getPC() + signedOffset & 0xffff; this.registers.setPC(newPC); return 13; // cycles } return 8; // cycles } /** * Check condition codes */ checkCondition(condition) { const f = this.registers.get('F'); switch (condition) { case 'NZ': return !this.flags.getFlag(f, this.flags.masks.Z); case 'Z': return this.flags.getFlag(f, this.flags.masks.Z); case 'NC': return !this.flags.getFlag(f, this.flags.masks.C); case 'C': return this.flags.getFlag(f, this.flags.masks.C); case 'PO': return !this.flags.getFlag(f, this.flags.masks.PV); case 'PE': return this.flags.getFlag(f, this.flags.masks.PV); case 'P': return !this.flags.getFlag(f, this.flags.masks.S); case 'M': return this.flags.getFlag(f, this.flags.masks.S); default: return false; } } } /** * Bit Manipulation Instructions * Handles BIT, SET, RES and rotate/shift operations (CB prefix) */ class BitInstructions { constructor(registers, flags, memory) { this.registers = registers; this.flags = flags; this.memory = memory; } /** * BIT bit, reg/memory * @param {number} bit - The bit position to test (0-7) * @param {number} value - The value to test * @param {boolean} isMemory - Whether this is a memory operation (HL) * @returns {number} Cycles: 4 for register, 8 for (HL) (not including CB prefix) */ bitTest(bit, value, isMemory = false) { const newF = this.flags.updateBitTestFlags(this.registers.get('F'), bit, value); this.registers.set('F', newF); return isMemory ? 8 : 4; } /** * SET bit, reg */ setBitReg(bit, regName) { const value = this.registers.get(regName); this.registers.set(regName, value | 1 << bit); return 8; // cycles } /** * RES bit, reg */ resBitReg(bit, regName) { const value = this.registers.get(regName); this.registers.set(regName, value & ~(1 << bit)); return 8; // cycles } /** * SET bit, (HL) */ setBitHL(bit) { const addr = this.registers.getHL(); const value = this.memory.readByte(addr); this.memory.writeByte(addr, value | 1 << bit); return 15; // cycles } /** * RES bit, (HL) */ resBitHL(bit) { const addr = this.registers.getHL(); const value = this.memory.readByte(addr); this.memory.writeByte(addr, value & ~(1 << bit)); return 15; // cycles } /** * RLC (Rotate Left Circular) */ rlc(value) { const carry = (value & 0x80) !== 0; const result = (value << 1 | (carry ? 1 : 0)) & 0xff; let newF = this.registers.get('F'); newF = this.flags.setFlag(newF, this.flags.masks.C, carry); newF = this.flags.setFlag(newF, this.flags.masks.H, false); newF = this.flags.setFlag(newF, this.flags.masks.N, false); newF = this.flags.updateFlags(newF, result, 'logical'); // F3 and F5 flags are already set by updateFlags this.registers.set('F', newF); return result; } /** * RRC (Rotate Right Circular) */ rrc(value) { const carry = (value & 0x01) !== 0; const result = (value >> 1 | (carry ? 0x80 : 0)) & 0xff; let newF = this.registers.get('F'); newF = this.flags.setFlag(newF, this.flags.masks.C, carry); newF = this.flags.setFlag(newF, this.flags.masks.H, false); newF = this.flags.setFlag(newF, this.flags.masks.N, false); newF = this.flags.updateFlags(newF, result, 'logical'); this.registers.set('F', newF); return result; } /** * RL (Rotate Left through Carry) */ rl(value) { const oldCarry = this.flags.getFlag(this.registers.get('F'), this.flags.masks.C) ? 1 : 0; const newCarry = (value & 0x80) !== 0; const result = (value << 1 | oldCarry) & 0xff; let newF = this.registers.get('F'); newF = this.flags.setFlag(newF, this.flags.masks.C, newCarry); newF = this.flags.setFlag(newF, this.flags.masks.H, false); newF = this.flags.setFlag(newF, this.flags.masks.N, false); newF = this.flags.updateFlags(newF, result, 'logical'); this.registers.set('F', newF); return result; } /** * RR (Rotate Right through Carry) */ rr(value) { const oldCarry = this.flags.getFlag(this.registers.get('F'), this.flags.masks.C) ? 0x80 : 0; const newCarry = (value & 0x01) !== 0; const result = (value >> 1 | oldCarry) & 0xff; let newF = this.registers.get('F'); newF = this.flags.setFlag(newF, this.flags.masks.C, newCarry); newF = this.flags.setFlag(newF, this.flags.masks.H, false); newF = this.flags.setFlag(newF, this.flags.masks.N, false); newF = this.flags.updateFlags(newF, result, 'logical'); this.registers.set('F', newF); return result; } /** * SLA (Shift Left Arithmetic) */ sla(value) { const carry = (value & 0x80) !== 0; const result = value << 1 & 0xff; let newF = this.registers.get('F'); newF = this.flags.setFlag(newF, this.flags.masks.C, carry); newF = this.flags.setFlag(newF, this.flags.masks.H, false); newF = this.flags.setFlag(newF, this.flags.masks.N, false); newF = this.flags.updateFlags(newF, result, 'logical'); this.registers.set('F', newF); return result; } /** * SRA (Shift Right Arithmetic) */ sra(value) { const carry = (value & 0x01) !== 0; const result = (value >> 1 | value & 0x80) & 0xff; let newF = this.registers.get('F'); newF = this.flags.se