Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Sprite Storage and X Matching

Fetched sprite pixels live in two dedicated 8-bit shift registers whose load path implements sprite-to-sprite priority, and a bank of SACU-clocked comparators decides — pixel by pixel — when a sprite fetch must fire.

At a glance

  • Two 8-bit planes with per-stage transparency-gated parallel load: priority is implemented in the register — the first-fetched (lowest-OAM-index) sprite's opaque pixels survive.
  • Stage-0 cells have d = const0 and the shifters have no reset domain: content enters only via load and persists across LCD-off, unobservably.
  • Ten combinational per-slot comparators (stored X vs PX) reduce through FEFY/FOVE to FEPO — the CLKPIPE freeze that is the sprite penalty mechanism.
  • The store holds NOT(X): the reset state decodes as X = 0xFF — unreachable — which is why a reset slot never fires and a written X=0 does.
  • The VEZO→VAVA mask pipe carries BG-over-OBJ priority (OAM attribute bit 7), not validity.

Sprite pixel shift registers

Gate familyRoleTypeClock / TriggerCountNotes
Sprite shifter plane ASprite tile data plane 0dffsrSACU8NYLU–VUPY range
Sprite shifter plane BSprite tile data plane 1dffsrSACU8NURO–WUFY range
Sprite temp AVRAM bus capture, plane 0dlatch_ee_qEnable: latch_sp_bp_a8PEFO, ROKA, MYTU, RAMU, SELE, SUTO, RAMA, RYDU
Sprite temp BVRAM bus capture, plane 1dlatch_ee_qEnable: latch_sp_bp_b8REWO, PEBA, MOFO, PUDU, SAJA, SUNY, SEMO, SEGA
Per-stage load gateOR3 per stageor3combinational8MEFU, MEVE, MYZO, RUDA, VOTO, VYSA, TORY, WOPE = OR3(XEFY, sprite_px_aN, sprite_px_bN)
Per-stage load enablesprite_onN drivernotper-stage OR38LESY, LOTA, LYKU, ROBY, TYTA, TYCO, SOKA, XOVU

The 16 shifter cells: LEFE, LESU, MASO, NATY, NURO, NYLU, PEFU, PYJO, VAFO, VANU, VARE, VUPY, WEBA, WORA, WUFY, WYHO. Chain order from dffsr d-pin connectivity:

  • Plane A: NYLU (stage 0, d = const0) → PEFU → NATY → PYJO → VARE → WEBA → VANU → VUPY (stage 7) — outputs sprite_px_a[0..7].
  • Plane B: NURO (stage 0, d = const0) → MASO → LEFE → LESU → WYHO → WORA → VAFO → WUFY (stage 7) — outputs sprite_px_b[0..7].

Stage-7 outputs feed the pixel mux (LCD output). Stage-0 cells have d = const0 — content arrives only via parallel load.

The transparency-gated parallel load

Unlike the BG shifter's single broadcast LOZE, each sprite stage gates its own load: sprite_onN = NOR3(XEFY, sprite_px_aN, sprite_px_bN), with XEFY = NOT(WUTY) the fetch-done pulse.

During a fetch (WUTY=0, XEFY=1) every stage's load is disabled — and SACU is frozen anyway. At fetch completion, WUTY pulses, XEFY drops, and sprite_onN = NOR2 of the stage's current bits: the load fires only where the current content is transparent (both planes 0). Opaque stages keep their content.

Rule: priority lives in the load path

The per-stage transparency gate is sprite-to-sprite priority: the first-fetched sprite's opaque pixels survive, and only still-transparent positions accept the second sprite's data. Combined with OAM-index fetch order, this yields the documented DMG rule — lower OAM index wins at the same X.

AspectBG shifterSprite shifter
Load enableBroadcast LOZE = NOT(NYXU)Per-stage sprite_onN
TimingAVAP / TEVO pulsesWUTY pulse per fetch
ScopeAll 8 stages, unconditionalTransparent stages only
RoleReplace tileOverlay sprite

A broadcast load would lose overlap semantics; a transparency-gated BG load would under-load tiles. The asymmetry is load-bearing.

The shifters have no reset domain — set/reset comes only from the load path, so content persists across LCD-off. Residual content is unobservable: VAVA gates sprite output at the mux so only active-sprite pixels reach the LCD, and no fetch has loaded data until the first X-match of a scanline.

Sprite X match

Matching is fully combinational from the store latches and the PX counter:

stored bits ⊕ NOT(PX) → two NOR4s (per nibble) → NAND3 with AROR (the LCDC.1 gate) → per-slot active-low match → FEFY/FOVE → FEPO

The ten per-slot matches reduce through FEFY/FOVE to FEPO; FEPO=1 → VYBO=0 → CLKPIPE freezes — the sprite penalty mechanism.

Gate familyRoleTypeClockCountNotes
Per-slot X storeHolds NOT(X) — captures the inverted OAM X byte on the slot's save strobedrlatch_eeEnable: per-slot save strobe8 × 10Slot 0: XEPE, YLAH, ZOLA, ZULU, WELO, XUNY, WOTE, XAKO; save strobe FUXU (store-counter = 0 decode); r_n = per-slot reset
Per-slot comparatorXOR per bit vs NOT(PX), NOR4 per nibblexor + nor4combinational10 × 10Slot 0: ZOGY, ZEBA, ZAHA, ZOKY / WOJU, YFUN, WYZA, YPUK → ZAKO / XEBA
Per-slot matchNAND3(AROR, low nibble, high nibble), active-lownand3combinational10FEFY group: YDUG (slot 0), DYDU, DEGO, YLOZ, XAGE; FOVE group: EGOM, YBEZ, DYKA, EFYL, YGEM
FEFY / FOVEMatch aggregates (slots 0–4 / 5–9)nand52
FEPOSprite X priority aggregateor2FEFY, FOVE1Feeds VYBO — the CLKPIPE freeze (the pixel clock)
Per-slot resetOR2(line reset, slot fetch-done)or2 + dffr10Slot 0: DYWE = OR2(DYBA, EBOJ); EBOJ = DFF(d = GUVA, clk = WUTY) — sets when slot 0's fetch completes
Sprite attribute pipesPer-pixel palette select + BG-over-OBJ prioritydffsrCLKPIPE16LYME (output: sprite_px_palette), MODA, NUKE, PALU, ROSA, RUGO, SATA, SOMY, VOSA, VUMO, WODA, WURU, WYFU, XETE + VEZO → VAVA
VAVA / VEZOPriority mask pipe MSB / LSBdffsrCLKPIPE2Load from DEPO (OAM attribute bit 7)

The sixteen CLKPIPE-clocked dffsr cells beside the pixel planes are attribute pipes — per-pixel palette-select and priority shift registers loaded at fetch time — not match logic. Match state is never clocked; it follows the store latches and PX combinationally.

The mask pipe carries priority, not validity. VEZO→VAVA holds the per-pixel BG-over-OBJ flag loaded from DEPO (OAM attribute bit 7). VAVA gates BG visibility at RYFU = AND2(RAJY, VAVA) in the output path: VAVA=0 — sprite priority, BG suppressed; VAVA=1 — BG shows through. (Fine scroll is clock suppression, not mask-pipe work.)

Why a reset slot cannot fire

The store holds the X coordinate inverted, and the comparator compares against the inverted PX bits — so a written slot matches at PX = X, and the all-zeros reset state decodes as X = 0xFF, a coordinate the PX counter (terminal count 167) never reaches. There is no hidden armed flag: the "armed" state is the latch contents themselves.

  • A written X=0 stores 0xFF internally and fires at PX = 0.
  • A reset-held slot stores 0x00 ≡ X = 0xFF and can never fire.
  • The same mechanism stops refiring after a fetch: EBOJ (the slot's fetch-done flag) sets at WUTY and holds the slot reset, clearing its X back to the unreachable state for the rest of the line.

Measured (dmg-sim, a locally patched 1-sprite variant of gambatte's 10spritesPrLine_10xposA7, sprite at X = 0xA7): slot 0's store reads 0x00 while reset-held and 0x58 = NOT(0xA7) between save and reset (22/22 lines); YDUG fires at exactly PX = 0xA7 (7/7); DYDU — slot 1, reset-held at 0x00 — never fires across the capture even though PX passes 0 every line.