Skip to content

cpu: calibrate 68010 MOVES/MOVE-from-CCR/RTE/dispatch cycles and fix STOP semantics#143

Merged
LinuxJedi merged 8 commits into
mainfrom
feature/cpu-timing-calibration
Jul 7, 2026
Merged

cpu: calibrate 68010 MOVES/MOVE-from-CCR/RTE/dispatch cycles and fix STOP semantics#143
LinuxJedi merged 8 commits into
mainfrom
feature/cpu-timing-calibration

Conversation

@LinuxJedi

@LinuxJedi LinuxJedi commented Jul 6, 2026

Copy link
Copy Markdown
Owner

Stacked on #144 (fix/ipl-poll-placement) - merge that first. This branch
is rebased onto it; the diff shown here against main includes #144's
commits until it merges.

Calibrates the remaining self-contained CPU instruction-timing buckets from
the vAmigaTS suite (CPU/Timing + CPU/Timing2) against measured 68000/68010
hardware behaviour, and fixes the STOP state machine. #144 already landed
the shared foundations (68010 interrupt dispatch 46 clocks with the idles
billed in place, 68010 MOVES per-EA internal clocks, format-0 RTE read
count, IPL poll placement); this branch adds what it does not cover:

Hardware fixes (on top of #144)

  • 68010 MOVE from CCR: 4 clocks to a register (was 6); memory
    destinations spend per-mode internal clocks and prefetch BEFORE the
    write ((An) = 10 total). vAmigaTS CPU/Timing2/MOVECCR, Moira
    execMoveCcrRg/execMoveCcrEa.
  • 68010 MOVES billed total: count the 2 internal clocks the -(An) and
    d8(An,Xn) address calculations spend inside resolve_ea (Moira computeEA
    SYNC(2)); -(An) byte/word = 20 clocks, d8(An,Xn) = 24. Placement was
    already right in m68k: align per-instruction IPL poll points and interrupt dispatch idle placement with the 68000 microcode #144; only the returned total under-counted.
  • STOP loads its SR verbatim (68000-040): a single-stepped STOP
    observes S and T exactly as written - the real-chip-validated SST
    m68000 STOP.json.bin pins this and passes locally. An S-clear SR
    stops only momentarily: the stopped state's supervisor check (run at
    every instruction boundary on all three step paths) raises a privilege
    violation 4 internal clocks later, stacking the STOP instruction itself
    so the handler's RTE re-executes it (4 + 38 clocks to the handler).
    Previously the core stopped with a user-mode SR and never recovered.
  • STOP with a pending trace (T set in the SR the instruction started
    with): the trace exception has priority over the stopped state and its
    supervisor check, and recovers from the stop; a T bit loaded BY the
    STOP does not fire while stopped. The 68060 keeps its distinct
    immediate-privilege model.
  • Stopped-CPU wake timing: the idle fast-forward treated a running
    (non-waiting) Copper as event-free, so a Copper MOVE to INTREQ right
    after a WAIT was overshot by whole slices - the CPU woke up to 72 lines
    late (stop1 never visibly woke). The fast-forward horizon is now two
    colour clocks while the Copper is streaming, and a pure idle single
    step advances one colour clock (a STOPped 68000 samples its IPL pins
    every bus-cycle period).

vAmigaTS results (summed divergence, cases)

bucket before (main) after
CPU/Timing/STOP (10) 557.3% 112.6%
CPU/Timing2/MOVES (30) 1332.6% ~1316%
CPU/Timing2/MOVECCR (8) 339.9% ~340%

stop1/stop2/stop5 drop to 0.7-3.2% (from 26-85%). stop3/stop4 remain
22-30%: their privilege-violation loop never re-syncs to the beam, so the
one-off interrupt-entry offset from the known Copper-write/IPL-recognition
class persists for the whole frame.

MOVES/MOVECCR payload cells now measure cycle-exact against vAmiga (the
bracketing COLOR00 spans match pixel-for-pixel), but the buckets' scores
are dominated by the cpu2.i harness's colour-painting main loop: its
stripe phase depends on the exact instruction boundary at which each of
the six per-frame interrupts is recognized, and Copperline's recognition
point sits a few colour clocks later in the instruction stream than
vAmiga's (the documented Copper-write-timing + IPL-pipe class; the pushed
return PCs differ by 1-2 NOPs at equal beam positions). Until that class
is fixed, the stripe-noise floor for these buckets stays ~40%; the same
payloads measured by the silent-main-loop CPU/Timing harness score
2-4.5%.

Verification

  • crates/m68k/tests/stop_and_68010_timing_tests.rs: 10 regression tests
    (MOVES per-mode totals both directions, MOVE from CCR, RTE 24, dispatch
    44/46, STOP verbatim-SR/privilege/trace/loaded-T semantics and cycles).
  • SST STOP.json.bin passes locally (M68K_SST_FIXTURES).
  • 14-demo byte-identity vs the m68k: align per-instruction IPL poll points and interrupt dispatch idle placement with the 68000 microcode #144 base: 11/14 IDENTICAL. eon, SOTA and
    Second Nature shift - isolated to the stopped-CPU wake-timing commit
    (the m68k-only commits keep full identity): these demos trackload
    through Kickstart, whose Exec idle loop STOPs between disk interrupts,
    and each wake now lands at the device event instead of up to a
    fast-forward quantum late. Verified benign: identical scenes with a
    sub-second timeline shift (SOTA's credits screen appears at 30s on the
    branch vs 31s on the base; eon/Second Nature differ by 1.6-2.2% of
    pixels). Same legitimate-shift class as this base's own dispatch-order
    change.
  • Full 535-case CPU/Timing+Timing2 sweep vs the main baseline (measured
    pre-rebase): zero regressions > 1pp outside the target buckets; most of
    the 68010 landscape improves.
  • cargo test --lib (1324), crates/m68k per-file suites, clippy
    --all-targets --all-features -D warnings, cargo fmt --check: clean.
    (The m68k SST/musashi fixture test bins fail to compile locally on
    clean main too - known baseline; CI runs SST on the PR.)

LinuxJedi added 8 commits July 6, 2026 23:48
The 68000 interrupt microcode spends 6 idle clocks before the PC-low
frame write, runs the 4-clock interrupt-acknowledge bus cycle next, and
spends 4 more internal clocks before the SR and PC-high writes (Moira
execInterrupt, same sequence as yacht). The 68010 spends 12 leading
idle clocks with the IACK at their end and writes its format-0 frame
in the order PC low, SR, PC high, vector word, for 46 clocks total.

The core previously ran the IACK and all frame writes back to back and
paid every idle clock after the handler-entry prefetch, so the
handler's first instruction started ~14 clocks early on every dispatch
- visible as a uniform early shift of all interrupt-handler activity
in the vAmigaTS CPU/IPL family (446 cases) against the vAmiga
reference.

The idle periods are now billed in place through the pending-sync
mechanism, landing each frame write, the IACK, and the vector fetch at
their hardware bus-time offsets; the dispatch totals stay 44 (68000)
and 46 (68010) clocks.
The 68000/68010 sample their IPL pins at ONE microcode-determined
point per instruction and the boundary interrupt decision consumes
that sample (Moira's POLL placement; pasti 68kPrefetch). The host
samples at the start of every bus access and by default consumes the
LAST access's sample, which is correct for the majority class whose
poll rides the final prefetch, but wrong wherever the poll point is
not the last access: under bus contention those instructions
recognized (or missed) an interrupt one boundary off.

Mechanism: AddressBus::ipl_hold_sample marks the instruction's poll
point right after the access that carries it; the host pins that
sample until the boundary decision consumes it. Exception dispatch
releases a pending hold (ipl_release_sample) because the vector jump's
handler-entry refill is a fresh poll point.

68000 placements (68010 where the microcode is shared):
- RMW instructions poll during the final prefetch that precedes the
  writeback: ANDI/ORI/EORI, ADDQ/SUBQ, CLR/NEG/NEGX/NOT, BCHG/BSET/
  BCLR, NBCD, Scc, AND/OR/EOR/ADD/SUB Dn,<ea>, memory shifts, and
  MOVE to -(An). ADDI/SUBI and MOVE from SR keep polling during the
  writeback itself (their microcode differs).
- CMPM, ABCD/SBCD and byte/word ADDX/SUBX -(Ay),-(Ax) poll at the
  start of the destination read, between the two operand reads
  (CMPM 68000 only; the 68010 polls at the final prefetch).
  ADDX/SUBX.L poll at the low-word write of the interleaved writeback.
- MOVEP memory-to-register polls at the final byte read.
- MOVE to (An)+ polls during the destination write for every source
  and size; MOVE.L with a register or immediate source to (An),
  d16(An) or d8(An,Xn) polls before the write's low word (the write is
  split into its two word cycles to keep the per-word sample).
- LINK polls right before the An push; UNLK before the low word of
  its stack read; PEA polls on the prefetch that precedes the push,
  except absolute modes which push FIRST and prefetch last (order
  fix).
- TAS runs read, 2 internal clocks, write, then the final prefetch
  (the indivisible RMW cycle precedes the poll-carrying prefetch),
  instead of borrowing the ordinary RMW prefetch-before-write order.
- DIVU/DIVS issue the final prefetch BEFORE the division's internal
  clocks (the poll rides it, so an interrupt rising during the
  division is taken one instruction later); division by zero spends
  8 internal clocks before the exception frame.
- The taken 68000 TRAPV performs its dummy program-space read before
  the exception frame; the trace exception spends 4 internal clocks
  before its first stack write.

The vAmigaTS CPU/IPL family (446 cases, each raising interrupts on a
2-cck grid across every instruction boundary) drops with this and the
dispatch-idle fix combined; per-case scores also carry a chaotic
mainloop-phase component, so family sums are the meaningful metric.
The TAS arbitration test gains the read-to-write internal cck.
MOVES billed a flat 4 clocks: the extension word, the mode-dependent
internal period, and the data cycle all ran unbilled, so every MOVES
finished 6-8 clocks early and shifted all later bus activity (the
vAmigaTS CPU/IPL/MOVES cases, whose whole screen is a CPU-phase
pattern, sat at 43-48% divergence). The 68010 spends 6 internal
clocks between the EA calculation and the data cycle for (An), -(An)
and d8(An,Xn), 8 for (An)+, and 4 for d16(An) and the absolute modes
(Moira execMoves), then performs the access and the final prefetch,
which carries the IPL poll.

68010 RTE re-read the format/vector word it had already probed,
spending an extra bus cycle and returning 20 clocks: the hardware
reads format word, SR and PC (four reads), discards the frame by
adjusting SP, and refills - 24 clocks.
…ory write)

68010 MOVE from CCR calibrated against the vAmigaTS CPU/Timing2/MOVECCR
measurements (Moira execMoveCcrRg/execMoveCcrEa): a register destination
costs only the final prefetch (4 clocks, was 6); a memory destination
spends per-mode internal clocks ((An) 2, (An)+ 4, -(An) 2), computes the
EA, prefetches, and only THEN writes ((An) = 10 clocks total, was 8 with
the write before the prefetch).

ea_calc_cycles() is a shared prefetch-model EA-calculation cost table
(extension-word fetches plus the internal clocks resolve_ea bills) used
by this path and the MOVES billed total.
The billed MOVES total missed the 2 internal clocks the predecrement and
brief-extension-word address calculations spend inside resolve_ea (Moira
computeEA SYNC(2); the placement was already right, only the returned
total under-counted). Route the EA cost through ea_calc_cycles so the
-(An) byte/word total is 20 clocks and d8(An,Xn) is 24, matching the
vAmigaTS CPU/Timing2/MOVES measurements.
…oundary

STOP semantics on the 68000-040, pinned by the real-chip-validated SST
m68000 single-step fixtures and the vAmigaTS CPU/Timing/STOP family:

- The SR operand is loaded VERBATIM: a single-stepped STOP observes S
  and T exactly as written (STOP.json.bin passes locally against
  M68K_SST_FIXTURES).
- An S-clear SR stops only momentarily: the stopped state's supervisor
  check - run at every instruction boundary on all three step paths -
  raises a privilege violation 4 internal clocks later, stacking the
  STOP instruction itself so the handler's RTE re-executes it (4 + 38
  clocks to the handler on a 68000). Previously the core simply stopped
  with a user-mode SR and never recovered.
- A trace pending when STOP executes (T set in the SR the instruction
  started with) has priority over both the stopped state and its
  supervisor check: the trace exception recovers from the stop. A T bit
  loaded BY the STOP does not fire while stopped.

The 68060 keeps its distinct immediate-privilege model (SR unchanged,
PC past the operand).

stop_and_68010_timing_tests.rs pins these plus the 68010 cycle totals
this branch and its base calibrate (MOVES both directions per EA mode,
MOVE from CCR, format-0 RTE, the 44/46-clock interrupt dispatch).
The stopped-CPU idle fast-forward bounded its horizon to the Copper's
WAIT position, but treated a running (non-waiting) Copper as
event-free: a Copper MOVE to INTREQ right after a WAIT completed was
overshot by whole fast-forward slices, waking the CPU up to 72 lines
late (vAmigaTS stop1 never visibly woke).

next_copper_wakeup_cck now returns a two-colour-clock horizon while the
Copper is fetching instructions (a halted Copper still imposes none),
and a pure idle single step advances one colour clock instead of a
whole instruction's worth of time: a STOPped 68000 samples its IPL pins
every bus-cycle period, so that is the hardware wake-up quantum.
@LinuxJedi LinuxJedi force-pushed the feature/cpu-timing-calibration branch from 9d8435a to 18ec267 Compare July 6, 2026 23:30
@LinuxJedi LinuxJedi merged commit 7cf1d0e into main Jul 7, 2026
7 checks passed
@LinuxJedi LinuxJedi deleted the feature/cpu-timing-calibration branch July 7, 2026 05:44
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