Skip to content

Fix DD/FD EX AF,AF' timing and the R count of CB instructions - #42

Merged
davidly merged 1 commit into
davidly:mainfrom
gloveboxes:fix/z80-prefixed-ex-af-timing-and-r-count
Nov 11, 2026
Merged

davidly merged 1 commit into
davidly:mainfrom
gloveboxes:fix/z80-prefixed-ex-af-timing-and-r-count

Conversation

@gloveboxes

Copy link
Copy Markdown
Contributor

Three fixes to the Z80 engine in x80.cxx, found by stepping every Z80 encoding through ntvcm and a second copy of the engine and comparing the results.

1. DD/FD EX AF,AF' counted 4 T-states instead of 8

z80_cycles held 4 for opcode 08h and z80_emulate returned 0 so the main loop wouldn't count it twice. z80_execute_unprefixed adds the prefix's 4 to what z80_emulate returns, so the prefixed form got 4 + 0.

The table entry is now 0, like the other opcodes z80_emulate times itself (DJNZ, JR, EXX, ED), and z80_emulate returns its default 4. Unprefixed EX AF,AF' is still 4.

2. R counted the wrong number of fetches for CB instructions

Only visible with TRACK_Z80_R_REGISTER on; the default build is unchanged.

A Z80 increments R twice for a CB instruction and twice for DD CB d / FD CB d (the displacement and final opcode aren't M1 cycles). Measured as LD R,A / instruction / LD A,R:

Instruction Before After
CB 00 +1 +2
DD CB 05 06 +3 +2
FD CB 05 46 +3 +2

NOP (+1), ED 44 and DD 23 (+2) and DD DD 23 (+3) are as before.

3. The repeated-prefix loop never ended on memory holding only prefixes

The loop that consumes repeated DD/FD prefixes now gives up after 65,536 of them and reports the instruction as not implemented, as prefixed HALT does. No test covers this limit.

Tests

tests/cyclecheck.py gains cases for:

  • a prefix on instructions that ignore it (EX AF,AF', NOP, LD BC,nn, JR, EXX, ED NEG);
  • repeated and mixed prefixes;
  • INIR, INDR, OTIR and OTDR;
  • CPIR and CPDR that match on the first byte.

Without this change it fails the two EX AF,AF' cases; with it everything passes.

Run on macOS (clang, -O3): ZEXALL and ZEXDOC (67 tests each, no errors), the exercisers in tests/ra.sh, flagcheck.py and aluflagcheck.py all pass. zexsup/zexusup were not run (no cpm_compilers checkout), and I did not build with Watcom or MSVC.

EX AF,AF' after a DD or FD prefix counted 4 T-states. It takes 8: the
prefix's fetch plus the instruction's own 4. z80_cycles held 4 for
opcode 08h and z80_emulate returned 0 so the main loop wouldn't count it
twice, but z80_execute_unprefixed adds the prefix's 4 to what
z80_emulate returns, so the prefixed form got 4 + 0. The table entry is
now 0, like the other opcodes z80_emulate times itself (DJNZ, JR, EXX,
ED), and z80_emulate returns its default 4. Unprefixed EX AF,AF' is
still 4.

With TRACK_Z80_R_REGISTER on, R counted one fetch for a CB-prefixed
instruction and three for DD CB d and FD CB d. A Z80 increments R twice
for both: for the CB prefix and its opcode, or for the DD/FD and CB
prefixes. The displacement and final opcode of the indexed forms aren't
M1 cycles. The CB handler now bumps R, and the DD/FD CB path no longer
bumps it a third time. Measured as LD R,A / instruction / LD A,R with
the switch on:

    CB 00          +2 (was +1)
    DD CB 05 06    +2 (was +3)
    FD CB 05 46    +2 (was +3)

NOP (+1), ED 44 and DD 23 (+2) and DD DD 23 (+3) are as before. The
default build doesn't track R, so it is unchanged.

The loop that consumes repeated DD and FD prefixes never ended when
memory held nothing but prefixes. It now gives up after 65,536 of them
and reports the instruction as not implemented, as prefixed HALT does.
No test covers that limit.

cyclecheck.py gains cases for a prefix on instructions that ignore it
(EX AF,AF', NOP, LD BC,nn, JR, EXX, ED NEG), repeated and mixed
prefixes, INIR, INDR, OTIR and OTDR, and CPIR and CPDR that match on
the first byte. Without this change it fails the two EX AF,AF' cases;
with it everything passes. ZEXALL, ZEXDOC, the exercisers in ra.sh,
flagcheck.py and aluflagcheck.py still pass.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
@davidly
davidly merged commit fdb53ab into davidly:main Nov 11, 2026
1 check passed
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.

2 participants