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
JavaScript
/**
* 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