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

The Tile Fetcher

The BG/window tile fetcher is a 3-bit counter, a drain-detect pipeline, and a combinational VRAM address generator. It runs continuously through Mode 3, producing one tile (two bitplane bytes) every 8 dots, in parallel with pixel shifting.

At a glance

  • A 3-bit ripple counter steps through six states (0–5) — one tile fetch = 3 VRAM accesses × 2 dots — then holds at 5 until the NYXU reset pulse restarts it.
  • The full tile cadence is 8 dots: 6 counting plus 2 while the drain cascade (NYKA → PORY → RENE/RYFA → SEKO) empties and retriggers NYXU.
  • NYXU fires 22 times per scanline at SCX=0, 23 at SCX&7 ∈ {1..7}.
  • LCDC.3/4/6 reach the VRAM address bus through level-sensitive paths — no clocked stage — and TILE_SEL is sampled twice per fetch.

The fetch counter

The counter advances through six states (0–5) to drive one tile fetch, then holds at 5 until the NYXU reset pulse restarts it for the next tile, window handoff, or Mode 3 startup. (The downstream drain-detect pipeline that closes each tile is tabulated in the next section.)

GateRoleTypeClock / TriggerNotes
LEBOFetch-counter gated clocknand2Inputs: MOCE, ALETWhen MOCE=0 (terminal count), LEBO freezes at 1; the counter stops
LAXUFetch counter bit 0dffrLEBOAsync reset by NYXU
MESUFetch counter bit 1dffrLAXU (~Q ripple)Async reset by NYXU
NYVAFetch counter bit 2dffrMESU (~Q ripple)Async reset by NYXU
MOCESelf-stop conditionnand3Inputs: LAXU, NYVA, NYXUDrives LEBO low at count 5 when NYXU=1
NYXUBG fetch counter resetnor3Inputs: MOSU, TEVO, AVAPAsync reset; also forces MOCE=1, re-enabling LEBO
LEBO (nand2) = NAND(MOCE, ALET) — gated clock, stops when MOCE=0
├── LAXU (dffr) [clk=LEBO] — bit 0
│   ├── MESU (dffr) [clk=LAXU.~Q] — bit 1 (ripple)
│   │   └── NYVA (dffr) [clk=MESU.~Q] — bit 2 (ripple)
│   └── data to LYZU
MOCE (nand3) = NAND(LAXU, NYVA, NYXU) — self-stop at count 5 when NYXU=1

Reset and restart. NYXU = NOR3(MOSU, TEVO, AVAP) combines three triggers: window activation (MOSU), tile boundary (TEVO), and OAM-scan completion (AVAP). A single ~0.5-dot NYXU pulse asynchronously resets the counter DFFs and forces MOCE=1, re-enabling LEBO — clear and clock-restart in one operation. At count 5 (LAXU=1, MESU=0, NYVA=1), MOCE freezes LEBO and the counter holds; the six states match the 6 dots of one tile fetch (3 VRAM accesses × 2 dots).

NYXU firings per scanline under normal BG rendering — 22 at SCX=0, 23 at SCX&7 ∈ {1..7}:

SourceCountFires atRole
AVAP1Mode 2→3 transitionStarts Mode 3, before any tile fetch
TAVE → TEVO1+5.996 dots from AVAPCloses tile 0 (the startup fetch)
SEKO → TEVO208-dot boundaries from +13.997 dotsCloses tiles 1–20 (the 20 visible BG tiles)
SEKO → TEVO (tail)0 or 1+173.997 dots, fixedCloses tile 21 at SCX&7 ∈ {1..7} only — suppressed at SCX=0 (the tail drain)

The 167-pixel SACU count per Mode 3 is decoupled from the TEVO firings — SACU runs at CLKPIPE cadence, not fetch cadence.

The 8-dot tile cycle

The counter counts 0→5 over 6 dots, then holds at 5 for 2 dots while the cascade (NYKA → PORY → RENE/RYFA) drains. When SEKO detects the drained state, TEVO fires and resets the counter for the next tile. The counter is a position within the current tile fetch, not an absolute Mode 3 position — fetching and pixel shifting run in parallel, and the per-tile TEVO reset is normal fetcher operation, not a stall.

GateRoleTypeClock / TriggerNotes
LYRY"Fetch complete" combinationalnot_x1Input: MOCEHigh when the counter sits at terminal with no reset — drives NYKA's data input
NYKAFetch-complete capturedffrALETData: LYRY
PORYNYKA pipeline stagedffrMYVOHalf-dot-later capture of NYKA
LYZUFetch counter bit 0 sampledffrALETData: LAXU
PYGOPORY pipeline stagedffrALETFeeds POKY
RENEDrain-detect stage 2dffrALETData: RYFA
RYFADrain-detect stage 1dffrSEGUData: PANY
TEVOTile-boundary / restart triggeror3Inputs: SEKO, SUZU, TAVEDrives NYXU per tile, per window trigger, per startup
SEKOTile-boundary drain detectornor2Inputs: RENE, RYFAFires when the pipeline drains (both 0)
SUZUWindow-activation triggernotInput: TUXYSee Window control
TAVEStartup / window-restart triggernotInput: SUVUSUVU = NAND4(NYKA, PORY, ROMO, mode3)

The 2-dot hold, stage by stage:

  1. The counter reaches 5: MOCE→0 freezes LEBO, and LYRY→1 starts propagating down the NYKA/PORY/PYGO half-dot pipeline; POKY sets; RYFA still holds 1.
  2. Hold-dot 1: RENE captures RYFA=1 — SEKO stays 0.
  3. Hold-dot 2: PANY falls, RYFA captures 0, RENE follows — and once both are 0, SEKO rises, TEVO fires, NYXU resets the counter, and LYRY's fall propagates back down, returning SEKO to 0.

The cascade does not drain during a sprite fetch. When FEPO=1 freezes SEGU at 1 (the pixel clock), RYFA is frozen at its pre-freeze 1 and RENE keeps re-capturing 1 — SEKO never rises, so neither TEVO nor NYXU fires during the sprite window. The counter holds at 5 by the ordinary MOCE self-stop, but the reset that would normally come 2 dots later simply does not arrive until the freeze lifts (sprite pipeline).

Pitfall: the PANY/NUKO slip

PANY's wxy_match input (NUKO) means any NUKO=1 landing inside PANY's natural 1-dot tile-boundary window perturbs the drain pulse, slipping the cascade by one dot. The slip delays the BG-shifter parallel-load unconditionally — this branch is not gated by LCDC.5 or in_window — and additionally delays the window tile-X increment only when in_window=1. NUKO=1 inside the window arises from a mid-Mode-3 WX rewrite or from the natural PX==WX sweep when REJO is armed with WX≡7 (mod 8) — including the armed-but-disabled state (REJO=1, LCDC.5=0), where the BG still slips even though no window renders. Full mechanism and measurements: Window edge cases.

Outside Mode 3 the MOSU/TEVO/AVAP cascade is idle, so NYXU=1 and the counter is not held in reset — it freezes via the self-stop at whatever value was last clocked (typically 5) until the next AVAP. Reset domains:

  • LAXU/MESU/NYVA — reset on every NYXU pulse;
  • PYGO/RENE/RYFA/LYZU — the mode3 reset net (held at 0 outside Mode 3);
  • NYKA/PORY — NAFY (VID_RST plus window-related events).

The fetch cycle and VRAM addressing

Each tile fetch takes 6 dots:

CounterWhat happensVRAM address
0Tilemap fetch: address setTilemap address
1Tilemap data arrives
2Tile data low: address setTile-data address (low byte)
3Tile data low arrives
4Tile data high: address setTile-data address (high byte)
5Tile data high arrives; MOCE stops the clock

The set-address/data-arrives pattern assumes the standard async-SRAM model — VRAM is external, and its access time is not determined by the on-die netlist. The address is driven combinationally from the fetch counter through the VRAM address adder (up to 66.2 ge for the full carry chain, though the high bits settle from the previous line in practice).

LCDC.3/4/6 sampling during the fetch

Three LCDC bits feed the fetcher's VRAM address generation, each through a purely combinational path gated by tri-state drivers that the fetch counter enables at specific stages:

LCDC bitCellWire nameDrivesPath
3 (BG_MAP)XAFOLCDC_BGMAPp~ma10 during BG tilemap readsXAFO → AMUV (not_if0, enable BAFY)
4 (TILE_SEL)WEXULCDC_BGTILEp~ma12 during tile-data readsWEXU → VUZA (nor2 with tile-index bit 7 PYJU) → VURY (not_if1, enable bp_cy)
6 (WIN_MAP)WOKYLCDC_WINMAPp~ma10 during window tilemap readsWOKY → VEVY (not_if0, enable WUKO)

The enables derive combinationally from the fetch counter:

  • BAFY / WUKO (BG vs window tilemap arm) — mutually exclusive, both fire at counter=0.
  • bp_cy (the NETA AND2 output) — active across counters 2–5 (measured 4.000 dots continuous per fetch), so TILE_SEL is read twice per fetch, once per bitplane.
  • PYJU (dffr_cc_q) — captures VRAM data bit 7 at the tile-index stage and feeds VUZA, implementing the signed/unsigned tile-index split.

The sampling cadence:

CounterStageLCDC bits sampled
0Tilemap addressLCDC.3 or LCDC.6 (BAFY/WUKO arbitration)
1Tilemap data— (tile index captured)
2Tile-data low addressLCDC.4
3
4Tile-data high addressLCDC.4 (second sample)
5

Rule: the fetch-address bits are level-sensitive, not edge-captured

There is no clocked stage between an LCDC latch and the VRAM address bus — the address bit at any instant is whatever the latch holds, gated by the tri-state enable. TILE_SEL is sampled twice per fetch, so a mid-fetch change mixes bitplane data from two different tile patterns; BG_MAP and WIN_MAP sample once per fetch, at counter=0.

Mid-fetch writes. A CUPA write landing mid-cycle takes effect by stage: before the sampling stage — normal; during it — the fetched byte is the one the address held when the access window opened; after it — next fetch. For BG_MAP/WIN_MAP this produces tile-aligned 8-pixel divergence; for TILE_SEL, a write landing between counter=2 and counter=4 produces a hybrid fetch — low bitplane from the old tile-data block, high bitplane from the new (the Mealybug m3_lcdc_tile_sel_win_change reference image). Model it by reading each bitplane once, at its own sample stage.

Measured: the LCDC.4 write path

From the Mealybug m3_lcdc_tile_sel_win_change ROM (dmg-sim measurement; 1,019 writes per direction, all ps-identical): the LCDC-write strobe WARU rises at sub-dot 0.507 with a 1.493-dot pulse (the CUPA width); ff40_d4 settles within ~1.4 ge after WARU↑ on a 0→1 write but ~32 ge on a 1→0 write — a measured rise/fall asymmetry of the drlatch cell; ~ma12 follows within a gate delay once bp_cy enables VURY.

LCDC.5 transients and the fetcher arbitration

The window-enable consumer chain enters the tilemap-select arbitration through PYNU only, and the chain PYNU → NOCU → PORE → in_window → AXAD/XEZE → BAFY/WUKO is purely combinational (~3 ge). A mid-Mode-3 LCDC.5 1→0 write therefore switches the fetcher back to BG-tilemap arbitration on the dot the write lands: every subsequent counter=0 stage drives ~ma10 from XAFO.

Measured across a 65-dot LCDC.5↓ gap: all 8 counter=0 stages in the gap drive the BG tilemap region, with VEVY tri-stated (high-Z) and AMUV active (dmg-sim measurement, Mealybug m3_lcdc_win_en_change_multiple ROM) — matching the hardware reference image, which shows BG content in the gap.

For implementors

Sample in_window live at counter=0 of every fetch. A latched fetcher-side window-mode flag that ignores PYNU↓ renders window content where hardware shows BG — there is no fetcher-side window latch in the netlist beyond the cycle-local tile-index capture.

SCX/SCY sampling during the fetch

SCX and SCY both feed the VRAM address adder, but they enter at different fetch stages, so a mid-Mode-3 scroll write splits differently for X and Y:

Scroll inputDrivesStage(s) sampledMid-fetch write effect
SCX, map columntilemap column bitscounter=0 onlytile-aligned 8-px shift (like BG_MAP); no hybrid
SCX, fine scroll (SCX & 7)startup pixel-clock suppressionstartup fine scroll onlyseparate mechanism; never a tile-data address
SCY, map rowtilemap row bitscounter=0 onlytile-aligned 8-px shift (like BG_MAP)
SCY, fine-Ytile-data row offsetcounter=2 and counter=4hybrid Y-fetch (like TILE_SEL): low/high bitplanes from different fine-Y rows

Like the LCDC fetch-address bits, the scroll address bits are level-sensitive, not edge-captured: each stage drives the adder from whatever the SCX/SCY latch holds at that stage's address dot (counter=0, then +2, then +4). Which SCY each of the three stages sees therefore depends on where any mid-fetch write commits. A write landing in the counter=2 → counter=4 gap leaves the map row (counter=0) and the low-bitplane fine-Y (counter=2) on the old SCY while the high-bitplane fine-Y (counter=4) takes the new one — splitting the two bitplanes vertically, the Y-direction analogue of the LCDC.4 hybrid fetch.

Measured: the SCY fetch path

From the Mealybug m3_scy_change ROM (dmg-sim, TIMING=default), which sweeps SCY as a triangle 0,1,2,3,4,3,2,1×3 (24 FF42 writes per line, one BEDY↑ every 8 dots), observed on the external VRAM address bus ma[12:0] — the adder output. Within each 8-dot tile the three address stages land 2 dots apart: counter=0 tilemap (map row = ma[9:5]), counter=2 tile-data low and counter=4 tile-data high (fine-Y = ma[3:1], bitplane = ma[0]). On a scanline where the sweep crosses a tile boundary the map row reads out directly — e.g. LY=44, the counter=0 read of the tile whose ma settles to $18c3 gives map_row=6, which only occurs at SCY=4, so the tilemap adder used SCY=4. That tile's counter=2 read (fineY=0) also used SCY=4, while its counter=4 read (fineY=7) used SCY=3: the SCY write (4→3) committed at BEDY↑+0.08 dots, landing between the two tile-data reads (counter=2 read 1.54 dots before the value settled, counter=4 read 0.44 dots after). The map row and the low bitplane always agree; only the high bitplane catches the intervening write.