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), then holds at 5 until the NYXU reset pulse restarts it. One tile fetch = 3 VRAM accesses × 2 dots.
  • 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. It 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. 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, and RYFA still holds 1.
  2. Hold-dot 1: RENE captures RYFA=1 — SEKO stays 0.
  3. Hold-dot 2: PANY falls, RYFA captures 0, and RENE follows. Once RYFA and RENE are both 0, SEKO rises, TEVO fires, and NYXU resets the counter. LYRY's fall then 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 therefore never rises, so neither TEVO nor NYXU fires during the sprite window. The counter holds at 5 by the ordinary MOCE self-stop. The reset that would normally come 2 dots later does not arrive until the freeze lifts (sprite pipeline).

Pitfall: the PANY/NUKO slip

PANY takes wxy_match (NUKO) as an input. 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; that branch is not gated by LCDC.5 or in_window. The slip also delays the window tile-X increment, but only when in_window=1. NUKO=1 inside the PANY 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). The sweep case includes 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. The counter instead freezes via the self-stop at whatever value was last clocked (typically 5), and holds there 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, so 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. The full carry chain takes up to 66.2 ge, though in practice the high bits settle from the previous line.

LCDC.3/4/6 sampling during the fetch

Three LCDC bits feed the fetcher's VRAM address generation. Each path is purely combinational, 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. TILE_SEL is therefore 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. A write before the sampling stage takes effect normally. A write during the sampling stage leaves the fetched byte as the one the address held when the access window opened. A write after the sampling stage takes effect on the next fetch. For BG_MAP and 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. The low bitplane comes from the old tile-data block, and the high bitplane from the new (the Mealybug m3_lcdc_tile_sel_win_change reference image). Model the hybrid fetch by reading each bitplane once, at its own sample stage.

Measured: the LCDC.4 write path

All values here come 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. That gap is 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. That 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 then 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). That matches the hardware reference image, which shows BG content in the gap.

For implementors

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

SCX/SCY sampling during the fetch

SCX and SCY both feed the VRAM address adder, but they enter at different fetch stages. A mid-Mode-3 scroll write therefore 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. The high-bitplane fine-Y (counter=4) takes the new SCY. The two bitplanes therefore split vertically — the Y-direction analogue of the LCDC.4 hybrid fetch.

Measured: the SCY fetch path

All values here come from the Mealybug m3_scy_change ROM (dmg-sim, TIMING=default), observed on the external VRAM address bus ma[12:0] — the adder output. The ROM sweeps SCY as a triangle 0,1,2,3,4,3,2,1×3, at 24 FF42 writes per line, one BEDY↑ every 8 dots. Within each 8-dot tile the three address stages land 2 dots apart. Counter=0 is the tilemap read, with map row = ma[9:5]. Counter=2 and counter=4 are the tile-data low and high reads, with fine-Y = ma[3:1] and bitplane = ma[0]. On a scanline where the sweep crosses a tile boundary the map row reads out directly. For example, at LY=44 the counter=0 read of the tile whose ma settles to $18c3 gives map_row=6. That value 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 cause: the SCY write (4→3) committed at BEDY↑+0.08 dots, landing between the two tile-data reads. The counter=2 read came 1.54 dots before the value settled, and the counter=4 read 0.44 dots after. The map row and the low bitplane always agree; only the high bitplane catches the intervening write.