Skip to content

Emulate per-model CPU bus access sequences (RMW, dummy reads, interrupt entry) - #289

Merged
highbyte merged 5 commits into
masterfrom
feature/cpu-bus-access-order
Aug 13, 2026
Merged

Emulate per-model CPU bus access sequences (RMW, dummy reads, interrupt entry)#289
highbyte merged 5 commits into
masterfrom
feature/cpu-bus-access-order

Conversation

@highbyte

Copy link
Copy Markdown
Owner

Makes CPU instructions perform their real per-model bus access sequences — every read and write in hardware order — so memory-mapped I/O with access side effects behaves as it does on real machines. Follow-up to the CPU model architecture PR (#288); milestone M2 of that work.

Behavioral highlights

  • Read-modify-write sequences per model: NMOS RMW instructions (official and undocumented) perform read-write-write — the unmodified value is written back before the result. The 65C02 performs read-read-write. Exhaustively guarded by a sequence matrix test over all 66 NMOS and 28 65C02 memory-RMW opcode bytes.
  • Apple II language card is now Sather-correct (write-enable requires two consecutive odd READS; a write never completes the sequence — previously a read+write could unlock). Combined with the RMW sequences this makes INC $C083 CPU-model observable: it unlocks the card on a 65C02 (read-read-write) but not on an NMOS 6502 (read-write-write) — exactly as on real hardware.
  • NMOS indexed dummy reads: reads that cross a page touch the un-carried address first; indexed stores and RMW always do. Classic consequence now emulated: STA $DC0D,X with X=0 silently acknowledges pending C64 CIA interrupts through its dummy read (covered by an acceptance test).
  • Zero-page pointer wrap: (zp,X), (zp),Y, and 65C02 (zp) pointers at $FF take their high byte from $00 (wrapping within zero page) instead of $0100 — a result-level fix on both models, same class as the JMP ($xxFF) fix in Add CPU model architecture with NCR 65C02 support #288.
  • Interrupt-entry sequences: IRQ/NMI entry performs the two hardware dummy reads of the next opcode byte before the stack pushes and vector reads; BRK fetches its padding byte as a real bus access (pushed return address unchanged).

Design

  • Sequences are composed at CPU-construction time per model (a PerformsIndexedDummyReads model trait; single source of truth per model); the 65C02 keeps its own read-read-write RMW handlers. No model branches on the hot path, no tracing hooks on the memory path.
  • Tests observe bus traffic via a BusAccessRecorder built purely on the production memory-mapped-I/O mechanism (Memory.MapReader/MapWriter).
  • Language-card rules verified against Sather (Understanding the Apple IIe, 5-23) via two independent emulator implementations before coding.

Deliberately deferred (documented in code)

  • 65C02-specific dummy-read addresses (the CMOS part re-targeted them; no observable case yet).
  • zp,X base-address dummy reads (not page-cross-related) and RTI/pull stack dummies.
  • Interrupt-entry cycle accounting (entry currently consumes 0 cycles, real hardware 7) — pre-existing gap, discovered here; changing it shifts C64/VIC-20 frame timing so it needs its own decision.

Tests & performance

  • ~40 new/updated tests: per-model sequence characterization, exhaustive RMW matrix, pointer-wrap, language-card model divergence, C64 $DC0D, interrupt-entry ordering.
  • All suites green incl. Klaus NMOS functional/BCD locally and the Klaus 65C02 extended-opcodes test on Windows CI.
  • Benchmarks: representative C64 workload and hot-path benchmarks at or below the milestone baseline after all changes (per-part measurements recorded during development; no benchmark moved ≥5%).

@sonarqubecloud

Copy link
Copy Markdown

@highbyte
highbyte merged commit 3665b97 into master Aug 13, 2026
10 checks passed
@highbyte
highbyte deleted the feature/cpu-bus-access-order branch August 13, 2026 09:28
highbyte added a commit that referenced this pull request Aug 13, 2026
…#290)

Hardware IRQ/NMI entry previously consumed 0 cycles in cycle accounting;
real 6502-family entry takes 7 (two dummy opcode reads, three stack
pushes, two vector reads — the bus sequence itself was implemented in
#289). Follow-up scheduled from that work.

## What changes

- `CPU.ProcessPendingInterrupts` (and the internal boundary servicing)
returns the cycles consumed: new `CPU.InterruptEntryCycles` (7) when an
interrupt was serviced, else 0.
- The entry cost is folded into the preceding instruction's
`InstructionExecResult` (new `WithAdditionalCycles`) before statistics
update, so every cycle-paced consumer sees real elapsed time with no
further changes: `ExecState` totals (Apple II disk
motor/speaker/game-port clocks), `LastInstructionExecResult` (Apple II /
VIC-20 frame budgets and VIA ticking), evaluators, and the
`InstructionExecuted` event.
- The C64's post-device-tick interrupt flush explicitly ticks CIA1/CIA2
and the cartridge slot with the entry cycles and includes them in the
raster advance and frame budget. The freeze-button NMI delivery
deliberately ignores them (one-shot event outside frame pacing;
commented).
- Note: on the full `Execute` path, `NmiAcknowledging` now fires before
`InstructionExecuted` (servicing moved before stats update); the only
NmiAcknowledging consumer (C64) uses the minimal path and is unaffected.

## Impact

Frame budgets now fit ~7 fewer CPU cycles per serviced interrupt (~0.04%
per raster-IRQ frame on C64) — matching real hardware. All existing
C64/VIC-20/Apple II timing tests pass unchanged; benchmarks at parity
(hot path gained only a return-value check).

## Tests

+4 in `CPUInterruptBoundaryTests`: IRQ entry costs 2+7 on the minimal
path (result and ExecState views), NMI costs 2+7 on the full path,
`ProcessPendingInterrupts` returns 7/0, and a no-interrupt control keeps
plain instruction cycles.

Public API note: `ProcessPendingInterrupts` changed `void` → `ulong`
(source-compatible for callers ignoring the result; package is alpha).
highbyte added a commit that referenced this pull request Aug 13, 2026
…ndlers (#291)

## What

Completes the CPU instruction-dispatch modernization started with the
per-model descriptor tables (#288) and ordered bus sequences (#289):
every opcode on both CPU models now executes through small operation
cores composed per addressing mode, or bespoke static handlers where
composition doesn't fit. The ~70 per-instruction classes and the
interface-probing composition path are deleted.

## Changes

- **Operation cores** (`InstructionCores`): one static method per
operation (loads/stores, transfers, logic, compares, shifts/rotates,
INC/DEC, ADC/SBC per model, TSB/TRB, flags, and all NMOS-undocumented
operations), bound to addressing modes by
`ComposeRead`/`ComposeStore`/`ComposeImplied`/`ComposeRmw`/`ComposeBranch`.
- **Bespoke static handlers** for instructions that don't fit
composition: shared (`SharedHandlers`: stack, flow, BRK, NOP),
NMOS-specific (`NmosHandlers`: JMP (addr) page-wrap bug, JAM), and
65C02-specific (`CmosHandlers`: JMP variants, PHX/PHY/PLX/PLY,
defined-NOP family).
- **`InstructionBindings`**: the single table both models compose their
256-entry descriptor tables from, with undocumented NMOS opcodes gated
by the CPU's compatibility profile at binding time.
- **`InstructionList` is now a projection**: its metadata is generated
from the NMOS model's descriptor table instead of the descriptor tables
being composed from instruction objects — the two views can no longer
drift. `InstructionDictionary`/`GetInstruction` and the `Instruction`
class hierarchy are removed (pre-1.0 clean break).
- **Deleted**: all `Instructions/*` classes, the four
`IInstructionUses*` interfaces, `IReadModifyWriteInstruction`,
`AddrModeCalcResult`, `InstructionExtraCyclesCalculator`, and the legacy
table composition. Net **−4.0k lines**.

## Behavior

Zero behavior change by design, verified per migration step against the
previous implementation:

- Byte-for-byte descriptor/metadata parity across all four compatibility
profiles (guarded by test).
- All test suites green on macOS and Windows (incl. Klaus Dormann
functional/BCD/65C02 tests and the exhaustive RMW bus-sequence matrix).
- Hot-path benchmarks at parity with the pre-migration baseline
(same-machine-state pairing); allocations unchanged.
- Live end-to-end check: C64 boots to BASIC and executes correctly in
the Avalonia desktop app.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant