MrKWatkins/ZXSpectrumNextTests — test inventory
The main community acceptance suite for Spectrum Next emulators and FPGA cores: .nex/.tap programs with reference screenshots from real hardware. Statuses flip from planned to live results as each test is wired into the headless harness (ZX Play work item #137). Upstream: https://github.com/MrKWatkins/ZXSpectrumNextTests.
base — CPU and register fundamentals
| Test | Status | Notes |
|---|---|---|
| Z80N | pass | All 23 Z80N instructions OK with green border (matches the real-hardware photo). First run caught LDDX/LDDRX direction and LDWS flags — fixed in pkg/z80 |
| Z80Nc2 | pass | Core-2 additions (barrel shifts, JP (C)) all OK; caught the JP (C) I/O-jump semantics bug — fixed in pkg/z80 |
| NextReg_defaults | pass | Full 256-cell verdict grid asserted (attribute decode). Matches the core-3.1.5 board photo except: 7 cells the board shows yellow from NextZXOS boot side-effects (OS-less harness earns strict green) and NR$0A/NR$10 red on core 3.2.x semantics (we follow master zxnext.vhd + MAME 0.282; test targets 3.1.5). Audit fixed: copper NR$61-$63 byte-granular cursor, NR$8E paging-lock bypass, NR$7F/$82-$89 reset defaults, ULA-first classic palette default |
| Copper | pass | Full visible surface asserted: both self-reported counters 03F3 (board + MAME), all five Swedish flags incl. the over-left-border one, the horizontal-wait >= probe's single yellow pixel, the Z80-animated line with its left-border segment one row lower, rulers and background. Drove the cycle-paced copper (MOVE 2 / NOOP 1 cycles at 28MHz, mode-transition-only list restart) and the live-palette ULA/border render (ULANext decode). Residual precision: palette sampled once per 7MHz pixel, so half-pixel flag detail collapses to one colour per pixel |
| DMA | pass | Full verdict surface asserted: all 24 A->B/B->A mode cells (IO rows yellow), short-init 4+4+1 and CONTINUE 4+4 areas, the 16-byte read-back stream (adjudicated against dma.vhd — the board photo predates the hex display and MAME 0.281's read-back is non-conformant), the auto-restart prescaler burst fill and the IM2 CPU-speed stepping. Fixed: $BF read-status, read-state machine, $83 disable, burst auto-restart, monotonic burst clock, turbo-scaled prescaler delay, harness DMA IO-bus wiring |
Graphics — Layer 2 and layer mixing
| Test | Status | Notes |
|---|---|---|
| Layer2Colours | pass | All 24 sprite/L2/ULA combinations in all six NR$15 orders: the 6-band 4x6 verdict grid pixel-pinned (cross-checked against the upstream SLU table + MAME 0.282), raster-banded border asserted. Drove the port $123B shadow-PAGING fix, the NR$1E/$1F cvc raster counter and the raster-stamped NR$15 replay |
| Layer2Port | pass | Write/read-over-ROM windows (visible + shadow banks), IM1-in-L2, and the core-3.0.7+ bank-offset form: green border + full verdict surface scanned (no red pixel anywhere). Drove the composed $123B read-back (zxnext.vhd:3933) |
| Layer2Scroll | pass | NR$16/$17 scroll to [196,133] + NR$18/$1A clips: self-reported register sheet OCR'd, ruler ULA-tick/L2-dot alignment pixel-pinned, clip margins show the fallback |
| LayersMixingHiCol | pass | The mixing matrix with Timex HI-COLOUR as the ULA layer (8x1 attributes, zxula.vhd:238): shared verdict grid + the per-pixel-row black banner as the mode witness |
| LayersMixingHiRes | pass | The matrix with Timex HI-RES 512x192: composited at native half-pixel granularity (ComposeHiResScanline), 640x240 frame, synthesized attribute border (index 130 — the core-2.00.25+ rule the upstream ReadMe documents), sub-pixel checker transitions asserted |
| LayersMixingLoRes | pass | The matrix with LORES as the ULA layer: wired the orphaned pkg/next/lores into the live render (NR$15 bit 7 per-pixel replace, zxnext.vhd:6980); L2-drawn header text over LoRes white as the mode witness |
| LightenDarken_L2_ULA | pass | Additive blend modes 6/7 through the FPGA-golden Mix per pixel: gradient sums/differences pixel-pinned per band incl. the clamps, and transparent-L2 cells show the fallback (upstream table sTlTuu->TT). The catalogued approximated-as-SLU gap row CLOSED as a fix |
| NextReg0x69 | pass | NR$69 as a composed live alias of $123B/$7FFD/port $FF: the 10 read/write cross-checks self-report 10/10, and the copper switches the display source per scanline band (L2 / shadow / scr1 / hi-colour / two hi-res rows / combined) — every band's verdict cells pixel-pinned. Runner pokes NR$12=9, the NextZXOS launch state the test assumes. Mode-switch settle artefacts not modelled (upstream marks them core-version-specific); mixed-frame hi-res text bands render decimated (known-gaps.md) |
Sprites
| Test | Status | Notes |
|---|---|---|
| Transparency | pass | White transparent squares + green opaque squares pixel-pinned (board core 2.00.26). Drove the faithful NR$4B transparency: the ENGINE compares the raw pattern value (8bpp byte / 4bpp nibble, sprites.vhd:971), replacing the compositor's index-based approximation |
| Relative | pass | Composite relatives: positions, palette-offset relativity + wrap, relative pattern names, scales, invisibility, over-border dot — all pixel-pinned; whole image scanned for forbidden red/violet/stray-green. Also DISPROVED the catalogued '8 rows lower' sprite offset (sprite fill exactly inside its ULA-drawn outline, same relation as the MAME capture) — gap row deleted |
| BigSprite | pass | Eight anchor mirror/rotate combinations transform the 10-sprite body rigidly: every arm's colour bounding box asserted per group; invisible ninth big sprite + red sub-sprite absent; violet over-border sprite present (board core 2.00.27) |
| BigSprite4b | pass | 4bpp big sprites: relative patterns crossing the N6 half boundary, palette-offset saturation. Per-group pixel count / bbox / 32-colour set / orientation samples asserted; border pattern sprites + red square (board core 3.1.5) |
| ScanlineDelay | pass | Copper-timed one-scanline sprite effects (visibility, rotate+move, NR$4B, palette — core 3.0.7+ WAIT_H=48 build) land on exactly the target line; 4/5-byte records 'ooo1'/'o1' and the NR$09 lockstep exercise '11' asserted. Drove the dual NR$34/IO sprite indexes + lockstep tie, and the per-line render budget now models the bandwidth limit ($303B bit 1) — gap row closed |
ULA — palettes, transparency, scroll
| Test | Status | Notes |
|---|---|---|
| DefaultTransparency | pass | Classic bright-magenta paper stays OPAQUE over Layer 2: whole paper + all four border regions asserted per pixel. Adjudicated the boot ULA palette's bright magenta to %111'001'111 ($E7 projection, dodging NR$14=$E3) — board photo + MAME 0.282 + CSpect 2.11.1 + ZEsarUX 8.0 all agree (255,36,255); fixed classicRGB333 |
| BorderTransparencyFallback | pass | All three key-driven phases asserted whole-surface (border + both paper halves): ULANext entry-128=$AA=NR$14 fallback in border+paper, ink-mask 255 background fallback, and Enhanced-ULA-off immunity. ZEsarUX 8.0 is the good reference (upstream marks the CSpect capture BAD) |
| ChangePaletteTransparency | pass | Every paper pixel asserted = Layer 2 identity-palette column (paper-7 entry 135 redefined to $E3), column 227 = NR$4A fallback cyan (L2's own $E3), border cyan via the transparent entry 135 |
| ChangePaletteTransparency_v2 | pass | Ink-mask 15 variant: paper-3 entry 131 transparent, border resolves entry 135 redefined to green %000'100'00 — full paper + border asserted per pixel |
| ChangePaletteTransparency_v3 | pass | ULANext OFF: redefining entry 128+7 must NOT affect the classic decode (white paper+border asserted whole-surface); the rainbow text via transparent INK-0 entry 0 asserted = Layer 2 columns |
| ClassicPaletized | pass | Classic screen through the Next ULA palettes with CPU raster timing: the mid-screen NR$43 displayed-palette flip (raster-stamped replay, landing row asserted ~95), attr cells through both palettes (incl. the $E7 bright magenta), and the full deduped left-border rainbow sequence — classic colours through palette 1, custom gradient + 'green/light blue' edges through palette 2. Closed the ULA-palette-colour gap row |
| UlaScroll | pass | NR$26/$27 ULA hardware scroll (new wiring + render per zxula.vhd:192-208) cross-checked against live register read-back after interactive OPQA scrolling; NR$1A clip window [8,8]->[239,175] hides the marker rows (zxula.vhd:562, border exempt); NR$69 bit 6 shadow-display (bank 7 highlight switch) asserted. Timex 512/8x1 M-modes not asserted (classic Timex render path — see known-gaps) |
Interrupts
| Test | Status | Notes |
|---|---|---|
| HaltAfterDisable | pass | DI + HALT must hang forever; asserted via the border-colour signature |
Timing
| Test | Status | Notes |
|---|---|---|
| Changing8kBank | pass | MMU bank-switch timing off the frame INT: 2048 Z80N NEXTREG r,n (20T) render a green border band ending at image row 151 — pinned by instruction/frame-INT arithmetic (border-change stamps at raster 4 and 191) and matching MAME 0.282's proportion, the only upstream reference (no board photo). zxplay_go's fixed 128K-flavour raster (228T lines under 48K personality) is ~2% short of the FPGA's 48K timing — known-gaps.md |
| Changing8kBank_NoContention | pass | NR$08 bit 6 contention-OFF variant: renders identically to the ON variant (equality asserted), matching MAME 0.282's identical capture pair. Machine-wide per-access contention is a catalogued deferral (known-gaps.md) — on the FPGA the ON band would be slightly taller; no board photo exists to pin that delta |
| ScanlineReadingAndInterrupt | pass | All three modes driven and pixel-pinned: NR$1F busy-wait markers land on their cvc row (incl. the 8-bit double trigger echoing into the displayed top border), the NR$22/$23 line interrupt's cyan band, and the copper WAIT band (paper + right border, left border clean). Drove the raster-stamped palette-CONTENT replay (palette.Bank write log; zxnext.vhd:4919-4930). READ/IRQ markers carry the documented one-line granularity (upstream ReadMe: marker starts 'somewhat midway'); NR$64 line offset is a catalogued no-op |
Misc — DMA deep dives
| Test | Status | Notes |
|---|---|---|
| ZilogDMA | pass | Zilog-DMA-mode behaviour through port $0B: the -1 counter seed (length N moves N+1 bytes), LOAD-latched source/destination roles surviving a direction flip, CONTINUE short-inits and the 4T/byte border timing bands — pinned to the core-3.1.5 board photo. Drove wiring port $0B (the dma_mode latch), the LOAD-latched src/dst pointer model and the harness Next's narrow frame-INT pulse |
| DmaInteractive | manual | Interactive; includes real Zilog hardware photos — manual comparison rather than CI. Status re-confirmed 2026-07-13 with a hand-run: boots, full UI + byte-stream parser work (commit/LOAD/FORCE-RDY/ENABLE all recognized), but it is a free-form WR-register editor with grey-+2 Zilog photos as ground truth — no self-verdicting surface to pin. The run surfaced one real finding — its DEFAULT port $0B (Zilog-compatibility decode) was unclaimed in zxplay_go — since closed: port $0B is wired and covered by the Misc/ZilogDMA runner |
ZX48 / ZX128 classic tests
| Test | Status | Notes |
|---|---|---|
| Z80BlockInstructionFlags | pass | Interrupted-block flags (David Banks) — matches real hardware (blockRepeatFlags); hard regression guard |
| Z80CcfScfOutcomeStability | pass | SCF/CCF outcomes must be deterministic frame over frame |
| Z80IntSkip (basics) | pass | NOP benchmark, DI inhibition, SCF/CCF chains, IFF2 reads during int-ack |
| Z80IntSkip (prefix/EI inhibition) | pass | Prefix/EI interrupt inhibition + level-triggered /INT pulse re-entry — conformant via the classic narrow-pulse frame INT; hard regression guard |
| ULAvsSJS | pass | Keyboard/Sinclair-joystick port mixing (not a timing test — the old note was wrong): $00FE whole-matrix reads must include every zero bit read on the $F7FE/$EFFE half-rows. Keys driven within and across half-rows; the live attribute cells, the hidden 'difference detected' message and the cyan border are asserted — the grey-+2 SJS quirk must never appear |