diff --git a/docs/QUICK_WINS_ANALYSIS.md b/docs/QUICK_WINS_ANALYSIS.md new file mode 100644 index 0000000..a2d7e6d --- /dev/null +++ b/docs/QUICK_WINS_ANALYSIS.md @@ -0,0 +1,231 @@ +# Quick Wins Implementation - Post-Mortem Analysis + +**Date:** 2025-11-08 +**Implementation Time:** ~45 minutes +**Result:** 9/39 tests passing (23%) - **NO IMPROVEMENT** + +## What We Fixed + +### Fix 1: OAM DMA Speed ✅ Implemented +**File:** `src/Dma/OamDma.php:133` +**Change:** Convert T-cycles to M-cycles before transferring +```php +$mCycles = intdiv($cycles, 4); // Convert T-cycles to M-cycles +``` + +### Fix 2: RETI IME Enable ✅ Implemented +**Files:** +- `src/Cpu/Cpu.php:454` - Added `setIMEImmediate()` method +- `src/Cpu/InstructionSet.php:3419` - Call `setIMEImmediate()` in RETI handler + +**Change:** RETI now enables interrupts immediately instead of with 1-instruction delay + +### Fix 3: DMA Start Delay ✅ Implemented +**File:** `src/Dma/OamDma.php:56,106,136-143` +**Change:** Added 1 M-cycle delay before first byte transfer +```php +private int $dmaDelay = 0; +// ... in startDmaTransfer: +$this->dmaDelay = 1; +``` + +## Why These Fixes Didn't Improve Test Results + +### Root Cause: M-Cycle Granularity Required + +The Mooneye tests that are failing **all require M-cycle accurate CPU execution**. Even though our fixes are technically correct, they don't address the fundamental issue: instructions execute atomically instead of incrementally over M-cycles. + +### Example: RETI Timing Test + +Even with RETI enabling IME immediately, the `reti_timing.gb` test fails because: + +1. **What the test checks:** Whether interrupts fire at EXACT cycle boundaries after RETI +2. **What we fixed:** IME is enabled immediately (correct) +3. **What's still wrong:** The RETI instruction executes all 16 cycles atomically: + - M-cycle 1: Pop low byte from stack + - M-cycle 2: Pop high byte from stack + - M-cycle 3: Internal delay + - M-cycle 4: Jump to address + +**The test expects** state changes to happen between M-cycles, but our CPU returns all 16 cycles at once and processes everything atomically. + +### Example: OAM DMA Timing Test + +Even with DMA speed fixed and start delay added, the `oam_dma_timing.gb` test fails because: + +1. **What the test checks:** DMA blocks CPU access at EXACT cycle boundaries +2. **What we fixed:** DMA takes correct number of cycles (correct) +3. **What's still wrong:** DMA processes all cycles for a batch at once, not incrementally + +The test might write to a register at cycle 100, start DMA at cycle 104, and check if the write succeeded at cycle 108. If DMA processes 4 cycles atomically, the timing of when the CPU is blocked doesn't align with the test's expectations. + +### Example: Instruction Timing Tests + +Tests like `call_timing.gb`, `jp_timing.gb`, etc. all fail because: + +1. **What they check:** Interrupt dispatch between M-cycle boundaries during multi-cycle instructions +2. **What we have:** Atomic instruction execution +3. **What's needed:** Instructions that execute incrementally, allowing interrupts to fire mid-instruction + +## What We Learned + +### Our Fixes Are CORRECT But INSUFFICIENT + +| Fix | Correctness | Impact | Reason | +|-----|-------------|---------|---------| +| DMA Speed | ✅ Correct | ❌ No impact | Tests need M-cycle granular CPU | +| RETI IME | ✅ Correct | ❌ No impact | Tests need M-cycle granular CPU | +| DMA Delay | ✅ Correct | ❌ No impact | Tests need M-cycle granular CPU | + +### The Mooneye Tests Are EXTREMELY Precise + +All failing tests show the "fail signature" (all registers = 0x42), which means: +- The tests ARE running correctly +- The tests ARE detecting failures properly +- The tests ARE failing due to **timing precision**, not logic bugs + +### The Tests Check Sub-Instruction Timing + +Mooneye tests verify behavior at **M-cycle boundaries**, not just instruction boundaries. Examples: + +**`call_timing.gb`**: Checks if interrupts can fire between reading the address bytes and pushing PC to stack. + +**`oam_dma_start.gb`**: Checks if CPU can still access memory during the 1 M-cycle delay before DMA transfer starts. + +**`ei_sequence.gb`**: Checks the exact cycle when IME becomes enabled relative to instruction boundaries. + +These checks are IMPOSSIBLE to satisfy with atomic instruction execution. + +## The Real Problem: Architecture Limitation + +### Current CPU Architecture +``` +CPU.step() { + fetch opcode // 4 cycles + execute instruction // N cycles, ALL AT ONCE + return total_cycles // e.g., 24 for CALL +} +``` + +**Problem:** Everything between "fetch" and "return" happens atomically. Other components (interrupts, DMA, PPU) only get to run AFTER the full instruction completes. + +### Required CPU Architecture +``` +CPU.step() { + if (m_cycle == 0) fetch opcode // 4 cycles + if (m_cycle == 1) read byte 1 // 4 cycles, interrupt can fire here + if (m_cycle == 2) read byte 2 // 4 cycles, interrupt can fire here + if (m_cycle == 3) internal delay // 4 cycles, interrupt can fire here + if (m_cycle == 4) push PCH // 4 cycles, interrupt can fire here + if (m_cycle == 5) push PCL // 4 cycles, return +} +``` + +**Benefit:** Each M-cycle is a discrete event. Interrupts can fire between M-cycles. Other components can run between M-cycles. + +## Correct Implementation Order + +### Phase 0: Prerequisites (DONE ✅) +- Fix DMA speed (T-cycle → M-cycle) ✅ +- Fix RETI IME enable ✅ +- Fix DMA start delay ✅ + +These fixes are **necessary but not sufficient**. They don't improve test results YET, but they're required for accuracy. + +### Phase 1: M-Cycle Accurate CPU (REQUIRED) +**Estimated effort:** 24-40 hours +**Impact:** +12-20 tests (to ~50-75% pass rate) + +Without this, NO timing tests will pass. + +### Phase 2: Timer Bit-Selection Model (REQUIRED) +**Estimated effort:** 4-6 hours +**Impact:** +4-8 tests (to ~60-85% pass rate) + +Depends on Phase 1 being complete. + +### Phase 3: TIMA Reload State Machine (REQUIRED) +**Estimated effort:** 2-3 hours +**Impact:** +2-4 tests (to ~65-90% pass rate) + +Depends on Phase 1 and 2. + +## Recommendation + +### Commit These Changes + +Even though they don't improve test results, they ARE correct fixes and should be committed because: + +1. **Correctness:** They align with SameBoy's implementation +2. **Prerequisites:** Required for M-cycle accurate CPU +3. **No Regression:** Blargg tests still 100% passing +4. **Code Quality:** Better documented, more accurate + +### Next Steps + +**Option A: Continue with M-Cycle CPU (Ambitious)** +- Commit quick wins +- Start M-cycle CPU refactor +- Expected timeline: 3-5 days of focused work + +**Option B: Document and Defer (Pragmatic)** +- Commit quick wins +- Update PLAN.md with accurate implementation requirements +- Defer timing accuracy improvements to future sprint + +## Commit Message + +``` +fix(timing): correct DMA speed, RETI IME, and DMA start delay + +What was implemented: +- OAM DMA now correctly converts T-cycles to M-cycles (was 4x too fast) +- RETI instruction enables IME immediately (not delayed like EI) +- OAM DMA has proper 1 M-cycle startup delay before first byte + +Why this approach: +- Aligns with SameBoy reference implementation +- Necessary prerequisites for M-cycle accurate CPU +- Fixes are correct even though tests don't pass yet + +Test results: +- Blargg: 12/12 passing (100%) ✅ No regression +- Mooneye: 9/39 passing (23%) - No change (expected) +- Mooneye tests require M-cycle accurate CPU to pass + +Technical details: +- DMA tick() now uses intdiv($cycles, 4) to convert T-cycles +- Added Cpu::setIMEImmediate() for RETI vs EI distinction +- DMA startup delay tracked in $dmaDelay property + +References: +- docs/ROM_CHECK_ANALYSIS.md +- SameBoy: https://github.com/LIJI32/SameBoy +- Pan Docs: Timing section +``` + +## Test Results Before/After + +### Before +- Blargg: 12/12 (100%) +- Mooneye: 9/39 (23%) + +### After +- Blargg: 12/12 (100%) ✅ No regression +- Mooneye: 9/39 (23%) - No change (expected) + +## Conclusion + +The "quick wins" are correctly implemented but have zero impact on test results because: + +1. **All failing Mooneye tests require M-cycle granularity** +2. **Our CPU executes instructions atomically** +3. **No amount of peripheral fixes will help without CPU refactor** + +The fixes should still be committed as they're technically correct and necessary prerequisites for future timing accuracy work. The path to 100% Mooneye pass rate goes through M-cycle accurate CPU execution - there's no shortcut. + +--- + +**Status:** Ready to commit (with updated expectations) +**Next Major Task:** M-cycle accurate CPU architecture +**Realistic Timeline:** 3-5 days for 50-85% Mooneye pass rate diff --git a/docs/ROM_CHECK_ANALYSIS.md b/docs/ROM_CHECK_ANALYSIS.md new file mode 100644 index 0000000..a5c8fda --- /dev/null +++ b/docs/ROM_CHECK_ANALYSIS.md @@ -0,0 +1,539 @@ +# PHPBoy ROM Check Test Analysis + +**Date:** 2025-11-08 +**Current Status:** 9/39 Mooneye tests passing (23%), 12/12 Blargg tests passing (100%) + +## Executive Summary + +PHPBoy has excellent **instruction correctness** (100% Blargg pass rate) but lacks **timing accuracy** (23% Mooneye pass rate). The main issues are: + +1. **DMA runs 4x too fast** - treating T-cycles as M-cycles +2. **RETI doesn't enable interrupts** - missing IME enable +3. **Timer uses wrong model** - counter accumulation vs bit-selection +4. **CPU executes atomically** - no M-cycle boundaries + +Quick wins (Fixes 1-3) can improve from 23% to 41-46% with ~5 hours of work. + +--- + +## Current Test Results + +### Blargg CPU Instruction Tests: 12/12 ✅ +- 01-special.gb ✅ +- 02-interrupts.gb ✅ +- 03-op sp,hl.gb ✅ +- 04-op r,imm.gb ✅ +- 05-op rp.gb ✅ +- 06-ld r,r.gb ✅ +- 07-jr,jp,call,ret,rst.gb ✅ +- 08-misc instrs.gb ✅ +- 09-op r,r.gb ✅ +- 10-bit ops.gb ✅ +- 11-op a,(hl).gb ✅ +- instr_timing.gb ✅ + +### Mooneye Acceptance Tests: 9/39 (23%) + +**Passing (9 tests):** +- di_timing-GS ✅ +- halt_ime0_ei ✅ +- halt_ime0_nointr_timing ✅ +- halt_ime1_timing ✅ +- if_ie_registers ✅ +- intr_timing ✅ +- instr/daa ✅ +- timer/tim00_div_trigger ✅ +- timer/tim01 ✅ +- timer/tim11_div_trigger ✅ + +**Failing by Category:** + +**Instruction Timing (12 tests):** +- add_sp_e_timing ❌ +- call_cc_timing ❌ +- call_timing ❌ +- jp_cc_timing ❌ +- jp_timing ❌ +- ld_hl_sp_e_timing ❌ +- pop_timing ❌ +- push_timing ❌ +- ret_cc_timing ❌ +- ret_timing ❌ +- reti_timing ❌ +- rst_timing ❌ + +**Interrupt/EI Timing (4 tests):** +- ei_sequence ❌ +- ei_timing ❌ +- rapid_di_ei ❌ +- reti_intr_timing ❌ + +**OAM DMA Timing (3 tests):** +- oam_dma_restart ❌ +- oam_dma_start ❌ +- oam_dma_timing ❌ + +**Timer Edge Cases (13 tests):** +- timer/div_write ❌ +- timer/rapid_toggle ❌ +- timer/tim00 ❌ +- timer/tim01_div_trigger ❌ +- timer/tim10 ❌ +- timer/tim10_div_trigger ❌ +- timer/tim11 ❌ +- timer/tima_reload ❌ +- timer/tima_write_reloading ❌ +- timer/tma_write_reloading ❌ + +--- + +## Root Cause Analysis + +### 1. OAM DMA Running 4x Too Fast (CRITICAL BUG) + +**Location:** `src/Dma/OamDma.php:119` + +**Problem:** DMA `tick()` receives T-cycles from CPU but treats them as M-cycles, making DMA complete in 160 T-cycles instead of 640 T-cycles (160 M-cycles). + +**Current Implementation:** +```php +public function tick(int $cycles): void +{ + if (!$this->dmaActive) { + return; + } + + // Transfer one byte per M-cycle + for ($i = 0; $i < $cycles; $i++) { // ← Treats $cycles as M-cycles + if ($this->dmaProgress >= self::TRANSFER_LENGTH) { + $this->dmaActive = false; + break; + } + // ... transfer logic + } +} +``` + +**Call Site (Emulator.php:309-310):** +```php +$cycles = $this->cpu->step(); // Returns T-cycles (e.g., 4, 8, 12) +$this->oamDma?->tick($cycles); // ← Passes T-cycles +``` + +**Issue:** The loop iterates `$cycles` times (T-cycles), but should iterate `$cycles / 4` times (M-cycles). + +**Impact:** Fixes ~2-3 tests +- oam_dma_timing ❌ +- oam_dma_start ❌ + +--- + +### 2. RETI Doesn't Enable Interrupts (CRITICAL BUG) + +**Location:** `src/Cpu/InstructionSet.php:3407-3422` + +**Problem:** RETI instruction returns from interrupt but doesn't set IME flag. + +**Current Implementation:** +```php +0xD9 => new Instruction( + opcode: 0xD9, + mnemonic: 'RETI', + cycles: 16, + handler: static function (Cpu $cpu): int { + $low = $cpu->getBus()->readByte($cpu->getSP()->get()); + $cpu->getSP()->increment(); + $high = $cpu->getBus()->readByte($cpu->getSP()->get()); + $cpu->getSP()->increment(); + $cpu->getPC()->set(($high << 8) | $low); + // Missing: Should enable IME immediately! + return 16; + }, +), +``` + +**SameBoy Implementation:** +```c +static void reti(GB_gameboy_t *gb, uint8_t opcode) { + ret(gb, opcode); + gb->ime = true; // ← Immediate enable +} +``` + +**Our Implementation:** Missing the `gb->ime = true` line! + +**Issue:** RETI should enable interrupts immediately (not with 1-instruction delay like EI). + +**Impact:** Fixes 2 tests +- reti_timing ❌ +- reti_intr_timing ❌ + +--- + +### 3. DMA Start Delay Missing + +**Location:** `src/Dma/OamDma.php:119-146` + +**Problem:** DMA starts transferring bytes immediately in the same M-cycle that DMA is triggered. According to Pan Docs, there should be a 1 M-cycle delay before first byte transfer. + +**Expected Behavior:** +1. **Write to 0xFF46:** Triggers DMA +2. **Delay:** 1 M-cycle delay before first byte transfer +3. **Transfer:** 160 M-cycles to transfer 160 bytes +4. **Total:** 161 M-cycles from trigger to completion + +**Current Behavior:** Transfer starts immediately, no startup delay. + +**Impact:** Fixes 0-1 tests +- oam_dma_start ❌ (may already be fixed by Fix #1) + +--- + +### 4. Timer Uses Wrong Architecture + +**Location:** `src/Timer/Timer.php` + +**Problem:** Uses cycle accumulation instead of bit-selection from DIV counter. + +**Current Approach:** +```php +$this->timaCounter += $cycles; +if ($this->timaCounter >= $frequency) { + $this->incrementTima(); +} +``` + +**SameBoy Approach:** +- Uses bit-selection: `TAC_TRIGGER_BITS[] = {512, 8, 32, 128}` +- Detects falling edge: "TIMA increases when a specific high-bit becomes a low-bit" +- Implements 3-state reload machine (RUNNING → RELOADING → RELOADED) + +**Why This Matters:** +- Writing to DIV can trigger TIMA increment (glitch behavior) +- Changing TAC can cause immediate TIMA increment if selected bit was high +- TIMA reload takes 4 M-cycles, during which writes to TIMA/TMA have edge cases + +**Impact:** Affects 13 timer tests (requires medium-hard effort) + +--- + +### 5. CPU Executes Instructions Atomically + +**Location:** `src/Cpu/InstructionSet.php` (entire file) + +**Problem:** Instructions execute all-at-once and return total cycles. Real hardware performs operations incrementally over M-cycles. + +**Example - CALL instruction:** +- **Current:** Reads address, pushes PC, jumps → returns 24 cycles atomically +- **Real hardware:** 6 M-cycles with state changes at specific boundaries + +**SameBoy Approach:** +- Uses `cycle_read()` and `cycle_write()` with `pending_cycles = 4` +- Memory operations happen at specific M-cycle boundaries +- Enables accurate modeling of register access conflicts + +**Impact:** Affects 12 instruction timing tests (requires major refactor) + +--- + +## Comparison with SameBoy + +| Feature | PHPBoy | SameBoy | Impact | +|---------|--------|---------|--------| +| **Instruction execution** | Atomic | M-cycle stepping | 12 timing tests | +| **RETI enables IME** | ❌ No | ✅ Immediate | 2 tests | +| **DMA speed** | ❌ 4x too fast | ✅ Correct | 3 tests | +| **DMA start delay** | ❌ No delay | ✅ 1 M-cycle | 1 test | +| **Timer model** | Counter accumulation | Bit-selection + edge detection | 13 tests | +| **TIMA reload** | Instant | 4 M-cycle state machine | 3 tests | +| **Cycle tracking** | T-cycles | T-cycles with M-cycle conversion | Foundation | + +--- + +## Prioritized Fix Roadmap + +### Quick Wins (4-5 hours work, +7-8 tests) + +**Fix 1: OAM DMA Speed** ⭐ Easy (30 min, +2-3 tests) +- **File:** `src/Dma/OamDma.php:119` +- **Change:** Convert T-cycles to M-cycles before transferring +- **Expected:** 11-12/39 tests passing (28-31%) + +**Fix 2: RETI IME Enable** ⭐ Easy (15 min, +2 tests) +- **Files:** `src/Cpu/Cpu.php`, `src/Cpu/InstructionSet.php:3418` +- **Change:** Add `setIMEImmediate()` and call in RETI +- **Expected:** 13-14/39 tests passing (33-36%) + +**Fix 3: DMA Start Delay** ⭐⭐ Medium (1 hour, +0-1 tests) +- **File:** `src/Dma/OamDma.php` +- **Change:** Add 1 M-cycle delay before first byte transfer +- **Expected:** 14-17/39 tests passing (36-44%) + +**Combined Expected Result:** 16-18/39 tests passing (41-46%) + +--- + +### Medium Effort (8 hours work, +10-11 tests) + +**Fix 4: TIMA Reload Delay** ⭐⭐ Medium (2 hours, +3 tests) +- **File:** `src/Timer/Timer.php` +- **Change:** Implement 4 M-cycle reload state machine +- **Expected:** +3 tests (tima_reload, tima_write_reloading, tma_write_reloading) + +**Fix 5: Timer Bit-Selection** ⭐⭐⭐ Hard (4 hours, +4 tests) +- **File:** `src/Timer/Timer.php` +- **Change:** Rewrite timer to use 16-bit DIV counter with bit-selection +- **Expected:** +4 tests (tim*_div_trigger tests) + +**Combined Expected Result:** 20-22/39 tests passing (51-56%) + +--- + +### Major Refactor (24+ hours work, +12 tests) + +**Fix 6: M-Cycle Accurate CPU** ⭐⭐⭐⭐ Very Hard (16+ hours, +12 tests) +- **File:** `src/Cpu/InstructionSet.php` (complete rewrite) +- **Change:** Implement M-cycle stepping for all 256 instructions +- **Expected:** All 12 instruction timing tests + +**Expected Result:** 32-34/39 tests passing (82-87%) + +--- + +## Recommended Implementation Order + +**Phase 1 (Today):** Fixes 1-3 → Get to 41-46% with minimal risk + +**Phase 2 (This week):** Fixes 4-5 → Get to 51-56% with moderate effort + +**Phase 3 (Next sprint):** Fix 6 → Get to 82-87% with architectural changes + +**Remaining gaps:** +- PPU timing edge cases +- APU timing (not tested by current suite) +- Hardware quirks/glitches + +--- + +## Specific Code Changes + +### Fix 1: OAM DMA Speed + +**File:** `src/Dma/OamDma.php:119` + +```php +public function tick(int $cycles): void +{ + if (!$this->dmaActive) { + return; + } + + // Convert T-cycles to M-cycles (1 M-cycle = 4 T-cycles) + $mCycles = intdiv($cycles, 4); + + // Transfer one byte per M-cycle + for ($i = 0; $i < $mCycles; $i++) { + if ($this->dmaProgress >= self::TRANSFER_LENGTH) { + $this->dmaActive = false; + break; + } + + // Read from source and write to OAM + $sourceAddress = $this->dmaSource + $this->dmaProgress; + $destAddress = self::OAM_START + $this->dmaProgress; + $value = $this->bus->readByte($sourceAddress); + $this->bus->writeByte($destAddress, $value); + + $this->dmaProgress++; + } +} +``` + +--- + +### Fix 2: RETI IME Enable + +**File 1:** `src/Cpu/Cpu.php` (add new method) + +```php +/** + * Enable interrupts immediately (used by RETI). + * Unlike setIME(true), this enables IME without a 1-instruction delay. + */ +public function setIMEImmediate(): void +{ + $this->ime = true; + $this->imeDelay = 0; +} +``` + +**File 2:** `src/Cpu/InstructionSet.php:3407-3422` (update RETI handler) + +```php +0xD9 => new Instruction( + opcode: 0xD9, + mnemonic: 'RETI', + cycles: 16, + handler: static function (Cpu $cpu): int { + $low = $cpu->getBus()->readByte($cpu->getSP()->get()); + $cpu->getSP()->increment(); + $high = $cpu->getBus()->readByte($cpu->getSP()->get()); + $cpu->getSP()->increment(); + $cpu->getPC()->set(($high << 8) | $low); + + // RETI enables interrupts immediately (not delayed like EI) + $cpu->setIMEImmediate(); + + return 16; + }, +), +``` + +--- + +### Fix 3: DMA Start Delay + +**File:** `src/Dma/OamDma.php` (update class) + +```php +final class OamDma +{ + private const OAM_START = 0xFE00; + private const TRANSFER_LENGTH = 160; + + private bool $dmaActive = false; + private int $dmaSource = 0x0000; + private int $dmaProgress = 0; + private int $dmaDelay = 0; // ← Add delay counter + + // ... other methods ... + + private function startDmaTransfer(int $sourcePage): void + { + $this->dmaSource = ($sourcePage << 8) & 0xFF00; + $this->dmaActive = true; + $this->dmaProgress = 0; + $this->dmaDelay = 1; // ← 1 M-cycle delay before first byte + } + + public function tick(int $cycles): void + { + if (!$this->dmaActive) { + return; + } + + // Convert T-cycles to M-cycles (1 M-cycle = 4 T-cycles) + $mCycles = intdiv($cycles, 4); + + // Handle startup delay + if ($this->dmaDelay > 0) { + $delayToProcess = min($this->dmaDelay, $mCycles); + $this->dmaDelay -= $delayToProcess; + $mCycles -= $delayToProcess; + + if ($mCycles <= 0) { + return; // Still in delay phase + } + } + + // Transfer one byte per M-cycle + for ($i = 0; $i < $mCycles; $i++) { + if ($this->dmaProgress >= self::TRANSFER_LENGTH) { + $this->dmaActive = false; + break; + } + + // Read from source and write to OAM + $sourceAddress = $this->dmaSource + $this->dmaProgress; + $destAddress = self::OAM_START + $this->dmaProgress; + $value = $this->bus->readByte($sourceAddress); + $this->bus->writeByte($destAddress, $value); + + $this->dmaProgress++; + } + } +} +``` + +--- + +## Testing Strategy + +### After Each Fix + +```bash +# Test DMA fixes +make test -- --filter="oam_dma" + +# Test RETI fix +make test -- --filter="reti" + +# Full Mooneye suite +make test -- tests/Integration/MooneyeTestRomsTest.php + +# Check overall progress +make test -- tests/Integration/ +``` + +### Track Progress + +- **Baseline:** 9/39 (23%) +- **After Fix 1:** ~11-12/39 (28-31%) +- **After Fix 2:** ~13-14/39 (33-36%) +- **After Fix 3:** ~14-17/39 (36-44%) +- **Target (Quick Wins):** 16-18/39 (41-46%) + +--- + +## References + +### SameBoy Implementation Study + +**Timer (bit-selection model):** +- Uses `TAC_TRIGGER_BITS[] = {512, 8, 32, 128}` for bit positions +- Detects falling edge: "TIMA increases when a specific high-bit becomes a low-bit" +- Implements 3-state reload machine via `advance_tima_state_machine()` + +**CPU Timing:** +- Uses `cycle_read()` and `cycle_write()` with `pending_cycles = 4` +- M-cycle stepping, not atomic execution +- Hardware conflict simulation for accurate register access timing + +**RETI Instruction:** +```c +static void reti(GB_gameboy_t *gb, uint8_t opcode) { + ret(gb, opcode); + gb->ime = true; // Immediate enable +} +``` + +**DMA:** +- Explicit M-cycle tracking +- Startup delay before first byte transfer +- 160 M-cycles (640 T-cycles) for full transfer + +### Documentation + +- **Pan Docs:** https://gbdev.io/pandocs/ +- **SameBoy Source:** https://github.com/LIJI32/SameBoy +- **PHPBoy Research:** `docs/research.md` + +--- + +## Conclusion + +PHPBoy has a solid foundation with 100% Blargg test pass rate, proving instruction correctness. The Mooneye failures are all **timing accuracy** issues, not logic bugs. + +The quick wins (Fixes 1-3) are low-risk, high-impact changes that can nearly double the Mooneye pass rate in a single afternoon. These fixes align PHPBoy's timing model with SameBoy's proven approach. + +**Next Steps:** +1. Implement Fix 1 (DMA speed) - 30 minutes +2. Implement Fix 2 (RETI IME) - 15 minutes +3. Implement Fix 3 (DMA delay) - 1 hour +4. Run full test suite and measure improvement +5. Commit with detailed conventional commit message + +**Long-term:** +- Medium effort fixes (4-5) → 51-56% pass rate +- Major CPU refactor (6) → 82-87% pass rate +- Final edge cases → 90-100% pass rate diff --git a/src/Cpu/Cpu.php b/src/Cpu/Cpu.php index 7dea36d..9c7e57a 100644 --- a/src/Cpu/Cpu.php +++ b/src/Cpu/Cpu.php @@ -40,6 +40,10 @@ final class Cpu private bool $ime = false; // Interrupt Master Enable private int $imeDelay = 0; // EI instruction has 1-instruction delay + // M-cycle tracking (1 M-cycle = 4 T-cycles) + private int $pendingCycles = 0; + private ?\Closure $cycleCallback = null; + /** * @param BusInterface $bus Memory bus for reading/writing memory * @param InterruptController $interruptController Interrupt controller @@ -65,11 +69,16 @@ public function __construct( * Execute one instruction and return the number of cycles consumed. * * Handles interrupts, HALT, and the EI delay. + * With M-cycle accuracy, cycles are advanced during execution via callbacks, + * but we still return the total for compatibility. * * @return int Number of CPU cycles consumed (typically 4, 8, 12, 16, 20, or 24) */ public function step(): int { + // Track total cycles for return value + $cyclesStart = $this->pendingCycles; + // Handle EI delay (IME is enabled after the instruction following EI) if ($this->imeDelay > 0) { $this->imeDelay--; @@ -83,7 +92,10 @@ public function step(): int $interrupt = $this->interruptController->getPendingInterrupt(); if ($interrupt !== null) { $this->halted = false; // Wake from HALT - return $this->serviceInterrupt($interrupt); + $this->serviceInterrupt($interrupt); + $this->flushPendingCycles(); + $totalCycles = 20; // Interrupt service always takes 20 T-cycles + return $totalCycles; } } @@ -96,6 +108,7 @@ public function step(): int // For now, we'll implement the simple behavior } else { // Still halted, consume 4 cycles + $this->advanceCycles(4); return 4; } } @@ -104,16 +117,26 @@ public function step(): int // Expected: 3-7% performance gain by removing decode() and execute() method calls $opcode = $this->fetch(); $instruction = InstructionSet::getInstruction($opcode); - return ($instruction->handler)($this); + $cycles = ($instruction->handler)($this); + + // Flush any remaining pending cycles + $this->flushPendingCycles(); + + return $cycles; } /** * Service an interrupt by pushing PC to stack and jumping to the vector. * + * Timing breakdown (5 M-cycles = 20 T-cycles): + * - 2 M-cycles: Internal delay + * - 1 M-cycle: Write PC high byte to stack + * - 1 M-cycle: Write PC low byte to stack + * - 1 M-cycle: Jump to interrupt vector + * * @param \Gb\Interrupts\InterruptType $interrupt The interrupt to service - * @return int Number of CPU cycles consumed (20) */ - private function serviceInterrupt(\Gb\Interrupts\InterruptType $interrupt): int + private function serviceInterrupt(\Gb\Interrupts\InterruptType $interrupt): void { // Disable IME $this->ime = false; @@ -122,28 +145,32 @@ private function serviceInterrupt(\Gb\Interrupts\InterruptType $interrupt): int // Acknowledge the interrupt $this->interruptController->acknowledgeInterrupt($interrupt); - // Push PC to stack (takes 2 M-cycles = 8 T-cycles) + // Internal delay: 2 M-cycles + $this->cycleNoAccess(); + $this->cycleNoAccess(); + + // Push PC to stack: 2 M-cycles (1 per write) $pc = $this->pc->get(); $this->sp->decrement(); - $this->bus->writeByte($this->sp->get(), ($pc >> 8) & 0xFF); // High byte + $this->cycleWrite($this->sp->get(), ($pc >> 8) & 0xFF); // High byte $this->sp->decrement(); - $this->bus->writeByte($this->sp->get(), $pc & 0xFF); // Low byte + $this->cycleWrite($this->sp->get(), $pc & 0xFF); // Low byte - // Jump to interrupt vector (takes 1 M-cycle = 4 T-cycles) + // Jump to interrupt vector: 1 M-cycle $this->pc->set($interrupt->getVector()); - - // Total: 5 M-cycles = 20 T-cycles - return 20; + $this->cycleNoAccess(); } /** * Fetch the next opcode byte from memory at PC and increment PC. * + * Takes 1 M-cycle (4 T-cycles) to read from memory. + * * @return int The opcode byte (0x00-0xFF) */ public function fetch(): int { - $opcode = $this->bus->readByte($this->pc->get()); + $opcode = $this->cycleRead($this->pc->get()); $this->pc->increment(); return $opcode; } @@ -447,6 +474,18 @@ public function setIME(bool $ime): void } } + /** + * Enable interrupts immediately without delay. + * + * Used by the RETI instruction, which enables IME immediately + * (unlike EI which has a 1-instruction delay). + */ + public function setIMEImmediate(): void + { + $this->ime = true; + $this->imeDelay = 0; + } + /** * Get the interrupt controller. * @@ -456,4 +495,104 @@ public function getInterruptController(): InterruptController { return $this->interruptController; } + + /** + * Set the cycle callback for M-cycle accurate emulation. + * + * The callback is invoked whenever cycles are advanced, allowing other + * components (PPU, APU, Timer, etc.) to update in real-time during + * instruction execution. + * + * @param \Closure $callback Function that takes int $cycles as parameter + */ + public function setCycleCallback(\Closure $callback): void + { + $this->cycleCallback = $callback; + } + + /** + * Advance CPU cycles and invoke the cycle callback. + * + * This is the core of M-cycle accurate emulation - cycles are advanced + * incrementally during instruction execution, not just at the end. + * + * @param int $cycles Number of T-cycles to advance (typically 4 per M-cycle) + */ + private function advanceCycles(int $cycles): void + { + if ($this->cycleCallback !== null) { + ($this->cycleCallback)($cycles); + } + } + + /** + * Flush any pending cycles. + * + * Called before branches, interrupts, or other control flow changes. + */ + private function flushPendingCycles(): void + { + if ($this->pendingCycles > 0) { + $this->advanceCycles($this->pendingCycles); + $this->pendingCycles = 0; + } + } + + /** + * Read a byte from memory with M-cycle accurate timing. + * + * This function: + * 1. Flushes any pending cycles + * 2. Reads the byte from memory + * 3. Sets pending cycles to 4 (1 M-cycle) + * + * @param int $address Memory address to read from + * @return int The byte value read + */ + public function cycleRead(int $address): int + { + $this->flushPendingCycles(); + $value = $this->bus->readByte($address); + $this->pendingCycles = 4; + return $value; + } + + /** + * Write a byte to memory with M-cycle accurate timing. + * + * This function: + * 1. Flushes any pending cycles + * 2. Writes the byte to memory + * 3. Sets pending cycles to 4 (1 M-cycle) + * + * @param int $address Memory address to write to + * @param int $value Byte value to write + */ + public function cycleWrite(int $address, int $value): void + { + $this->flushPendingCycles(); + $this->bus->writeByte($address, $value); + $this->pendingCycles = 4; + } + + /** + * Internal operation with no memory access (1 M-cycle). + * + * Used for ALU operations, internal register transfers, etc. + * Accumulates cycles without flushing. + */ + public function cycleNoAccess(): void + { + $this->pendingCycles += 4; + } + + /** + * Get pending cycles count. + * + * @return int Number of pending T-cycles + */ + public function getPendingCycles(): int + { + return $this->pendingCycles; + } } diff --git a/src/Cpu/InstructionSet.php b/src/Cpu/InstructionSet.php index 8ba58bf..ee2e09f 100644 --- a/src/Cpu/InstructionSet.php +++ b/src/Cpu/InstructionSet.php @@ -1,4161 +1,4349 @@ - Cached instruction table */ - private static array $instructions = []; - - /** @var array Cached CB-prefixed instruction table */ - private static array $cbInstructions = []; - - /** - * Pre-build all instructions to eliminate lazy initialization overhead. - * - * Optimization (Step 14): Build all 512 instructions upfront during initialization. - * Trade-off: ~100KB additional memory for faster instruction dispatch (no isset check). - * Expected: 1-2% performance gain by eliminating branch prediction overhead. - * - * Call this during emulator initialization for best performance. - */ - public static function warmCache(): void - { - // Pre-build all 256 base instructions - for ($opcode = 0x00; $opcode <= 0xFF; $opcode++) { - if (!isset(self::$instructions[$opcode])) { - self::$instructions[$opcode] = self::buildInstruction($opcode); - } - } - - // Pre-build all 256 CB-prefixed instructions - for ($opcode = 0x00; $opcode <= 0xFF; $opcode++) { - if (!isset(self::$cbInstructions[$opcode])) { - self::$cbInstructions[$opcode] = self::buildCBInstruction($opcode); - } - } - } - - /** - * Get instruction metadata for a given opcode. - * - * @param int $opcode The opcode byte (0x00-0xFF) - * @return Instruction The instruction metadata and handler - * @throws RuntimeException If opcode is not implemented - */ - public static function getInstruction(int $opcode): Instruction - { - if (!isset(self::$instructions[$opcode])) { - self::$instructions[$opcode] = self::buildInstruction($opcode); - } - - return self::$instructions[$opcode]; - } - - /** - * Get CB-prefixed instruction metadata for a given opcode. - * - * @param int $opcode The CB opcode byte (0x00-0xFF) - * @return Instruction The instruction metadata and handler - * @throws RuntimeException If opcode is not implemented - */ - public static function getCBInstruction(int $opcode): Instruction - { - if (!isset(self::$cbInstructions[$opcode])) { - self::$cbInstructions[$opcode] = self::buildCBInstruction($opcode); - } - - return self::$cbInstructions[$opcode]; - } - - /** - * Helper: Read next byte from PC and increment PC - */ - private static function readImm8(Cpu $cpu): int - { - return $cpu->fetch(); - } - - /** - * Helper: Read next word (16-bit) from PC and increment PC twice - */ - private static function readImm16(Cpu $cpu): int - { - $low = $cpu->fetch(); - $high = $cpu->fetch(); - return ($high << 8) | $low; - } - - /** - * Helper: Check for half-carry on 8-bit addition - */ - private static function halfCarry8Add(int $a, int $b, int $carry = 0): bool - { - return ((($a & 0x0F) + ($b & 0x0F) + $carry) & 0x10) !== 0; - } - - /** - * Helper: Check for half-carry on 8-bit subtraction - */ - private static function halfCarry8Sub(int $a, int $b, int $carry = 0): bool - { - return ((($a & 0x0F) - ($b & 0x0F) - $carry) & 0x10) !== 0; - } - - /** - * Helper: Check for half-carry on 16-bit addition (bit 11 to bit 12) - */ - private static function halfCarry16Add(int $a, int $b): bool - { - return ((($a & 0x0FFF) + ($b & 0x0FFF)) & 0x1000) !== 0; - } - - /** - * Build instruction metadata for a given opcode. - * - * @param int $opcode The opcode byte - * @return Instruction The instruction - * @throws RuntimeException If opcode is not implemented - */ - private static function buildInstruction(int $opcode): Instruction - { - return match ($opcode) { - // 0x00: NOP - No operation - 0x00 => new Instruction( - opcode: 0x00, - mnemonic: 'NOP', - length: 1, - cycles: 4, - handler: static fn(Cpu $cpu): int => 4, - ), - - // 0x01: LD BC,nn - Load 16-bit immediate into BC - 0x01 => new Instruction( - opcode: 0x01, - mnemonic: 'LD BC,nn', - length: 3, - cycles: 12, - handler: static function (Cpu $cpu): int { - $value = self::readImm16($cpu); - $cpu->getBC()->set($value); - return 12; - }, - ), - - // 0x02: LD (BC),A - Store A into memory at address BC - 0x02 => new Instruction( - opcode: 0x02, - mnemonic: 'LD (BC),A', - length: 1, - cycles: 8, - handler: static function (Cpu $cpu): int { - $address = $cpu->getBC()->get(); - $cpu->getBus()->writeByte($address, $cpu->getA()); - return 8; - }, - ), - - // 0x03: INC BC - Increment BC - 0x03 => new Instruction( - opcode: 0x03, - mnemonic: 'INC BC', - length: 1, - cycles: 8, - handler: static function (Cpu $cpu): int { - $cpu->getBC()->increment(); - return 8; - }, - ), - - // 0x04: INC B - Increment B - 0x04 => new Instruction( - opcode: 0x04, - mnemonic: 'INC B', - length: 1, - cycles: 4, - handler: static function (Cpu $cpu): int { - $value = $cpu->getB(); - $result = ($value + 1) & 0xFF; - $cpu->setB($result); - $cpu->getFlags()->setZ($result === 0); - $cpu->getFlags()->setN(false); - $cpu->getFlags()->setH((($value & 0x0F) + 1) > 0x0F); - return 4; - }, - ), - - // 0x05: DEC B - Decrement B - 0x05 => new Instruction( - opcode: 0x05, - mnemonic: 'DEC B', - length: 1, - cycles: 4, - handler: static function (Cpu $cpu): int { - $value = $cpu->getB(); - $result = ($value - 1) & 0xFF; - $cpu->setB($result); - $cpu->getFlags()->setZ($result === 0); - $cpu->getFlags()->setN(true); - $cpu->getFlags()->setH(($value & 0x0F) === 0); - return 4; - }, - ), - - // 0x06: LD B,n - Load 8-bit immediate into B - 0x06 => new Instruction( - opcode: 0x06, - mnemonic: 'LD B,n', - length: 2, - cycles: 8, - handler: static function (Cpu $cpu): int { - $cpu->setB(self::readImm8($cpu)); - return 8; - }, - ), - - // 0x07: RLCA - Rotate A left, old bit 7 to carry - 0x07 => new Instruction( - opcode: 0x07, - mnemonic: 'RLCA', - length: 1, - cycles: 4, - handler: static function (Cpu $cpu): int { - $value = $cpu->getA(); - $carry = ($value & 0x80) !== 0; - $result = (($value << 1) & 0xFF) | ($carry ? 1 : 0); - $cpu->setA($result); - $cpu->getFlags()->setZ(false); - $cpu->getFlags()->setN(false); - $cpu->getFlags()->setH(false); - $cpu->getFlags()->setC($carry); - return 4; - }, - ), - - // 0x08: LD (nn),SP - Store SP at address nn - 0x08 => new Instruction( - opcode: 0x08, - mnemonic: 'LD (nn),SP', - length: 3, - cycles: 20, - handler: static function (Cpu $cpu): int { - $address = self::readImm16($cpu); - $sp = $cpu->getSP()->get(); - $cpu->getBus()->writeByte($address, $sp & 0xFF); - $cpu->getBus()->writeByte($address + 1, ($sp >> 8) & 0xFF); - return 20; - }, - ), - - // 0x09: ADD HL,BC - Add BC to HL - 0x09 => new Instruction( - opcode: 0x09, - mnemonic: 'ADD HL,BC', - length: 1, - cycles: 8, - handler: static function (Cpu $cpu): int { - $hl = $cpu->getHL()->get(); - $bc = $cpu->getBC()->get(); - $result = $hl + $bc; - $cpu->getHL()->set($result & 0xFFFF); - $cpu->getFlags()->setN(false); - $cpu->getFlags()->setH(self::halfCarry16Add($hl, $bc)); - $cpu->getFlags()->setC($result > 0xFFFF); - return 8; - }, - ), - - // 0x0A: LD A,(BC) - Load byte at address BC into A - 0x0A => new Instruction( - opcode: 0x0A, - mnemonic: 'LD A,(BC)', - length: 1, - cycles: 8, - handler: static function (Cpu $cpu): int { - $address = $cpu->getBC()->get(); - $cpu->setA($cpu->getBus()->readByte($address)); - return 8; - }, - ), - - // 0x0B: DEC BC - Decrement BC - 0x0B => new Instruction( - opcode: 0x0B, - mnemonic: 'DEC BC', - length: 1, - cycles: 8, - handler: static function (Cpu $cpu): int { - $cpu->getBC()->decrement(); - return 8; - }, - ), - - // 0x0C: INC C - Increment C - 0x0C => new Instruction( - opcode: 0x0C, - mnemonic: 'INC C', - length: 1, - cycles: 4, - handler: static function (Cpu $cpu): int { - $value = $cpu->getC(); - $result = ($value + 1) & 0xFF; - $cpu->setC($result); - $cpu->getFlags()->setZ($result === 0); - $cpu->getFlags()->setN(false); - $cpu->getFlags()->setH((($value & 0x0F) + 1) > 0x0F); - return 4; - }, - ), - - // 0x0D: DEC C - Decrement C - 0x0D => new Instruction( - opcode: 0x0D, - mnemonic: 'DEC C', - length: 1, - cycles: 4, - handler: static function (Cpu $cpu): int { - $value = $cpu->getC(); - $result = ($value - 1) & 0xFF; - $cpu->setC($result); - $cpu->getFlags()->setZ($result === 0); - $cpu->getFlags()->setN(true); - $cpu->getFlags()->setH(($value & 0x0F) === 0); - return 4; - }, - ), - - // 0x0E: LD C,n - Load 8-bit immediate into C - 0x0E => new Instruction( - opcode: 0x0E, - mnemonic: 'LD C,n', - length: 2, - cycles: 8, - handler: static function (Cpu $cpu): int { - $cpu->setC(self::readImm8($cpu)); - return 8; - }, - ), - - // 0x0F: RRCA - Rotate A right, old bit 0 to carry - 0x0F => new Instruction( - opcode: 0x0F, - mnemonic: 'RRCA', - length: 1, - cycles: 4, - handler: static function (Cpu $cpu): int { - $value = $cpu->getA(); - $carry = ($value & 0x01) !== 0; - $result = ($value >> 1) | ($carry ? 0x80 : 0); - $cpu->setA($result); - $cpu->getFlags()->setZ(false); - $cpu->getFlags()->setN(false); - $cpu->getFlags()->setH(false); - $cpu->getFlags()->setC($carry); - return 4; - }, - ), - - // 0x10: STOP - Stop CPU and LCD until button press - 0x10 => new Instruction( - opcode: 0x10, - mnemonic: 'STOP', - length: 2, - cycles: 4, - handler: static function (Cpu $cpu): int { - // Read next byte (should be 0x00) - self::readImm8($cpu); - $cpu->setHalted(true); - $cpu->setStopped(true); - return 4; - }, - ), - - // 0x11: LD DE,nn - Load 16-bit immediate into DE - 0x11 => new Instruction( - opcode: 0x11, - mnemonic: 'LD DE,nn', - length: 3, - cycles: 12, - handler: static function (Cpu $cpu): int { - $value = self::readImm16($cpu); - $cpu->getDE()->set($value); - return 12; - }, - ), - - // 0x12: LD (DE),A - Store A into memory at address DE - 0x12 => new Instruction( - opcode: 0x12, - mnemonic: 'LD (DE),A', - length: 1, - cycles: 8, - handler: static function (Cpu $cpu): int { - $address = $cpu->getDE()->get(); - $cpu->getBus()->writeByte($address, $cpu->getA()); - return 8; - }, - ), - - // 0x13: INC DE - Increment DE - 0x13 => new Instruction( - opcode: 0x13, - mnemonic: 'INC DE', - length: 1, - cycles: 8, - handler: static function (Cpu $cpu): int { - $cpu->getDE()->increment(); - return 8; - }, - ), - - // 0x14: INC D - Increment D - 0x14 => new Instruction( - opcode: 0x14, - mnemonic: 'INC D', - length: 1, - cycles: 4, - handler: static function (Cpu $cpu): int { - $value = $cpu->getD(); - $result = ($value + 1) & 0xFF; - $cpu->setD($result); - $cpu->getFlags()->setZ($result === 0); - $cpu->getFlags()->setN(false); - $cpu->getFlags()->setH((($value & 0x0F) + 1) > 0x0F); - return 4; - }, - ), - - // 0x15: DEC D - Decrement D - 0x15 => new Instruction( - opcode: 0x15, - mnemonic: 'DEC D', - length: 1, - cycles: 4, - handler: static function (Cpu $cpu): int { - $value = $cpu->getD(); - $result = ($value - 1) & 0xFF; - $cpu->setD($result); - $cpu->getFlags()->setZ($result === 0); - $cpu->getFlags()->setN(true); - $cpu->getFlags()->setH(($value & 0x0F) === 0); - return 4; - }, - ), - - // 0x16: LD D,n - Load 8-bit immediate into D - 0x16 => new Instruction( - opcode: 0x16, - mnemonic: 'LD D,n', - length: 2, - cycles: 8, - handler: static function (Cpu $cpu): int { - $cpu->setD(self::readImm8($cpu)); - return 8; - }, - ), - - // 0x17: RLA - Rotate A left through carry - 0x17 => new Instruction( - opcode: 0x17, - mnemonic: 'RLA', - length: 1, - cycles: 4, - handler: static function (Cpu $cpu): int { - $value = $cpu->getA(); - $oldCarry = $cpu->getFlags()->getC(); - $newCarry = ($value & 0x80) !== 0; - $result = (($value << 1) & 0xFF) | ($oldCarry ? 1 : 0); - $cpu->setA($result); - $cpu->getFlags()->setZ(false); - $cpu->getFlags()->setN(false); - $cpu->getFlags()->setH(false); - $cpu->getFlags()->setC($newCarry); - return 4; - }, - ), - - // 0x18: JR e - Relative jump - 0x18 => new Instruction( - opcode: 0x18, - mnemonic: 'JR e', - length: 2, - cycles: 12, - handler: static function (Cpu $cpu): int { - $offset = self::readImm8($cpu); - // Sign extend - if ($offset > 0x7F) { - $offset -= 0x100; - } - $pc = $cpu->getPC()->get(); - $cpu->getPC()->set($pc + $offset); - return 12; - }, - ), - - // 0x19: ADD HL,DE - Add DE to HL - 0x19 => new Instruction( - opcode: 0x19, - mnemonic: 'ADD HL,DE', - length: 1, - cycles: 8, - handler: static function (Cpu $cpu): int { - $hl = $cpu->getHL()->get(); - $de = $cpu->getDE()->get(); - $result = $hl + $de; - $cpu->getHL()->set($result & 0xFFFF); - $cpu->getFlags()->setN(false); - $cpu->getFlags()->setH(self::halfCarry16Add($hl, $de)); - $cpu->getFlags()->setC($result > 0xFFFF); - return 8; - }, - ), - - // 0x1A: LD A,(DE) - Load byte at address DE into A - 0x1A => new Instruction( - opcode: 0x1A, - mnemonic: 'LD A,(DE)', - length: 1, - cycles: 8, - handler: static function (Cpu $cpu): int { - $address = $cpu->getDE()->get(); - $cpu->setA($cpu->getBus()->readByte($address)); - return 8; - }, - ), - - // 0x1B: DEC DE - Decrement DE - 0x1B => new Instruction( - opcode: 0x1B, - mnemonic: 'DEC DE', - length: 1, - cycles: 8, - handler: static function (Cpu $cpu): int { - $cpu->getDE()->decrement(); - return 8; - }, - ), - - // 0x1C: INC E - Increment E - 0x1C => new Instruction( - opcode: 0x1C, - mnemonic: 'INC E', - length: 1, - cycles: 4, - handler: static function (Cpu $cpu): int { - $value = $cpu->getE(); - $result = ($value + 1) & 0xFF; - $cpu->setE($result); - $cpu->getFlags()->setZ($result === 0); - $cpu->getFlags()->setN(false); - $cpu->getFlags()->setH((($value & 0x0F) + 1) > 0x0F); - return 4; - }, - ), - - // 0x1D: DEC E - Decrement E - 0x1D => new Instruction( - opcode: 0x1D, - mnemonic: 'DEC E', - length: 1, - cycles: 4, - handler: static function (Cpu $cpu): int { - $value = $cpu->getE(); - $result = ($value - 1) & 0xFF; - $cpu->setE($result); - $cpu->getFlags()->setZ($result === 0); - $cpu->getFlags()->setN(true); - $cpu->getFlags()->setH(($value & 0x0F) === 0); - return 4; - }, - ), - - // 0x1E: LD E,n - Load 8-bit immediate into E - 0x1E => new Instruction( - opcode: 0x1E, - mnemonic: 'LD E,n', - length: 2, - cycles: 8, - handler: static function (Cpu $cpu): int { - $cpu->setE(self::readImm8($cpu)); - return 8; - }, - ), - - // 0x1F: RRA - Rotate A right through carry - 0x1F => new Instruction( - opcode: 0x1F, - mnemonic: 'RRA', - length: 1, - cycles: 4, - handler: static function (Cpu $cpu): int { - $value = $cpu->getA(); - $oldCarry = $cpu->getFlags()->getC(); - $newCarry = ($value & 0x01) !== 0; - $result = ($value >> 1) | ($oldCarry ? 0x80 : 0); - $cpu->setA($result); - $cpu->getFlags()->setZ(false); - $cpu->getFlags()->setN(false); - $cpu->getFlags()->setH(false); - $cpu->getFlags()->setC($newCarry); - return 4; - }, - ), - - // 0x20: JR NZ,e - Relative jump if not zero - 0x20 => new Instruction( - opcode: 0x20, - mnemonic: 'JR NZ,e', - length: 2, - cycles: 8, // 12 if taken, 8 if not taken - handler: static function (Cpu $cpu): int { - $offset = self::readImm8($cpu); - if (!$cpu->getFlags()->getZ()) { - // Sign extend - if ($offset > 0x7F) { - $offset -= 0x100; - } - $pc = $cpu->getPC()->get(); - $cpu->getPC()->set($pc + $offset); - return 12; - } - return 8; - }, - ), - - // 0x21: LD HL,nn - Load 16-bit immediate into HL - 0x21 => new Instruction( - opcode: 0x21, - mnemonic: 'LD HL,nn', - length: 3, - cycles: 12, - handler: static function (Cpu $cpu): int { - $value = self::readImm16($cpu); - $cpu->getHL()->set($value); - return 12; - }, - ), - - // 0x22: LD (HL+),A - Store A at HL, increment HL - 0x22 => new Instruction( - opcode: 0x22, - mnemonic: 'LD (HL+),A', - length: 1, - cycles: 8, - handler: static function (Cpu $cpu): int { - $address = $cpu->getHL()->get(); - $cpu->getBus()->writeByte($address, $cpu->getA()); - $cpu->getHL()->increment(); - return 8; - }, - ), - - // 0x23: INC HL - Increment HL - 0x23 => new Instruction( - opcode: 0x23, - mnemonic: 'INC HL', - length: 1, - cycles: 8, - handler: static function (Cpu $cpu): int { - $cpu->getHL()->increment(); - return 8; - }, - ), - - // 0x24: INC H - Increment H - 0x24 => new Instruction( - opcode: 0x24, - mnemonic: 'INC H', - length: 1, - cycles: 4, - handler: static function (Cpu $cpu): int { - $value = $cpu->getH(); - $result = ($value + 1) & 0xFF; - $cpu->setH($result); - $cpu->getFlags()->setZ($result === 0); - $cpu->getFlags()->setN(false); - $cpu->getFlags()->setH((($value & 0x0F) + 1) > 0x0F); - return 4; - }, - ), - - // 0x25: DEC H - Decrement H - 0x25 => new Instruction( - opcode: 0x25, - mnemonic: 'DEC H', - length: 1, - cycles: 4, - handler: static function (Cpu $cpu): int { - $value = $cpu->getH(); - $result = ($value - 1) & 0xFF; - $cpu->setH($result); - $cpu->getFlags()->setZ($result === 0); - $cpu->getFlags()->setN(true); - $cpu->getFlags()->setH(($value & 0x0F) === 0); - return 4; - }, - ), - - // 0x26: LD H,n - Load 8-bit immediate into H - 0x26 => new Instruction( - opcode: 0x26, - mnemonic: 'LD H,n', - length: 2, - cycles: 8, - handler: static function (Cpu $cpu): int { - $cpu->setH(self::readImm8($cpu)); - return 8; - }, - ), - - // 0x27: DAA - Decimal Adjust Accumulator - 0x27 => new Instruction( - opcode: 0x27, - mnemonic: 'DAA', - length: 1, - cycles: 4, - handler: static function (Cpu $cpu): int { - $a = $cpu->getA(); - $flags = $cpu->getFlags(); - - if (!$flags->getN()) { - // After addition (ADD, ADC, INC) - // Upper nibble first - if ($flags->getC() || $a > 0x99) { - $a = ($a + 0x60) & 0xFF; - $flags->setC(true); - } - // Lower nibble second - if ($flags->getH() || ($a & 0x0F) > 0x09) { - $a = ($a + 0x06) & 0xFF; - } - } else { - // After subtraction (SUB, SBC, DEC) - // Carry first - if ($flags->getC()) { - $a = ($a - 0x60) & 0xFF; - } - // Half-carry second - if ($flags->getH()) { - $a = ($a - 0x06) & 0xFF; - } - // Carry unchanged in subtraction - } - - $cpu->setA($a); - $flags->setZ($a === 0); - $flags->setH(false); - // N flag unchanged (not set here) - // C flag already set in addition mode, unchanged in subtraction mode - return 4; - }, - ), - - // 0x28: JR Z,e - Relative jump if zero - 0x28 => new Instruction( - opcode: 0x28, - mnemonic: 'JR Z,e', - length: 2, - cycles: 8, // 12 if taken, 8 if not taken - handler: static function (Cpu $cpu): int { - $offset = self::readImm8($cpu); - if ($cpu->getFlags()->getZ()) { - // Sign extend - if ($offset > 0x7F) { - $offset -= 0x100; - } - $pc = $cpu->getPC()->get(); - $cpu->getPC()->set($pc + $offset); - return 12; - } - return 8; - }, - ), - - // 0x29: ADD HL,HL - Add HL to HL - 0x29 => new Instruction( - opcode: 0x29, - mnemonic: 'ADD HL,HL', - length: 1, - cycles: 8, - handler: static function (Cpu $cpu): int { - $hl = $cpu->getHL()->get(); - $result = $hl + $hl; - $cpu->getHL()->set($result & 0xFFFF); - $cpu->getFlags()->setN(false); - $cpu->getFlags()->setH(self::halfCarry16Add($hl, $hl)); - $cpu->getFlags()->setC($result > 0xFFFF); - return 8; - }, - ), - - // 0x2A: LD A,(HL+) - Load byte at HL into A, increment HL - 0x2A => new Instruction( - opcode: 0x2A, - mnemonic: 'LD A,(HL+)', - length: 1, - cycles: 8, - handler: static function (Cpu $cpu): int { - $address = $cpu->getHL()->get(); - $cpu->setA($cpu->getBus()->readByte($address)); - $cpu->getHL()->increment(); - return 8; - }, - ), - - // 0x2B: DEC HL - Decrement HL - 0x2B => new Instruction( - opcode: 0x2B, - mnemonic: 'DEC HL', - length: 1, - cycles: 8, - handler: static function (Cpu $cpu): int { - $cpu->getHL()->decrement(); - return 8; - }, - ), - - // 0x2C: INC L - Increment L - 0x2C => new Instruction( - opcode: 0x2C, - mnemonic: 'INC L', - length: 1, - cycles: 4, - handler: static function (Cpu $cpu): int { - $value = $cpu->getL(); - $result = ($value + 1) & 0xFF; - $cpu->setL($result); - $cpu->getFlags()->setZ($result === 0); - $cpu->getFlags()->setN(false); - $cpu->getFlags()->setH((($value & 0x0F) + 1) > 0x0F); - return 4; - }, - ), - - // 0x2D: DEC L - Decrement L - 0x2D => new Instruction( - opcode: 0x2D, - mnemonic: 'DEC L', - length: 1, - cycles: 4, - handler: static function (Cpu $cpu): int { - $value = $cpu->getL(); - $result = ($value - 1) & 0xFF; - $cpu->setL($result); - $cpu->getFlags()->setZ($result === 0); - $cpu->getFlags()->setN(true); - $cpu->getFlags()->setH(($value & 0x0F) === 0); - return 4; - }, - ), - - // 0x2E: LD L,n - Load 8-bit immediate into L - 0x2E => new Instruction( - opcode: 0x2E, - mnemonic: 'LD L,n', - length: 2, - cycles: 8, - handler: static function (Cpu $cpu): int { - $cpu->setL(self::readImm8($cpu)); - return 8; - }, - ), - - // 0x2F: CPL - Complement A (flip all bits) - 0x2F => new Instruction( - opcode: 0x2F, - mnemonic: 'CPL', - length: 1, - cycles: 4, - handler: static function (Cpu $cpu): int { - $cpu->setA($cpu->getA() ^ 0xFF); - $cpu->getFlags()->setN(true); - $cpu->getFlags()->setH(true); - return 4; - }, - ), - - // 0x30: JR NC,e - Relative jump if not carry - 0x30 => new Instruction( - opcode: 0x30, - mnemonic: 'JR NC,e', - length: 2, - cycles: 8, // 12 if taken, 8 if not taken - handler: static function (Cpu $cpu): int { - $offset = self::readImm8($cpu); - if (!$cpu->getFlags()->getC()) { - // Sign extend - if ($offset > 0x7F) { - $offset -= 0x100; - } - $pc = $cpu->getPC()->get(); - $cpu->getPC()->set($pc + $offset); - return 12; - } - return 8; - }, - ), - - // 0x31: LD SP,nn - Load 16-bit immediate into SP - 0x31 => new Instruction( - opcode: 0x31, - mnemonic: 'LD SP,nn', - length: 3, - cycles: 12, - handler: static function (Cpu $cpu): int { - $value = self::readImm16($cpu); - $cpu->getSP()->set($value); - return 12; - }, - ), - - // 0x32: LD (HL-),A - Store A at HL, decrement HL - 0x32 => new Instruction( - opcode: 0x32, - mnemonic: 'LD (HL-),A', - length: 1, - cycles: 8, - handler: static function (Cpu $cpu): int { - $address = $cpu->getHL()->get(); - $cpu->getBus()->writeByte($address, $cpu->getA()); - $cpu->getHL()->decrement(); - return 8; - }, - ), - - // 0x33: INC SP - Increment SP - 0x33 => new Instruction( - opcode: 0x33, - mnemonic: 'INC SP', - length: 1, - cycles: 8, - handler: static function (Cpu $cpu): int { - $cpu->getSP()->increment(); - return 8; - }, - ), - - // 0x34: INC (HL) - Increment byte at address HL - 0x34 => new Instruction( - opcode: 0x34, - mnemonic: 'INC (HL)', - length: 1, - cycles: 12, - handler: static function (Cpu $cpu): int { - $address = $cpu->getHL()->get(); - $value = $cpu->getBus()->readByte($address); - $result = ($value + 1) & 0xFF; - $cpu->getBus()->writeByte($address, $result); - $cpu->getFlags()->setZ($result === 0); - $cpu->getFlags()->setN(false); - $cpu->getFlags()->setH((($value & 0x0F) + 1) > 0x0F); - return 12; - }, - ), - - // 0x35: DEC (HL) - Decrement byte at address HL - 0x35 => new Instruction( - opcode: 0x35, - mnemonic: 'DEC (HL)', - length: 1, - cycles: 12, - handler: static function (Cpu $cpu): int { - $address = $cpu->getHL()->get(); - $value = $cpu->getBus()->readByte($address); - $result = ($value - 1) & 0xFF; - $cpu->getBus()->writeByte($address, $result); - $cpu->getFlags()->setZ($result === 0); - $cpu->getFlags()->setN(true); - $cpu->getFlags()->setH(($value & 0x0F) === 0); - return 12; - }, - ), - - // 0x36: LD (HL),n - Load immediate byte into address HL - 0x36 => new Instruction( - opcode: 0x36, - mnemonic: 'LD (HL),n', - length: 2, - cycles: 12, - handler: static function (Cpu $cpu): int { - $value = self::readImm8($cpu); - $address = $cpu->getHL()->get(); - $cpu->getBus()->writeByte($address, $value); - return 12; - }, - ), - - // 0x37: SCF - Set Carry Flag - 0x37 => new Instruction( - opcode: 0x37, - mnemonic: 'SCF', - length: 1, - cycles: 4, - handler: static function (Cpu $cpu): int { - $cpu->getFlags()->setN(false); - $cpu->getFlags()->setH(false); - $cpu->getFlags()->setC(true); - return 4; - }, - ), - - // 0x38: JR C,e - Relative jump if carry - 0x38 => new Instruction( - opcode: 0x38, - mnemonic: 'JR C,e', - length: 2, - cycles: 8, // 12 if taken, 8 if not taken - handler: static function (Cpu $cpu): int { - $offset = self::readImm8($cpu); - if ($cpu->getFlags()->getC()) { - // Sign extend - if ($offset > 0x7F) { - $offset -= 0x100; - } - $pc = $cpu->getPC()->get(); - $cpu->getPC()->set($pc + $offset); - return 12; - } - return 8; - }, - ), - - // 0x39: ADD HL,SP - Add SP to HL - 0x39 => new Instruction( - opcode: 0x39, - mnemonic: 'ADD HL,SP', - length: 1, - cycles: 8, - handler: static function (Cpu $cpu): int { - $hl = $cpu->getHL()->get(); - $sp = $cpu->getSP()->get(); - $result = $hl + $sp; - $cpu->getHL()->set($result & 0xFFFF); - $cpu->getFlags()->setN(false); - $cpu->getFlags()->setH(self::halfCarry16Add($hl, $sp)); - $cpu->getFlags()->setC($result > 0xFFFF); - return 8; - }, - ), - - // 0x3A: LD A,(HL-) - Load byte at HL into A, decrement HL - 0x3A => new Instruction( - opcode: 0x3A, - mnemonic: 'LD A,(HL-)', - length: 1, - cycles: 8, - handler: static function (Cpu $cpu): int { - $address = $cpu->getHL()->get(); - $cpu->setA($cpu->getBus()->readByte($address)); - $cpu->getHL()->decrement(); - return 8; - }, - ), - - // 0x3B: DEC SP - Decrement SP - 0x3B => new Instruction( - opcode: 0x3B, - mnemonic: 'DEC SP', - length: 1, - cycles: 8, - handler: static function (Cpu $cpu): int { - $cpu->getSP()->decrement(); - return 8; - }, - ), - - // 0x3C: INC A - Increment A - 0x3C => new Instruction( - opcode: 0x3C, - mnemonic: 'INC A', - length: 1, - cycles: 4, - handler: static function (Cpu $cpu): int { - $value = $cpu->getA(); - $result = ($value + 1) & 0xFF; - $cpu->setA($result); - $cpu->getFlags()->setZ($result === 0); - $cpu->getFlags()->setN(false); - $cpu->getFlags()->setH((($value & 0x0F) + 1) > 0x0F); - return 4; - }, - ), - - // 0x3D: DEC A - Decrement A - 0x3D => new Instruction( - opcode: 0x3D, - mnemonic: 'DEC A', - length: 1, - cycles: 4, - handler: static function (Cpu $cpu): int { - $value = $cpu->getA(); - $result = ($value - 1) & 0xFF; - $cpu->setA($result); - $cpu->getFlags()->setZ($result === 0); - $cpu->getFlags()->setN(true); - $cpu->getFlags()->setH(($value & 0x0F) === 0); - return 4; - }, - ), - - // 0x3E: LD A,n - Load 8-bit immediate into A - 0x3E => new Instruction( - opcode: 0x3E, - mnemonic: 'LD A,n', - length: 2, - cycles: 8, - handler: static function (Cpu $cpu): int { - $cpu->setA(self::readImm8($cpu)); - return 8; - }, - ), - - // 0x3F: CCF - Complement Carry Flag - 0x3F => new Instruction( - opcode: 0x3F, - mnemonic: 'CCF', - length: 1, - cycles: 4, - handler: static function (Cpu $cpu): int { - $cpu->getFlags()->setN(false); - $cpu->getFlags()->setH(false); - $cpu->getFlags()->setC(!$cpu->getFlags()->getC()); - return 4; - }, - ), - - // 0x40-0x7F: LD r,r instructions (load register to register) - // B=0, C=1, D=2, E=3, H=4, L=5, (HL)=6, A=7 - - // 0x40: LD B,B - 0x40 => new Instruction( - opcode: 0x40, - mnemonic: 'LD B,B', - length: 1, - cycles: 4, - handler: static fn(Cpu $cpu): int => 4, - ), - - // 0x41: LD B,C - 0x41 => new Instruction( - opcode: 0x41, - mnemonic: 'LD B,C', - length: 1, - cycles: 4, - handler: static function (Cpu $cpu): int { - $cpu->setB($cpu->getC()); - return 4; - }, - ), - - // 0x42: LD B,D - 0x42 => new Instruction( - opcode: 0x42, - mnemonic: 'LD B,D', - length: 1, - cycles: 4, - handler: static function (Cpu $cpu): int { - $cpu->setB($cpu->getD()); - return 4; - }, - ), - - // 0x43: LD B,E - 0x43 => new Instruction( - opcode: 0x43, - mnemonic: 'LD B,E', - length: 1, - cycles: 4, - handler: static function (Cpu $cpu): int { - $cpu->setB($cpu->getE()); - return 4; - }, - ), - - // 0x44: LD B,H - 0x44 => new Instruction( - opcode: 0x44, - mnemonic: 'LD B,H', - length: 1, - cycles: 4, - handler: static function (Cpu $cpu): int { - $cpu->setB($cpu->getH()); - return 4; - }, - ), - - // 0x45: LD B,L - 0x45 => new Instruction( - opcode: 0x45, - mnemonic: 'LD B,L', - length: 1, - cycles: 4, - handler: static function (Cpu $cpu): int { - $cpu->setB($cpu->getL()); - return 4; - }, - ), - - // 0x46: LD B,(HL) - 0x46 => new Instruction( - opcode: 0x46, - mnemonic: 'LD B,(HL)', - length: 1, - cycles: 8, - handler: static function (Cpu $cpu): int { - $address = $cpu->getHL()->get(); - $cpu->setB($cpu->getBus()->readByte($address)); - return 8; - }, - ), - - // 0x47: LD B,A - 0x47 => new Instruction( - opcode: 0x47, - mnemonic: 'LD B,A', - length: 1, - cycles: 4, - handler: static function (Cpu $cpu): int { - $cpu->setB($cpu->getA()); - return 4; - }, - ), - - // 0x48: LD C,B - 0x48 => new Instruction( - opcode: 0x48, - mnemonic: 'LD C,B', - length: 1, - cycles: 4, - handler: static function (Cpu $cpu): int { - $cpu->setC($cpu->getB()); - return 4; - }, - ), - - // 0x49: LD C,C - 0x49 => new Instruction( - opcode: 0x49, - mnemonic: 'LD C,C', - length: 1, - cycles: 4, - handler: static fn(Cpu $cpu): int => 4, - ), - - // 0x4A: LD C,D - 0x4A => new Instruction( - opcode: 0x4A, - mnemonic: 'LD C,D', - length: 1, - cycles: 4, - handler: static function (Cpu $cpu): int { - $cpu->setC($cpu->getD()); - return 4; - }, - ), - - // 0x4B: LD C,E - 0x4B => new Instruction( - opcode: 0x4B, - mnemonic: 'LD C,E', - length: 1, - cycles: 4, - handler: static function (Cpu $cpu): int { - $cpu->setC($cpu->getE()); - return 4; - }, - ), - - // 0x4C: LD C,H - 0x4C => new Instruction( - opcode: 0x4C, - mnemonic: 'LD C,H', - length: 1, - cycles: 4, - handler: static function (Cpu $cpu): int { - $cpu->setC($cpu->getH()); - return 4; - }, - ), - - // 0x4D: LD C,L - 0x4D => new Instruction( - opcode: 0x4D, - mnemonic: 'LD C,L', - length: 1, - cycles: 4, - handler: static function (Cpu $cpu): int { - $cpu->setC($cpu->getL()); - return 4; - }, - ), - - // 0x4E: LD C,(HL) - 0x4E => new Instruction( - opcode: 0x4E, - mnemonic: 'LD C,(HL)', - length: 1, - cycles: 8, - handler: static function (Cpu $cpu): int { - $address = $cpu->getHL()->get(); - $cpu->setC($cpu->getBus()->readByte($address)); - return 8; - }, - ), - - // 0x4F: LD C,A - 0x4F => new Instruction( - opcode: 0x4F, - mnemonic: 'LD C,A', - length: 1, - cycles: 4, - handler: static function (Cpu $cpu): int { - $cpu->setC($cpu->getA()); - return 4; - }, - ), - - // 0x50: LD D,B - 0x50 => new Instruction( - opcode: 0x50, - mnemonic: 'LD D,B', - length: 1, - cycles: 4, - handler: static function (Cpu $cpu): int { - $cpu->setD($cpu->getB()); - return 4; - }, - ), - - // 0x51: LD D,C - 0x51 => new Instruction( - opcode: 0x51, - mnemonic: 'LD D,C', - length: 1, - cycles: 4, - handler: static function (Cpu $cpu): int { - $cpu->setD($cpu->getC()); - return 4; - }, - ), - - // 0x52: LD D,D - 0x52 => new Instruction( - opcode: 0x52, - mnemonic: 'LD D,D', - length: 1, - cycles: 4, - handler: static fn(Cpu $cpu): int => 4, - ), - - // 0x53: LD D,E - 0x53 => new Instruction( - opcode: 0x53, - mnemonic: 'LD D,E', - length: 1, - cycles: 4, - handler: static function (Cpu $cpu): int { - $cpu->setD($cpu->getE()); - return 4; - }, - ), - - // 0x54: LD D,H - 0x54 => new Instruction( - opcode: 0x54, - mnemonic: 'LD D,H', - length: 1, - cycles: 4, - handler: static function (Cpu $cpu): int { - $cpu->setD($cpu->getH()); - return 4; - }, - ), - - // 0x55: LD D,L - 0x55 => new Instruction( - opcode: 0x55, - mnemonic: 'LD D,L', - length: 1, - cycles: 4, - handler: static function (Cpu $cpu): int { - $cpu->setD($cpu->getL()); - return 4; - }, - ), - - // 0x56: LD D,(HL) - 0x56 => new Instruction( - opcode: 0x56, - mnemonic: 'LD D,(HL)', - length: 1, - cycles: 8, - handler: static function (Cpu $cpu): int { - $address = $cpu->getHL()->get(); - $cpu->setD($cpu->getBus()->readByte($address)); - return 8; - }, - ), - - // 0x57: LD D,A - 0x57 => new Instruction( - opcode: 0x57, - mnemonic: 'LD D,A', - length: 1, - cycles: 4, - handler: static function (Cpu $cpu): int { - $cpu->setD($cpu->getA()); - return 4; - }, - ), - - // 0x58: LD E,B - 0x58 => new Instruction( - opcode: 0x58, - mnemonic: 'LD E,B', - length: 1, - cycles: 4, - handler: static function (Cpu $cpu): int { - $cpu->setE($cpu->getB()); - return 4; - }, - ), - - // 0x59: LD E,C - 0x59 => new Instruction( - opcode: 0x59, - mnemonic: 'LD E,C', - length: 1, - cycles: 4, - handler: static function (Cpu $cpu): int { - $cpu->setE($cpu->getC()); - return 4; - }, - ), - - // 0x5A: LD E,D - 0x5A => new Instruction( - opcode: 0x5A, - mnemonic: 'LD E,D', - length: 1, - cycles: 4, - handler: static function (Cpu $cpu): int { - $cpu->setE($cpu->getD()); - return 4; - }, - ), - - // 0x5B: LD E,E - 0x5B => new Instruction( - opcode: 0x5B, - mnemonic: 'LD E,E', - length: 1, - cycles: 4, - handler: static fn(Cpu $cpu): int => 4, - ), - - // 0x5C: LD E,H - 0x5C => new Instruction( - opcode: 0x5C, - mnemonic: 'LD E,H', - length: 1, - cycles: 4, - handler: static function (Cpu $cpu): int { - $cpu->setE($cpu->getH()); - return 4; - }, - ), - - // 0x5D: LD E,L - 0x5D => new Instruction( - opcode: 0x5D, - mnemonic: 'LD E,L', - length: 1, - cycles: 4, - handler: static function (Cpu $cpu): int { - $cpu->setE($cpu->getL()); - return 4; - }, - ), - - // 0x5E: LD E,(HL) - 0x5E => new Instruction( - opcode: 0x5E, - mnemonic: 'LD E,(HL)', - length: 1, - cycles: 8, - handler: static function (Cpu $cpu): int { - $address = $cpu->getHL()->get(); - $cpu->setE($cpu->getBus()->readByte($address)); - return 8; - }, - ), - - // 0x5F: LD E,A - 0x5F => new Instruction( - opcode: 0x5F, - mnemonic: 'LD E,A', - length: 1, - cycles: 4, - handler: static function (Cpu $cpu): int { - $cpu->setE($cpu->getA()); - return 4; - }, - ), - - // 0x60: LD H,B - 0x60 => new Instruction( - opcode: 0x60, - mnemonic: 'LD H,B', - length: 1, - cycles: 4, - handler: static function (Cpu $cpu): int { - $cpu->setH($cpu->getB()); - return 4; - }, - ), - - // 0x61: LD H,C - 0x61 => new Instruction( - opcode: 0x61, - mnemonic: 'LD H,C', - length: 1, - cycles: 4, - handler: static function (Cpu $cpu): int { - $cpu->setH($cpu->getC()); - return 4; - }, - ), - - // 0x62: LD H,D - 0x62 => new Instruction( - opcode: 0x62, - mnemonic: 'LD H,D', - length: 1, - cycles: 4, - handler: static function (Cpu $cpu): int { - $cpu->setH($cpu->getD()); - return 4; - }, - ), - - // 0x63: LD H,E - 0x63 => new Instruction( - opcode: 0x63, - mnemonic: 'LD H,E', - length: 1, - cycles: 4, - handler: static function (Cpu $cpu): int { - $cpu->setH($cpu->getE()); - return 4; - }, - ), - - // 0x64: LD H,H - 0x64 => new Instruction( - opcode: 0x64, - mnemonic: 'LD H,H', - length: 1, - cycles: 4, - handler: static fn(Cpu $cpu): int => 4, - ), - - // 0x65: LD H,L - 0x65 => new Instruction( - opcode: 0x65, - mnemonic: 'LD H,L', - length: 1, - cycles: 4, - handler: static function (Cpu $cpu): int { - $cpu->setH($cpu->getL()); - return 4; - }, - ), - - // 0x66: LD H,(HL) - 0x66 => new Instruction( - opcode: 0x66, - mnemonic: 'LD H,(HL)', - length: 1, - cycles: 8, - handler: static function (Cpu $cpu): int { - $address = $cpu->getHL()->get(); - $cpu->setH($cpu->getBus()->readByte($address)); - return 8; - }, - ), - - // 0x67: LD H,A - 0x67 => new Instruction( - opcode: 0x67, - mnemonic: 'LD H,A', - length: 1, - cycles: 4, - handler: static function (Cpu $cpu): int { - $cpu->setH($cpu->getA()); - return 4; - }, - ), - - // 0x68: LD L,B - 0x68 => new Instruction( - opcode: 0x68, - mnemonic: 'LD L,B', - length: 1, - cycles: 4, - handler: static function (Cpu $cpu): int { - $cpu->setL($cpu->getB()); - return 4; - }, - ), - - // 0x69: LD L,C - 0x69 => new Instruction( - opcode: 0x69, - mnemonic: 'LD L,C', - length: 1, - cycles: 4, - handler: static function (Cpu $cpu): int { - $cpu->setL($cpu->getC()); - return 4; - }, - ), - - // 0x6A: LD L,D - 0x6A => new Instruction( - opcode: 0x6A, - mnemonic: 'LD L,D', - length: 1, - cycles: 4, - handler: static function (Cpu $cpu): int { - $cpu->setL($cpu->getD()); - return 4; - }, - ), - - // 0x6B: LD L,E - 0x6B => new Instruction( - opcode: 0x6B, - mnemonic: 'LD L,E', - length: 1, - cycles: 4, - handler: static function (Cpu $cpu): int { - $cpu->setL($cpu->getE()); - return 4; - }, - ), - - // 0x6C: LD L,H - 0x6C => new Instruction( - opcode: 0x6C, - mnemonic: 'LD L,H', - length: 1, - cycles: 4, - handler: static function (Cpu $cpu): int { - $cpu->setL($cpu->getH()); - return 4; - }, - ), - - // 0x6D: LD L,L - 0x6D => new Instruction( - opcode: 0x6D, - mnemonic: 'LD L,L', - length: 1, - cycles: 4, - handler: static fn(Cpu $cpu): int => 4, - ), - - // 0x6E: LD L,(HL) - 0x6E => new Instruction( - opcode: 0x6E, - mnemonic: 'LD L,(HL)', - length: 1, - cycles: 8, - handler: static function (Cpu $cpu): int { - $address = $cpu->getHL()->get(); - $cpu->setL($cpu->getBus()->readByte($address)); - return 8; - }, - ), - - // 0x6F: LD L,A - 0x6F => new Instruction( - opcode: 0x6F, - mnemonic: 'LD L,A', - length: 1, - cycles: 4, - handler: static function (Cpu $cpu): int { - $cpu->setL($cpu->getA()); - return 4; - }, - ), - - // 0x70: LD (HL),B - 0x70 => new Instruction( - opcode: 0x70, - mnemonic: 'LD (HL),B', - length: 1, - cycles: 8, - handler: static function (Cpu $cpu): int { - $address = $cpu->getHL()->get(); - $cpu->getBus()->writeByte($address, $cpu->getB()); - return 8; - }, - ), - - // 0x71: LD (HL),C - 0x71 => new Instruction( - opcode: 0x71, - mnemonic: 'LD (HL),C', - length: 1, - cycles: 8, - handler: static function (Cpu $cpu): int { - $address = $cpu->getHL()->get(); - $cpu->getBus()->writeByte($address, $cpu->getC()); - return 8; - }, - ), - - // 0x72: LD (HL),D - 0x72 => new Instruction( - opcode: 0x72, - mnemonic: 'LD (HL),D', - length: 1, - cycles: 8, - handler: static function (Cpu $cpu): int { - $address = $cpu->getHL()->get(); - $cpu->getBus()->writeByte($address, $cpu->getD()); - return 8; - }, - ), - - // 0x73: LD (HL),E - 0x73 => new Instruction( - opcode: 0x73, - mnemonic: 'LD (HL),E', - length: 1, - cycles: 8, - handler: static function (Cpu $cpu): int { - $address = $cpu->getHL()->get(); - $cpu->getBus()->writeByte($address, $cpu->getE()); - return 8; - }, - ), - - // 0x74: LD (HL),H - 0x74 => new Instruction( - opcode: 0x74, - mnemonic: 'LD (HL),H', - length: 1, - cycles: 8, - handler: static function (Cpu $cpu): int { - $address = $cpu->getHL()->get(); - $cpu->getBus()->writeByte($address, $cpu->getH()); - return 8; - }, - ), - - // 0x75: LD (HL),L - 0x75 => new Instruction( - opcode: 0x75, - mnemonic: 'LD (HL),L', - length: 1, - cycles: 8, - handler: static function (Cpu $cpu): int { - $address = $cpu->getHL()->get(); - $cpu->getBus()->writeByte($address, $cpu->getL()); - return 8; - }, - ), - - // 0x76: HALT - Halt CPU until interrupt - 0x76 => new Instruction( - opcode: 0x76, - mnemonic: 'HALT', - length: 1, - cycles: 4, - handler: static function (Cpu $cpu): int { - $cpu->setHalted(true); - return 4; - }, - ), - - // 0x77: LD (HL),A - 0x77 => new Instruction( - opcode: 0x77, - mnemonic: 'LD (HL),A', - length: 1, - cycles: 8, - handler: static function (Cpu $cpu): int { - $address = $cpu->getHL()->get(); - $cpu->getBus()->writeByte($address, $cpu->getA()); - return 8; - }, - ), - - // 0x78: LD A,B - 0x78 => new Instruction( - opcode: 0x78, - mnemonic: 'LD A,B', - length: 1, - cycles: 4, - handler: static function (Cpu $cpu): int { - $cpu->setA($cpu->getB()); - return 4; - }, - ), - - // 0x79: LD A,C - 0x79 => new Instruction( - opcode: 0x79, - mnemonic: 'LD A,C', - length: 1, - cycles: 4, - handler: static function (Cpu $cpu): int { - $cpu->setA($cpu->getC()); - return 4; - }, - ), - - // 0x7A: LD A,D - 0x7A => new Instruction( - opcode: 0x7A, - mnemonic: 'LD A,D', - length: 1, - cycles: 4, - handler: static function (Cpu $cpu): int { - $cpu->setA($cpu->getD()); - return 4; - }, - ), - - // 0x7B: LD A,E - 0x7B => new Instruction( - opcode: 0x7B, - mnemonic: 'LD A,E', - length: 1, - cycles: 4, - handler: static function (Cpu $cpu): int { - $cpu->setA($cpu->getE()); - return 4; - }, - ), - - // 0x7C: LD A,H - 0x7C => new Instruction( - opcode: 0x7C, - mnemonic: 'LD A,H', - length: 1, - cycles: 4, - handler: static function (Cpu $cpu): int { - $cpu->setA($cpu->getH()); - return 4; - }, - ), - - // 0x7D: LD A,L - 0x7D => new Instruction( - opcode: 0x7D, - mnemonic: 'LD A,L', - length: 1, - cycles: 4, - handler: static function (Cpu $cpu): int { - $cpu->setA($cpu->getL()); - return 4; - }, - ), - - // 0x7E: LD A,(HL) - 0x7E => new Instruction( - opcode: 0x7E, - mnemonic: 'LD A,(HL)', - length: 1, - cycles: 8, - handler: static function (Cpu $cpu): int { - $address = $cpu->getHL()->get(); - $cpu->setA($cpu->getBus()->readByte($address)); - return 8; - }, - ), - - // 0x7F: LD A,A - 0x7F => new Instruction( - opcode: 0x7F, - mnemonic: 'LD A,A', - length: 1, - cycles: 4, - handler: static fn(Cpu $cpu): int => 4, - ), - - // 0x80-0xBF: ALU instructions with A - - // 0x80: ADD A,B - 0x80 => new Instruction( - opcode: 0x80, - mnemonic: 'ADD A,B', - length: 1, - cycles: 4, - handler: static function (Cpu $cpu): int { - $a = $cpu->getA(); - $b = $cpu->getB(); - $result = $a + $b; - $cpu->setA($result & 0xFF); - $cpu->getFlags()->setZ(($result & 0xFF) === 0); - $cpu->getFlags()->setN(false); - $cpu->getFlags()->setH(self::halfCarry8Add($a, $b)); - $cpu->getFlags()->setC($result > 0xFF); - return 4; - }, - ), - - // 0x81: ADD A,C - 0x81 => new Instruction( - opcode: 0x81, - mnemonic: 'ADD A,C', - length: 1, - cycles: 4, - handler: static function (Cpu $cpu): int { - $a = $cpu->getA(); - $c = $cpu->getC(); - $result = $a + $c; - $cpu->setA($result & 0xFF); - $cpu->getFlags()->setZ(($result & 0xFF) === 0); - $cpu->getFlags()->setN(false); - $cpu->getFlags()->setH(self::halfCarry8Add($a, $c)); - $cpu->getFlags()->setC($result > 0xFF); - return 4; - }, - ), - - // 0x82: ADD A,D - 0x82 => new Instruction( - opcode: 0x82, - mnemonic: 'ADD A,D', - length: 1, - cycles: 4, - handler: static function (Cpu $cpu): int { - $a = $cpu->getA(); - $d = $cpu->getD(); - $result = $a + $d; - $cpu->setA($result & 0xFF); - $cpu->getFlags()->setZ(($result & 0xFF) === 0); - $cpu->getFlags()->setN(false); - $cpu->getFlags()->setH(self::halfCarry8Add($a, $d)); - $cpu->getFlags()->setC($result > 0xFF); - return 4; - }, - ), - - // 0x83: ADD A,E - 0x83 => new Instruction( - opcode: 0x83, - mnemonic: 'ADD A,E', - length: 1, - cycles: 4, - handler: static function (Cpu $cpu): int { - $a = $cpu->getA(); - $e = $cpu->getE(); - $result = $a + $e; - $cpu->setA($result & 0xFF); - $cpu->getFlags()->setZ(($result & 0xFF) === 0); - $cpu->getFlags()->setN(false); - $cpu->getFlags()->setH(self::halfCarry8Add($a, $e)); - $cpu->getFlags()->setC($result > 0xFF); - return 4; - }, - ), - - // 0x84: ADD A,H - 0x84 => new Instruction( - opcode: 0x84, - mnemonic: 'ADD A,H', - length: 1, - cycles: 4, - handler: static function (Cpu $cpu): int { - $a = $cpu->getA(); - $h = $cpu->getH(); - $result = $a + $h; - $cpu->setA($result & 0xFF); - $cpu->getFlags()->setZ(($result & 0xFF) === 0); - $cpu->getFlags()->setN(false); - $cpu->getFlags()->setH(self::halfCarry8Add($a, $h)); - $cpu->getFlags()->setC($result > 0xFF); - return 4; - }, - ), - - // 0x85: ADD A,L - 0x85 => new Instruction( - opcode: 0x85, - mnemonic: 'ADD A,L', - length: 1, - cycles: 4, - handler: static function (Cpu $cpu): int { - $a = $cpu->getA(); - $l = $cpu->getL(); - $result = $a + $l; - $cpu->setA($result & 0xFF); - $cpu->getFlags()->setZ(($result & 0xFF) === 0); - $cpu->getFlags()->setN(false); - $cpu->getFlags()->setH(self::halfCarry8Add($a, $l)); - $cpu->getFlags()->setC($result > 0xFF); - return 4; - }, - ), - - // 0x86: ADD A,(HL) - 0x86 => new Instruction( - opcode: 0x86, - mnemonic: 'ADD A,(HL)', - length: 1, - cycles: 8, - handler: static function (Cpu $cpu): int { - $a = $cpu->getA(); - $address = $cpu->getHL()->get(); - $value = $cpu->getBus()->readByte($address); - $result = $a + $value; - $cpu->setA($result & 0xFF); - $cpu->getFlags()->setZ(($result & 0xFF) === 0); - $cpu->getFlags()->setN(false); - $cpu->getFlags()->setH(self::halfCarry8Add($a, $value)); - $cpu->getFlags()->setC($result > 0xFF); - return 8; - }, - ), - - // 0x87: ADD A,A - 0x87 => new Instruction( - opcode: 0x87, - mnemonic: 'ADD A,A', - length: 1, - cycles: 4, - handler: static function (Cpu $cpu): int { - $a = $cpu->getA(); - $result = $a + $a; - $cpu->setA($result & 0xFF); - $cpu->getFlags()->setZ(($result & 0xFF) === 0); - $cpu->getFlags()->setN(false); - $cpu->getFlags()->setH(self::halfCarry8Add($a, $a)); - $cpu->getFlags()->setC($result > 0xFF); - return 4; - }, - ), - - // 0x88-0x8F: ADC A,r (Add with carry) - 0x88 => new Instruction( - opcode: 0x88, - mnemonic: 'ADC A,B', - length: 1, - cycles: 4, - handler: static function (Cpu $cpu): int { - $a = $cpu->getA(); - $b = $cpu->getB(); - $carry = $cpu->getFlags()->getC() ? 1 : 0; - $result = $a + $b + $carry; - $cpu->setA($result & 0xFF); - $cpu->getFlags()->setZ(($result & 0xFF) === 0); - $cpu->getFlags()->setN(false); - $cpu->getFlags()->setH(self::halfCarry8Add($a, $b, $carry)); - $cpu->getFlags()->setC($result > 0xFF); - return 4; - }, - ), - - 0x89 => new Instruction( - opcode: 0x89, - mnemonic: 'ADC A,C', - length: 1, - cycles: 4, - handler: static function (Cpu $cpu): int { - $a = $cpu->getA(); - $c = $cpu->getC(); - $carry = $cpu->getFlags()->getC() ? 1 : 0; - $result = $a + $c + $carry; - $cpu->setA($result & 0xFF); - $cpu->getFlags()->setZ(($result & 0xFF) === 0); - $cpu->getFlags()->setN(false); - $cpu->getFlags()->setH(self::halfCarry8Add($a, $c, $carry)); - $cpu->getFlags()->setC($result > 0xFF); - return 4; - }, - ), - - 0x8A => new Instruction( - opcode: 0x8A, - mnemonic: 'ADC A,D', - length: 1, - cycles: 4, - handler: static function (Cpu $cpu): int { - $a = $cpu->getA(); - $d = $cpu->getD(); - $carry = $cpu->getFlags()->getC() ? 1 : 0; - $result = $a + $d + $carry; - $cpu->setA($result & 0xFF); - $cpu->getFlags()->setZ(($result & 0xFF) === 0); - $cpu->getFlags()->setN(false); - $cpu->getFlags()->setH(self::halfCarry8Add($a, $d, $carry)); - $cpu->getFlags()->setC($result > 0xFF); - return 4; - }, - ), - - 0x8B => new Instruction( - opcode: 0x8B, - mnemonic: 'ADC A,E', - length: 1, - cycles: 4, - handler: static function (Cpu $cpu): int { - $a = $cpu->getA(); - $e = $cpu->getE(); - $carry = $cpu->getFlags()->getC() ? 1 : 0; - $result = $a + $e + $carry; - $cpu->setA($result & 0xFF); - $cpu->getFlags()->setZ(($result & 0xFF) === 0); - $cpu->getFlags()->setN(false); - $cpu->getFlags()->setH(self::halfCarry8Add($a, $e, $carry)); - $cpu->getFlags()->setC($result > 0xFF); - return 4; - }, - ), - - 0x8C => new Instruction( - opcode: 0x8C, - mnemonic: 'ADC A,H', - length: 1, - cycles: 4, - handler: static function (Cpu $cpu): int { - $a = $cpu->getA(); - $h = $cpu->getH(); - $carry = $cpu->getFlags()->getC() ? 1 : 0; - $result = $a + $h + $carry; - $cpu->setA($result & 0xFF); - $cpu->getFlags()->setZ(($result & 0xFF) === 0); - $cpu->getFlags()->setN(false); - $cpu->getFlags()->setH(self::halfCarry8Add($a, $h, $carry)); - $cpu->getFlags()->setC($result > 0xFF); - return 4; - }, - ), - - 0x8D => new Instruction( - opcode: 0x8D, - mnemonic: 'ADC A,L', - length: 1, - cycles: 4, - handler: static function (Cpu $cpu): int { - $a = $cpu->getA(); - $l = $cpu->getL(); - $carry = $cpu->getFlags()->getC() ? 1 : 0; - $result = $a + $l + $carry; - $cpu->setA($result & 0xFF); - $cpu->getFlags()->setZ(($result & 0xFF) === 0); - $cpu->getFlags()->setN(false); - $cpu->getFlags()->setH(self::halfCarry8Add($a, $l, $carry)); - $cpu->getFlags()->setC($result > 0xFF); - return 4; - }, - ), - - 0x8E => new Instruction( - opcode: 0x8E, - mnemonic: 'ADC A,(HL)', - length: 1, - cycles: 8, - handler: static function (Cpu $cpu): int { - $a = $cpu->getA(); - $address = $cpu->getHL()->get(); - $value = $cpu->getBus()->readByte($address); - $carry = $cpu->getFlags()->getC() ? 1 : 0; - $result = $a + $value + $carry; - $cpu->setA($result & 0xFF); - $cpu->getFlags()->setZ(($result & 0xFF) === 0); - $cpu->getFlags()->setN(false); - $cpu->getFlags()->setH(self::halfCarry8Add($a, $value, $carry)); - $cpu->getFlags()->setC($result > 0xFF); - return 8; - }, - ), - - 0x8F => new Instruction( - opcode: 0x8F, - mnemonic: 'ADC A,A', - length: 1, - cycles: 4, - handler: static function (Cpu $cpu): int { - $a = $cpu->getA(); - $carry = $cpu->getFlags()->getC() ? 1 : 0; - $result = $a + $a + $carry; - $cpu->setA($result & 0xFF); - $cpu->getFlags()->setZ(($result & 0xFF) === 0); - $cpu->getFlags()->setN(false); - $cpu->getFlags()->setH(self::halfCarry8Add($a, $a, $carry)); - $cpu->getFlags()->setC($result > 0xFF); - return 4; - }, - ), - - // 0x90-0x97: SUB r (Subtract) - 0x90 => new Instruction( - opcode: 0x90, - mnemonic: 'SUB B', - length: 1, - cycles: 4, - handler: static function (Cpu $cpu): int { - $a = $cpu->getA(); - $b = $cpu->getB(); - $result = $a - $b; - $cpu->setA($result & 0xFF); - $cpu->getFlags()->setZ(($result & 0xFF) === 0); - $cpu->getFlags()->setN(true); - $cpu->getFlags()->setH(self::halfCarry8Sub($a, $b)); - $cpu->getFlags()->setC($result < 0); - return 4; - }, - ), - - 0x91 => new Instruction( - opcode: 0x91, - mnemonic: 'SUB C', - length: 1, - cycles: 4, - handler: static function (Cpu $cpu): int { - $a = $cpu->getA(); - $c = $cpu->getC(); - $result = $a - $c; - $cpu->setA($result & 0xFF); - $cpu->getFlags()->setZ(($result & 0xFF) === 0); - $cpu->getFlags()->setN(true); - $cpu->getFlags()->setH(self::halfCarry8Sub($a, $c)); - $cpu->getFlags()->setC($result < 0); - return 4; - }, - ), - - 0x92 => new Instruction( - opcode: 0x92, - mnemonic: 'SUB D', - length: 1, - cycles: 4, - handler: static function (Cpu $cpu): int { - $a = $cpu->getA(); - $d = $cpu->getD(); - $result = $a - $d; - $cpu->setA($result & 0xFF); - $cpu->getFlags()->setZ(($result & 0xFF) === 0); - $cpu->getFlags()->setN(true); - $cpu->getFlags()->setH(self::halfCarry8Sub($a, $d)); - $cpu->getFlags()->setC($result < 0); - return 4; - }, - ), - - 0x93 => new Instruction( - opcode: 0x93, - mnemonic: 'SUB E', - length: 1, - cycles: 4, - handler: static function (Cpu $cpu): int { - $a = $cpu->getA(); - $e = $cpu->getE(); - $result = $a - $e; - $cpu->setA($result & 0xFF); - $cpu->getFlags()->setZ(($result & 0xFF) === 0); - $cpu->getFlags()->setN(true); - $cpu->getFlags()->setH(self::halfCarry8Sub($a, $e)); - $cpu->getFlags()->setC($result < 0); - return 4; - }, - ), - - 0x94 => new Instruction( - opcode: 0x94, - mnemonic: 'SUB H', - length: 1, - cycles: 4, - handler: static function (Cpu $cpu): int { - $a = $cpu->getA(); - $h = $cpu->getH(); - $result = $a - $h; - $cpu->setA($result & 0xFF); - $cpu->getFlags()->setZ(($result & 0xFF) === 0); - $cpu->getFlags()->setN(true); - $cpu->getFlags()->setH(self::halfCarry8Sub($a, $h)); - $cpu->getFlags()->setC($result < 0); - return 4; - }, - ), - - 0x95 => new Instruction( - opcode: 0x95, - mnemonic: 'SUB L', - length: 1, - cycles: 4, - handler: static function (Cpu $cpu): int { - $a = $cpu->getA(); - $l = $cpu->getL(); - $result = $a - $l; - $cpu->setA($result & 0xFF); - $cpu->getFlags()->setZ(($result & 0xFF) === 0); - $cpu->getFlags()->setN(true); - $cpu->getFlags()->setH(self::halfCarry8Sub($a, $l)); - $cpu->getFlags()->setC($result < 0); - return 4; - }, - ), - - 0x96 => new Instruction( - opcode: 0x96, - mnemonic: 'SUB (HL)', - length: 1, - cycles: 8, - handler: static function (Cpu $cpu): int { - $a = $cpu->getA(); - $address = $cpu->getHL()->get(); - $value = $cpu->getBus()->readByte($address); - $result = $a - $value; - $cpu->setA($result & 0xFF); - $cpu->getFlags()->setZ(($result & 0xFF) === 0); - $cpu->getFlags()->setN(true); - $cpu->getFlags()->setH(self::halfCarry8Sub($a, $value)); - $cpu->getFlags()->setC($result < 0); - return 8; - }, - ), - - 0x97 => new Instruction( - opcode: 0x97, - mnemonic: 'SUB A', - length: 1, - cycles: 4, - handler: static function (Cpu $cpu): int { - $cpu->setA(0); - $cpu->getFlags()->setZ(true); - $cpu->getFlags()->setN(true); - $cpu->getFlags()->setH(false); - $cpu->getFlags()->setC(false); - return 4; - }, - ), - - // 0x98-0x9F: SBC A,r (Subtract with carry) - 0x98 => new Instruction( - opcode: 0x98, - mnemonic: 'SBC A,B', - length: 1, - cycles: 4, - handler: static function (Cpu $cpu): int { - $a = $cpu->getA(); - $b = $cpu->getB(); - $carry = $cpu->getFlags()->getC() ? 1 : 0; - $result = $a - $b - $carry; - $cpu->setA($result & 0xFF); - $cpu->getFlags()->setZ(($result & 0xFF) === 0); - $cpu->getFlags()->setN(true); - $cpu->getFlags()->setH(self::halfCarry8Sub($a, $b, $carry)); - $cpu->getFlags()->setC($result < 0); - return 4; - }, - ), - - 0x99 => new Instruction( - opcode: 0x99, - mnemonic: 'SBC A,C', - length: 1, - cycles: 4, - handler: static function (Cpu $cpu): int { - $a = $cpu->getA(); - $c = $cpu->getC(); - $carry = $cpu->getFlags()->getC() ? 1 : 0; - $result = $a - $c - $carry; - $cpu->setA($result & 0xFF); - $cpu->getFlags()->setZ(($result & 0xFF) === 0); - $cpu->getFlags()->setN(true); - $cpu->getFlags()->setH(self::halfCarry8Sub($a, $c, $carry)); - $cpu->getFlags()->setC($result < 0); - return 4; - }, - ), - - 0x9A => new Instruction( - opcode: 0x9A, - mnemonic: 'SBC A,D', - length: 1, - cycles: 4, - handler: static function (Cpu $cpu): int { - $a = $cpu->getA(); - $d = $cpu->getD(); - $carry = $cpu->getFlags()->getC() ? 1 : 0; - $result = $a - $d - $carry; - $cpu->setA($result & 0xFF); - $cpu->getFlags()->setZ(($result & 0xFF) === 0); - $cpu->getFlags()->setN(true); - $cpu->getFlags()->setH(self::halfCarry8Sub($a, $d, $carry)); - $cpu->getFlags()->setC($result < 0); - return 4; - }, - ), - - 0x9B => new Instruction( - opcode: 0x9B, - mnemonic: 'SBC A,E', - length: 1, - cycles: 4, - handler: static function (Cpu $cpu): int { - $a = $cpu->getA(); - $e = $cpu->getE(); - $carry = $cpu->getFlags()->getC() ? 1 : 0; - $result = $a - $e - $carry; - $cpu->setA($result & 0xFF); - $cpu->getFlags()->setZ(($result & 0xFF) === 0); - $cpu->getFlags()->setN(true); - $cpu->getFlags()->setH(self::halfCarry8Sub($a, $e, $carry)); - $cpu->getFlags()->setC($result < 0); - return 4; - }, - ), - - 0x9C => new Instruction( - opcode: 0x9C, - mnemonic: 'SBC A,H', - length: 1, - cycles: 4, - handler: static function (Cpu $cpu): int { - $a = $cpu->getA(); - $h = $cpu->getH(); - $carry = $cpu->getFlags()->getC() ? 1 : 0; - $result = $a - $h - $carry; - $cpu->setA($result & 0xFF); - $cpu->getFlags()->setZ(($result & 0xFF) === 0); - $cpu->getFlags()->setN(true); - $cpu->getFlags()->setH(self::halfCarry8Sub($a, $h, $carry)); - $cpu->getFlags()->setC($result < 0); - return 4; - }, - ), - - 0x9D => new Instruction( - opcode: 0x9D, - mnemonic: 'SBC A,L', - length: 1, - cycles: 4, - handler: static function (Cpu $cpu): int { - $a = $cpu->getA(); - $l = $cpu->getL(); - $carry = $cpu->getFlags()->getC() ? 1 : 0; - $result = $a - $l - $carry; - $cpu->setA($result & 0xFF); - $cpu->getFlags()->setZ(($result & 0xFF) === 0); - $cpu->getFlags()->setN(true); - $cpu->getFlags()->setH(self::halfCarry8Sub($a, $l, $carry)); - $cpu->getFlags()->setC($result < 0); - return 4; - }, - ), - - 0x9E => new Instruction( - opcode: 0x9E, - mnemonic: 'SBC A,(HL)', - length: 1, - cycles: 8, - handler: static function (Cpu $cpu): int { - $a = $cpu->getA(); - $address = $cpu->getHL()->get(); - $value = $cpu->getBus()->readByte($address); - $carry = $cpu->getFlags()->getC() ? 1 : 0; - $result = $a - $value - $carry; - $cpu->setA($result & 0xFF); - $cpu->getFlags()->setZ(($result & 0xFF) === 0); - $cpu->getFlags()->setN(true); - $cpu->getFlags()->setH(self::halfCarry8Sub($a, $value, $carry)); - $cpu->getFlags()->setC($result < 0); - return 8; - }, - ), - - 0x9F => new Instruction( - opcode: 0x9F, - mnemonic: 'SBC A,A', - length: 1, - cycles: 4, - handler: static function (Cpu $cpu): int { - $carry = $cpu->getFlags()->getC() ? 1 : 0; - $result = -$carry; - $cpu->setA($result & 0xFF); - $cpu->getFlags()->setZ($result === 0); - $cpu->getFlags()->setN(true); - $cpu->getFlags()->setH($carry !== 0); - $cpu->getFlags()->setC($carry !== 0); - return 4; - }, - ), - - // 0xA0-0xA7: AND r - - 0xA0 => new Instruction( - opcode: 0xA0, - mnemonic: 'AND B', - length: 1, - cycles: 4, - handler: static function (Cpu $cpu): int { - $result = $cpu->getA() & $cpu->getB(); - $cpu->setA($result); - $cpu->getFlags()->setZ($result === 0); - $cpu->getFlags()->setN(false); - $cpu->getFlags()->setH(true); - $cpu->getFlags()->setC(false); - return 4; - }, - ), - - 0xA1 => new Instruction( - opcode: 0xA1, - mnemonic: 'AND C', - length: 1, - cycles: 4, - handler: static function (Cpu $cpu): int { - $result = $cpu->getA() & $cpu->getC(); - $cpu->setA($result); - $cpu->getFlags()->setZ($result === 0); - $cpu->getFlags()->setN(false); - $cpu->getFlags()->setH(true); - $cpu->getFlags()->setC(false); - return 4; - }, - ), - - 0xA2 => new Instruction( - opcode: 0xA2, - mnemonic: 'AND D', - length: 1, - cycles: 4, - handler: static function (Cpu $cpu): int { - $result = $cpu->getA() & $cpu->getD(); - $cpu->setA($result); - $cpu->getFlags()->setZ($result === 0); - $cpu->getFlags()->setN(false); - $cpu->getFlags()->setH(true); - $cpu->getFlags()->setC(false); - return 4; - }, - ), - - 0xA3 => new Instruction( - opcode: 0xA3, - mnemonic: 'AND E', - length: 1, - cycles: 4, - handler: static function (Cpu $cpu): int { - $result = $cpu->getA() & $cpu->getE(); - $cpu->setA($result); - $cpu->getFlags()->setZ($result === 0); - $cpu->getFlags()->setN(false); - $cpu->getFlags()->setH(true); - $cpu->getFlags()->setC(false); - return 4; - }, - ), - - 0xA4 => new Instruction( - opcode: 0xA4, - mnemonic: 'AND H', - length: 1, - cycles: 4, - handler: static function (Cpu $cpu): int { - $result = $cpu->getA() & $cpu->getH(); - $cpu->setA($result); - $cpu->getFlags()->setZ($result === 0); - $cpu->getFlags()->setN(false); - $cpu->getFlags()->setH(true); - $cpu->getFlags()->setC(false); - return 4; - }, - ), - - 0xA5 => new Instruction( - opcode: 0xA5, - mnemonic: 'AND L', - length: 1, - cycles: 4, - handler: static function (Cpu $cpu): int { - $result = $cpu->getA() & $cpu->getL(); - $cpu->setA($result); - $cpu->getFlags()->setZ($result === 0); - $cpu->getFlags()->setN(false); - $cpu->getFlags()->setH(true); - $cpu->getFlags()->setC(false); - return 4; - }, - ), - - 0xA6 => new Instruction( - opcode: 0xA6, - mnemonic: 'AND (HL)', - length: 1, - cycles: 8, - handler: static function (Cpu $cpu): int { - $address = $cpu->getHL()->get(); - $value = $cpu->getBus()->readByte($address); - $result = $cpu->getA() & $value; - $cpu->setA($result); - $cpu->getFlags()->setZ($result === 0); - $cpu->getFlags()->setN(false); - $cpu->getFlags()->setH(true); - $cpu->getFlags()->setC(false); - return 8; - }, - ), - - 0xA7 => new Instruction( - opcode: 0xA7, - mnemonic: 'AND A', - length: 1, - cycles: 4, - handler: static function (Cpu $cpu): int { - $result = $cpu->getA(); - $cpu->getFlags()->setZ($result === 0); - $cpu->getFlags()->setN(false); - $cpu->getFlags()->setH(true); - $cpu->getFlags()->setC(false); - return 4; - }, - ), - - // 0xA8-0xAF: XOR r - - 0xA8 => new Instruction( - opcode: 0xA8, - mnemonic: 'XOR B', - length: 1, - cycles: 4, - handler: static function (Cpu $cpu): int { - $result = $cpu->getA() ^ $cpu->getB(); - $cpu->setA($result); - $cpu->getFlags()->setZ($result === 0); - $cpu->getFlags()->setN(false); - $cpu->getFlags()->setH(false); - $cpu->getFlags()->setC(false); - return 4; - }, - ), - - 0xA9 => new Instruction( - opcode: 0xA9, - mnemonic: 'XOR C', - length: 1, - cycles: 4, - handler: static function (Cpu $cpu): int { - $result = $cpu->getA() ^ $cpu->getC(); - $cpu->setA($result); - $cpu->getFlags()->setZ($result === 0); - $cpu->getFlags()->setN(false); - $cpu->getFlags()->setH(false); - $cpu->getFlags()->setC(false); - return 4; - }, - ), - - 0xAA => new Instruction( - opcode: 0xAA, - mnemonic: 'XOR D', - length: 1, - cycles: 4, - handler: static function (Cpu $cpu): int { - $result = $cpu->getA() ^ $cpu->getD(); - $cpu->setA($result); - $cpu->getFlags()->setZ($result === 0); - $cpu->getFlags()->setN(false); - $cpu->getFlags()->setH(false); - $cpu->getFlags()->setC(false); - return 4; - }, - ), - - 0xAB => new Instruction( - opcode: 0xAB, - mnemonic: 'XOR E', - length: 1, - cycles: 4, - handler: static function (Cpu $cpu): int { - $result = $cpu->getA() ^ $cpu->getE(); - $cpu->setA($result); - $cpu->getFlags()->setZ($result === 0); - $cpu->getFlags()->setN(false); - $cpu->getFlags()->setH(false); - $cpu->getFlags()->setC(false); - return 4; - }, - ), - - 0xAC => new Instruction( - opcode: 0xAC, - mnemonic: 'XOR H', - length: 1, - cycles: 4, - handler: static function (Cpu $cpu): int { - $result = $cpu->getA() ^ $cpu->getH(); - $cpu->setA($result); - $cpu->getFlags()->setZ($result === 0); - $cpu->getFlags()->setN(false); - $cpu->getFlags()->setH(false); - $cpu->getFlags()->setC(false); - return 4; - }, - ), - - 0xAD => new Instruction( - opcode: 0xAD, - mnemonic: 'XOR L', - length: 1, - cycles: 4, - handler: static function (Cpu $cpu): int { - $result = $cpu->getA() ^ $cpu->getL(); - $cpu->setA($result); - $cpu->getFlags()->setZ($result === 0); - $cpu->getFlags()->setN(false); - $cpu->getFlags()->setH(false); - $cpu->getFlags()->setC(false); - return 4; - }, - ), - - 0xAE => new Instruction( - opcode: 0xAE, - mnemonic: 'XOR (HL)', - length: 1, - cycles: 8, - handler: static function (Cpu $cpu): int { - $address = $cpu->getHL()->get(); - $value = $cpu->getBus()->readByte($address); - $result = $cpu->getA() ^ $value; - $cpu->setA($result); - $cpu->getFlags()->setZ($result === 0); - $cpu->getFlags()->setN(false); - $cpu->getFlags()->setH(false); - $cpu->getFlags()->setC(false); - return 8; - }, - ), - - 0xAF => new Instruction( - opcode: 0xAF, - mnemonic: 'XOR A', - length: 1, - cycles: 4, - handler: static function (Cpu $cpu): int { - $cpu->setA(0); - $cpu->getFlags()->setZ(true); - $cpu->getFlags()->setN(false); - $cpu->getFlags()->setH(false); - $cpu->getFlags()->setC(false); - return 4; - }, - ), - - // 0xB0-0xB7: OR r - - 0xB0 => new Instruction( - opcode: 0xB0, - mnemonic: 'OR B', - length: 1, - cycles: 4, - handler: static function (Cpu $cpu): int { - $result = $cpu->getA() | $cpu->getB(); - $cpu->setA($result); - $cpu->getFlags()->setZ($result === 0); - $cpu->getFlags()->setN(false); - $cpu->getFlags()->setH(false); - $cpu->getFlags()->setC(false); - return 4; - }, - ), - - 0xB1 => new Instruction( - opcode: 0xB1, - mnemonic: 'OR C', - length: 1, - cycles: 4, - handler: static function (Cpu $cpu): int { - $result = $cpu->getA() | $cpu->getC(); - $cpu->setA($result); - $cpu->getFlags()->setZ($result === 0); - $cpu->getFlags()->setN(false); - $cpu->getFlags()->setH(false); - $cpu->getFlags()->setC(false); - return 4; - }, - ), - - 0xB2 => new Instruction( - opcode: 0xB2, - mnemonic: 'OR D', - length: 1, - cycles: 4, - handler: static function (Cpu $cpu): int { - $result = $cpu->getA() | $cpu->getD(); - $cpu->setA($result); - $cpu->getFlags()->setZ($result === 0); - $cpu->getFlags()->setN(false); - $cpu->getFlags()->setH(false); - $cpu->getFlags()->setC(false); - return 4; - }, - ), - - 0xB3 => new Instruction( - opcode: 0xB3, - mnemonic: 'OR E', - length: 1, - cycles: 4, - handler: static function (Cpu $cpu): int { - $result = $cpu->getA() | $cpu->getE(); - $cpu->setA($result); - $cpu->getFlags()->setZ($result === 0); - $cpu->getFlags()->setN(false); - $cpu->getFlags()->setH(false); - $cpu->getFlags()->setC(false); - return 4; - }, - ), - - 0xB4 => new Instruction( - opcode: 0xB4, - mnemonic: 'OR H', - length: 1, - cycles: 4, - handler: static function (Cpu $cpu): int { - $result = $cpu->getA() | $cpu->getH(); - $cpu->setA($result); - $cpu->getFlags()->setZ($result === 0); - $cpu->getFlags()->setN(false); - $cpu->getFlags()->setH(false); - $cpu->getFlags()->setC(false); - return 4; - }, - ), - - 0xB5 => new Instruction( - opcode: 0xB5, - mnemonic: 'OR L', - length: 1, - cycles: 4, - handler: static function (Cpu $cpu): int { - $result = $cpu->getA() | $cpu->getL(); - $cpu->setA($result); - $cpu->getFlags()->setZ($result === 0); - $cpu->getFlags()->setN(false); - $cpu->getFlags()->setH(false); - $cpu->getFlags()->setC(false); - return 4; - }, - ), - - 0xB6 => new Instruction( - opcode: 0xB6, - mnemonic: 'OR (HL)', - length: 1, - cycles: 8, - handler: static function (Cpu $cpu): int { - $address = $cpu->getHL()->get(); - $value = $cpu->getBus()->readByte($address); - $result = $cpu->getA() | $value; - $cpu->setA($result); - $cpu->getFlags()->setZ($result === 0); - $cpu->getFlags()->setN(false); - $cpu->getFlags()->setH(false); - $cpu->getFlags()->setC(false); - return 8; - }, - ), - - 0xB7 => new Instruction( - opcode: 0xB7, - mnemonic: 'OR A', - length: 1, - cycles: 4, - handler: static function (Cpu $cpu): int { - $result = $cpu->getA(); - $cpu->getFlags()->setZ($result === 0); - $cpu->getFlags()->setN(false); - $cpu->getFlags()->setH(false); - $cpu->getFlags()->setC(false); - return 4; - }, - ), - - // 0xB8-0xBF: CP r (compare) - - 0xB8 => new Instruction( - opcode: 0xB8, - mnemonic: 'CP B', - length: 1, - cycles: 4, - handler: static function (Cpu $cpu): int { - $a = $cpu->getA(); - $b = $cpu->getB(); - $result = $a - $b; - $cpu->getFlags()->setZ(($result & 0xFF) === 0); - $cpu->getFlags()->setN(true); - $cpu->getFlags()->setH(self::halfCarry8Sub($a, $b)); - $cpu->getFlags()->setC($result < 0); - return 4; - }, - ), - - 0xB9 => new Instruction( - opcode: 0xB9, - mnemonic: 'CP C', - length: 1, - cycles: 4, - handler: static function (Cpu $cpu): int { - $a = $cpu->getA(); - $c = $cpu->getC(); - $result = $a - $c; - $cpu->getFlags()->setZ(($result & 0xFF) === 0); - $cpu->getFlags()->setN(true); - $cpu->getFlags()->setH(self::halfCarry8Sub($a, $c)); - $cpu->getFlags()->setC($result < 0); - return 4; - }, - ), - - 0xBA => new Instruction( - opcode: 0xBA, - mnemonic: 'CP D', - length: 1, - cycles: 4, - handler: static function (Cpu $cpu): int { - $a = $cpu->getA(); - $d = $cpu->getD(); - $result = $a - $d; - $cpu->getFlags()->setZ(($result & 0xFF) === 0); - $cpu->getFlags()->setN(true); - $cpu->getFlags()->setH(self::halfCarry8Sub($a, $d)); - $cpu->getFlags()->setC($result < 0); - return 4; - }, - ), - - 0xBB => new Instruction( - opcode: 0xBB, - mnemonic: 'CP E', - length: 1, - cycles: 4, - handler: static function (Cpu $cpu): int { - $a = $cpu->getA(); - $e = $cpu->getE(); - $result = $a - $e; - $cpu->getFlags()->setZ(($result & 0xFF) === 0); - $cpu->getFlags()->setN(true); - $cpu->getFlags()->setH(self::halfCarry8Sub($a, $e)); - $cpu->getFlags()->setC($result < 0); - return 4; - }, - ), - - 0xBC => new Instruction( - opcode: 0xBC, - mnemonic: 'CP H', - length: 1, - cycles: 4, - handler: static function (Cpu $cpu): int { - $a = $cpu->getA(); - $h = $cpu->getH(); - $result = $a - $h; - $cpu->getFlags()->setZ(($result & 0xFF) === 0); - $cpu->getFlags()->setN(true); - $cpu->getFlags()->setH(self::halfCarry8Sub($a, $h)); - $cpu->getFlags()->setC($result < 0); - return 4; - }, - ), - - 0xBD => new Instruction( - opcode: 0xBD, - mnemonic: 'CP L', - length: 1, - cycles: 4, - handler: static function (Cpu $cpu): int { - $a = $cpu->getA(); - $l = $cpu->getL(); - $result = $a - $l; - $cpu->getFlags()->setZ(($result & 0xFF) === 0); - $cpu->getFlags()->setN(true); - $cpu->getFlags()->setH(self::halfCarry8Sub($a, $l)); - $cpu->getFlags()->setC($result < 0); - return 4; - }, - ), - - 0xBE => new Instruction( - opcode: 0xBE, - mnemonic: 'CP (HL)', - length: 1, - cycles: 8, - handler: static function (Cpu $cpu): int { - $a = $cpu->getA(); - $address = $cpu->getHL()->get(); - $value = $cpu->getBus()->readByte($address); - $result = $a - $value; - $cpu->getFlags()->setZ(($result & 0xFF) === 0); - $cpu->getFlags()->setN(true); - $cpu->getFlags()->setH(self::halfCarry8Sub($a, $value)); - $cpu->getFlags()->setC($result < 0); - return 8; - }, - ), - - 0xBF => new Instruction( - opcode: 0xBF, - mnemonic: 'CP A', - length: 1, - cycles: 4, - handler: static function (Cpu $cpu): int { - $cpu->getFlags()->setZ(true); - $cpu->getFlags()->setN(true); - $cpu->getFlags()->setH(false); - $cpu->getFlags()->setC(false); - return 4; - }, - ), - - // 0xC0-0xCF: Control flow and stack operations - - 0xC0 => new Instruction( - opcode: 0xC0, - mnemonic: 'RET NZ', - length: 1, - cycles: 8, // 20 if taken, 8 if not taken - handler: static function (Cpu $cpu): int { - if (!$cpu->getFlags()->getZ()) { - $low = $cpu->getBus()->readByte($cpu->getSP()->get()); - $cpu->getSP()->increment(); - $high = $cpu->getBus()->readByte($cpu->getSP()->get()); - $cpu->getSP()->increment(); - $cpu->getPC()->set(($high << 8) | $low); - return 20; - } - return 8; - }, - ), - - 0xC1 => new Instruction( - opcode: 0xC1, - mnemonic: 'POP BC', - length: 1, - cycles: 12, - handler: static function (Cpu $cpu): int { - $low = $cpu->getBus()->readByte($cpu->getSP()->get()); - $cpu->getSP()->increment(); - $high = $cpu->getBus()->readByte($cpu->getSP()->get()); - $cpu->getSP()->increment(); - $cpu->getBC()->set(($high << 8) | $low); - return 12; - }, - ), - - 0xC2 => new Instruction( - opcode: 0xC2, - mnemonic: 'JP NZ,nn', - length: 3, - cycles: 12, // 16 if taken, 12 if not taken - handler: static function (Cpu $cpu): int { - $address = self::readImm16($cpu); - if (!$cpu->getFlags()->getZ()) { - $cpu->getPC()->set($address); - return 16; - } - return 12; - }, - ), - - 0xC3 => new Instruction( - opcode: 0xC3, - mnemonic: 'JP nn', - length: 3, - cycles: 16, - handler: static function (Cpu $cpu): int { - $address = self::readImm16($cpu); - $cpu->getPC()->set($address); - return 16; - }, - ), - - 0xC4 => new Instruction( - opcode: 0xC4, - mnemonic: 'CALL NZ,nn', - length: 3, - cycles: 12, // 24 if taken, 12 if not taken - handler: static function (Cpu $cpu): int { - $address = self::readImm16($cpu); - if (!$cpu->getFlags()->getZ()) { - $pc = $cpu->getPC()->get(); - $cpu->getSP()->decrement(); - $cpu->getBus()->writeByte($cpu->getSP()->get(), ($pc >> 8) & 0xFF); - $cpu->getSP()->decrement(); - $cpu->getBus()->writeByte($cpu->getSP()->get(), $pc & 0xFF); - $cpu->getPC()->set($address); - return 24; - } - return 12; - }, - ), - - 0xC5 => new Instruction( - opcode: 0xC5, - mnemonic: 'PUSH BC', - length: 1, - cycles: 16, - handler: static function (Cpu $cpu): int { - $value = $cpu->getBC()->get(); - $cpu->getSP()->decrement(); - $cpu->getBus()->writeByte($cpu->getSP()->get(), ($value >> 8) & 0xFF); - $cpu->getSP()->decrement(); - $cpu->getBus()->writeByte($cpu->getSP()->get(), $value & 0xFF); - return 16; - }, - ), - - 0xC6 => new Instruction( - opcode: 0xC6, - mnemonic: 'ADD A,n', - length: 2, - cycles: 8, - handler: static function (Cpu $cpu): int { - $a = $cpu->getA(); - $n = self::readImm8($cpu); - $result = $a + $n; - $cpu->setA($result & 0xFF); - $cpu->getFlags()->setZ(($result & 0xFF) === 0); - $cpu->getFlags()->setN(false); - $cpu->getFlags()->setH(self::halfCarry8Add($a, $n)); - $cpu->getFlags()->setC($result > 0xFF); - return 8; - }, - ), - - 0xC7 => new Instruction( - opcode: 0xC7, - mnemonic: 'RST 00H', - length: 1, - cycles: 16, - handler: static function (Cpu $cpu): int { - $pc = $cpu->getPC()->get(); - $cpu->getSP()->decrement(); - $cpu->getBus()->writeByte($cpu->getSP()->get(), ($pc >> 8) & 0xFF); - $cpu->getSP()->decrement(); - $cpu->getBus()->writeByte($cpu->getSP()->get(), $pc & 0xFF); - $cpu->getPC()->set(0x0000); - return 16; - }, - ), - - 0xC8 => new Instruction( - opcode: 0xC8, - mnemonic: 'RET Z', - length: 1, - cycles: 8, // 20 if taken, 8 if not taken - handler: static function (Cpu $cpu): int { - if ($cpu->getFlags()->getZ()) { - $low = $cpu->getBus()->readByte($cpu->getSP()->get()); - $cpu->getSP()->increment(); - $high = $cpu->getBus()->readByte($cpu->getSP()->get()); - $cpu->getSP()->increment(); - $cpu->getPC()->set(($high << 8) | $low); - return 20; - } - return 8; - }, - ), - - 0xC9 => new Instruction( - opcode: 0xC9, - mnemonic: 'RET', - length: 1, - cycles: 16, - handler: static function (Cpu $cpu): int { - $low = $cpu->getBus()->readByte($cpu->getSP()->get()); - $cpu->getSP()->increment(); - $high = $cpu->getBus()->readByte($cpu->getSP()->get()); - $cpu->getSP()->increment(); - $cpu->getPC()->set(($high << 8) | $low); - return 16; - }, - ), - - 0xCA => new Instruction( - opcode: 0xCA, - mnemonic: 'JP Z,nn', - length: 3, - cycles: 12, // 16 if taken, 12 if not taken - handler: static function (Cpu $cpu): int { - $address = self::readImm16($cpu); - if ($cpu->getFlags()->getZ()) { - $cpu->getPC()->set($address); - return 16; - } - return 12; - }, - ), - - 0xCB => new Instruction( - opcode: 0xCB, - mnemonic: 'PREFIX CB', - length: 2, - cycles: 4, - handler: static function (Cpu $cpu): int { - $cb = self::readImm8($cpu); - return (self::getCBInstruction($cb)->handler)($cpu); - }, - ), - - 0xCC => new Instruction( - opcode: 0xCC, - mnemonic: 'CALL Z,nn', - length: 3, - cycles: 12, // 24 if taken, 12 if not taken - handler: static function (Cpu $cpu): int { - $address = self::readImm16($cpu); - if ($cpu->getFlags()->getZ()) { - $pc = $cpu->getPC()->get(); - $cpu->getSP()->decrement(); - $cpu->getBus()->writeByte($cpu->getSP()->get(), ($pc >> 8) & 0xFF); - $cpu->getSP()->decrement(); - $cpu->getBus()->writeByte($cpu->getSP()->get(), $pc & 0xFF); - $cpu->getPC()->set($address); - return 24; - } - return 12; - }, - ), - - 0xCD => new Instruction( - opcode: 0xCD, - mnemonic: 'CALL nn', - length: 3, - cycles: 24, - handler: static function (Cpu $cpu): int { - $address = self::readImm16($cpu); - $pc = $cpu->getPC()->get(); - $cpu->getSP()->decrement(); - $cpu->getBus()->writeByte($cpu->getSP()->get(), ($pc >> 8) & 0xFF); - $cpu->getSP()->decrement(); - $cpu->getBus()->writeByte($cpu->getSP()->get(), $pc & 0xFF); - $cpu->getPC()->set($address); - return 24; - }, - ), - - 0xCE => new Instruction( - opcode: 0xCE, - mnemonic: 'ADC A,n', - length: 2, - cycles: 8, - handler: static function (Cpu $cpu): int { - $a = $cpu->getA(); - $n = self::readImm8($cpu); - $carry = $cpu->getFlags()->getC() ? 1 : 0; - $result = $a + $n + $carry; - $cpu->setA($result & 0xFF); - $cpu->getFlags()->setZ(($result & 0xFF) === 0); - $cpu->getFlags()->setN(false); - $cpu->getFlags()->setH(self::halfCarry8Add($a, $n, $carry)); - $cpu->getFlags()->setC($result > 0xFF); - return 8; - }, - ), - - 0xCF => new Instruction( - opcode: 0xCF, - mnemonic: 'RST 08H', - length: 1, - cycles: 16, - handler: static function (Cpu $cpu): int { - $pc = $cpu->getPC()->get(); - $cpu->getSP()->decrement(); - $cpu->getBus()->writeByte($cpu->getSP()->get(), ($pc >> 8) & 0xFF); - $cpu->getSP()->decrement(); - $cpu->getBus()->writeByte($cpu->getSP()->get(), $pc & 0xFF); - $cpu->getPC()->set(0x0008); - return 16; - }, - ), - - // 0xD0-0xDF: More control flow and stack operations - - 0xD0 => new Instruction( - opcode: 0xD0, - mnemonic: 'RET NC', - length: 1, - cycles: 8, // 20 if taken, 8 if not taken - handler: static function (Cpu $cpu): int { - if (!$cpu->getFlags()->getC()) { - $low = $cpu->getBus()->readByte($cpu->getSP()->get()); - $cpu->getSP()->increment(); - $high = $cpu->getBus()->readByte($cpu->getSP()->get()); - $cpu->getSP()->increment(); - $cpu->getPC()->set(($high << 8) | $low); - return 20; - } - return 8; - }, - ), - - 0xD1 => new Instruction( - opcode: 0xD1, - mnemonic: 'POP DE', - length: 1, - cycles: 12, - handler: static function (Cpu $cpu): int { - $low = $cpu->getBus()->readByte($cpu->getSP()->get()); - $cpu->getSP()->increment(); - $high = $cpu->getBus()->readByte($cpu->getSP()->get()); - $cpu->getSP()->increment(); - $cpu->getDE()->set(($high << 8) | $low); - return 12; - }, - ), - - 0xD2 => new Instruction( - opcode: 0xD2, - mnemonic: 'JP NC,nn', - length: 3, - cycles: 12, // 16 if taken, 12 if not taken - handler: static function (Cpu $cpu): int { - $address = self::readImm16($cpu); - if (!$cpu->getFlags()->getC()) { - $cpu->getPC()->set($address); - return 16; - } - return 12; - }, - ), - - 0xD4 => new Instruction( - opcode: 0xD4, - mnemonic: 'CALL NC,nn', - length: 3, - cycles: 12, // 24 if taken, 12 if not taken - handler: static function (Cpu $cpu): int { - $address = self::readImm16($cpu); - if (!$cpu->getFlags()->getC()) { - $pc = $cpu->getPC()->get(); - $cpu->getSP()->decrement(); - $cpu->getBus()->writeByte($cpu->getSP()->get(), ($pc >> 8) & 0xFF); - $cpu->getSP()->decrement(); - $cpu->getBus()->writeByte($cpu->getSP()->get(), $pc & 0xFF); - $cpu->getPC()->set($address); - return 24; - } - return 12; - }, - ), - - 0xD5 => new Instruction( - opcode: 0xD5, - mnemonic: 'PUSH DE', - length: 1, - cycles: 16, - handler: static function (Cpu $cpu): int { - $value = $cpu->getDE()->get(); - $cpu->getSP()->decrement(); - $cpu->getBus()->writeByte($cpu->getSP()->get(), ($value >> 8) & 0xFF); - $cpu->getSP()->decrement(); - $cpu->getBus()->writeByte($cpu->getSP()->get(), $value & 0xFF); - return 16; - }, - ), - - 0xD6 => new Instruction( - opcode: 0xD6, - mnemonic: 'SUB n', - length: 2, - cycles: 8, - handler: static function (Cpu $cpu): int { - $a = $cpu->getA(); - $n = self::readImm8($cpu); - $result = $a - $n; - $cpu->setA($result & 0xFF); - $cpu->getFlags()->setZ(($result & 0xFF) === 0); - $cpu->getFlags()->setN(true); - $cpu->getFlags()->setH(self::halfCarry8Sub($a, $n)); - $cpu->getFlags()->setC($result < 0); - return 8; - }, - ), - - 0xD7 => new Instruction( - opcode: 0xD7, - mnemonic: 'RST 10H', - length: 1, - cycles: 16, - handler: static function (Cpu $cpu): int { - $pc = $cpu->getPC()->get(); - $cpu->getSP()->decrement(); - $cpu->getBus()->writeByte($cpu->getSP()->get(), ($pc >> 8) & 0xFF); - $cpu->getSP()->decrement(); - $cpu->getBus()->writeByte($cpu->getSP()->get(), $pc & 0xFF); - $cpu->getPC()->set(0x0010); - return 16; - }, - ), - - 0xD8 => new Instruction( - opcode: 0xD8, - mnemonic: 'RET C', - length: 1, - cycles: 8, // 20 if taken, 8 if not taken - handler: static function (Cpu $cpu): int { - if ($cpu->getFlags()->getC()) { - $low = $cpu->getBus()->readByte($cpu->getSP()->get()); - $cpu->getSP()->increment(); - $high = $cpu->getBus()->readByte($cpu->getSP()->get()); - $cpu->getSP()->increment(); - $cpu->getPC()->set(($high << 8) | $low); - return 20; - } - return 8; - }, - ), - - 0xD9 => new Instruction( - opcode: 0xD9, - mnemonic: 'RETI', - length: 1, - cycles: 16, - handler: static function (Cpu $cpu): int { - $low = $cpu->getBus()->readByte($cpu->getSP()->get()); - $cpu->getSP()->increment(); - $high = $cpu->getBus()->readByte($cpu->getSP()->get()); - $cpu->getSP()->increment(); - $cpu->getPC()->set(($high << 8) | $low); - $cpu->setIME(true); - return 16; - }, - ), - - 0xDA => new Instruction( - opcode: 0xDA, - mnemonic: 'JP C,nn', - length: 3, - cycles: 12, // 16 if taken, 12 if not taken - handler: static function (Cpu $cpu): int { - $address = self::readImm16($cpu); - if ($cpu->getFlags()->getC()) { - $cpu->getPC()->set($address); - return 16; - } - return 12; - }, - ), - - 0xDC => new Instruction( - opcode: 0xDC, - mnemonic: 'CALL C,nn', - length: 3, - cycles: 12, // 24 if taken, 12 if not taken - handler: static function (Cpu $cpu): int { - $address = self::readImm16($cpu); - if ($cpu->getFlags()->getC()) { - $pc = $cpu->getPC()->get(); - $cpu->getSP()->decrement(); - $cpu->getBus()->writeByte($cpu->getSP()->get(), ($pc >> 8) & 0xFF); - $cpu->getSP()->decrement(); - $cpu->getBus()->writeByte($cpu->getSP()->get(), $pc & 0xFF); - $cpu->getPC()->set($address); - return 24; - } - return 12; - }, - ), - - 0xDE => new Instruction( - opcode: 0xDE, - mnemonic: 'SBC A,n', - length: 2, - cycles: 8, - handler: static function (Cpu $cpu): int { - $a = $cpu->getA(); - $n = self::readImm8($cpu); - $carry = $cpu->getFlags()->getC() ? 1 : 0; - $result = $a - $n - $carry; - $cpu->setA($result & 0xFF); - $cpu->getFlags()->setZ(($result & 0xFF) === 0); - $cpu->getFlags()->setN(true); - $cpu->getFlags()->setH(self::halfCarry8Sub($a, $n, $carry)); - $cpu->getFlags()->setC($result < 0); - return 8; - }, - ), - - 0xDF => new Instruction( - opcode: 0xDF, - mnemonic: 'RST 18H', - length: 1, - cycles: 16, - handler: static function (Cpu $cpu): int { - $pc = $cpu->getPC()->get(); - $cpu->getSP()->decrement(); - $cpu->getBus()->writeByte($cpu->getSP()->get(), ($pc >> 8) & 0xFF); - $cpu->getSP()->decrement(); - $cpu->getBus()->writeByte($cpu->getSP()->get(), $pc & 0xFF); - $cpu->getPC()->set(0x0018); - return 16; - }, - ), - - // 0xE0-0xEF: I/O operations and more control flow - - 0xE0 => new Instruction( - opcode: 0xE0, - mnemonic: 'LDH (n),A', - length: 2, - cycles: 12, - handler: static function (Cpu $cpu): int { - $n = self::readImm8($cpu); - $address = 0xFF00 + $n; - $cpu->getBus()->writeByte($address, $cpu->getA()); - return 12; - }, - ), - - 0xE1 => new Instruction( - opcode: 0xE1, - mnemonic: 'POP HL', - length: 1, - cycles: 12, - handler: static function (Cpu $cpu): int { - $low = $cpu->getBus()->readByte($cpu->getSP()->get()); - $cpu->getSP()->increment(); - $high = $cpu->getBus()->readByte($cpu->getSP()->get()); - $cpu->getSP()->increment(); - $cpu->getHL()->set(($high << 8) | $low); - return 12; - }, - ), - - 0xE2 => new Instruction( - opcode: 0xE2, - mnemonic: 'LD (C),A', - length: 1, - cycles: 8, - handler: static function (Cpu $cpu): int { - $address = 0xFF00 + $cpu->getC(); - $cpu->getBus()->writeByte($address, $cpu->getA()); - return 8; - }, - ), - - 0xE5 => new Instruction( - opcode: 0xE5, - mnemonic: 'PUSH HL', - length: 1, - cycles: 16, - handler: static function (Cpu $cpu): int { - $value = $cpu->getHL()->get(); - $cpu->getSP()->decrement(); - $cpu->getBus()->writeByte($cpu->getSP()->get(), ($value >> 8) & 0xFF); - $cpu->getSP()->decrement(); - $cpu->getBus()->writeByte($cpu->getSP()->get(), $value & 0xFF); - return 16; - }, - ), - - 0xE6 => new Instruction( - opcode: 0xE6, - mnemonic: 'AND n', - length: 2, - cycles: 8, - handler: static function (Cpu $cpu): int { - $n = self::readImm8($cpu); - $result = $cpu->getA() & $n; - $cpu->setA($result); - $cpu->getFlags()->setZ($result === 0); - $cpu->getFlags()->setN(false); - $cpu->getFlags()->setH(true); - $cpu->getFlags()->setC(false); - return 8; - }, - ), - - 0xE7 => new Instruction( - opcode: 0xE7, - mnemonic: 'RST 20H', - length: 1, - cycles: 16, - handler: static function (Cpu $cpu): int { - $pc = $cpu->getPC()->get(); - $cpu->getSP()->decrement(); - $cpu->getBus()->writeByte($cpu->getSP()->get(), ($pc >> 8) & 0xFF); - $cpu->getSP()->decrement(); - $cpu->getBus()->writeByte($cpu->getSP()->get(), $pc & 0xFF); - $cpu->getPC()->set(0x0020); - return 16; - }, - ), - - 0xE8 => new Instruction( - opcode: 0xE8, - mnemonic: 'ADD SP,e', - length: 2, - cycles: 16, - handler: static function (Cpu $cpu): int { - $sp = $cpu->getSP()->get(); - $e_unsigned = self::readImm8($cpu); - // Sign extend for the actual addition - $e_signed = $e_unsigned > 0x7F ? $e_unsigned - 0x100 : $e_unsigned; - $result = $sp + $e_signed; - $cpu->getSP()->set($result & 0xFFFF); - $cpu->getFlags()->setZ(false); - $cpu->getFlags()->setN(false); - // Flags are calculated on the lower byte using unsigned arithmetic - $cpu->getFlags()->setH(((($sp & 0x0F) + ($e_unsigned & 0x0F)) & 0x10) !== 0); - $cpu->getFlags()->setC(((($sp & 0xFF) + $e_unsigned) & 0x100) !== 0); - return 16; - }, - ), - - 0xE9 => new Instruction( - opcode: 0xE9, - mnemonic: 'JP (HL)', - length: 1, - cycles: 4, - handler: static function (Cpu $cpu): int { - $cpu->getPC()->set($cpu->getHL()->get()); - return 4; - }, - ), - - 0xEA => new Instruction( - opcode: 0xEA, - mnemonic: 'LD (nn),A', - length: 3, - cycles: 16, - handler: static function (Cpu $cpu): int { - $address = self::readImm16($cpu); - $cpu->getBus()->writeByte($address, $cpu->getA()); - return 16; - }, - ), - - 0xEE => new Instruction( - opcode: 0xEE, - mnemonic: 'XOR n', - length: 2, - cycles: 8, - handler: static function (Cpu $cpu): int { - $n = self::readImm8($cpu); - $result = $cpu->getA() ^ $n; - $cpu->setA($result); - $cpu->getFlags()->setZ($result === 0); - $cpu->getFlags()->setN(false); - $cpu->getFlags()->setH(false); - $cpu->getFlags()->setC(false); - return 8; - }, - ), - - 0xEF => new Instruction( - opcode: 0xEF, - mnemonic: 'RST 28H', - length: 1, - cycles: 16, - handler: static function (Cpu $cpu): int { - $pc = $cpu->getPC()->get(); - $cpu->getSP()->decrement(); - $cpu->getBus()->writeByte($cpu->getSP()->get(), ($pc >> 8) & 0xFF); - $cpu->getSP()->decrement(); - $cpu->getBus()->writeByte($cpu->getSP()->get(), $pc & 0xFF); - $cpu->getPC()->set(0x0028); - return 16; - }, - ), - - // 0xF0-0xFF: Final I/O operations and control flow - - 0xF0 => new Instruction( - opcode: 0xF0, - mnemonic: 'LDH A,(n)', - length: 2, - cycles: 12, - handler: static function (Cpu $cpu): int { - $n = self::readImm8($cpu); - $address = 0xFF00 + $n; - $cpu->setA($cpu->getBus()->readByte($address)); - return 12; - }, - ), - - 0xF1 => new Instruction( - opcode: 0xF1, - mnemonic: 'POP AF', - length: 1, - cycles: 12, - handler: static function (Cpu $cpu): int { - $low = $cpu->getBus()->readByte($cpu->getSP()->get()); - $cpu->getSP()->increment(); - $high = $cpu->getBus()->readByte($cpu->getSP()->get()); - $cpu->getSP()->increment(); - $cpu->getAF()->set(($high << 8) | ($low & 0xF0)); // Lower 4 bits of F are always 0 - $cpu->getFlags()->syncFromAF(); // Sync flags from AF register - return 12; - }, - ), - - 0xF2 => new Instruction( - opcode: 0xF2, - mnemonic: 'LD A,(C)', - length: 1, - cycles: 8, - handler: static function (Cpu $cpu): int { - $address = 0xFF00 + $cpu->getC(); - $cpu->setA($cpu->getBus()->readByte($address)); - return 8; - }, - ), - - 0xF3 => new Instruction( - opcode: 0xF3, - mnemonic: 'DI', - length: 1, - cycles: 4, - handler: static function (Cpu $cpu): int { - $cpu->setIME(false); - return 4; - }, - ), - - 0xF5 => new Instruction( - opcode: 0xF5, - mnemonic: 'PUSH AF', - length: 1, - cycles: 16, - handler: static function (Cpu $cpu): int { - $value = $cpu->getAF()->get(); - $cpu->getSP()->decrement(); - $cpu->getBus()->writeByte($cpu->getSP()->get(), ($value >> 8) & 0xFF); - $cpu->getSP()->decrement(); - $cpu->getBus()->writeByte($cpu->getSP()->get(), $value & 0xFF); - return 16; - }, - ), - - 0xF6 => new Instruction( - opcode: 0xF6, - mnemonic: 'OR n', - length: 2, - cycles: 8, - handler: static function (Cpu $cpu): int { - $n = self::readImm8($cpu); - $result = $cpu->getA() | $n; - $cpu->setA($result); - $cpu->getFlags()->setZ($result === 0); - $cpu->getFlags()->setN(false); - $cpu->getFlags()->setH(false); - $cpu->getFlags()->setC(false); - return 8; - }, - ), - - 0xF7 => new Instruction( - opcode: 0xF7, - mnemonic: 'RST 30H', - length: 1, - cycles: 16, - handler: static function (Cpu $cpu): int { - $pc = $cpu->getPC()->get(); - $cpu->getSP()->decrement(); - $cpu->getBus()->writeByte($cpu->getSP()->get(), ($pc >> 8) & 0xFF); - $cpu->getSP()->decrement(); - $cpu->getBus()->writeByte($cpu->getSP()->get(), $pc & 0xFF); - $cpu->getPC()->set(0x0030); - return 16; - }, - ), - - 0xF8 => new Instruction( - opcode: 0xF8, - mnemonic: 'LD HL,SP+e', - length: 2, - cycles: 12, - handler: static function (Cpu $cpu): int { - $sp = $cpu->getSP()->get(); - $e_unsigned = self::readImm8($cpu); - // Sign extend for the actual addition - $e_signed = $e_unsigned > 0x7F ? $e_unsigned - 0x100 : $e_unsigned; - $result = $sp + $e_signed; - $cpu->getHL()->set($result & 0xFFFF); - $cpu->getFlags()->setZ(false); - $cpu->getFlags()->setN(false); - // Flags are calculated on the lower byte using unsigned arithmetic - $cpu->getFlags()->setH(((($sp & 0x0F) + ($e_unsigned & 0x0F)) & 0x10) !== 0); - $cpu->getFlags()->setC(((($sp & 0xFF) + $e_unsigned) & 0x100) !== 0); - return 12; - }, - ), - - 0xF9 => new Instruction( - opcode: 0xF9, - mnemonic: 'LD SP,HL', - length: 1, - cycles: 8, - handler: static function (Cpu $cpu): int { - $cpu->getSP()->set($cpu->getHL()->get()); - return 8; - }, - ), - - 0xFA => new Instruction( - opcode: 0xFA, - mnemonic: 'LD A,(nn)', - length: 3, - cycles: 16, - handler: static function (Cpu $cpu): int { - $address = self::readImm16($cpu); - $cpu->setA($cpu->getBus()->readByte($address)); - return 16; - }, - ), - - 0xFB => new Instruction( - opcode: 0xFB, - mnemonic: 'EI', - length: 1, - cycles: 4, - handler: static function (Cpu $cpu): int { - $cpu->setIME(true); - return 4; - }, - ), - - 0xFE => new Instruction( - opcode: 0xFE, - mnemonic: 'CP n', - length: 2, - cycles: 8, - handler: static function (Cpu $cpu): int { - $a = $cpu->getA(); - $n = self::readImm8($cpu); - $result = $a - $n; - $cpu->getFlags()->setZ(($result & 0xFF) === 0); - $cpu->getFlags()->setN(true); - $cpu->getFlags()->setH(self::halfCarry8Sub($a, $n)); - $cpu->getFlags()->setC($result < 0); - return 8; - }, - ), - - 0xFF => new Instruction( - opcode: 0xFF, - mnemonic: 'RST 38H', - length: 1, - cycles: 16, - handler: static function (Cpu $cpu): int { - $pc = $cpu->getPC()->get(); - $cpu->getSP()->decrement(); - $cpu->getBus()->writeByte($cpu->getSP()->get(), ($pc >> 8) & 0xFF); - $cpu->getSP()->decrement(); - $cpu->getBus()->writeByte($cpu->getSP()->get(), $pc & 0xFF); - $cpu->getPC()->set(0x0038); - return 16; - }, - ), - - default => throw new RuntimeException(sprintf('Unknown opcode: 0x%02X', $opcode)), - }; - } - - /** - * Build CB-prefixed instruction metadata. - */ - private static function buildCBInstruction(int $opcode): Instruction - { - // Extract register index (0-7: B, C, D, E, H, L, (HL), A) - $regIndex = $opcode & 0x07; - - // Determine register name for mnemonic - $regName = match ($regIndex) { - 0 => 'B', - 1 => 'C', - 2 => 'D', - 3 => 'E', - 4 => 'H', - 5 => 'L', - 6 => '(HL)', - 7 => 'A', - }; - - // (HL) operations take 16 cycles, register operations take 8 cycles - $cycles = ($regIndex === 6) ? 16 : 8; - - return match (true) { - // RLC r (0x00-0x07) - Rotate Left Circular - $opcode >= 0x00 && $opcode <= 0x07 => new Instruction( - opcode: 0xCB00 | $opcode, - mnemonic: "RLC $regName", - length: 2, - cycles: $cycles, - handler: static function (Cpu $cpu) use ($regIndex, $cycles): int { - $value = self::getRegByIndex($cpu, $regIndex); - $bit7 = ($value & 0x80) !== 0; - $result = (($value << 1) & 0xFF) | ($bit7 ? 1 : 0); - - $cpu->getFlags()->setZ($result === 0); - $cpu->getFlags()->setN(false); - $cpu->getFlags()->setH(false); - $cpu->getFlags()->setC($bit7); - - self::setRegByIndex($cpu, $regIndex, $result); - return $cycles; - }, - ), - - // RRC r (0x08-0x0F) - Rotate Right Circular - $opcode >= 0x08 && $opcode <= 0x0F => new Instruction( - opcode: 0xCB00 | $opcode, - mnemonic: "RRC $regName", - length: 2, - cycles: $cycles, - handler: static function (Cpu $cpu) use ($regIndex, $cycles): int { - $value = self::getRegByIndex($cpu, $regIndex); - $bit0 = ($value & 0x01) !== 0; - $result = ($value >> 1) | ($bit0 ? 0x80 : 0); - - $cpu->getFlags()->setZ($result === 0); - $cpu->getFlags()->setN(false); - $cpu->getFlags()->setH(false); - $cpu->getFlags()->setC($bit0); - - self::setRegByIndex($cpu, $regIndex, $result); - return $cycles; - }, - ), - - // RL r (0x10-0x17) - Rotate Left through Carry - $opcode >= 0x10 && $opcode <= 0x17 => new Instruction( - opcode: 0xCB00 | $opcode, - mnemonic: "RL $regName", - length: 2, - cycles: $cycles, - handler: static function (Cpu $cpu) use ($regIndex, $cycles): int { - $value = self::getRegByIndex($cpu, $regIndex); - $oldCarry = $cpu->getFlags()->getC(); - [$result, $newCarry] = BitOps::rotateLeft($value, $oldCarry); - - $cpu->getFlags()->setZ($result === 0); - $cpu->getFlags()->setN(false); - $cpu->getFlags()->setH(false); - $cpu->getFlags()->setC($newCarry); - - self::setRegByIndex($cpu, $regIndex, $result); - return $cycles; - }, - ), - - // RR r (0x18-0x1F) - Rotate Right through Carry - $opcode >= 0x18 && $opcode <= 0x1F => new Instruction( - opcode: 0xCB00 | $opcode, - mnemonic: "RR $regName", - length: 2, - cycles: $cycles, - handler: static function (Cpu $cpu) use ($regIndex, $cycles): int { - $value = self::getRegByIndex($cpu, $regIndex); - $oldCarry = $cpu->getFlags()->getC(); - [$result, $newCarry] = BitOps::rotateRight($value, $oldCarry); - - $cpu->getFlags()->setZ($result === 0); - $cpu->getFlags()->setN(false); - $cpu->getFlags()->setH(false); - $cpu->getFlags()->setC($newCarry); - - self::setRegByIndex($cpu, $regIndex, $result); - return $cycles; - }, - ), - - // SLA r (0x20-0x27) - Shift Left Arithmetic - $opcode >= 0x20 && $opcode <= 0x27 => new Instruction( - opcode: 0xCB00 | $opcode, - mnemonic: "SLA $regName", - length: 2, - cycles: $cycles, - handler: static function (Cpu $cpu) use ($regIndex, $cycles): int { - $value = self::getRegByIndex($cpu, $regIndex); - [$result, $carry] = BitOps::shiftLeft($value); - - $cpu->getFlags()->setZ($result === 0); - $cpu->getFlags()->setN(false); - $cpu->getFlags()->setH(false); - $cpu->getFlags()->setC($carry); - - self::setRegByIndex($cpu, $regIndex, $result); - return $cycles; - }, - ), - - // SRA r (0x28-0x2F) - Shift Right Arithmetic (preserve bit 7) - $opcode >= 0x28 && $opcode <= 0x2F => new Instruction( - opcode: 0xCB00 | $opcode, - mnemonic: "SRA $regName", - length: 2, - cycles: $cycles, - handler: static function (Cpu $cpu) use ($regIndex, $cycles): int { - $value = self::getRegByIndex($cpu, $regIndex); - [$result, $carry] = BitOps::shiftRight($value, true); - - $cpu->getFlags()->setZ($result === 0); - $cpu->getFlags()->setN(false); - $cpu->getFlags()->setH(false); - $cpu->getFlags()->setC($carry); - - self::setRegByIndex($cpu, $regIndex, $result); - return $cycles; - }, - ), - - // SWAP r (0x30-0x37) - Swap nibbles - $opcode >= 0x30 && $opcode <= 0x37 => new Instruction( - opcode: 0xCB00 | $opcode, - mnemonic: "SWAP $regName", - length: 2, - cycles: $cycles, - handler: static function (Cpu $cpu) use ($regIndex, $cycles): int { - $value = self::getRegByIndex($cpu, $regIndex); - $result = BitOps::swap($value); - - $cpu->getFlags()->setZ($result === 0); - $cpu->getFlags()->setN(false); - $cpu->getFlags()->setH(false); - $cpu->getFlags()->setC(false); - - self::setRegByIndex($cpu, $regIndex, $result); - return $cycles; - }, - ), - - // SRL r (0x38-0x3F) - Shift Right Logical (bit 7 becomes 0) - $opcode >= 0x38 && $opcode <= 0x3F => new Instruction( - opcode: 0xCB00 | $opcode, - mnemonic: "SRL $regName", - length: 2, - cycles: $cycles, - handler: static function (Cpu $cpu) use ($regIndex, $cycles): int { - $value = self::getRegByIndex($cpu, $regIndex); - [$result, $carry] = BitOps::shiftRight($value, false); - - $cpu->getFlags()->setZ($result === 0); - $cpu->getFlags()->setN(false); - $cpu->getFlags()->setH(false); - $cpu->getFlags()->setC($carry); - - self::setRegByIndex($cpu, $regIndex, $result); - return $cycles; - }, - ), - - // BIT b,r (0x40-0x7F) - Test bit b in register r - $opcode >= 0x40 && $opcode <= 0x7F => new Instruction( - opcode: 0xCB00 | $opcode, - mnemonic: sprintf('BIT %d,%s', ($opcode - 0x40) >> 3, $regName), - length: 2, - cycles: ($regIndex === 6) ? 12 : 8, // BIT b,(HL) is 12 cycles, not 16 - handler: static function (Cpu $cpu) use ($opcode, $regIndex): int { - $bit = ($opcode - 0x40) >> 3; - $value = self::getRegByIndex($cpu, $regIndex); - $bitValue = BitOps::getBit($value, $bit); - - $cpu->getFlags()->setZ(!$bitValue); - $cpu->getFlags()->setN(false); - $cpu->getFlags()->setH(true); - // C flag unchanged - - return ($regIndex === 6) ? 12 : 8; - }, - ), - - // RES b,r (0x80-0xBF) - Reset (clear) bit b in register r - $opcode >= 0x80 && $opcode <= 0xBF => new Instruction( - opcode: 0xCB00 | $opcode, - mnemonic: sprintf('RES %d,%s', ($opcode - 0x80) >> 3, $regName), - length: 2, - cycles: $cycles, - handler: static function (Cpu $cpu) use ($opcode, $regIndex, $cycles): int { - $bit = ($opcode - 0x80) >> 3; - $value = self::getRegByIndex($cpu, $regIndex); - $result = BitOps::setBit($value, $bit, false); - - // Flags unchanged for RES - self::setRegByIndex($cpu, $regIndex, $result); - return $cycles; - }, - ), - - // SET b,r (0xC0-0xFF) - Set bit b in register r - $opcode >= 0xC0 && $opcode <= 0xFF => new Instruction( - opcode: 0xCB00 | $opcode, - mnemonic: sprintf('SET %d,%s', ($opcode - 0xC0) >> 3, $regName), - length: 2, - cycles: $cycles, - handler: static function (Cpu $cpu) use ($opcode, $regIndex, $cycles): int { - $bit = ($opcode - 0xC0) >> 3; - $value = self::getRegByIndex($cpu, $regIndex); - $result = BitOps::setBit($value, $bit, true); - - // Flags unchanged for SET - self::setRegByIndex($cpu, $regIndex, $result); - return $cycles; - }, - ), - - default => throw new RuntimeException(sprintf('Invalid CB opcode: 0xCB%02X', $opcode)), - }; - } - - /** - * Helper: Get register value by index (0-7: B,C,D,E,H,L,(HL),A) - */ - private static function getRegByIndex(Cpu $cpu, int $index): int - { - return match ($index) { - 0 => $cpu->getB(), - 1 => $cpu->getC(), - 2 => $cpu->getD(), - 3 => $cpu->getE(), - 4 => $cpu->getH(), - 5 => $cpu->getL(), - 6 => $cpu->getBus()->readByte($cpu->getHL()->get()), - 7 => $cpu->getA(), - default => throw new \InvalidArgumentException("Invalid register index: {$index}"), - }; - } - - /** - * Helper: Set register value by index (0-7: B,C,D,E,H,L,(HL),A) - */ - private static function setRegByIndex(Cpu $cpu, int $index, int $value): void - { - match ($index) { - 0 => $cpu->setB($value), - 1 => $cpu->setC($value), - 2 => $cpu->setD($value), - 3 => $cpu->setE($value), - 4 => $cpu->setH($value), - 5 => $cpu->setL($value), - 6 => $cpu->getBus()->writeByte($cpu->getHL()->get(), $value), - 7 => $cpu->setA($value), - default => throw new \InvalidArgumentException("Invalid register index: {$index}"), - }; - } -} + Cached instruction table */ + private static array $instructions = []; + + /** @var array Cached CB-prefixed instruction table */ + private static array $cbInstructions = []; + + /** + * Pre-build all instructions to eliminate lazy initialization overhead. + * + * Optimization (Step 14): Build all 512 instructions upfront during initialization. + * Trade-off: ~100KB additional memory for faster instruction dispatch (no isset check). + * Expected: 1-2% performance gain by eliminating branch prediction overhead. + * + * Call this during emulator initialization for best performance. + */ + public static function warmCache(): void + { + // Pre-build all 256 base instructions + for ($opcode = 0x00; $opcode <= 0xFF; $opcode++) { + if (!isset(self::$instructions[$opcode])) { + self::$instructions[$opcode] = self::buildInstruction($opcode); + } + } + + // Pre-build all 256 CB-prefixed instructions + for ($opcode = 0x00; $opcode <= 0xFF; $opcode++) { + if (!isset(self::$cbInstructions[$opcode])) { + self::$cbInstructions[$opcode] = self::buildCBInstruction($opcode); + } + } + } + + /** + * Get instruction metadata for a given opcode. + * + * @param int $opcode The opcode byte (0x00-0xFF) + * @return Instruction The instruction metadata and handler + * @throws RuntimeException If opcode is not implemented + */ + public static function getInstruction(int $opcode): Instruction + { + if (!isset(self::$instructions[$opcode])) { + self::$instructions[$opcode] = self::buildInstruction($opcode); + } + + return self::$instructions[$opcode]; + } + + /** + * Get CB-prefixed instruction metadata for a given opcode. + * + * @param int $opcode The CB opcode byte (0x00-0xFF) + * @return Instruction The instruction metadata and handler + * @throws RuntimeException If opcode is not implemented + */ + public static function getCBInstruction(int $opcode): Instruction + { + if (!isset(self::$cbInstructions[$opcode])) { + self::$cbInstructions[$opcode] = self::buildCBInstruction($opcode); + } + + return self::$cbInstructions[$opcode]; + } + + /** + * Helper: Read next byte from PC and increment PC + */ + private static function readImm8(Cpu $cpu): int + { + return $cpu->fetch(); + } + + /** + * Helper: Read next word (16-bit) from PC and increment PC twice + */ + private static function readImm16(Cpu $cpu): int + { + $low = $cpu->fetch(); + $high = $cpu->fetch(); + return ($high << 8) | $low; + } + + /** + * Helper: Check for half-carry on 8-bit addition + */ + private static function halfCarry8Add(int $a, int $b, int $carry = 0): bool + { + return ((($a & 0x0F) + ($b & 0x0F) + $carry) & 0x10) !== 0; + } + + /** + * Helper: Check for half-carry on 8-bit subtraction + */ + private static function halfCarry8Sub(int $a, int $b, int $carry = 0): bool + { + return ((($a & 0x0F) - ($b & 0x0F) - $carry) & 0x10) !== 0; + } + + /** + * Helper: Check for half-carry on 16-bit addition (bit 11 to bit 12) + */ + private static function halfCarry16Add(int $a, int $b): bool + { + return ((($a & 0x0FFF) + ($b & 0x0FFF)) & 0x1000) !== 0; + } + + /** + * Build instruction metadata for a given opcode. + * + * @param int $opcode The opcode byte + * @return Instruction The instruction + * @throws RuntimeException If opcode is not implemented + */ + private static function buildInstruction(int $opcode): Instruction + { + return match ($opcode) { + // 0x00: NOP - No operation + 0x00 => new Instruction( + opcode: 0x00, + mnemonic: 'NOP', + length: 1, + cycles: 4, + handler: static fn(Cpu $cpu): int => 4, + ), + + // 0x01: LD BC,nn - Load 16-bit immediate into BC + 0x01 => new Instruction( + opcode: 0x01, + mnemonic: 'LD BC,nn', + length: 3, + cycles: 12, + handler: static function (Cpu $cpu): int { + $value = self::readImm16($cpu); + $cpu->getBC()->set($value); + return 12; + }, + ), + + // 0x02: LD (BC),A - Store A into memory at address BC + 0x02 => new Instruction( + opcode: 0x02, + mnemonic: 'LD (BC),A', + length: 1, + cycles: 8, + handler: static function (Cpu $cpu): int { + $address = $cpu->getBC()->get(); + $cpu->cycleWrite($address, $cpu->getA()); + return 8; + }, + ), + + // 0x03: INC BC - Increment BC + 0x03 => new Instruction( + opcode: 0x03, + mnemonic: 'INC BC', + length: 1, + cycles: 8, + handler: static function (Cpu $cpu): int { + $cpu->getBC()->increment(); + $cpu->cycleNoAccess(); // Internal operation: 1 M-cycle + return 8; + }, + ), + + // 0x04: INC B - Increment B + 0x04 => new Instruction( + opcode: 0x04, + mnemonic: 'INC B', + length: 1, + cycles: 4, + handler: static function (Cpu $cpu): int { + $value = $cpu->getB(); + $result = ($value + 1) & 0xFF; + $cpu->setB($result); + $cpu->getFlags()->setZ($result === 0); + $cpu->getFlags()->setN(false); + $cpu->getFlags()->setH((($value & 0x0F) + 1) > 0x0F); + return 4; + }, + ), + + // 0x05: DEC B - Decrement B + 0x05 => new Instruction( + opcode: 0x05, + mnemonic: 'DEC B', + length: 1, + cycles: 4, + handler: static function (Cpu $cpu): int { + $value = $cpu->getB(); + $result = ($value - 1) & 0xFF; + $cpu->setB($result); + $cpu->getFlags()->setZ($result === 0); + $cpu->getFlags()->setN(true); + $cpu->getFlags()->setH(($value & 0x0F) === 0); + return 4; + }, + ), + + // 0x06: LD B,n - Load 8-bit immediate into B + 0x06 => new Instruction( + opcode: 0x06, + mnemonic: 'LD B,n', + length: 2, + cycles: 8, + handler: static function (Cpu $cpu): int { + $cpu->setB(self::readImm8($cpu)); + return 8; + }, + ), + + // 0x07: RLCA - Rotate A left, old bit 7 to carry + 0x07 => new Instruction( + opcode: 0x07, + mnemonic: 'RLCA', + length: 1, + cycles: 4, + handler: static function (Cpu $cpu): int { + $value = $cpu->getA(); + $carry = ($value & 0x80) !== 0; + $result = (($value << 1) & 0xFF) | ($carry ? 1 : 0); + $cpu->setA($result); + $cpu->getFlags()->setZ(false); + $cpu->getFlags()->setN(false); + $cpu->getFlags()->setH(false); + $cpu->getFlags()->setC($carry); + return 4; + }, + ), + + // 0x08: LD (nn),SP - Store SP at address nn + 0x08 => new Instruction( + opcode: 0x08, + mnemonic: 'LD (nn),SP', + length: 3, + cycles: 20, + handler: static function (Cpu $cpu): int { + $address = self::readImm16($cpu); + $sp = $cpu->getSP()->get(); + $cpu->cycleWrite($address, $sp & 0xFF); + $cpu->cycleWrite($address + 1, ($sp >> 8) & 0xFF); + return 20; + }, + ), + + // 0x09: ADD HL,BC - Add BC to HL + 0x09 => new Instruction( + opcode: 0x09, + mnemonic: 'ADD HL,BC', + length: 1, + cycles: 8, + handler: static function (Cpu $cpu): int { + $hl = $cpu->getHL()->get(); + $bc = $cpu->getBC()->get(); + $result = $hl + $bc; + $cpu->getHL()->set($result & 0xFFFF); + $cpu->getFlags()->setN(false); + $cpu->getFlags()->setH(self::halfCarry16Add($hl, $bc)); + $cpu->getFlags()->setC($result > 0xFFFF); + $cpu->cycleNoAccess(); // Internal ALU operation: 1 M-cycle + return 8; + }, + ), + + // 0x0A: LD A,(BC) - Load byte at address BC into A + 0x0A => new Instruction( + opcode: 0x0A, + mnemonic: 'LD A,(BC)', + length: 1, + cycles: 8, + handler: static function (Cpu $cpu): int { + $address = $cpu->getBC()->get(); + $cpu->setA($cpu->cycleRead($address)); + return 8; + }, + ), + + // 0x0B: DEC BC - Decrement BC + 0x0B => new Instruction( + opcode: 0x0B, + mnemonic: 'DEC BC', + length: 1, + cycles: 8, + handler: static function (Cpu $cpu): int { + $cpu->getBC()->decrement(); + $cpu->cycleNoAccess(); // Internal operation: 1 M-cycle + return 8; + }, + ), + + // 0x0C: INC C - Increment C + 0x0C => new Instruction( + opcode: 0x0C, + mnemonic: 'INC C', + length: 1, + cycles: 4, + handler: static function (Cpu $cpu): int { + $value = $cpu->getC(); + $result = ($value + 1) & 0xFF; + $cpu->setC($result); + $cpu->getFlags()->setZ($result === 0); + $cpu->getFlags()->setN(false); + $cpu->getFlags()->setH((($value & 0x0F) + 1) > 0x0F); + return 4; + }, + ), + + // 0x0D: DEC C - Decrement C + 0x0D => new Instruction( + opcode: 0x0D, + mnemonic: 'DEC C', + length: 1, + cycles: 4, + handler: static function (Cpu $cpu): int { + $value = $cpu->getC(); + $result = ($value - 1) & 0xFF; + $cpu->setC($result); + $cpu->getFlags()->setZ($result === 0); + $cpu->getFlags()->setN(true); + $cpu->getFlags()->setH(($value & 0x0F) === 0); + return 4; + }, + ), + + // 0x0E: LD C,n - Load 8-bit immediate into C + 0x0E => new Instruction( + opcode: 0x0E, + mnemonic: 'LD C,n', + length: 2, + cycles: 8, + handler: static function (Cpu $cpu): int { + $cpu->setC(self::readImm8($cpu)); + return 8; + }, + ), + + // 0x0F: RRCA - Rotate A right, old bit 0 to carry + 0x0F => new Instruction( + opcode: 0x0F, + mnemonic: 'RRCA', + length: 1, + cycles: 4, + handler: static function (Cpu $cpu): int { + $value = $cpu->getA(); + $carry = ($value & 0x01) !== 0; + $result = ($value >> 1) | ($carry ? 0x80 : 0); + $cpu->setA($result); + $cpu->getFlags()->setZ(false); + $cpu->getFlags()->setN(false); + $cpu->getFlags()->setH(false); + $cpu->getFlags()->setC($carry); + return 4; + }, + ), + + // 0x10: STOP - Stop CPU and LCD until button press + 0x10 => new Instruction( + opcode: 0x10, + mnemonic: 'STOP', + length: 2, + cycles: 4, + handler: static function (Cpu $cpu): int { + // Read next byte (should be 0x00) + self::readImm8($cpu); + $cpu->setHalted(true); + $cpu->setStopped(true); + return 4; + }, + ), + + // 0x11: LD DE,nn - Load 16-bit immediate into DE + 0x11 => new Instruction( + opcode: 0x11, + mnemonic: 'LD DE,nn', + length: 3, + cycles: 12, + handler: static function (Cpu $cpu): int { + $value = self::readImm16($cpu); + $cpu->getDE()->set($value); + return 12; + }, + ), + + // 0x12: LD (DE),A - Store A into memory at address DE + 0x12 => new Instruction( + opcode: 0x12, + mnemonic: 'LD (DE),A', + length: 1, + cycles: 8, + handler: static function (Cpu $cpu): int { + $address = $cpu->getDE()->get(); + $cpu->cycleWrite($address, $cpu->getA()); + return 8; + }, + ), + + // 0x13: INC DE - Increment DE + 0x13 => new Instruction( + opcode: 0x13, + mnemonic: 'INC DE', + length: 1, + cycles: 8, + handler: static function (Cpu $cpu): int { + $cpu->getDE()->increment(); + $cpu->cycleNoAccess(); // Internal operation: 1 M-cycle + return 8; + }, + ), + + // 0x14: INC D - Increment D + 0x14 => new Instruction( + opcode: 0x14, + mnemonic: 'INC D', + length: 1, + cycles: 4, + handler: static function (Cpu $cpu): int { + $value = $cpu->getD(); + $result = ($value + 1) & 0xFF; + $cpu->setD($result); + $cpu->getFlags()->setZ($result === 0); + $cpu->getFlags()->setN(false); + $cpu->getFlags()->setH((($value & 0x0F) + 1) > 0x0F); + return 4; + }, + ), + + // 0x15: DEC D - Decrement D + 0x15 => new Instruction( + opcode: 0x15, + mnemonic: 'DEC D', + length: 1, + cycles: 4, + handler: static function (Cpu $cpu): int { + $value = $cpu->getD(); + $result = ($value - 1) & 0xFF; + $cpu->setD($result); + $cpu->getFlags()->setZ($result === 0); + $cpu->getFlags()->setN(true); + $cpu->getFlags()->setH(($value & 0x0F) === 0); + return 4; + }, + ), + + // 0x16: LD D,n - Load 8-bit immediate into D + 0x16 => new Instruction( + opcode: 0x16, + mnemonic: 'LD D,n', + length: 2, + cycles: 8, + handler: static function (Cpu $cpu): int { + $cpu->setD(self::readImm8($cpu)); + return 8; + }, + ), + + // 0x17: RLA - Rotate A left through carry + 0x17 => new Instruction( + opcode: 0x17, + mnemonic: 'RLA', + length: 1, + cycles: 4, + handler: static function (Cpu $cpu): int { + $value = $cpu->getA(); + $oldCarry = $cpu->getFlags()->getC(); + $newCarry = ($value & 0x80) !== 0; + $result = (($value << 1) & 0xFF) | ($oldCarry ? 1 : 0); + $cpu->setA($result); + $cpu->getFlags()->setZ(false); + $cpu->getFlags()->setN(false); + $cpu->getFlags()->setH(false); + $cpu->getFlags()->setC($newCarry); + return 4; + }, + ), + + // 0x18: JR e - Relative jump + 0x18 => new Instruction( + opcode: 0x18, + mnemonic: 'JR e', + length: 2, + cycles: 12, + handler: static function (Cpu $cpu): int { + $offset = self::readImm8($cpu); + // Sign extend + if ($offset > 0x7F) { + $offset -= 0x100; + } + $pc = $cpu->getPC()->get(); + $cpu->getPC()->set($pc + $offset); + $cpu->cycleNoAccess(); // Internal delay for taken branch: 1 M-cycle + return 12; + }, + ), + + // 0x19: ADD HL,DE - Add DE to HL + 0x19 => new Instruction( + opcode: 0x19, + mnemonic: 'ADD HL,DE', + length: 1, + cycles: 8, + handler: static function (Cpu $cpu): int { + $hl = $cpu->getHL()->get(); + $de = $cpu->getDE()->get(); + $result = $hl + $de; + $cpu->getHL()->set($result & 0xFFFF); + $cpu->getFlags()->setN(false); + $cpu->getFlags()->setH(self::halfCarry16Add($hl, $de)); + $cpu->getFlags()->setC($result > 0xFFFF); + $cpu->cycleNoAccess(); // Internal ALU operation: 1 M-cycle + return 8; + }, + ), + + // 0x1A: LD A,(DE) - Load byte at address DE into A + 0x1A => new Instruction( + opcode: 0x1A, + mnemonic: 'LD A,(DE)', + length: 1, + cycles: 8, + handler: static function (Cpu $cpu): int { + $address = $cpu->getDE()->get(); + $cpu->setA($cpu->cycleRead($address)); + return 8; + }, + ), + + // 0x1B: DEC DE - Decrement DE + 0x1B => new Instruction( + opcode: 0x1B, + mnemonic: 'DEC DE', + length: 1, + cycles: 8, + handler: static function (Cpu $cpu): int { + $cpu->getDE()->decrement(); + $cpu->cycleNoAccess(); // Internal operation: 1 M-cycle + return 8; + }, + ), + + // 0x1C: INC E - Increment E + 0x1C => new Instruction( + opcode: 0x1C, + mnemonic: 'INC E', + length: 1, + cycles: 4, + handler: static function (Cpu $cpu): int { + $value = $cpu->getE(); + $result = ($value + 1) & 0xFF; + $cpu->setE($result); + $cpu->getFlags()->setZ($result === 0); + $cpu->getFlags()->setN(false); + $cpu->getFlags()->setH((($value & 0x0F) + 1) > 0x0F); + return 4; + }, + ), + + // 0x1D: DEC E - Decrement E + 0x1D => new Instruction( + opcode: 0x1D, + mnemonic: 'DEC E', + length: 1, + cycles: 4, + handler: static function (Cpu $cpu): int { + $value = $cpu->getE(); + $result = ($value - 1) & 0xFF; + $cpu->setE($result); + $cpu->getFlags()->setZ($result === 0); + $cpu->getFlags()->setN(true); + $cpu->getFlags()->setH(($value & 0x0F) === 0); + return 4; + }, + ), + + // 0x1E: LD E,n - Load 8-bit immediate into E + 0x1E => new Instruction( + opcode: 0x1E, + mnemonic: 'LD E,n', + length: 2, + cycles: 8, + handler: static function (Cpu $cpu): int { + $cpu->setE(self::readImm8($cpu)); + return 8; + }, + ), + + // 0x1F: RRA - Rotate A right through carry + 0x1F => new Instruction( + opcode: 0x1F, + mnemonic: 'RRA', + length: 1, + cycles: 4, + handler: static function (Cpu $cpu): int { + $value = $cpu->getA(); + $oldCarry = $cpu->getFlags()->getC(); + $newCarry = ($value & 0x01) !== 0; + $result = ($value >> 1) | ($oldCarry ? 0x80 : 0); + $cpu->setA($result); + $cpu->getFlags()->setZ(false); + $cpu->getFlags()->setN(false); + $cpu->getFlags()->setH(false); + $cpu->getFlags()->setC($newCarry); + return 4; + }, + ), + + // 0x20: JR NZ,e - Relative jump if not zero + 0x20 => new Instruction( + opcode: 0x20, + mnemonic: 'JR NZ,e', + length: 2, + cycles: 8, // 12 if taken, 8 if not taken + handler: static function (Cpu $cpu): int { + $offset = self::readImm8($cpu); + if (!$cpu->getFlags()->getZ()) { + // Sign extend + if ($offset > 0x7F) { + $offset -= 0x100; + } + $pc = $cpu->getPC()->get(); + $cpu->getPC()->set($pc + $offset); + $cpu->cycleNoAccess(); // Internal delay for taken branch: 1 M-cycle + return 12; + } + return 8; + }, + ), + + // 0x21: LD HL,nn - Load 16-bit immediate into HL + 0x21 => new Instruction( + opcode: 0x21, + mnemonic: 'LD HL,nn', + length: 3, + cycles: 12, + handler: static function (Cpu $cpu): int { + $value = self::readImm16($cpu); + $cpu->getHL()->set($value); + return 12; + }, + ), + + // 0x22: LD (HL+),A - Store A at HL, increment HL + 0x22 => new Instruction( + opcode: 0x22, + mnemonic: 'LD (HL+),A', + length: 1, + cycles: 8, + handler: static function (Cpu $cpu): int { + $address = $cpu->getHL()->get(); + $cpu->cycleWrite($address, $cpu->getA()); + $cpu->getHL()->increment(); + return 8; + }, + ), + + // 0x23: INC HL - Increment HL + 0x23 => new Instruction( + opcode: 0x23, + mnemonic: 'INC HL', + length: 1, + cycles: 8, + handler: static function (Cpu $cpu): int { + $cpu->getHL()->increment(); + $cpu->cycleNoAccess(); // Internal operation: 1 M-cycle + return 8; + }, + ), + + // 0x24: INC H - Increment H + 0x24 => new Instruction( + opcode: 0x24, + mnemonic: 'INC H', + length: 1, + cycles: 4, + handler: static function (Cpu $cpu): int { + $value = $cpu->getH(); + $result = ($value + 1) & 0xFF; + $cpu->setH($result); + $cpu->getFlags()->setZ($result === 0); + $cpu->getFlags()->setN(false); + $cpu->getFlags()->setH((($value & 0x0F) + 1) > 0x0F); + return 4; + }, + ), + + // 0x25: DEC H - Decrement H + 0x25 => new Instruction( + opcode: 0x25, + mnemonic: 'DEC H', + length: 1, + cycles: 4, + handler: static function (Cpu $cpu): int { + $value = $cpu->getH(); + $result = ($value - 1) & 0xFF; + $cpu->setH($result); + $cpu->getFlags()->setZ($result === 0); + $cpu->getFlags()->setN(true); + $cpu->getFlags()->setH(($value & 0x0F) === 0); + return 4; + }, + ), + + // 0x26: LD H,n - Load 8-bit immediate into H + 0x26 => new Instruction( + opcode: 0x26, + mnemonic: 'LD H,n', + length: 2, + cycles: 8, + handler: static function (Cpu $cpu): int { + $cpu->setH(self::readImm8($cpu)); + return 8; + }, + ), + + // 0x27: DAA - Decimal Adjust Accumulator + 0x27 => new Instruction( + opcode: 0x27, + mnemonic: 'DAA', + length: 1, + cycles: 4, + handler: static function (Cpu $cpu): int { + $a = $cpu->getA(); + $flags = $cpu->getFlags(); + + if (!$flags->getN()) { + // After addition (ADD, ADC, INC) + // Upper nibble first + if ($flags->getC() || $a > 0x99) { + $a = ($a + 0x60) & 0xFF; + $flags->setC(true); + } + // Lower nibble second + if ($flags->getH() || ($a & 0x0F) > 0x09) { + $a = ($a + 0x06) & 0xFF; + } + } else { + // After subtraction (SUB, SBC, DEC) + // Carry first + if ($flags->getC()) { + $a = ($a - 0x60) & 0xFF; + } + // Half-carry second + if ($flags->getH()) { + $a = ($a - 0x06) & 0xFF; + } + // Carry unchanged in subtraction + } + + $cpu->setA($a); + $flags->setZ($a === 0); + $flags->setH(false); + // N flag unchanged (not set here) + // C flag already set in addition mode, unchanged in subtraction mode + return 4; + }, + ), + + // 0x28: JR Z,e - Relative jump if zero + 0x28 => new Instruction( + opcode: 0x28, + mnemonic: 'JR Z,e', + length: 2, + cycles: 8, // 12 if taken, 8 if not taken + handler: static function (Cpu $cpu): int { + $offset = self::readImm8($cpu); + if ($cpu->getFlags()->getZ()) { + // Sign extend + if ($offset > 0x7F) { + $offset -= 0x100; + } + $pc = $cpu->getPC()->get(); + $cpu->getPC()->set($pc + $offset); + $cpu->cycleNoAccess(); // Internal delay for taken branch: 1 M-cycle + return 12; + } + return 8; + }, + ), + + // 0x29: ADD HL,HL - Add HL to HL + 0x29 => new Instruction( + opcode: 0x29, + mnemonic: 'ADD HL,HL', + length: 1, + cycles: 8, + handler: static function (Cpu $cpu): int { + $hl = $cpu->getHL()->get(); + $result = $hl + $hl; + $cpu->getHL()->set($result & 0xFFFF); + $cpu->getFlags()->setN(false); + $cpu->getFlags()->setH(self::halfCarry16Add($hl, $hl)); + $cpu->getFlags()->setC($result > 0xFFFF); + $cpu->cycleNoAccess(); // Internal ALU operation: 1 M-cycle + return 8; + }, + ), + + // 0x2A: LD A,(HL+) - Load byte at HL into A, increment HL + 0x2A => new Instruction( + opcode: 0x2A, + mnemonic: 'LD A,(HL+)', + length: 1, + cycles: 8, + handler: static function (Cpu $cpu): int { + $address = $cpu->getHL()->get(); + $cpu->setA($cpu->cycleRead($address)); + $cpu->getHL()->increment(); + return 8; + }, + ), + + // 0x2B: DEC HL - Decrement HL + 0x2B => new Instruction( + opcode: 0x2B, + mnemonic: 'DEC HL', + length: 1, + cycles: 8, + handler: static function (Cpu $cpu): int { + $cpu->getHL()->decrement(); + $cpu->cycleNoAccess(); // Internal operation: 1 M-cycle + return 8; + }, + ), + + // 0x2C: INC L - Increment L + 0x2C => new Instruction( + opcode: 0x2C, + mnemonic: 'INC L', + length: 1, + cycles: 4, + handler: static function (Cpu $cpu): int { + $value = $cpu->getL(); + $result = ($value + 1) & 0xFF; + $cpu->setL($result); + $cpu->getFlags()->setZ($result === 0); + $cpu->getFlags()->setN(false); + $cpu->getFlags()->setH((($value & 0x0F) + 1) > 0x0F); + return 4; + }, + ), + + // 0x2D: DEC L - Decrement L + 0x2D => new Instruction( + opcode: 0x2D, + mnemonic: 'DEC L', + length: 1, + cycles: 4, + handler: static function (Cpu $cpu): int { + $value = $cpu->getL(); + $result = ($value - 1) & 0xFF; + $cpu->setL($result); + $cpu->getFlags()->setZ($result === 0); + $cpu->getFlags()->setN(true); + $cpu->getFlags()->setH(($value & 0x0F) === 0); + return 4; + }, + ), + + // 0x2E: LD L,n - Load 8-bit immediate into L + 0x2E => new Instruction( + opcode: 0x2E, + mnemonic: 'LD L,n', + length: 2, + cycles: 8, + handler: static function (Cpu $cpu): int { + $cpu->setL(self::readImm8($cpu)); + return 8; + }, + ), + + // 0x2F: CPL - Complement A (flip all bits) + 0x2F => new Instruction( + opcode: 0x2F, + mnemonic: 'CPL', + length: 1, + cycles: 4, + handler: static function (Cpu $cpu): int { + $cpu->setA($cpu->getA() ^ 0xFF); + $cpu->getFlags()->setN(true); + $cpu->getFlags()->setH(true); + return 4; + }, + ), + + // 0x30: JR NC,e - Relative jump if not carry + 0x30 => new Instruction( + opcode: 0x30, + mnemonic: 'JR NC,e', + length: 2, + cycles: 8, // 12 if taken, 8 if not taken + handler: static function (Cpu $cpu): int { + $offset = self::readImm8($cpu); + if (!$cpu->getFlags()->getC()) { + // Sign extend + if ($offset > 0x7F) { + $offset -= 0x100; + } + $pc = $cpu->getPC()->get(); + $cpu->getPC()->set($pc + $offset); + $cpu->cycleNoAccess(); // Internal delay for taken branch: 1 M-cycle + return 12; + } + return 8; + }, + ), + + // 0x31: LD SP,nn - Load 16-bit immediate into SP + 0x31 => new Instruction( + opcode: 0x31, + mnemonic: 'LD SP,nn', + length: 3, + cycles: 12, + handler: static function (Cpu $cpu): int { + $value = self::readImm16($cpu); + $cpu->getSP()->set($value); + return 12; + }, + ), + + // 0x32: LD (HL-),A - Store A at HL, decrement HL + 0x32 => new Instruction( + opcode: 0x32, + mnemonic: 'LD (HL-),A', + length: 1, + cycles: 8, + handler: static function (Cpu $cpu): int { + $address = $cpu->getHL()->get(); + $cpu->cycleWrite($address, $cpu->getA()); + $cpu->getHL()->decrement(); + return 8; + }, + ), + + // 0x33: INC SP - Increment SP + 0x33 => new Instruction( + opcode: 0x33, + mnemonic: 'INC SP', + length: 1, + cycles: 8, + handler: static function (Cpu $cpu): int { + $cpu->getSP()->increment(); + $cpu->cycleNoAccess(); // Internal operation: 1 M-cycle + return 8; + }, + ), + + // 0x34: INC (HL) - Increment byte at address HL + 0x34 => new Instruction( + opcode: 0x34, + mnemonic: 'INC (HL)', + length: 1, + cycles: 12, + handler: static function (Cpu $cpu): int { + $address = $cpu->getHL()->get(); + $value = $cpu->cycleRead($address); + $result = ($value + 1) & 0xFF; + $cpu->cycleWrite($address, $result); + $cpu->getFlags()->setZ($result === 0); + $cpu->getFlags()->setN(false); + $cpu->getFlags()->setH((($value & 0x0F) + 1) > 0x0F); + return 12; + }, + ), + + // 0x35: DEC (HL) - Decrement byte at address HL + 0x35 => new Instruction( + opcode: 0x35, + mnemonic: 'DEC (HL)', + length: 1, + cycles: 12, + handler: static function (Cpu $cpu): int { + $address = $cpu->getHL()->get(); + $value = $cpu->cycleRead($address); + $result = ($value - 1) & 0xFF; + $cpu->cycleWrite($address, $result); + $cpu->getFlags()->setZ($result === 0); + $cpu->getFlags()->setN(true); + $cpu->getFlags()->setH(($value & 0x0F) === 0); + return 12; + }, + ), + + // 0x36: LD (HL),n - Load immediate byte into address HL + 0x36 => new Instruction( + opcode: 0x36, + mnemonic: 'LD (HL),n', + length: 2, + cycles: 12, + handler: static function (Cpu $cpu): int { + $value = self::readImm8($cpu); + $address = $cpu->getHL()->get(); + $cpu->cycleWrite($address, $value); + return 12; + }, + ), + + // 0x37: SCF - Set Carry Flag + 0x37 => new Instruction( + opcode: 0x37, + mnemonic: 'SCF', + length: 1, + cycles: 4, + handler: static function (Cpu $cpu): int { + $cpu->getFlags()->setN(false); + $cpu->getFlags()->setH(false); + $cpu->getFlags()->setC(true); + return 4; + }, + ), + + // 0x38: JR C,e - Relative jump if carry + 0x38 => new Instruction( + opcode: 0x38, + mnemonic: 'JR C,e', + length: 2, + cycles: 8, // 12 if taken, 8 if not taken + handler: static function (Cpu $cpu): int { + $offset = self::readImm8($cpu); + if ($cpu->getFlags()->getC()) { + // Sign extend + if ($offset > 0x7F) { + $offset -= 0x100; + } + $pc = $cpu->getPC()->get(); + $cpu->getPC()->set($pc + $offset); + $cpu->cycleNoAccess(); // Internal delay for taken branch: 1 M-cycle + return 12; + } + return 8; + }, + ), + + // 0x39: ADD HL,SP - Add SP to HL + 0x39 => new Instruction( + opcode: 0x39, + mnemonic: 'ADD HL,SP', + length: 1, + cycles: 8, + handler: static function (Cpu $cpu): int { + $hl = $cpu->getHL()->get(); + $sp = $cpu->getSP()->get(); + $result = $hl + $sp; + $cpu->getHL()->set($result & 0xFFFF); + $cpu->getFlags()->setN(false); + $cpu->getFlags()->setH(self::halfCarry16Add($hl, $sp)); + $cpu->getFlags()->setC($result > 0xFFFF); + $cpu->cycleNoAccess(); // Internal ALU operation: 1 M-cycle + return 8; + }, + ), + + // 0x3A: LD A,(HL-) - Load byte at HL into A, decrement HL + 0x3A => new Instruction( + opcode: 0x3A, + mnemonic: 'LD A,(HL-)', + length: 1, + cycles: 8, + handler: static function (Cpu $cpu): int { + $address = $cpu->getHL()->get(); + $cpu->setA($cpu->cycleRead($address)); + $cpu->getHL()->decrement(); + return 8; + }, + ), + + // 0x3B: DEC SP - Decrement SP + 0x3B => new Instruction( + opcode: 0x3B, + mnemonic: 'DEC SP', + length: 1, + cycles: 8, + handler: static function (Cpu $cpu): int { + $cpu->getSP()->decrement(); + $cpu->cycleNoAccess(); // Internal operation: 1 M-cycle + return 8; + }, + ), + + // 0x3C: INC A - Increment A + 0x3C => new Instruction( + opcode: 0x3C, + mnemonic: 'INC A', + length: 1, + cycles: 4, + handler: static function (Cpu $cpu): int { + $value = $cpu->getA(); + $result = ($value + 1) & 0xFF; + $cpu->setA($result); + $cpu->getFlags()->setZ($result === 0); + $cpu->getFlags()->setN(false); + $cpu->getFlags()->setH((($value & 0x0F) + 1) > 0x0F); + return 4; + }, + ), + + // 0x3D: DEC A - Decrement A + 0x3D => new Instruction( + opcode: 0x3D, + mnemonic: 'DEC A', + length: 1, + cycles: 4, + handler: static function (Cpu $cpu): int { + $value = $cpu->getA(); + $result = ($value - 1) & 0xFF; + $cpu->setA($result); + $cpu->getFlags()->setZ($result === 0); + $cpu->getFlags()->setN(true); + $cpu->getFlags()->setH(($value & 0x0F) === 0); + return 4; + }, + ), + + // 0x3E: LD A,n - Load 8-bit immediate into A + 0x3E => new Instruction( + opcode: 0x3E, + mnemonic: 'LD A,n', + length: 2, + cycles: 8, + handler: static function (Cpu $cpu): int { + $cpu->setA(self::readImm8($cpu)); + return 8; + }, + ), + + // 0x3F: CCF - Complement Carry Flag + 0x3F => new Instruction( + opcode: 0x3F, + mnemonic: 'CCF', + length: 1, + cycles: 4, + handler: static function (Cpu $cpu): int { + $cpu->getFlags()->setN(false); + $cpu->getFlags()->setH(false); + $cpu->getFlags()->setC(!$cpu->getFlags()->getC()); + return 4; + }, + ), + + // 0x40-0x7F: LD r,r instructions (load register to register) + // B=0, C=1, D=2, E=3, H=4, L=5, (HL)=6, A=7 + + // 0x40: LD B,B + 0x40 => new Instruction( + opcode: 0x40, + mnemonic: 'LD B,B', + length: 1, + cycles: 4, + handler: static fn(Cpu $cpu): int => 4, + ), + + // 0x41: LD B,C + 0x41 => new Instruction( + opcode: 0x41, + mnemonic: 'LD B,C', + length: 1, + cycles: 4, + handler: static function (Cpu $cpu): int { + $cpu->setB($cpu->getC()); + return 4; + }, + ), + + // 0x42: LD B,D + 0x42 => new Instruction( + opcode: 0x42, + mnemonic: 'LD B,D', + length: 1, + cycles: 4, + handler: static function (Cpu $cpu): int { + $cpu->setB($cpu->getD()); + return 4; + }, + ), + + // 0x43: LD B,E + 0x43 => new Instruction( + opcode: 0x43, + mnemonic: 'LD B,E', + length: 1, + cycles: 4, + handler: static function (Cpu $cpu): int { + $cpu->setB($cpu->getE()); + return 4; + }, + ), + + // 0x44: LD B,H + 0x44 => new Instruction( + opcode: 0x44, + mnemonic: 'LD B,H', + length: 1, + cycles: 4, + handler: static function (Cpu $cpu): int { + $cpu->setB($cpu->getH()); + return 4; + }, + ), + + // 0x45: LD B,L + 0x45 => new Instruction( + opcode: 0x45, + mnemonic: 'LD B,L', + length: 1, + cycles: 4, + handler: static function (Cpu $cpu): int { + $cpu->setB($cpu->getL()); + return 4; + }, + ), + + // 0x46: LD B,(HL) + 0x46 => new Instruction( + opcode: 0x46, + mnemonic: 'LD B,(HL)', + length: 1, + cycles: 8, + handler: static function (Cpu $cpu): int { + $address = $cpu->getHL()->get(); + $cpu->setB($cpu->cycleRead($address)); + return 8; + }, + ), + + // 0x47: LD B,A + 0x47 => new Instruction( + opcode: 0x47, + mnemonic: 'LD B,A', + length: 1, + cycles: 4, + handler: static function (Cpu $cpu): int { + $cpu->setB($cpu->getA()); + return 4; + }, + ), + + // 0x48: LD C,B + 0x48 => new Instruction( + opcode: 0x48, + mnemonic: 'LD C,B', + length: 1, + cycles: 4, + handler: static function (Cpu $cpu): int { + $cpu->setC($cpu->getB()); + return 4; + }, + ), + + // 0x49: LD C,C + 0x49 => new Instruction( + opcode: 0x49, + mnemonic: 'LD C,C', + length: 1, + cycles: 4, + handler: static fn(Cpu $cpu): int => 4, + ), + + // 0x4A: LD C,D + 0x4A => new Instruction( + opcode: 0x4A, + mnemonic: 'LD C,D', + length: 1, + cycles: 4, + handler: static function (Cpu $cpu): int { + $cpu->setC($cpu->getD()); + return 4; + }, + ), + + // 0x4B: LD C,E + 0x4B => new Instruction( + opcode: 0x4B, + mnemonic: 'LD C,E', + length: 1, + cycles: 4, + handler: static function (Cpu $cpu): int { + $cpu->setC($cpu->getE()); + return 4; + }, + ), + + // 0x4C: LD C,H + 0x4C => new Instruction( + opcode: 0x4C, + mnemonic: 'LD C,H', + length: 1, + cycles: 4, + handler: static function (Cpu $cpu): int { + $cpu->setC($cpu->getH()); + return 4; + }, + ), + + // 0x4D: LD C,L + 0x4D => new Instruction( + opcode: 0x4D, + mnemonic: 'LD C,L', + length: 1, + cycles: 4, + handler: static function (Cpu $cpu): int { + $cpu->setC($cpu->getL()); + return 4; + }, + ), + + // 0x4E: LD C,(HL) + 0x4E => new Instruction( + opcode: 0x4E, + mnemonic: 'LD C,(HL)', + length: 1, + cycles: 8, + handler: static function (Cpu $cpu): int { + $address = $cpu->getHL()->get(); + $cpu->setC($cpu->cycleRead($address)); + return 8; + }, + ), + + // 0x4F: LD C,A + 0x4F => new Instruction( + opcode: 0x4F, + mnemonic: 'LD C,A', + length: 1, + cycles: 4, + handler: static function (Cpu $cpu): int { + $cpu->setC($cpu->getA()); + return 4; + }, + ), + + // 0x50: LD D,B + 0x50 => new Instruction( + opcode: 0x50, + mnemonic: 'LD D,B', + length: 1, + cycles: 4, + handler: static function (Cpu $cpu): int { + $cpu->setD($cpu->getB()); + return 4; + }, + ), + + // 0x51: LD D,C + 0x51 => new Instruction( + opcode: 0x51, + mnemonic: 'LD D,C', + length: 1, + cycles: 4, + handler: static function (Cpu $cpu): int { + $cpu->setD($cpu->getC()); + return 4; + }, + ), + + // 0x52: LD D,D + 0x52 => new Instruction( + opcode: 0x52, + mnemonic: 'LD D,D', + length: 1, + cycles: 4, + handler: static fn(Cpu $cpu): int => 4, + ), + + // 0x53: LD D,E + 0x53 => new Instruction( + opcode: 0x53, + mnemonic: 'LD D,E', + length: 1, + cycles: 4, + handler: static function (Cpu $cpu): int { + $cpu->setD($cpu->getE()); + return 4; + }, + ), + + // 0x54: LD D,H + 0x54 => new Instruction( + opcode: 0x54, + mnemonic: 'LD D,H', + length: 1, + cycles: 4, + handler: static function (Cpu $cpu): int { + $cpu->setD($cpu->getH()); + return 4; + }, + ), + + // 0x55: LD D,L + 0x55 => new Instruction( + opcode: 0x55, + mnemonic: 'LD D,L', + length: 1, + cycles: 4, + handler: static function (Cpu $cpu): int { + $cpu->setD($cpu->getL()); + return 4; + }, + ), + + // 0x56: LD D,(HL) + 0x56 => new Instruction( + opcode: 0x56, + mnemonic: 'LD D,(HL)', + length: 1, + cycles: 8, + handler: static function (Cpu $cpu): int { + $address = $cpu->getHL()->get(); + $cpu->setD($cpu->cycleRead($address)); + return 8; + }, + ), + + // 0x57: LD D,A + 0x57 => new Instruction( + opcode: 0x57, + mnemonic: 'LD D,A', + length: 1, + cycles: 4, + handler: static function (Cpu $cpu): int { + $cpu->setD($cpu->getA()); + return 4; + }, + ), + + // 0x58: LD E,B + 0x58 => new Instruction( + opcode: 0x58, + mnemonic: 'LD E,B', + length: 1, + cycles: 4, + handler: static function (Cpu $cpu): int { + $cpu->setE($cpu->getB()); + return 4; + }, + ), + + // 0x59: LD E,C + 0x59 => new Instruction( + opcode: 0x59, + mnemonic: 'LD E,C', + length: 1, + cycles: 4, + handler: static function (Cpu $cpu): int { + $cpu->setE($cpu->getC()); + return 4; + }, + ), + + // 0x5A: LD E,D + 0x5A => new Instruction( + opcode: 0x5A, + mnemonic: 'LD E,D', + length: 1, + cycles: 4, + handler: static function (Cpu $cpu): int { + $cpu->setE($cpu->getD()); + return 4; + }, + ), + + // 0x5B: LD E,E + 0x5B => new Instruction( + opcode: 0x5B, + mnemonic: 'LD E,E', + length: 1, + cycles: 4, + handler: static fn(Cpu $cpu): int => 4, + ), + + // 0x5C: LD E,H + 0x5C => new Instruction( + opcode: 0x5C, + mnemonic: 'LD E,H', + length: 1, + cycles: 4, + handler: static function (Cpu $cpu): int { + $cpu->setE($cpu->getH()); + return 4; + }, + ), + + // 0x5D: LD E,L + 0x5D => new Instruction( + opcode: 0x5D, + mnemonic: 'LD E,L', + length: 1, + cycles: 4, + handler: static function (Cpu $cpu): int { + $cpu->setE($cpu->getL()); + return 4; + }, + ), + + // 0x5E: LD E,(HL) + 0x5E => new Instruction( + opcode: 0x5E, + mnemonic: 'LD E,(HL)', + length: 1, + cycles: 8, + handler: static function (Cpu $cpu): int { + $address = $cpu->getHL()->get(); + $cpu->setE($cpu->cycleRead($address)); + return 8; + }, + ), + + // 0x5F: LD E,A + 0x5F => new Instruction( + opcode: 0x5F, + mnemonic: 'LD E,A', + length: 1, + cycles: 4, + handler: static function (Cpu $cpu): int { + $cpu->setE($cpu->getA()); + return 4; + }, + ), + + // 0x60: LD H,B + 0x60 => new Instruction( + opcode: 0x60, + mnemonic: 'LD H,B', + length: 1, + cycles: 4, + handler: static function (Cpu $cpu): int { + $cpu->setH($cpu->getB()); + return 4; + }, + ), + + // 0x61: LD H,C + 0x61 => new Instruction( + opcode: 0x61, + mnemonic: 'LD H,C', + length: 1, + cycles: 4, + handler: static function (Cpu $cpu): int { + $cpu->setH($cpu->getC()); + return 4; + }, + ), + + // 0x62: LD H,D + 0x62 => new Instruction( + opcode: 0x62, + mnemonic: 'LD H,D', + length: 1, + cycles: 4, + handler: static function (Cpu $cpu): int { + $cpu->setH($cpu->getD()); + return 4; + }, + ), + + // 0x63: LD H,E + 0x63 => new Instruction( + opcode: 0x63, + mnemonic: 'LD H,E', + length: 1, + cycles: 4, + handler: static function (Cpu $cpu): int { + $cpu->setH($cpu->getE()); + return 4; + }, + ), + + // 0x64: LD H,H + 0x64 => new Instruction( + opcode: 0x64, + mnemonic: 'LD H,H', + length: 1, + cycles: 4, + handler: static fn(Cpu $cpu): int => 4, + ), + + // 0x65: LD H,L + 0x65 => new Instruction( + opcode: 0x65, + mnemonic: 'LD H,L', + length: 1, + cycles: 4, + handler: static function (Cpu $cpu): int { + $cpu->setH($cpu->getL()); + return 4; + }, + ), + + // 0x66: LD H,(HL) + 0x66 => new Instruction( + opcode: 0x66, + mnemonic: 'LD H,(HL)', + length: 1, + cycles: 8, + handler: static function (Cpu $cpu): int { + $address = $cpu->getHL()->get(); + $cpu->setH($cpu->cycleRead($address)); + return 8; + }, + ), + + // 0x67: LD H,A + 0x67 => new Instruction( + opcode: 0x67, + mnemonic: 'LD H,A', + length: 1, + cycles: 4, + handler: static function (Cpu $cpu): int { + $cpu->setH($cpu->getA()); + return 4; + }, + ), + + // 0x68: LD L,B + 0x68 => new Instruction( + opcode: 0x68, + mnemonic: 'LD L,B', + length: 1, + cycles: 4, + handler: static function (Cpu $cpu): int { + $cpu->setL($cpu->getB()); + return 4; + }, + ), + + // 0x69: LD L,C + 0x69 => new Instruction( + opcode: 0x69, + mnemonic: 'LD L,C', + length: 1, + cycles: 4, + handler: static function (Cpu $cpu): int { + $cpu->setL($cpu->getC()); + return 4; + }, + ), + + // 0x6A: LD L,D + 0x6A => new Instruction( + opcode: 0x6A, + mnemonic: 'LD L,D', + length: 1, + cycles: 4, + handler: static function (Cpu $cpu): int { + $cpu->setL($cpu->getD()); + return 4; + }, + ), + + // 0x6B: LD L,E + 0x6B => new Instruction( + opcode: 0x6B, + mnemonic: 'LD L,E', + length: 1, + cycles: 4, + handler: static function (Cpu $cpu): int { + $cpu->setL($cpu->getE()); + return 4; + }, + ), + + // 0x6C: LD L,H + 0x6C => new Instruction( + opcode: 0x6C, + mnemonic: 'LD L,H', + length: 1, + cycles: 4, + handler: static function (Cpu $cpu): int { + $cpu->setL($cpu->getH()); + return 4; + }, + ), + + // 0x6D: LD L,L + 0x6D => new Instruction( + opcode: 0x6D, + mnemonic: 'LD L,L', + length: 1, + cycles: 4, + handler: static fn(Cpu $cpu): int => 4, + ), + + // 0x6E: LD L,(HL) + 0x6E => new Instruction( + opcode: 0x6E, + mnemonic: 'LD L,(HL)', + length: 1, + cycles: 8, + handler: static function (Cpu $cpu): int { + $address = $cpu->getHL()->get(); + $cpu->setL($cpu->cycleRead($address)); + return 8; + }, + ), + + // 0x6F: LD L,A + 0x6F => new Instruction( + opcode: 0x6F, + mnemonic: 'LD L,A', + length: 1, + cycles: 4, + handler: static function (Cpu $cpu): int { + $cpu->setL($cpu->getA()); + return 4; + }, + ), + + // 0x70: LD (HL),B + 0x70 => new Instruction( + opcode: 0x70, + mnemonic: 'LD (HL),B', + length: 1, + cycles: 8, + handler: static function (Cpu $cpu): int { + $address = $cpu->getHL()->get(); + $cpu->cycleWrite($address, $cpu->getB()); + return 8; + }, + ), + + // 0x71: LD (HL),C + 0x71 => new Instruction( + opcode: 0x71, + mnemonic: 'LD (HL),C', + length: 1, + cycles: 8, + handler: static function (Cpu $cpu): int { + $address = $cpu->getHL()->get(); + $cpu->cycleWrite($address, $cpu->getC()); + return 8; + }, + ), + + // 0x72: LD (HL),D + 0x72 => new Instruction( + opcode: 0x72, + mnemonic: 'LD (HL),D', + length: 1, + cycles: 8, + handler: static function (Cpu $cpu): int { + $address = $cpu->getHL()->get(); + $cpu->cycleWrite($address, $cpu->getD()); + return 8; + }, + ), + + // 0x73: LD (HL),E + 0x73 => new Instruction( + opcode: 0x73, + mnemonic: 'LD (HL),E', + length: 1, + cycles: 8, + handler: static function (Cpu $cpu): int { + $address = $cpu->getHL()->get(); + $cpu->cycleWrite($address, $cpu->getE()); + return 8; + }, + ), + + // 0x74: LD (HL),H + 0x74 => new Instruction( + opcode: 0x74, + mnemonic: 'LD (HL),H', + length: 1, + cycles: 8, + handler: static function (Cpu $cpu): int { + $address = $cpu->getHL()->get(); + $cpu->cycleWrite($address, $cpu->getH()); + return 8; + }, + ), + + // 0x75: LD (HL),L + 0x75 => new Instruction( + opcode: 0x75, + mnemonic: 'LD (HL),L', + length: 1, + cycles: 8, + handler: static function (Cpu $cpu): int { + $address = $cpu->getHL()->get(); + $cpu->cycleWrite($address, $cpu->getL()); + return 8; + }, + ), + + // 0x76: HALT - Halt CPU until interrupt + 0x76 => new Instruction( + opcode: 0x76, + mnemonic: 'HALT', + length: 1, + cycles: 4, + handler: static function (Cpu $cpu): int { + $cpu->setHalted(true); + return 4; + }, + ), + + // 0x77: LD (HL),A + 0x77 => new Instruction( + opcode: 0x77, + mnemonic: 'LD (HL),A', + length: 1, + cycles: 8, + handler: static function (Cpu $cpu): int { + $address = $cpu->getHL()->get(); + $cpu->cycleWrite($address, $cpu->getA()); + return 8; + }, + ), + + // 0x78: LD A,B + 0x78 => new Instruction( + opcode: 0x78, + mnemonic: 'LD A,B', + length: 1, + cycles: 4, + handler: static function (Cpu $cpu): int { + $cpu->setA($cpu->getB()); + return 4; + }, + ), + + // 0x79: LD A,C + 0x79 => new Instruction( + opcode: 0x79, + mnemonic: 'LD A,C', + length: 1, + cycles: 4, + handler: static function (Cpu $cpu): int { + $cpu->setA($cpu->getC()); + return 4; + }, + ), + + // 0x7A: LD A,D + 0x7A => new Instruction( + opcode: 0x7A, + mnemonic: 'LD A,D', + length: 1, + cycles: 4, + handler: static function (Cpu $cpu): int { + $cpu->setA($cpu->getD()); + return 4; + }, + ), + + // 0x7B: LD A,E + 0x7B => new Instruction( + opcode: 0x7B, + mnemonic: 'LD A,E', + length: 1, + cycles: 4, + handler: static function (Cpu $cpu): int { + $cpu->setA($cpu->getE()); + return 4; + }, + ), + + // 0x7C: LD A,H + 0x7C => new Instruction( + opcode: 0x7C, + mnemonic: 'LD A,H', + length: 1, + cycles: 4, + handler: static function (Cpu $cpu): int { + $cpu->setA($cpu->getH()); + return 4; + }, + ), + + // 0x7D: LD A,L + 0x7D => new Instruction( + opcode: 0x7D, + mnemonic: 'LD A,L', + length: 1, + cycles: 4, + handler: static function (Cpu $cpu): int { + $cpu->setA($cpu->getL()); + return 4; + }, + ), + + // 0x7E: LD A,(HL) + 0x7E => new Instruction( + opcode: 0x7E, + mnemonic: 'LD A,(HL)', + length: 1, + cycles: 8, + handler: static function (Cpu $cpu): int { + $address = $cpu->getHL()->get(); + $cpu->setA($cpu->cycleRead($address)); + return 8; + }, + ), + + // 0x7F: LD A,A + 0x7F => new Instruction( + opcode: 0x7F, + mnemonic: 'LD A,A', + length: 1, + cycles: 4, + handler: static fn(Cpu $cpu): int => 4, + ), + + // 0x80-0xBF: ALU instructions with A + + // 0x80: ADD A,B + 0x80 => new Instruction( + opcode: 0x80, + mnemonic: 'ADD A,B', + length: 1, + cycles: 4, + handler: static function (Cpu $cpu): int { + $a = $cpu->getA(); + $b = $cpu->getB(); + $result = $a + $b; + $cpu->setA($result & 0xFF); + $cpu->getFlags()->setZ(($result & 0xFF) === 0); + $cpu->getFlags()->setN(false); + $cpu->getFlags()->setH(self::halfCarry8Add($a, $b)); + $cpu->getFlags()->setC($result > 0xFF); + return 4; + }, + ), + + // 0x81: ADD A,C + 0x81 => new Instruction( + opcode: 0x81, + mnemonic: 'ADD A,C', + length: 1, + cycles: 4, + handler: static function (Cpu $cpu): int { + $a = $cpu->getA(); + $c = $cpu->getC(); + $result = $a + $c; + $cpu->setA($result & 0xFF); + $cpu->getFlags()->setZ(($result & 0xFF) === 0); + $cpu->getFlags()->setN(false); + $cpu->getFlags()->setH(self::halfCarry8Add($a, $c)); + $cpu->getFlags()->setC($result > 0xFF); + return 4; + }, + ), + + // 0x82: ADD A,D + 0x82 => new Instruction( + opcode: 0x82, + mnemonic: 'ADD A,D', + length: 1, + cycles: 4, + handler: static function (Cpu $cpu): int { + $a = $cpu->getA(); + $d = $cpu->getD(); + $result = $a + $d; + $cpu->setA($result & 0xFF); + $cpu->getFlags()->setZ(($result & 0xFF) === 0); + $cpu->getFlags()->setN(false); + $cpu->getFlags()->setH(self::halfCarry8Add($a, $d)); + $cpu->getFlags()->setC($result > 0xFF); + return 4; + }, + ), + + // 0x83: ADD A,E + 0x83 => new Instruction( + opcode: 0x83, + mnemonic: 'ADD A,E', + length: 1, + cycles: 4, + handler: static function (Cpu $cpu): int { + $a = $cpu->getA(); + $e = $cpu->getE(); + $result = $a + $e; + $cpu->setA($result & 0xFF); + $cpu->getFlags()->setZ(($result & 0xFF) === 0); + $cpu->getFlags()->setN(false); + $cpu->getFlags()->setH(self::halfCarry8Add($a, $e)); + $cpu->getFlags()->setC($result > 0xFF); + return 4; + }, + ), + + // 0x84: ADD A,H + 0x84 => new Instruction( + opcode: 0x84, + mnemonic: 'ADD A,H', + length: 1, + cycles: 4, + handler: static function (Cpu $cpu): int { + $a = $cpu->getA(); + $h = $cpu->getH(); + $result = $a + $h; + $cpu->setA($result & 0xFF); + $cpu->getFlags()->setZ(($result & 0xFF) === 0); + $cpu->getFlags()->setN(false); + $cpu->getFlags()->setH(self::halfCarry8Add($a, $h)); + $cpu->getFlags()->setC($result > 0xFF); + return 4; + }, + ), + + // 0x85: ADD A,L + 0x85 => new Instruction( + opcode: 0x85, + mnemonic: 'ADD A,L', + length: 1, + cycles: 4, + handler: static function (Cpu $cpu): int { + $a = $cpu->getA(); + $l = $cpu->getL(); + $result = $a + $l; + $cpu->setA($result & 0xFF); + $cpu->getFlags()->setZ(($result & 0xFF) === 0); + $cpu->getFlags()->setN(false); + $cpu->getFlags()->setH(self::halfCarry8Add($a, $l)); + $cpu->getFlags()->setC($result > 0xFF); + return 4; + }, + ), + + // 0x86: ADD A,(HL) + 0x86 => new Instruction( + opcode: 0x86, + mnemonic: 'ADD A,(HL)', + length: 1, + cycles: 8, + handler: static function (Cpu $cpu): int { + $a = $cpu->getA(); + $address = $cpu->getHL()->get(); + $value = $cpu->cycleRead($address); + $result = $a + $value; + $cpu->setA($result & 0xFF); + $cpu->getFlags()->setZ(($result & 0xFF) === 0); + $cpu->getFlags()->setN(false); + $cpu->getFlags()->setH(self::halfCarry8Add($a, $value)); + $cpu->getFlags()->setC($result > 0xFF); + return 8; + }, + ), + + // 0x87: ADD A,A + 0x87 => new Instruction( + opcode: 0x87, + mnemonic: 'ADD A,A', + length: 1, + cycles: 4, + handler: static function (Cpu $cpu): int { + $a = $cpu->getA(); + $result = $a + $a; + $cpu->setA($result & 0xFF); + $cpu->getFlags()->setZ(($result & 0xFF) === 0); + $cpu->getFlags()->setN(false); + $cpu->getFlags()->setH(self::halfCarry8Add($a, $a)); + $cpu->getFlags()->setC($result > 0xFF); + return 4; + }, + ), + + // 0x88-0x8F: ADC A,r (Add with carry) + 0x88 => new Instruction( + opcode: 0x88, + mnemonic: 'ADC A,B', + length: 1, + cycles: 4, + handler: static function (Cpu $cpu): int { + $a = $cpu->getA(); + $b = $cpu->getB(); + $carry = $cpu->getFlags()->getC() ? 1 : 0; + $result = $a + $b + $carry; + $cpu->setA($result & 0xFF); + $cpu->getFlags()->setZ(($result & 0xFF) === 0); + $cpu->getFlags()->setN(false); + $cpu->getFlags()->setH(self::halfCarry8Add($a, $b, $carry)); + $cpu->getFlags()->setC($result > 0xFF); + return 4; + }, + ), + + 0x89 => new Instruction( + opcode: 0x89, + mnemonic: 'ADC A,C', + length: 1, + cycles: 4, + handler: static function (Cpu $cpu): int { + $a = $cpu->getA(); + $c = $cpu->getC(); + $carry = $cpu->getFlags()->getC() ? 1 : 0; + $result = $a + $c + $carry; + $cpu->setA($result & 0xFF); + $cpu->getFlags()->setZ(($result & 0xFF) === 0); + $cpu->getFlags()->setN(false); + $cpu->getFlags()->setH(self::halfCarry8Add($a, $c, $carry)); + $cpu->getFlags()->setC($result > 0xFF); + return 4; + }, + ), + + 0x8A => new Instruction( + opcode: 0x8A, + mnemonic: 'ADC A,D', + length: 1, + cycles: 4, + handler: static function (Cpu $cpu): int { + $a = $cpu->getA(); + $d = $cpu->getD(); + $carry = $cpu->getFlags()->getC() ? 1 : 0; + $result = $a + $d + $carry; + $cpu->setA($result & 0xFF); + $cpu->getFlags()->setZ(($result & 0xFF) === 0); + $cpu->getFlags()->setN(false); + $cpu->getFlags()->setH(self::halfCarry8Add($a, $d, $carry)); + $cpu->getFlags()->setC($result > 0xFF); + return 4; + }, + ), + + 0x8B => new Instruction( + opcode: 0x8B, + mnemonic: 'ADC A,E', + length: 1, + cycles: 4, + handler: static function (Cpu $cpu): int { + $a = $cpu->getA(); + $e = $cpu->getE(); + $carry = $cpu->getFlags()->getC() ? 1 : 0; + $result = $a + $e + $carry; + $cpu->setA($result & 0xFF); + $cpu->getFlags()->setZ(($result & 0xFF) === 0); + $cpu->getFlags()->setN(false); + $cpu->getFlags()->setH(self::halfCarry8Add($a, $e, $carry)); + $cpu->getFlags()->setC($result > 0xFF); + return 4; + }, + ), + + 0x8C => new Instruction( + opcode: 0x8C, + mnemonic: 'ADC A,H', + length: 1, + cycles: 4, + handler: static function (Cpu $cpu): int { + $a = $cpu->getA(); + $h = $cpu->getH(); + $carry = $cpu->getFlags()->getC() ? 1 : 0; + $result = $a + $h + $carry; + $cpu->setA($result & 0xFF); + $cpu->getFlags()->setZ(($result & 0xFF) === 0); + $cpu->getFlags()->setN(false); + $cpu->getFlags()->setH(self::halfCarry8Add($a, $h, $carry)); + $cpu->getFlags()->setC($result > 0xFF); + return 4; + }, + ), + + 0x8D => new Instruction( + opcode: 0x8D, + mnemonic: 'ADC A,L', + length: 1, + cycles: 4, + handler: static function (Cpu $cpu): int { + $a = $cpu->getA(); + $l = $cpu->getL(); + $carry = $cpu->getFlags()->getC() ? 1 : 0; + $result = $a + $l + $carry; + $cpu->setA($result & 0xFF); + $cpu->getFlags()->setZ(($result & 0xFF) === 0); + $cpu->getFlags()->setN(false); + $cpu->getFlags()->setH(self::halfCarry8Add($a, $l, $carry)); + $cpu->getFlags()->setC($result > 0xFF); + return 4; + }, + ), + + 0x8E => new Instruction( + opcode: 0x8E, + mnemonic: 'ADC A,(HL)', + length: 1, + cycles: 8, + handler: static function (Cpu $cpu): int { + $a = $cpu->getA(); + $address = $cpu->getHL()->get(); + $value = $cpu->cycleRead($address); + $carry = $cpu->getFlags()->getC() ? 1 : 0; + $result = $a + $value + $carry; + $cpu->setA($result & 0xFF); + $cpu->getFlags()->setZ(($result & 0xFF) === 0); + $cpu->getFlags()->setN(false); + $cpu->getFlags()->setH(self::halfCarry8Add($a, $value, $carry)); + $cpu->getFlags()->setC($result > 0xFF); + return 8; + }, + ), + + 0x8F => new Instruction( + opcode: 0x8F, + mnemonic: 'ADC A,A', + length: 1, + cycles: 4, + handler: static function (Cpu $cpu): int { + $a = $cpu->getA(); + $carry = $cpu->getFlags()->getC() ? 1 : 0; + $result = $a + $a + $carry; + $cpu->setA($result & 0xFF); + $cpu->getFlags()->setZ(($result & 0xFF) === 0); + $cpu->getFlags()->setN(false); + $cpu->getFlags()->setH(self::halfCarry8Add($a, $a, $carry)); + $cpu->getFlags()->setC($result > 0xFF); + return 4; + }, + ), + + // 0x90-0x97: SUB r (Subtract) + 0x90 => new Instruction( + opcode: 0x90, + mnemonic: 'SUB B', + length: 1, + cycles: 4, + handler: static function (Cpu $cpu): int { + $a = $cpu->getA(); + $b = $cpu->getB(); + $result = $a - $b; + $cpu->setA($result & 0xFF); + $cpu->getFlags()->setZ(($result & 0xFF) === 0); + $cpu->getFlags()->setN(true); + $cpu->getFlags()->setH(self::halfCarry8Sub($a, $b)); + $cpu->getFlags()->setC($result < 0); + return 4; + }, + ), + + 0x91 => new Instruction( + opcode: 0x91, + mnemonic: 'SUB C', + length: 1, + cycles: 4, + handler: static function (Cpu $cpu): int { + $a = $cpu->getA(); + $c = $cpu->getC(); + $result = $a - $c; + $cpu->setA($result & 0xFF); + $cpu->getFlags()->setZ(($result & 0xFF) === 0); + $cpu->getFlags()->setN(true); + $cpu->getFlags()->setH(self::halfCarry8Sub($a, $c)); + $cpu->getFlags()->setC($result < 0); + return 4; + }, + ), + + 0x92 => new Instruction( + opcode: 0x92, + mnemonic: 'SUB D', + length: 1, + cycles: 4, + handler: static function (Cpu $cpu): int { + $a = $cpu->getA(); + $d = $cpu->getD(); + $result = $a - $d; + $cpu->setA($result & 0xFF); + $cpu->getFlags()->setZ(($result & 0xFF) === 0); + $cpu->getFlags()->setN(true); + $cpu->getFlags()->setH(self::halfCarry8Sub($a, $d)); + $cpu->getFlags()->setC($result < 0); + return 4; + }, + ), + + 0x93 => new Instruction( + opcode: 0x93, + mnemonic: 'SUB E', + length: 1, + cycles: 4, + handler: static function (Cpu $cpu): int { + $a = $cpu->getA(); + $e = $cpu->getE(); + $result = $a - $e; + $cpu->setA($result & 0xFF); + $cpu->getFlags()->setZ(($result & 0xFF) === 0); + $cpu->getFlags()->setN(true); + $cpu->getFlags()->setH(self::halfCarry8Sub($a, $e)); + $cpu->getFlags()->setC($result < 0); + return 4; + }, + ), + + 0x94 => new Instruction( + opcode: 0x94, + mnemonic: 'SUB H', + length: 1, + cycles: 4, + handler: static function (Cpu $cpu): int { + $a = $cpu->getA(); + $h = $cpu->getH(); + $result = $a - $h; + $cpu->setA($result & 0xFF); + $cpu->getFlags()->setZ(($result & 0xFF) === 0); + $cpu->getFlags()->setN(true); + $cpu->getFlags()->setH(self::halfCarry8Sub($a, $h)); + $cpu->getFlags()->setC($result < 0); + return 4; + }, + ), + + 0x95 => new Instruction( + opcode: 0x95, + mnemonic: 'SUB L', + length: 1, + cycles: 4, + handler: static function (Cpu $cpu): int { + $a = $cpu->getA(); + $l = $cpu->getL(); + $result = $a - $l; + $cpu->setA($result & 0xFF); + $cpu->getFlags()->setZ(($result & 0xFF) === 0); + $cpu->getFlags()->setN(true); + $cpu->getFlags()->setH(self::halfCarry8Sub($a, $l)); + $cpu->getFlags()->setC($result < 0); + return 4; + }, + ), + + 0x96 => new Instruction( + opcode: 0x96, + mnemonic: 'SUB (HL)', + length: 1, + cycles: 8, + handler: static function (Cpu $cpu): int { + $a = $cpu->getA(); + $address = $cpu->getHL()->get(); + $value = $cpu->cycleRead($address); + $result = $a - $value; + $cpu->setA($result & 0xFF); + $cpu->getFlags()->setZ(($result & 0xFF) === 0); + $cpu->getFlags()->setN(true); + $cpu->getFlags()->setH(self::halfCarry8Sub($a, $value)); + $cpu->getFlags()->setC($result < 0); + return 8; + }, + ), + + 0x97 => new Instruction( + opcode: 0x97, + mnemonic: 'SUB A', + length: 1, + cycles: 4, + handler: static function (Cpu $cpu): int { + $cpu->setA(0); + $cpu->getFlags()->setZ(true); + $cpu->getFlags()->setN(true); + $cpu->getFlags()->setH(false); + $cpu->getFlags()->setC(false); + return 4; + }, + ), + + // 0x98-0x9F: SBC A,r (Subtract with carry) + 0x98 => new Instruction( + opcode: 0x98, + mnemonic: 'SBC A,B', + length: 1, + cycles: 4, + handler: static function (Cpu $cpu): int { + $a = $cpu->getA(); + $b = $cpu->getB(); + $carry = $cpu->getFlags()->getC() ? 1 : 0; + $result = $a - $b - $carry; + $cpu->setA($result & 0xFF); + $cpu->getFlags()->setZ(($result & 0xFF) === 0); + $cpu->getFlags()->setN(true); + $cpu->getFlags()->setH(self::halfCarry8Sub($a, $b, $carry)); + $cpu->getFlags()->setC($result < 0); + return 4; + }, + ), + + 0x99 => new Instruction( + opcode: 0x99, + mnemonic: 'SBC A,C', + length: 1, + cycles: 4, + handler: static function (Cpu $cpu): int { + $a = $cpu->getA(); + $c = $cpu->getC(); + $carry = $cpu->getFlags()->getC() ? 1 : 0; + $result = $a - $c - $carry; + $cpu->setA($result & 0xFF); + $cpu->getFlags()->setZ(($result & 0xFF) === 0); + $cpu->getFlags()->setN(true); + $cpu->getFlags()->setH(self::halfCarry8Sub($a, $c, $carry)); + $cpu->getFlags()->setC($result < 0); + return 4; + }, + ), + + 0x9A => new Instruction( + opcode: 0x9A, + mnemonic: 'SBC A,D', + length: 1, + cycles: 4, + handler: static function (Cpu $cpu): int { + $a = $cpu->getA(); + $d = $cpu->getD(); + $carry = $cpu->getFlags()->getC() ? 1 : 0; + $result = $a - $d - $carry; + $cpu->setA($result & 0xFF); + $cpu->getFlags()->setZ(($result & 0xFF) === 0); + $cpu->getFlags()->setN(true); + $cpu->getFlags()->setH(self::halfCarry8Sub($a, $d, $carry)); + $cpu->getFlags()->setC($result < 0); + return 4; + }, + ), + + 0x9B => new Instruction( + opcode: 0x9B, + mnemonic: 'SBC A,E', + length: 1, + cycles: 4, + handler: static function (Cpu $cpu): int { + $a = $cpu->getA(); + $e = $cpu->getE(); + $carry = $cpu->getFlags()->getC() ? 1 : 0; + $result = $a - $e - $carry; + $cpu->setA($result & 0xFF); + $cpu->getFlags()->setZ(($result & 0xFF) === 0); + $cpu->getFlags()->setN(true); + $cpu->getFlags()->setH(self::halfCarry8Sub($a, $e, $carry)); + $cpu->getFlags()->setC($result < 0); + return 4; + }, + ), + + 0x9C => new Instruction( + opcode: 0x9C, + mnemonic: 'SBC A,H', + length: 1, + cycles: 4, + handler: static function (Cpu $cpu): int { + $a = $cpu->getA(); + $h = $cpu->getH(); + $carry = $cpu->getFlags()->getC() ? 1 : 0; + $result = $a - $h - $carry; + $cpu->setA($result & 0xFF); + $cpu->getFlags()->setZ(($result & 0xFF) === 0); + $cpu->getFlags()->setN(true); + $cpu->getFlags()->setH(self::halfCarry8Sub($a, $h, $carry)); + $cpu->getFlags()->setC($result < 0); + return 4; + }, + ), + + 0x9D => new Instruction( + opcode: 0x9D, + mnemonic: 'SBC A,L', + length: 1, + cycles: 4, + handler: static function (Cpu $cpu): int { + $a = $cpu->getA(); + $l = $cpu->getL(); + $carry = $cpu->getFlags()->getC() ? 1 : 0; + $result = $a - $l - $carry; + $cpu->setA($result & 0xFF); + $cpu->getFlags()->setZ(($result & 0xFF) === 0); + $cpu->getFlags()->setN(true); + $cpu->getFlags()->setH(self::halfCarry8Sub($a, $l, $carry)); + $cpu->getFlags()->setC($result < 0); + return 4; + }, + ), + + 0x9E => new Instruction( + opcode: 0x9E, + mnemonic: 'SBC A,(HL)', + length: 1, + cycles: 8, + handler: static function (Cpu $cpu): int { + $a = $cpu->getA(); + $address = $cpu->getHL()->get(); + $value = $cpu->cycleRead($address); + $carry = $cpu->getFlags()->getC() ? 1 : 0; + $result = $a - $value - $carry; + $cpu->setA($result & 0xFF); + $cpu->getFlags()->setZ(($result & 0xFF) === 0); + $cpu->getFlags()->setN(true); + $cpu->getFlags()->setH(self::halfCarry8Sub($a, $value, $carry)); + $cpu->getFlags()->setC($result < 0); + return 8; + }, + ), + + 0x9F => new Instruction( + opcode: 0x9F, + mnemonic: 'SBC A,A', + length: 1, + cycles: 4, + handler: static function (Cpu $cpu): int { + $carry = $cpu->getFlags()->getC() ? 1 : 0; + $result = -$carry; + $cpu->setA($result & 0xFF); + $cpu->getFlags()->setZ($result === 0); + $cpu->getFlags()->setN(true); + $cpu->getFlags()->setH($carry !== 0); + $cpu->getFlags()->setC($carry !== 0); + return 4; + }, + ), + + // 0xA0-0xA7: AND r + + 0xA0 => new Instruction( + opcode: 0xA0, + mnemonic: 'AND B', + length: 1, + cycles: 4, + handler: static function (Cpu $cpu): int { + $result = $cpu->getA() & $cpu->getB(); + $cpu->setA($result); + $cpu->getFlags()->setZ($result === 0); + $cpu->getFlags()->setN(false); + $cpu->getFlags()->setH(true); + $cpu->getFlags()->setC(false); + return 4; + }, + ), + + 0xA1 => new Instruction( + opcode: 0xA1, + mnemonic: 'AND C', + length: 1, + cycles: 4, + handler: static function (Cpu $cpu): int { + $result = $cpu->getA() & $cpu->getC(); + $cpu->setA($result); + $cpu->getFlags()->setZ($result === 0); + $cpu->getFlags()->setN(false); + $cpu->getFlags()->setH(true); + $cpu->getFlags()->setC(false); + return 4; + }, + ), + + 0xA2 => new Instruction( + opcode: 0xA2, + mnemonic: 'AND D', + length: 1, + cycles: 4, + handler: static function (Cpu $cpu): int { + $result = $cpu->getA() & $cpu->getD(); + $cpu->setA($result); + $cpu->getFlags()->setZ($result === 0); + $cpu->getFlags()->setN(false); + $cpu->getFlags()->setH(true); + $cpu->getFlags()->setC(false); + return 4; + }, + ), + + 0xA3 => new Instruction( + opcode: 0xA3, + mnemonic: 'AND E', + length: 1, + cycles: 4, + handler: static function (Cpu $cpu): int { + $result = $cpu->getA() & $cpu->getE(); + $cpu->setA($result); + $cpu->getFlags()->setZ($result === 0); + $cpu->getFlags()->setN(false); + $cpu->getFlags()->setH(true); + $cpu->getFlags()->setC(false); + return 4; + }, + ), + + 0xA4 => new Instruction( + opcode: 0xA4, + mnemonic: 'AND H', + length: 1, + cycles: 4, + handler: static function (Cpu $cpu): int { + $result = $cpu->getA() & $cpu->getH(); + $cpu->setA($result); + $cpu->getFlags()->setZ($result === 0); + $cpu->getFlags()->setN(false); + $cpu->getFlags()->setH(true); + $cpu->getFlags()->setC(false); + return 4; + }, + ), + + 0xA5 => new Instruction( + opcode: 0xA5, + mnemonic: 'AND L', + length: 1, + cycles: 4, + handler: static function (Cpu $cpu): int { + $result = $cpu->getA() & $cpu->getL(); + $cpu->setA($result); + $cpu->getFlags()->setZ($result === 0); + $cpu->getFlags()->setN(false); + $cpu->getFlags()->setH(true); + $cpu->getFlags()->setC(false); + return 4; + }, + ), + + 0xA6 => new Instruction( + opcode: 0xA6, + mnemonic: 'AND (HL)', + length: 1, + cycles: 8, + handler: static function (Cpu $cpu): int { + $address = $cpu->getHL()->get(); + $value = $cpu->cycleRead($address); + $result = $cpu->getA() & $value; + $cpu->setA($result); + $cpu->getFlags()->setZ($result === 0); + $cpu->getFlags()->setN(false); + $cpu->getFlags()->setH(true); + $cpu->getFlags()->setC(false); + return 8; + }, + ), + + 0xA7 => new Instruction( + opcode: 0xA7, + mnemonic: 'AND A', + length: 1, + cycles: 4, + handler: static function (Cpu $cpu): int { + $result = $cpu->getA(); + $cpu->getFlags()->setZ($result === 0); + $cpu->getFlags()->setN(false); + $cpu->getFlags()->setH(true); + $cpu->getFlags()->setC(false); + return 4; + }, + ), + + // 0xA8-0xAF: XOR r + + 0xA8 => new Instruction( + opcode: 0xA8, + mnemonic: 'XOR B', + length: 1, + cycles: 4, + handler: static function (Cpu $cpu): int { + $result = $cpu->getA() ^ $cpu->getB(); + $cpu->setA($result); + $cpu->getFlags()->setZ($result === 0); + $cpu->getFlags()->setN(false); + $cpu->getFlags()->setH(false); + $cpu->getFlags()->setC(false); + return 4; + }, + ), + + 0xA9 => new Instruction( + opcode: 0xA9, + mnemonic: 'XOR C', + length: 1, + cycles: 4, + handler: static function (Cpu $cpu): int { + $result = $cpu->getA() ^ $cpu->getC(); + $cpu->setA($result); + $cpu->getFlags()->setZ($result === 0); + $cpu->getFlags()->setN(false); + $cpu->getFlags()->setH(false); + $cpu->getFlags()->setC(false); + return 4; + }, + ), + + 0xAA => new Instruction( + opcode: 0xAA, + mnemonic: 'XOR D', + length: 1, + cycles: 4, + handler: static function (Cpu $cpu): int { + $result = $cpu->getA() ^ $cpu->getD(); + $cpu->setA($result); + $cpu->getFlags()->setZ($result === 0); + $cpu->getFlags()->setN(false); + $cpu->getFlags()->setH(false); + $cpu->getFlags()->setC(false); + return 4; + }, + ), + + 0xAB => new Instruction( + opcode: 0xAB, + mnemonic: 'XOR E', + length: 1, + cycles: 4, + handler: static function (Cpu $cpu): int { + $result = $cpu->getA() ^ $cpu->getE(); + $cpu->setA($result); + $cpu->getFlags()->setZ($result === 0); + $cpu->getFlags()->setN(false); + $cpu->getFlags()->setH(false); + $cpu->getFlags()->setC(false); + return 4; + }, + ), + + 0xAC => new Instruction( + opcode: 0xAC, + mnemonic: 'XOR H', + length: 1, + cycles: 4, + handler: static function (Cpu $cpu): int { + $result = $cpu->getA() ^ $cpu->getH(); + $cpu->setA($result); + $cpu->getFlags()->setZ($result === 0); + $cpu->getFlags()->setN(false); + $cpu->getFlags()->setH(false); + $cpu->getFlags()->setC(false); + return 4; + }, + ), + + 0xAD => new Instruction( + opcode: 0xAD, + mnemonic: 'XOR L', + length: 1, + cycles: 4, + handler: static function (Cpu $cpu): int { + $result = $cpu->getA() ^ $cpu->getL(); + $cpu->setA($result); + $cpu->getFlags()->setZ($result === 0); + $cpu->getFlags()->setN(false); + $cpu->getFlags()->setH(false); + $cpu->getFlags()->setC(false); + return 4; + }, + ), + + 0xAE => new Instruction( + opcode: 0xAE, + mnemonic: 'XOR (HL)', + length: 1, + cycles: 8, + handler: static function (Cpu $cpu): int { + $address = $cpu->getHL()->get(); + $value = $cpu->cycleRead($address); + $result = $cpu->getA() ^ $value; + $cpu->setA($result); + $cpu->getFlags()->setZ($result === 0); + $cpu->getFlags()->setN(false); + $cpu->getFlags()->setH(false); + $cpu->getFlags()->setC(false); + return 8; + }, + ), + + 0xAF => new Instruction( + opcode: 0xAF, + mnemonic: 'XOR A', + length: 1, + cycles: 4, + handler: static function (Cpu $cpu): int { + $cpu->setA(0); + $cpu->getFlags()->setZ(true); + $cpu->getFlags()->setN(false); + $cpu->getFlags()->setH(false); + $cpu->getFlags()->setC(false); + return 4; + }, + ), + + // 0xB0-0xB7: OR r + + 0xB0 => new Instruction( + opcode: 0xB0, + mnemonic: 'OR B', + length: 1, + cycles: 4, + handler: static function (Cpu $cpu): int { + $result = $cpu->getA() | $cpu->getB(); + $cpu->setA($result); + $cpu->getFlags()->setZ($result === 0); + $cpu->getFlags()->setN(false); + $cpu->getFlags()->setH(false); + $cpu->getFlags()->setC(false); + return 4; + }, + ), + + 0xB1 => new Instruction( + opcode: 0xB1, + mnemonic: 'OR C', + length: 1, + cycles: 4, + handler: static function (Cpu $cpu): int { + $result = $cpu->getA() | $cpu->getC(); + $cpu->setA($result); + $cpu->getFlags()->setZ($result === 0); + $cpu->getFlags()->setN(false); + $cpu->getFlags()->setH(false); + $cpu->getFlags()->setC(false); + return 4; + }, + ), + + 0xB2 => new Instruction( + opcode: 0xB2, + mnemonic: 'OR D', + length: 1, + cycles: 4, + handler: static function (Cpu $cpu): int { + $result = $cpu->getA() | $cpu->getD(); + $cpu->setA($result); + $cpu->getFlags()->setZ($result === 0); + $cpu->getFlags()->setN(false); + $cpu->getFlags()->setH(false); + $cpu->getFlags()->setC(false); + return 4; + }, + ), + + 0xB3 => new Instruction( + opcode: 0xB3, + mnemonic: 'OR E', + length: 1, + cycles: 4, + handler: static function (Cpu $cpu): int { + $result = $cpu->getA() | $cpu->getE(); + $cpu->setA($result); + $cpu->getFlags()->setZ($result === 0); + $cpu->getFlags()->setN(false); + $cpu->getFlags()->setH(false); + $cpu->getFlags()->setC(false); + return 4; + }, + ), + + 0xB4 => new Instruction( + opcode: 0xB4, + mnemonic: 'OR H', + length: 1, + cycles: 4, + handler: static function (Cpu $cpu): int { + $result = $cpu->getA() | $cpu->getH(); + $cpu->setA($result); + $cpu->getFlags()->setZ($result === 0); + $cpu->getFlags()->setN(false); + $cpu->getFlags()->setH(false); + $cpu->getFlags()->setC(false); + return 4; + }, + ), + + 0xB5 => new Instruction( + opcode: 0xB5, + mnemonic: 'OR L', + length: 1, + cycles: 4, + handler: static function (Cpu $cpu): int { + $result = $cpu->getA() | $cpu->getL(); + $cpu->setA($result); + $cpu->getFlags()->setZ($result === 0); + $cpu->getFlags()->setN(false); + $cpu->getFlags()->setH(false); + $cpu->getFlags()->setC(false); + return 4; + }, + ), + + 0xB6 => new Instruction( + opcode: 0xB6, + mnemonic: 'OR (HL)', + length: 1, + cycles: 8, + handler: static function (Cpu $cpu): int { + $address = $cpu->getHL()->get(); + $value = $cpu->cycleRead($address); + $result = $cpu->getA() | $value; + $cpu->setA($result); + $cpu->getFlags()->setZ($result === 0); + $cpu->getFlags()->setN(false); + $cpu->getFlags()->setH(false); + $cpu->getFlags()->setC(false); + return 8; + }, + ), + + 0xB7 => new Instruction( + opcode: 0xB7, + mnemonic: 'OR A', + length: 1, + cycles: 4, + handler: static function (Cpu $cpu): int { + $result = $cpu->getA(); + $cpu->getFlags()->setZ($result === 0); + $cpu->getFlags()->setN(false); + $cpu->getFlags()->setH(false); + $cpu->getFlags()->setC(false); + return 4; + }, + ), + + // 0xB8-0xBF: CP r (compare) + + 0xB8 => new Instruction( + opcode: 0xB8, + mnemonic: 'CP B', + length: 1, + cycles: 4, + handler: static function (Cpu $cpu): int { + $a = $cpu->getA(); + $b = $cpu->getB(); + $result = $a - $b; + $cpu->getFlags()->setZ(($result & 0xFF) === 0); + $cpu->getFlags()->setN(true); + $cpu->getFlags()->setH(self::halfCarry8Sub($a, $b)); + $cpu->getFlags()->setC($result < 0); + return 4; + }, + ), + + 0xB9 => new Instruction( + opcode: 0xB9, + mnemonic: 'CP C', + length: 1, + cycles: 4, + handler: static function (Cpu $cpu): int { + $a = $cpu->getA(); + $c = $cpu->getC(); + $result = $a - $c; + $cpu->getFlags()->setZ(($result & 0xFF) === 0); + $cpu->getFlags()->setN(true); + $cpu->getFlags()->setH(self::halfCarry8Sub($a, $c)); + $cpu->getFlags()->setC($result < 0); + return 4; + }, + ), + + 0xBA => new Instruction( + opcode: 0xBA, + mnemonic: 'CP D', + length: 1, + cycles: 4, + handler: static function (Cpu $cpu): int { + $a = $cpu->getA(); + $d = $cpu->getD(); + $result = $a - $d; + $cpu->getFlags()->setZ(($result & 0xFF) === 0); + $cpu->getFlags()->setN(true); + $cpu->getFlags()->setH(self::halfCarry8Sub($a, $d)); + $cpu->getFlags()->setC($result < 0); + return 4; + }, + ), + + 0xBB => new Instruction( + opcode: 0xBB, + mnemonic: 'CP E', + length: 1, + cycles: 4, + handler: static function (Cpu $cpu): int { + $a = $cpu->getA(); + $e = $cpu->getE(); + $result = $a - $e; + $cpu->getFlags()->setZ(($result & 0xFF) === 0); + $cpu->getFlags()->setN(true); + $cpu->getFlags()->setH(self::halfCarry8Sub($a, $e)); + $cpu->getFlags()->setC($result < 0); + return 4; + }, + ), + + 0xBC => new Instruction( + opcode: 0xBC, + mnemonic: 'CP H', + length: 1, + cycles: 4, + handler: static function (Cpu $cpu): int { + $a = $cpu->getA(); + $h = $cpu->getH(); + $result = $a - $h; + $cpu->getFlags()->setZ(($result & 0xFF) === 0); + $cpu->getFlags()->setN(true); + $cpu->getFlags()->setH(self::halfCarry8Sub($a, $h)); + $cpu->getFlags()->setC($result < 0); + return 4; + }, + ), + + 0xBD => new Instruction( + opcode: 0xBD, + mnemonic: 'CP L', + length: 1, + cycles: 4, + handler: static function (Cpu $cpu): int { + $a = $cpu->getA(); + $l = $cpu->getL(); + $result = $a - $l; + $cpu->getFlags()->setZ(($result & 0xFF) === 0); + $cpu->getFlags()->setN(true); + $cpu->getFlags()->setH(self::halfCarry8Sub($a, $l)); + $cpu->getFlags()->setC($result < 0); + return 4; + }, + ), + + 0xBE => new Instruction( + opcode: 0xBE, + mnemonic: 'CP (HL)', + length: 1, + cycles: 8, + handler: static function (Cpu $cpu): int { + $a = $cpu->getA(); + $address = $cpu->getHL()->get(); + $value = $cpu->cycleRead($address); + $result = $a - $value; + $cpu->getFlags()->setZ(($result & 0xFF) === 0); + $cpu->getFlags()->setN(true); + $cpu->getFlags()->setH(self::halfCarry8Sub($a, $value)); + $cpu->getFlags()->setC($result < 0); + return 8; + }, + ), + + 0xBF => new Instruction( + opcode: 0xBF, + mnemonic: 'CP A', + length: 1, + cycles: 4, + handler: static function (Cpu $cpu): int { + $cpu->getFlags()->setZ(true); + $cpu->getFlags()->setN(true); + $cpu->getFlags()->setH(false); + $cpu->getFlags()->setC(false); + return 4; + }, + ), + + // 0xC0-0xCF: Control flow and stack operations + + 0xC0 => new Instruction( + opcode: 0xC0, + mnemonic: 'RET NZ', + length: 1, + cycles: 8, // 20 if taken, 8 if not taken + handler: static function (Cpu $cpu): int { + $cpu->cycleNoAccess(); // Internal delay for condition check: 1 M-cycle + if (!$cpu->getFlags()->getZ()) { + $low = $cpu->cycleRead($cpu->getSP()->get()); + $cpu->getSP()->increment(); + $high = $cpu->cycleRead($cpu->getSP()->get()); + $cpu->getSP()->increment(); + $cpu->getPC()->set(($high << 8) | $low); + $cpu->cycleNoAccess(); // Internal delay for jump: 1 M-cycle + return 20; + } + return 8; + }, + ), + + 0xC1 => new Instruction( + opcode: 0xC1, + mnemonic: 'POP BC', + length: 1, + cycles: 12, + handler: static function (Cpu $cpu): int { + $low = $cpu->cycleRead($cpu->getSP()->get()); + $cpu->getSP()->increment(); + $high = $cpu->cycleRead($cpu->getSP()->get()); + $cpu->getSP()->increment(); + $cpu->getBC()->set(($high << 8) | $low); + return 12; + }, + ), + + 0xC2 => new Instruction( + opcode: 0xC2, + mnemonic: 'JP NZ,nn', + length: 3, + cycles: 12, // 16 if taken, 12 if not taken + handler: static function (Cpu $cpu): int { + $address = self::readImm16($cpu); + if (!$cpu->getFlags()->getZ()) { + $cpu->getPC()->set($address); + $cpu->cycleNoAccess(); // Internal delay for jump: 1 M-cycle + return 16; + } + return 12; + }, + ), + + 0xC3 => new Instruction( + opcode: 0xC3, + mnemonic: 'JP nn', + length: 3, + cycles: 16, + handler: static function (Cpu $cpu): int { + $address = self::readImm16($cpu); + $cpu->getPC()->set($address); + $cpu->cycleNoAccess(); // Internal delay for jump: 1 M-cycle + return 16; + }, + ), + + 0xC4 => new Instruction( + opcode: 0xC4, + mnemonic: 'CALL NZ,nn', + length: 3, + cycles: 12, // 24 if taken, 12 if not taken + handler: static function (Cpu $cpu): int { + $address = self::readImm16($cpu); + if (!$cpu->getFlags()->getZ()) { + $pc = $cpu->getPC()->get(); + $cpu->cycleNoAccess(); // Internal delay: 1 M-cycle + $cpu->getSP()->decrement(); + $cpu->cycleWrite($cpu->getSP()->get(), ($pc >> 8) & 0xFF); + $cpu->getSP()->decrement(); + $cpu->cycleWrite($cpu->getSP()->get(), $pc & 0xFF); + $cpu->getPC()->set($address); + return 24; + } + return 12; + }, + ), + + 0xC5 => new Instruction( + opcode: 0xC5, + mnemonic: 'PUSH BC', + length: 1, + cycles: 16, + handler: static function (Cpu $cpu): int { + $cpu->cycleNoAccess(); // Internal delay: 1 M-cycle + $value = $cpu->getBC()->get(); + $cpu->getSP()->decrement(); + $cpu->cycleWrite($cpu->getSP()->get(), ($value >> 8) & 0xFF); + $cpu->getSP()->decrement(); + $cpu->cycleWrite($cpu->getSP()->get(), $value & 0xFF); + return 16; + }, + ), + + 0xC6 => new Instruction( + opcode: 0xC6, + mnemonic: 'ADD A,n', + length: 2, + cycles: 8, + handler: static function (Cpu $cpu): int { + $a = $cpu->getA(); + $n = self::readImm8($cpu); + $result = $a + $n; + $cpu->setA($result & 0xFF); + $cpu->getFlags()->setZ(($result & 0xFF) === 0); + $cpu->getFlags()->setN(false); + $cpu->getFlags()->setH(self::halfCarry8Add($a, $n)); + $cpu->getFlags()->setC($result > 0xFF); + return 8; + }, + ), + + 0xC7 => new Instruction( + opcode: 0xC7, + mnemonic: 'RST 00H', + length: 1, + cycles: 16, + handler: static function (Cpu $cpu): int { + $pc = $cpu->getPC()->get(); + $cpu->cycleNoAccess(); // Internal delay: 1 M-cycle + $cpu->getSP()->decrement(); + $cpu->cycleWrite($cpu->getSP()->get(), ($pc >> 8) & 0xFF); + $cpu->getSP()->decrement(); + $cpu->cycleWrite($cpu->getSP()->get(), $pc & 0xFF); + $cpu->getPC()->set(0x0000); + return 16; + }, + ), + + 0xC8 => new Instruction( + opcode: 0xC8, + mnemonic: 'RET Z', + length: 1, + cycles: 8, // 20 if taken, 8 if not taken + handler: static function (Cpu $cpu): int { + $cpu->cycleNoAccess(); // Internal delay for condition check: 1 M-cycle + if ($cpu->getFlags()->getZ()) { + $low = $cpu->cycleRead($cpu->getSP()->get()); + $cpu->getSP()->increment(); + $high = $cpu->cycleRead($cpu->getSP()->get()); + $cpu->getSP()->increment(); + $cpu->getPC()->set(($high << 8) | $low); + $cpu->cycleNoAccess(); // Internal delay for jump: 1 M-cycle + return 20; + } + return 8; + }, + ), + + 0xC9 => new Instruction( + opcode: 0xC9, + mnemonic: 'RET', + length: 1, + cycles: 16, + handler: static function (Cpu $cpu): int { + $low = $cpu->cycleRead($cpu->getSP()->get()); + $cpu->getSP()->increment(); + $high = $cpu->cycleRead($cpu->getSP()->get()); + $cpu->getSP()->increment(); + $cpu->getPC()->set(($high << 8) | $low); + $cpu->cycleNoAccess(); // Internal delay: 1 M-cycle + return 16; + }, + ), + + 0xCA => new Instruction( + opcode: 0xCA, + mnemonic: 'JP Z,nn', + length: 3, + cycles: 12, // 16 if taken, 12 if not taken + handler: static function (Cpu $cpu): int { + $address = self::readImm16($cpu); + if ($cpu->getFlags()->getZ()) { + $cpu->getPC()->set($address); + $cpu->cycleNoAccess(); // Internal delay for jump: 1 M-cycle + return 16; + } + return 12; + }, + ), + + 0xCB => new Instruction( + opcode: 0xCB, + mnemonic: 'PREFIX CB', + length: 2, + cycles: 4, + handler: static function (Cpu $cpu): int { + $cb = self::readImm8($cpu); + return (self::getCBInstruction($cb)->handler)($cpu); + }, + ), + + 0xCC => new Instruction( + opcode: 0xCC, + mnemonic: 'CALL Z,nn', + length: 3, + cycles: 12, // 24 if taken, 12 if not taken + handler: static function (Cpu $cpu): int { + $address = self::readImm16($cpu); + if ($cpu->getFlags()->getZ()) { + $pc = $cpu->getPC()->get(); + $cpu->cycleNoAccess(); // Internal delay: 1 M-cycle + $cpu->getSP()->decrement(); + $cpu->cycleWrite($cpu->getSP()->get(), ($pc >> 8) & 0xFF); + $cpu->getSP()->decrement(); + $cpu->cycleWrite($cpu->getSP()->get(), $pc & 0xFF); + $cpu->getPC()->set($address); + return 24; + } + return 12; + }, + ), + + 0xCD => new Instruction( + opcode: 0xCD, + mnemonic: 'CALL nn', + length: 3, + cycles: 24, + handler: static function (Cpu $cpu): int { + $address = self::readImm16($cpu); + $pc = $cpu->getPC()->get(); + $cpu->cycleNoAccess(); // Internal delay: 1 M-cycle + $cpu->getSP()->decrement(); + $cpu->cycleWrite($cpu->getSP()->get(), ($pc >> 8) & 0xFF); + $cpu->getSP()->decrement(); + $cpu->cycleWrite($cpu->getSP()->get(), $pc & 0xFF); + $cpu->getPC()->set($address); + return 24; + }, + ), + + 0xCE => new Instruction( + opcode: 0xCE, + mnemonic: 'ADC A,n', + length: 2, + cycles: 8, + handler: static function (Cpu $cpu): int { + $a = $cpu->getA(); + $n = self::readImm8($cpu); + $carry = $cpu->getFlags()->getC() ? 1 : 0; + $result = $a + $n + $carry; + $cpu->setA($result & 0xFF); + $cpu->getFlags()->setZ(($result & 0xFF) === 0); + $cpu->getFlags()->setN(false); + $cpu->getFlags()->setH(self::halfCarry8Add($a, $n, $carry)); + $cpu->getFlags()->setC($result > 0xFF); + return 8; + }, + ), + + 0xCF => new Instruction( + opcode: 0xCF, + mnemonic: 'RST 08H', + length: 1, + cycles: 16, + handler: static function (Cpu $cpu): int { + $pc = $cpu->getPC()->get(); + $cpu->cycleNoAccess(); // Internal delay: 1 M-cycle + $cpu->getSP()->decrement(); + $cpu->cycleWrite($cpu->getSP()->get(), ($pc >> 8) & 0xFF); + $cpu->getSP()->decrement(); + $cpu->cycleWrite($cpu->getSP()->get(), $pc & 0xFF); + $cpu->getPC()->set(0x0008); + return 16; + }, + ), + + // 0xD0-0xDF: More control flow and stack operations + + 0xD0 => new Instruction( + opcode: 0xD0, + mnemonic: 'RET NC', + length: 1, + cycles: 8, // 20 if taken, 8 if not taken + handler: static function (Cpu $cpu): int { + $cpu->cycleNoAccess(); // Internal delay for condition check: 1 M-cycle + if (!$cpu->getFlags()->getC()) { + $low = $cpu->cycleRead($cpu->getSP()->get()); + $cpu->getSP()->increment(); + $high = $cpu->cycleRead($cpu->getSP()->get()); + $cpu->getSP()->increment(); + $cpu->getPC()->set(($high << 8) | $low); + $cpu->cycleNoAccess(); // Internal delay for jump: 1 M-cycle + return 20; + } + return 8; + }, + ), + + 0xD1 => new Instruction( + opcode: 0xD1, + mnemonic: 'POP DE', + length: 1, + cycles: 12, + handler: static function (Cpu $cpu): int { + $low = $cpu->cycleRead($cpu->getSP()->get()); + $cpu->getSP()->increment(); + $high = $cpu->cycleRead($cpu->getSP()->get()); + $cpu->getSP()->increment(); + $cpu->getDE()->set(($high << 8) | $low); + return 12; + }, + ), + + 0xD2 => new Instruction( + opcode: 0xD2, + mnemonic: 'JP NC,nn', + length: 3, + cycles: 12, // 16 if taken, 12 if not taken + handler: static function (Cpu $cpu): int { + $address = self::readImm16($cpu); + if (!$cpu->getFlags()->getC()) { + $cpu->getPC()->set($address); + $cpu->cycleNoAccess(); // Internal delay for jump: 1 M-cycle + return 16; + } + return 12; + }, + ), + + // 0xD3: Illegal opcode - locks up CPU + 0xD3 => new Instruction( + opcode: 0xD3, + mnemonic: 'ILLEGAL_D3', + length: 1, + cycles: 4, + handler: static function (Cpu $cpu): int { + $cpu->setStopped(true); + return 4; + }, + ), + + 0xD4 => new Instruction( + opcode: 0xD4, + mnemonic: 'CALL NC,nn', + length: 3, + cycles: 12, // 24 if taken, 12 if not taken + handler: static function (Cpu $cpu): int { + $address = self::readImm16($cpu); + if (!$cpu->getFlags()->getC()) { + $pc = $cpu->getPC()->get(); + $cpu->cycleNoAccess(); // Internal delay: 1 M-cycle + $cpu->getSP()->decrement(); + $cpu->cycleWrite($cpu->getSP()->get(), ($pc >> 8) & 0xFF); + $cpu->getSP()->decrement(); + $cpu->cycleWrite($cpu->getSP()->get(), $pc & 0xFF); + $cpu->getPC()->set($address); + return 24; + } + return 12; + }, + ), + + 0xD5 => new Instruction( + opcode: 0xD5, + mnemonic: 'PUSH DE', + length: 1, + cycles: 16, + handler: static function (Cpu $cpu): int { + $cpu->cycleNoAccess(); // Internal delay: 1 M-cycle + $value = $cpu->getDE()->get(); + $cpu->getSP()->decrement(); + $cpu->cycleWrite($cpu->getSP()->get(), ($value >> 8) & 0xFF); + $cpu->getSP()->decrement(); + $cpu->cycleWrite($cpu->getSP()->get(), $value & 0xFF); + return 16; + }, + ), + + 0xD6 => new Instruction( + opcode: 0xD6, + mnemonic: 'SUB n', + length: 2, + cycles: 8, + handler: static function (Cpu $cpu): int { + $a = $cpu->getA(); + $n = self::readImm8($cpu); + $result = $a - $n; + $cpu->setA($result & 0xFF); + $cpu->getFlags()->setZ(($result & 0xFF) === 0); + $cpu->getFlags()->setN(true); + $cpu->getFlags()->setH(self::halfCarry8Sub($a, $n)); + $cpu->getFlags()->setC($result < 0); + return 8; + }, + ), + + 0xD7 => new Instruction( + opcode: 0xD7, + mnemonic: 'RST 10H', + length: 1, + cycles: 16, + handler: static function (Cpu $cpu): int { + $pc = $cpu->getPC()->get(); + $cpu->cycleNoAccess(); // Internal delay: 1 M-cycle + $cpu->getSP()->decrement(); + $cpu->cycleWrite($cpu->getSP()->get(), ($pc >> 8) & 0xFF); + $cpu->getSP()->decrement(); + $cpu->cycleWrite($cpu->getSP()->get(), $pc & 0xFF); + $cpu->getPC()->set(0x0010); + return 16; + }, + ), + + 0xD8 => new Instruction( + opcode: 0xD8, + mnemonic: 'RET C', + length: 1, + cycles: 8, // 20 if taken, 8 if not taken + handler: static function (Cpu $cpu): int { + $cpu->cycleNoAccess(); // Internal delay for condition check: 1 M-cycle + if ($cpu->getFlags()->getC()) { + $low = $cpu->cycleRead($cpu->getSP()->get()); + $cpu->getSP()->increment(); + $high = $cpu->cycleRead($cpu->getSP()->get()); + $cpu->getSP()->increment(); + $cpu->getPC()->set(($high << 8) | $low); + $cpu->cycleNoAccess(); // Internal delay for jump: 1 M-cycle + return 20; + } + return 8; + }, + ), + + 0xD9 => new Instruction( + opcode: 0xD9, + mnemonic: 'RETI', + length: 1, + cycles: 16, + handler: static function (Cpu $cpu): int { + $low = $cpu->cycleRead($cpu->getSP()->get()); + $cpu->getSP()->increment(); + $high = $cpu->cycleRead($cpu->getSP()->get()); + $cpu->getSP()->increment(); + $cpu->getPC()->set(($high << 8) | $low); + $cpu->cycleNoAccess(); // Internal delay: 1 M-cycle + // RETI enables interrupts immediately (not delayed like EI) + $cpu->setIMEImmediate(); + return 16; + }, + ), + + 0xDA => new Instruction( + opcode: 0xDA, + mnemonic: 'JP C,nn', + length: 3, + cycles: 12, // 16 if taken, 12 if not taken + handler: static function (Cpu $cpu): int { + $address = self::readImm16($cpu); + if ($cpu->getFlags()->getC()) { + $cpu->getPC()->set($address); + $cpu->cycleNoAccess(); // Internal delay for jump: 1 M-cycle + return 16; + } + return 12; + }, + ), + + // 0xDB: Illegal opcode - locks up CPU + 0xDB => new Instruction( + opcode: 0xDB, + mnemonic: 'ILLEGAL_DB', + length: 1, + cycles: 4, + handler: static function (Cpu $cpu): int { + $cpu->setStopped(true); + return 4; + }, + ), + + 0xDC => new Instruction( + opcode: 0xDC, + mnemonic: 'CALL C,nn', + length: 3, + cycles: 12, // 24 if taken, 12 if not taken + handler: static function (Cpu $cpu): int { + $address = self::readImm16($cpu); + if ($cpu->getFlags()->getC()) { + $pc = $cpu->getPC()->get(); + $cpu->cycleNoAccess(); // Internal delay: 1 M-cycle + $cpu->getSP()->decrement(); + $cpu->cycleWrite($cpu->getSP()->get(), ($pc >> 8) & 0xFF); + $cpu->getSP()->decrement(); + $cpu->cycleWrite($cpu->getSP()->get(), $pc & 0xFF); + $cpu->getPC()->set($address); + return 24; + } + return 12; + }, + ), + + // 0xDD: Illegal opcode - locks up CPU + 0xDD => new Instruction( + opcode: 0xDD, + mnemonic: 'ILLEGAL_DD', + length: 1, + cycles: 4, + handler: static function (Cpu $cpu): int { + $cpu->setStopped(true); + return 4; + }, + ), + + 0xDE => new Instruction( + opcode: 0xDE, + mnemonic: 'SBC A,n', + length: 2, + cycles: 8, + handler: static function (Cpu $cpu): int { + $a = $cpu->getA(); + $n = self::readImm8($cpu); + $carry = $cpu->getFlags()->getC() ? 1 : 0; + $result = $a - $n - $carry; + $cpu->setA($result & 0xFF); + $cpu->getFlags()->setZ(($result & 0xFF) === 0); + $cpu->getFlags()->setN(true); + $cpu->getFlags()->setH(self::halfCarry8Sub($a, $n, $carry)); + $cpu->getFlags()->setC($result < 0); + return 8; + }, + ), + + 0xDF => new Instruction( + opcode: 0xDF, + mnemonic: 'RST 18H', + length: 1, + cycles: 16, + handler: static function (Cpu $cpu): int { + $pc = $cpu->getPC()->get(); + $cpu->cycleNoAccess(); // Internal delay: 1 M-cycle + $cpu->getSP()->decrement(); + $cpu->cycleWrite($cpu->getSP()->get(), ($pc >> 8) & 0xFF); + $cpu->getSP()->decrement(); + $cpu->cycleWrite($cpu->getSP()->get(), $pc & 0xFF); + $cpu->getPC()->set(0x0018); + return 16; + }, + ), + + // 0xE0-0xEF: I/O operations and more control flow + + 0xE0 => new Instruction( + opcode: 0xE0, + mnemonic: 'LDH (n),A', + length: 2, + cycles: 12, + handler: static function (Cpu $cpu): int { + $n = self::readImm8($cpu); + $address = 0xFF00 + $n; + $cpu->cycleWrite($address, $cpu->getA()); + return 12; + }, + ), + + 0xE1 => new Instruction( + opcode: 0xE1, + mnemonic: 'POP HL', + length: 1, + cycles: 12, + handler: static function (Cpu $cpu): int { + $low = $cpu->cycleRead($cpu->getSP()->get()); + $cpu->getSP()->increment(); + $high = $cpu->cycleRead($cpu->getSP()->get()); + $cpu->getSP()->increment(); + $cpu->getHL()->set(($high << 8) | $low); + return 12; + }, + ), + + 0xE2 => new Instruction( + opcode: 0xE2, + mnemonic: 'LD (C),A', + length: 1, + cycles: 8, + handler: static function (Cpu $cpu): int { + $address = 0xFF00 + $cpu->getC(); + $cpu->cycleWrite($address, $cpu->getA()); + return 8; + }, + ), + + // 0xE3: Illegal opcode - locks up CPU + 0xE3 => new Instruction( + opcode: 0xE3, + mnemonic: 'ILLEGAL_E3', + length: 1, + cycles: 4, + handler: static function (Cpu $cpu): int { + $cpu->setStopped(true); + return 4; + }, + ), + + // 0xE4: Illegal opcode - locks up CPU + 0xE4 => new Instruction( + opcode: 0xE4, + mnemonic: 'ILLEGAL_E4', + length: 1, + cycles: 4, + handler: static function (Cpu $cpu): int { + $cpu->setStopped(true); + return 4; + }, + ), + + 0xE5 => new Instruction( + opcode: 0xE5, + mnemonic: 'PUSH HL', + length: 1, + cycles: 16, + handler: static function (Cpu $cpu): int { + $cpu->cycleNoAccess(); // Internal delay: 1 M-cycle + $value = $cpu->getHL()->get(); + $cpu->getSP()->decrement(); + $cpu->cycleWrite($cpu->getSP()->get(), ($value >> 8) & 0xFF); + $cpu->getSP()->decrement(); + $cpu->cycleWrite($cpu->getSP()->get(), $value & 0xFF); + return 16; + }, + ), + + 0xE6 => new Instruction( + opcode: 0xE6, + mnemonic: 'AND n', + length: 2, + cycles: 8, + handler: static function (Cpu $cpu): int { + $n = self::readImm8($cpu); + $result = $cpu->getA() & $n; + $cpu->setA($result); + $cpu->getFlags()->setZ($result === 0); + $cpu->getFlags()->setN(false); + $cpu->getFlags()->setH(true); + $cpu->getFlags()->setC(false); + return 8; + }, + ), + + 0xE7 => new Instruction( + opcode: 0xE7, + mnemonic: 'RST 20H', + length: 1, + cycles: 16, + handler: static function (Cpu $cpu): int { + $pc = $cpu->getPC()->get(); + $cpu->cycleNoAccess(); // Internal delay: 1 M-cycle + $cpu->getSP()->decrement(); + $cpu->cycleWrite($cpu->getSP()->get(), ($pc >> 8) & 0xFF); + $cpu->getSP()->decrement(); + $cpu->cycleWrite($cpu->getSP()->get(), $pc & 0xFF); + $cpu->getPC()->set(0x0020); + return 16; + }, + ), + + 0xE8 => new Instruction( + opcode: 0xE8, + mnemonic: 'ADD SP,e', + length: 2, + cycles: 16, + handler: static function (Cpu $cpu): int { + $sp = $cpu->getSP()->get(); + $e_unsigned = self::readImm8($cpu); + // Sign extend for the actual addition + $e_signed = $e_unsigned > 0x7F ? $e_unsigned - 0x100 : $e_unsigned; + $result = $sp + $e_signed; + $cpu->getSP()->set($result & 0xFFFF); + $cpu->getFlags()->setZ(false); + $cpu->getFlags()->setN(false); + // Flags are calculated on the lower byte using unsigned arithmetic + $cpu->getFlags()->setH(((($sp & 0x0F) + ($e_unsigned & 0x0F)) & 0x10) !== 0); + $cpu->getFlags()->setC(((($sp & 0xFF) + $e_unsigned) & 0x100) !== 0); + // Internal operations: 2 M-cycles + $cpu->cycleNoAccess(); + $cpu->cycleNoAccess(); + return 16; + }, + ), + + 0xE9 => new Instruction( + opcode: 0xE9, + mnemonic: 'JP (HL)', + length: 1, + cycles: 4, + handler: static function (Cpu $cpu): int { + $cpu->getPC()->set($cpu->getHL()->get()); + return 4; + }, + ), + + 0xEA => new Instruction( + opcode: 0xEA, + mnemonic: 'LD (nn),A', + length: 3, + cycles: 16, + handler: static function (Cpu $cpu): int { + $address = self::readImm16($cpu); + $cpu->cycleWrite($address, $cpu->getA()); + return 16; + }, + ), + + // 0xEB: Illegal opcode - locks up CPU + 0xEB => new Instruction( + opcode: 0xEB, + mnemonic: 'ILLEGAL_EB', + length: 1, + cycles: 4, + handler: static function (Cpu $cpu): int { + $cpu->setStopped(true); + return 4; + }, + ), + + // 0xEC: Illegal opcode - locks up CPU + 0xEC => new Instruction( + opcode: 0xEC, + mnemonic: 'ILLEGAL_EC', + length: 1, + cycles: 4, + handler: static function (Cpu $cpu): int { + $cpu->setStopped(true); + return 4; + }, + ), + + // 0xED: Illegal opcode - locks up CPU + 0xED => new Instruction( + opcode: 0xED, + mnemonic: 'ILLEGAL_ED', + length: 1, + cycles: 4, + handler: static function (Cpu $cpu): int { + $cpu->setStopped(true); + return 4; + }, + ), + + 0xEE => new Instruction( + opcode: 0xEE, + mnemonic: 'XOR n', + length: 2, + cycles: 8, + handler: static function (Cpu $cpu): int { + $n = self::readImm8($cpu); + $result = $cpu->getA() ^ $n; + $cpu->setA($result); + $cpu->getFlags()->setZ($result === 0); + $cpu->getFlags()->setN(false); + $cpu->getFlags()->setH(false); + $cpu->getFlags()->setC(false); + return 8; + }, + ), + + 0xEF => new Instruction( + opcode: 0xEF, + mnemonic: 'RST 28H', + length: 1, + cycles: 16, + handler: static function (Cpu $cpu): int { + $pc = $cpu->getPC()->get(); + $cpu->cycleNoAccess(); // Internal delay: 1 M-cycle + $cpu->getSP()->decrement(); + $cpu->cycleWrite($cpu->getSP()->get(), ($pc >> 8) & 0xFF); + $cpu->getSP()->decrement(); + $cpu->cycleWrite($cpu->getSP()->get(), $pc & 0xFF); + $cpu->getPC()->set(0x0028); + return 16; + }, + ), + + // 0xF0-0xFF: Final I/O operations and control flow + + 0xF0 => new Instruction( + opcode: 0xF0, + mnemonic: 'LDH A,(n)', + length: 2, + cycles: 12, + handler: static function (Cpu $cpu): int { + $n = self::readImm8($cpu); + $address = 0xFF00 + $n; + $cpu->setA($cpu->cycleRead($address)); + return 12; + }, + ), + + 0xF1 => new Instruction( + opcode: 0xF1, + mnemonic: 'POP AF', + length: 1, + cycles: 12, + handler: static function (Cpu $cpu): int { + $low = $cpu->cycleRead($cpu->getSP()->get()); + $cpu->getSP()->increment(); + $high = $cpu->cycleRead($cpu->getSP()->get()); + $cpu->getSP()->increment(); + $cpu->getAF()->set(($high << 8) | ($low & 0xF0)); // Lower 4 bits of F are always 0 + $cpu->getFlags()->syncFromAF(); // Sync flags from AF register + return 12; + }, + ), + + 0xF2 => new Instruction( + opcode: 0xF2, + mnemonic: 'LD A,(C)', + length: 1, + cycles: 8, + handler: static function (Cpu $cpu): int { + $address = 0xFF00 + $cpu->getC(); + $cpu->setA($cpu->cycleRead($address)); + return 8; + }, + ), + + 0xF3 => new Instruction( + opcode: 0xF3, + mnemonic: 'DI', + length: 1, + cycles: 4, + handler: static function (Cpu $cpu): int { + $cpu->setIME(false); + return 4; + }, + ), + + // 0xF4: Illegal opcode - locks up CPU + 0xF4 => new Instruction( + opcode: 0xF4, + mnemonic: 'ILLEGAL_F4', + length: 1, + cycles: 4, + handler: static function (Cpu $cpu): int { + $cpu->setStopped(true); + return 4; + }, + ), + + 0xF5 => new Instruction( + opcode: 0xF5, + mnemonic: 'PUSH AF', + length: 1, + cycles: 16, + handler: static function (Cpu $cpu): int { + $cpu->cycleNoAccess(); // Internal delay: 1 M-cycle + $value = $cpu->getAF()->get(); + $cpu->getSP()->decrement(); + $cpu->cycleWrite($cpu->getSP()->get(), ($value >> 8) & 0xFF); + $cpu->getSP()->decrement(); + $cpu->cycleWrite($cpu->getSP()->get(), $value & 0xFF); + return 16; + }, + ), + + 0xF6 => new Instruction( + opcode: 0xF6, + mnemonic: 'OR n', + length: 2, + cycles: 8, + handler: static function (Cpu $cpu): int { + $n = self::readImm8($cpu); + $result = $cpu->getA() | $n; + $cpu->setA($result); + $cpu->getFlags()->setZ($result === 0); + $cpu->getFlags()->setN(false); + $cpu->getFlags()->setH(false); + $cpu->getFlags()->setC(false); + return 8; + }, + ), + + 0xF7 => new Instruction( + opcode: 0xF7, + mnemonic: 'RST 30H', + length: 1, + cycles: 16, + handler: static function (Cpu $cpu): int { + $pc = $cpu->getPC()->get(); + $cpu->cycleNoAccess(); // Internal delay: 1 M-cycle + $cpu->getSP()->decrement(); + $cpu->cycleWrite($cpu->getSP()->get(), ($pc >> 8) & 0xFF); + $cpu->getSP()->decrement(); + $cpu->cycleWrite($cpu->getSP()->get(), $pc & 0xFF); + $cpu->getPC()->set(0x0030); + return 16; + }, + ), + + 0xF8 => new Instruction( + opcode: 0xF8, + mnemonic: 'LD HL,SP+e', + length: 2, + cycles: 12, + handler: static function (Cpu $cpu): int { + $sp = $cpu->getSP()->get(); + $e_unsigned = self::readImm8($cpu); + // Sign extend for the actual addition + $e_signed = $e_unsigned > 0x7F ? $e_unsigned - 0x100 : $e_unsigned; + $result = $sp + $e_signed; + $cpu->getHL()->set($result & 0xFFFF); + $cpu->getFlags()->setZ(false); + $cpu->getFlags()->setN(false); + // Flags are calculated on the lower byte using unsigned arithmetic + $cpu->getFlags()->setH(((($sp & 0x0F) + ($e_unsigned & 0x0F)) & 0x10) !== 0); + $cpu->getFlags()->setC(((($sp & 0xFF) + $e_unsigned) & 0x100) !== 0); + // Internal operation: 1 M-cycle + $cpu->cycleNoAccess(); + return 12; + }, + ), + + 0xF9 => new Instruction( + opcode: 0xF9, + mnemonic: 'LD SP,HL', + length: 1, + cycles: 8, + handler: static function (Cpu $cpu): int { + $cpu->getSP()->set($cpu->getHL()->get()); + $cpu->cycleNoAccess(); // Internal operation: 1 M-cycle + return 8; + }, + ), + + 0xFA => new Instruction( + opcode: 0xFA, + mnemonic: 'LD A,(nn)', + length: 3, + cycles: 16, + handler: static function (Cpu $cpu): int { + $address = self::readImm16($cpu); + $cpu->setA($cpu->cycleRead($address)); + return 16; + }, + ), + + 0xFB => new Instruction( + opcode: 0xFB, + mnemonic: 'EI', + length: 1, + cycles: 4, + handler: static function (Cpu $cpu): int { + $cpu->setIME(true); + return 4; + }, + ), + + // 0xFC: Illegal opcode - locks up CPU + 0xFC => new Instruction( + opcode: 0xFC, + mnemonic: 'ILLEGAL_FC', + length: 1, + cycles: 4, + handler: static function (Cpu $cpu): int { + $cpu->setStopped(true); + return 4; + }, + ), + + // 0xFD: Illegal opcode - locks up CPU + 0xFD => new Instruction( + opcode: 0xFD, + mnemonic: 'ILLEGAL_FD', + length: 1, + cycles: 4, + handler: static function (Cpu $cpu): int { + $cpu->setStopped(true); + return 4; + }, + ), + + 0xFE => new Instruction( + opcode: 0xFE, + mnemonic: 'CP n', + length: 2, + cycles: 8, + handler: static function (Cpu $cpu): int { + $a = $cpu->getA(); + $n = self::readImm8($cpu); + $result = $a - $n; + $cpu->getFlags()->setZ(($result & 0xFF) === 0); + $cpu->getFlags()->setN(true); + $cpu->getFlags()->setH(self::halfCarry8Sub($a, $n)); + $cpu->getFlags()->setC($result < 0); + return 8; + }, + ), + + 0xFF => new Instruction( + opcode: 0xFF, + mnemonic: 'RST 38H', + length: 1, + cycles: 16, + handler: static function (Cpu $cpu): int { + $pc = $cpu->getPC()->get(); + $cpu->cycleNoAccess(); // Internal delay: 1 M-cycle + $cpu->getSP()->decrement(); + $cpu->cycleWrite($cpu->getSP()->get(), ($pc >> 8) & 0xFF); + $cpu->getSP()->decrement(); + $cpu->cycleWrite($cpu->getSP()->get(), $pc & 0xFF); + $cpu->getPC()->set(0x0038); + return 16; + }, + ), + + default => throw new RuntimeException(sprintf('Unknown opcode: 0x%02X', $opcode)), + }; + } + + /** + * Build CB-prefixed instruction metadata. + */ + private static function buildCBInstruction(int $opcode): Instruction + { + // Extract register index (0-7: B, C, D, E, H, L, (HL), A) + $regIndex = $opcode & 0x07; + + // Determine register name for mnemonic + $regName = match ($regIndex) { + 0 => 'B', + 1 => 'C', + 2 => 'D', + 3 => 'E', + 4 => 'H', + 5 => 'L', + 6 => '(HL)', + 7 => 'A', + }; + + // (HL) operations take 16 cycles, register operations take 8 cycles + $cycles = ($regIndex === 6) ? 16 : 8; + + return match (true) { + // RLC r (0x00-0x07) - Rotate Left Circular + $opcode >= 0x00 && $opcode <= 0x07 => new Instruction( + opcode: 0xCB00 | $opcode, + mnemonic: "RLC $regName", + length: 2, + cycles: $cycles, + handler: static function (Cpu $cpu) use ($regIndex, $cycles): int { + $value = self::getRegByIndex($cpu, $regIndex); + $bit7 = ($value & 0x80) !== 0; + $result = (($value << 1) & 0xFF) | ($bit7 ? 1 : 0); + + $cpu->getFlags()->setZ($result === 0); + $cpu->getFlags()->setN(false); + $cpu->getFlags()->setH(false); + $cpu->getFlags()->setC($bit7); + + self::setRegByIndex($cpu, $regIndex, $result); + return $cycles; + }, + ), + + // RRC r (0x08-0x0F) - Rotate Right Circular + $opcode >= 0x08 && $opcode <= 0x0F => new Instruction( + opcode: 0xCB00 | $opcode, + mnemonic: "RRC $regName", + length: 2, + cycles: $cycles, + handler: static function (Cpu $cpu) use ($regIndex, $cycles): int { + $value = self::getRegByIndex($cpu, $regIndex); + $bit0 = ($value & 0x01) !== 0; + $result = ($value >> 1) | ($bit0 ? 0x80 : 0); + + $cpu->getFlags()->setZ($result === 0); + $cpu->getFlags()->setN(false); + $cpu->getFlags()->setH(false); + $cpu->getFlags()->setC($bit0); + + self::setRegByIndex($cpu, $regIndex, $result); + return $cycles; + }, + ), + + // RL r (0x10-0x17) - Rotate Left through Carry + $opcode >= 0x10 && $opcode <= 0x17 => new Instruction( + opcode: 0xCB00 | $opcode, + mnemonic: "RL $regName", + length: 2, + cycles: $cycles, + handler: static function (Cpu $cpu) use ($regIndex, $cycles): int { + $value = self::getRegByIndex($cpu, $regIndex); + $oldCarry = $cpu->getFlags()->getC(); + [$result, $newCarry] = BitOps::rotateLeft($value, $oldCarry); + + $cpu->getFlags()->setZ($result === 0); + $cpu->getFlags()->setN(false); + $cpu->getFlags()->setH(false); + $cpu->getFlags()->setC($newCarry); + + self::setRegByIndex($cpu, $regIndex, $result); + return $cycles; + }, + ), + + // RR r (0x18-0x1F) - Rotate Right through Carry + $opcode >= 0x18 && $opcode <= 0x1F => new Instruction( + opcode: 0xCB00 | $opcode, + mnemonic: "RR $regName", + length: 2, + cycles: $cycles, + handler: static function (Cpu $cpu) use ($regIndex, $cycles): int { + $value = self::getRegByIndex($cpu, $regIndex); + $oldCarry = $cpu->getFlags()->getC(); + [$result, $newCarry] = BitOps::rotateRight($value, $oldCarry); + + $cpu->getFlags()->setZ($result === 0); + $cpu->getFlags()->setN(false); + $cpu->getFlags()->setH(false); + $cpu->getFlags()->setC($newCarry); + + self::setRegByIndex($cpu, $regIndex, $result); + return $cycles; + }, + ), + + // SLA r (0x20-0x27) - Shift Left Arithmetic + $opcode >= 0x20 && $opcode <= 0x27 => new Instruction( + opcode: 0xCB00 | $opcode, + mnemonic: "SLA $regName", + length: 2, + cycles: $cycles, + handler: static function (Cpu $cpu) use ($regIndex, $cycles): int { + $value = self::getRegByIndex($cpu, $regIndex); + [$result, $carry] = BitOps::shiftLeft($value); + + $cpu->getFlags()->setZ($result === 0); + $cpu->getFlags()->setN(false); + $cpu->getFlags()->setH(false); + $cpu->getFlags()->setC($carry); + + self::setRegByIndex($cpu, $regIndex, $result); + return $cycles; + }, + ), + + // SRA r (0x28-0x2F) - Shift Right Arithmetic (preserve bit 7) + $opcode >= 0x28 && $opcode <= 0x2F => new Instruction( + opcode: 0xCB00 | $opcode, + mnemonic: "SRA $regName", + length: 2, + cycles: $cycles, + handler: static function (Cpu $cpu) use ($regIndex, $cycles): int { + $value = self::getRegByIndex($cpu, $regIndex); + [$result, $carry] = BitOps::shiftRight($value, true); + + $cpu->getFlags()->setZ($result === 0); + $cpu->getFlags()->setN(false); + $cpu->getFlags()->setH(false); + $cpu->getFlags()->setC($carry); + + self::setRegByIndex($cpu, $regIndex, $result); + return $cycles; + }, + ), + + // SWAP r (0x30-0x37) - Swap nibbles + $opcode >= 0x30 && $opcode <= 0x37 => new Instruction( + opcode: 0xCB00 | $opcode, + mnemonic: "SWAP $regName", + length: 2, + cycles: $cycles, + handler: static function (Cpu $cpu) use ($regIndex, $cycles): int { + $value = self::getRegByIndex($cpu, $regIndex); + $result = BitOps::swap($value); + + $cpu->getFlags()->setZ($result === 0); + $cpu->getFlags()->setN(false); + $cpu->getFlags()->setH(false); + $cpu->getFlags()->setC(false); + + self::setRegByIndex($cpu, $regIndex, $result); + return $cycles; + }, + ), + + // SRL r (0x38-0x3F) - Shift Right Logical (bit 7 becomes 0) + $opcode >= 0x38 && $opcode <= 0x3F => new Instruction( + opcode: 0xCB00 | $opcode, + mnemonic: "SRL $regName", + length: 2, + cycles: $cycles, + handler: static function (Cpu $cpu) use ($regIndex, $cycles): int { + $value = self::getRegByIndex($cpu, $regIndex); + [$result, $carry] = BitOps::shiftRight($value, false); + + $cpu->getFlags()->setZ($result === 0); + $cpu->getFlags()->setN(false); + $cpu->getFlags()->setH(false); + $cpu->getFlags()->setC($carry); + + self::setRegByIndex($cpu, $regIndex, $result); + return $cycles; + }, + ), + + // BIT b,r (0x40-0x7F) - Test bit b in register r + $opcode >= 0x40 && $opcode <= 0x7F => new Instruction( + opcode: 0xCB00 | $opcode, + mnemonic: sprintf('BIT %d,%s', ($opcode - 0x40) >> 3, $regName), + length: 2, + cycles: ($regIndex === 6) ? 12 : 8, // BIT b,(HL) is 12 cycles, not 16 + handler: static function (Cpu $cpu) use ($opcode, $regIndex): int { + $bit = ($opcode - 0x40) >> 3; + $value = self::getRegByIndex($cpu, $regIndex); + $bitValue = BitOps::getBit($value, $bit); + + $cpu->getFlags()->setZ(!$bitValue); + $cpu->getFlags()->setN(false); + $cpu->getFlags()->setH(true); + // C flag unchanged + + return ($regIndex === 6) ? 12 : 8; + }, + ), + + // RES b,r (0x80-0xBF) - Reset (clear) bit b in register r + $opcode >= 0x80 && $opcode <= 0xBF => new Instruction( + opcode: 0xCB00 | $opcode, + mnemonic: sprintf('RES %d,%s', ($opcode - 0x80) >> 3, $regName), + length: 2, + cycles: $cycles, + handler: static function (Cpu $cpu) use ($opcode, $regIndex, $cycles): int { + $bit = ($opcode - 0x80) >> 3; + $value = self::getRegByIndex($cpu, $regIndex); + $result = BitOps::setBit($value, $bit, false); + + // Flags unchanged for RES + self::setRegByIndex($cpu, $regIndex, $result); + return $cycles; + }, + ), + + // SET b,r (0xC0-0xFF) - Set bit b in register r + $opcode >= 0xC0 && $opcode <= 0xFF => new Instruction( + opcode: 0xCB00 | $opcode, + mnemonic: sprintf('SET %d,%s', ($opcode - 0xC0) >> 3, $regName), + length: 2, + cycles: $cycles, + handler: static function (Cpu $cpu) use ($opcode, $regIndex, $cycles): int { + $bit = ($opcode - 0xC0) >> 3; + $value = self::getRegByIndex($cpu, $regIndex); + $result = BitOps::setBit($value, $bit, true); + + // Flags unchanged for SET + self::setRegByIndex($cpu, $regIndex, $result); + return $cycles; + }, + ), + + default => throw new RuntimeException(sprintf('Invalid CB opcode: 0xCB%02X', $opcode)), + }; + } + + /** + * Helper: Get register value by index (0-7: B,C,D,E,H,L,(HL),A) + */ + private static function getRegByIndex(Cpu $cpu, int $index): int + { + return match ($index) { + 0 => $cpu->getB(), + 1 => $cpu->getC(), + 2 => $cpu->getD(), + 3 => $cpu->getE(), + 4 => $cpu->getH(), + 5 => $cpu->getL(), + 6 => $cpu->cycleRead($cpu->getHL()->get()), + 7 => $cpu->getA(), + default => throw new \InvalidArgumentException("Invalid register index: {$index}"), + }; + } + + /** + * Helper: Set register value by index (0-7: B,C,D,E,H,L,(HL),A) + */ + private static function setRegByIndex(Cpu $cpu, int $index, int $value): void + { + match ($index) { + 0 => $cpu->setB($value), + 1 => $cpu->setC($value), + 2 => $cpu->setD($value), + 3 => $cpu->setE($value), + 4 => $cpu->setH($value), + 5 => $cpu->setL($value), + 6 => $cpu->cycleWrite($cpu->getHL()->get(), $value), + 7 => $cpu->setA($value), + default => throw new \InvalidArgumentException("Invalid register index: {$index}"), + }; + } +} diff --git a/src/Dma/OamDma.php b/src/Dma/OamDma.php index e7b0ebb..2a3eeb7 100644 --- a/src/Dma/OamDma.php +++ b/src/Dma/OamDma.php @@ -49,6 +49,12 @@ final class OamDma implements DeviceInterface */ private int $dmaSource = 0x0000; + /** + * Startup delay in M-cycles before first byte transfer. + * DMA has a 1 M-cycle delay after trigger before transferring bytes. + */ + private int $dmaDelay = 0; + /** * @param BusInterface $bus Memory bus for reading source and writing to OAM */ @@ -97,6 +103,7 @@ private function startDmaTransfer(int $sourcePage): void $this->dmaSource = ($sourcePage << 8) & 0xFF00; $this->dmaActive = true; $this->dmaProgress = 0; + $this->dmaDelay = 1; // 1 M-cycle delay before first byte transfer } /** @@ -110,11 +117,11 @@ public function isDmaActive(): bool } /** - * Update the DMA transfer by the specified number of M-cycles. + * Update the DMA transfer by the specified number of CPU cycles. * - * Each M-cycle transfers one byte. DMA completes after 160 M-cycles. + * Each M-cycle (4 T-cycles) transfers one byte. DMA completes after 160 M-cycles. * - * @param int $cycles Number of M-cycles elapsed (typically 1, 2, 3, 4, 5, or 6) + * @param int $cycles Number of T-cycles (CPU cycles) elapsed */ public function tick(int $cycles): void { @@ -122,8 +129,22 @@ public function tick(int $cycles): void return; } + // Convert T-cycles to M-cycles (1 M-cycle = 4 T-cycles) + $mCycles = intdiv($cycles, 4); + + // Handle startup delay (1 M-cycle before first byte transfer) + if ($this->dmaDelay > 0) { + $delayToProcess = min($this->dmaDelay, $mCycles); + $this->dmaDelay -= $delayToProcess; + $mCycles -= $delayToProcess; + + if ($mCycles <= 0) { + return; // Still in delay phase + } + } + // Transfer one byte per M-cycle - for ($i = 0; $i < $cycles; $i++) { + for ($i = 0; $i < $mCycles; $i++) { if ($this->dmaProgress >= self::TRANSFER_LENGTH) { $this->dmaActive = false; break; diff --git a/src/Emulator.php b/src/Emulator.php index aa59f32..c85c92b 100644 --- a/src/Emulator.php +++ b/src/Emulator.php @@ -201,6 +201,17 @@ private function initializeSystem(): void // Expected: 1-2% performance gain by eliminating lazy initialization checks \Gb\Cpu\InstructionSet::warmCache(); + // Set up M-cycle accurate callback + // This callback is invoked by the CPU during instruction execution + // to advance all other components in real-time + $this->cpu->setCycleCallback(function (int $cycles) { + $this->ppu?->step($cycles); + $this->apu?->step($cycles); + $this->timer?->tick($cycles); + $this->oamDma?->tick($cycles); + $this->clock->tick($cycles); + }); + // Reset clock $this->clock->reset(); } @@ -305,23 +316,21 @@ public function step(): void while ($frameCycles < self::CYCLES_PER_FRAME) { // Execute one CPU instruction + // With M-cycle accuracy, all components are advanced during + // instruction execution via the cycle callback $cycles = $this->cpu->step(); - // Step all other components by the same number of cycles - $this->ppu->step($cycles); - $this->apu->step($cycles); - $this->timer?->tick($cycles); - $this->oamDma?->tick($cycles); - // Accumulate cycles $frameCycles += $cycles; - $this->clock->tick($cycles); } } /** * Execute a single CPU instruction. * + * With M-cycle accuracy, all components are automatically advanced + * during instruction execution via the cycle callback. + * * @return int Number of cycles executed */ public function stepInstruction(): int @@ -330,16 +339,10 @@ public function stepInstruction(): int throw new \RuntimeException("Cannot step instruction: CPU not initialized"); } + // Execute CPU instruction + // All components are automatically stepped via the cycle callback $cycles = $this->cpu->step(); - // Step other components - $this->ppu?->step($cycles); - $this->apu?->step($cycles); - $this->timer?->tick($cycles); - $this->oamDma?->tick($cycles); - - $this->clock->tick($cycles); - return $cycles; } diff --git a/tests/Unit/Dma/OamDmaTest.php b/tests/Unit/Dma/OamDmaTest.php index 3b6be8a..ac0a14d 100644 --- a/tests/Unit/Dma/OamDmaTest.php +++ b/tests/Unit/Dma/OamDmaTest.php @@ -54,8 +54,8 @@ public function it_transfers_160_bytes_from_source_to_oam(): void // Start DMA transfer from 0xC100 $this->dma->writeByte(0xFF46, 0xC1); - // Transfer completes after 160 M-cycles - $this->dma->tick(160); + // Transfer completes after 161 M-cycles (1 delay + 160 transfer) = 644 T-cycles + $this->dma->tick(644); // Verify all 160 bytes were copied to OAM (0xFE00-0xFE9F) for ($i = 0; $i < 160; $i++) { @@ -76,8 +76,8 @@ public function it_handles_partial_transfer(): void // Start DMA transfer $this->dma->writeByte(0xFF46, 0xD0); - // Transfer 50 bytes - $this->dma->tick(50); + // Transfer 50 bytes (1 delay + 50 transfer = 51 M-cycles = 204 T-cycles) + $this->dma->tick(204); $this->assertTrue($this->dma->isDmaActive()); @@ -105,11 +105,12 @@ public function it_completes_transfer_after_160_cycles(): void $this->assertTrue($this->dma->isDmaActive()); - // Transfer in chunks - $this->dma->tick(100); + // Transfer in chunks (100 M-cycles = 400 T-cycles) + $this->dma->tick(400); $this->assertTrue($this->dma->isDmaActive()); - $this->dma->tick(60); + // Complete transfer (61 more M-cycles = 244 T-cycles) + $this->dma->tick(244); $this->assertFalse($this->dma->isDmaActive()); // Verify all bytes transferred @@ -126,7 +127,7 @@ public function it_handles_multiple_transfers(): void $this->bus->writeByte(0xC000 + $i, $i); } $this->dma->writeByte(0xFF46, 0xC0); - $this->dma->tick(160); + $this->dma->tick(644); // 161 M-cycles = 644 T-cycles // Verify first transfer $this->assertSame(0x00, $this->bus->readByte(0xFE00)); @@ -137,7 +138,7 @@ public function it_handles_multiple_transfers(): void $this->bus->writeByte(0xD000 + $i, 0xFF - $i); } $this->dma->writeByte(0xFF46, 0xD0); - $this->dma->tick(160); + $this->dma->tick(644); // 161 M-cycles = 644 T-cycles // Verify second transfer overwrote OAM $this->assertSame(0xFF, $this->bus->readByte(0xFE00)); @@ -154,7 +155,7 @@ public function it_uses_source_address_low_byte_as_zero(): void // Write 0xC1 to DMA register (should use 0xC100 as source, not 0xC146) $this->dma->writeByte(0xFF46, 0xC1); - $this->dma->tick(160); + $this->dma->tick(644); // 161 M-cycles = 644 T-cycles // All OAM bytes should be 0x42 for ($i = 0; $i < 160; $i++) {