Window Control
The window subsystem decides when the BG fetcher switches from background to window tiles within a scanline, and triggers a full fetch-pipeline reset at the handoff. It comprises a WY-match decoder with a frame-held latch (REJO), a per-dot WX-match decoder (NUKO), a four-stage trigger chain gated by LCDC.5, a hit-and-latch condition (RYDY), a window-mode DFF (NOPA), and the window-trigger pulse (MOSU). The trigger chain is PYCO → NUNU → PYNU → NUNY: three capture stages feeding the combinational NUNY. MOSU drives NYXU alongside AVAP and TEVO.
- The window arms in two stages: REJO (WY matched this frame — set once, cleared only by VBlank) gates NUKO (PX == WX, per dot).
- The window-trigger dot MOSU↑ scales with WX and SCX&7 (model below); the window penalty is +6.000 dots — one BG-fetch restart.
- WX = 167 never fires; WX ∈ [~162, 166] fires but is observationally inert (XYMU closes the pipe first).
- NUKO's second consumer (PANY) produces the one-dot BG slip — even in the armed-but-disabled state (REJO=1, LCDC.5=0), where no window renders.
| Gate | Role | Type | Clock / Trigger | Notes |
|---|---|---|---|---|
| PALO | WY bits 4–7 + LCDC.5 decode | nand5 | ff40_d5, NOJO, PAGA, PEZO, NUPA | The XNOR inputs compare WY bits 4–7 to LY bits 4–7. LCDC.5 is part of the WY-match decode itself |
| NELE / PAFU / ROGE | WY decode completion | not / nand5 / not | + NAZE, PEBO, POMO, NEVU (WY bits 0–3) | wy_match = LCDC.5 ∧ (LY == WY) |
| SARY | WY-match sampler | dffr | clk = hclk (TALU) | Latches wy_match; reset by ppu_reset_n |
| REPU | REJO reset | or2 | mode1, ppu_reset2 | Holds REJO cleared throughout every VBlank |
| REJO | WY-match frame latch | nor_latch | s = SARY; r = REPU | wy_latch gates the WX decode — no PX==WX cascade can start without it |
| PUKY / NUFA / NOGY / NUKO | WX-match decode | nand5 / not / nand5 / not | PX bits vs WX bits + wy_latch | NUKO = wy_latch ∧ (PX == WX) |
| PYCO | WX-match capture 1 | dffr | clk = ROCO (pixel-clock-derived; requires POKY=1) | The POKY gate blocks WX=0 from firing before the first BG fetch completes |
| NUNU | WX-match capture 2 | dffr | clk = MEHE (= NOT(ALET)) | Feeds PYNU.s |
| XOFO | PYNU reset (WIN_EN gate) | nand3 | ff40_d5, NOT(ATEJ), ppu_reset_n | Releases PYNU only when LCDC.5=1 outside the line-end pulse |
| PYNU | Window-armed latch | nor_latch | s = NUNU; r = XOFO | |
| NUNY | Window-trigger pulse | and2 | PYNU, nopa_n | Wire name win_start; high while armed but not yet captured |
| PUKU / RYDY | Window hit-and-latch | nor2 / nor3 | NUNY+RYDY feedback / PUKU, PORY, ppu_reset2 | RYDY self-latches via PUKU until PORY clears it |
| NOPA | Window mode | dffr | clk = ALET; d = PYNU | Once captured, the fetcher targets the window tilemap |
| NYFO / MOSU | Trigger buffer | not / not | win_start | MOSU drives NYXU |
| WAZY / VYNO / VUJO | Window line counter | not_x1 / dffr ripple | wy_clk = NOT(PYNU) | Counts PYNU falling edges |
What's where
| Page | Covers |
|---|---|
| Window edge cases | The right edge (WX = 167 never fires; the inert WX ∈ [~162, 166] zone), multiple window activations on one scanline, and the NUKO/PANY one-dot BG slip |
The WY-match frame latch (REJO)
wy_match(= LCDC.5 ∧ LY==WY) → SARY (TALU-clocked) → REJO.s, with REJO.r = REPU = mode1 ∨ppu_reset2
Three properties carry all the behavioural weight:
- LCDC.5 lives inside the decode. PALO's NAND5 includes
ff40_d5, sowy_matchis force-low whenever the window is disabled — SARY can only capture 1 during scanlines where LCDC.5 is held high. - REPU holds REJO cleared through every VBlank. The set chain only
becomes effective at REPU's fall at VBlank exit: REJO sets there if SARY
is already 1, else it waits for a later in-frame
wy_matchcapture. Under a fixed WY, a missed set at VBlank exit locks REJO at 0 for the whole frame (LY cannot re-cross WY except via the 153→0 wrap, which lands back inside VBlank). - Once set, REJO cannot be disarmed mid-frame. A WY rewrite that drops
wy_matchcannot clear the NOR latch; only the next VBlank's REPU does.
The capture clock hclk is TALU — one rising edge every 4.000 dots at a
fixed phase relative to AVAP (measured anchors at ≈ +0.99, +4.99, +8.99,
+12.99, +16.99 … dots; SCX-invariant, since SCX gates SACU, not the TALU
distribution). A fresh wy_match↑ therefore reaches REJO within
0.083 dots of combinational delay plus a race to the next TALU edge —
uniform in [0, 4.000] dots.
The VBlank WY-rewrite trap (gambatte window_late_wy family, dmg-sim
measurement): a test inherits WY=0, enables LCDC.5 mid-VBlank, and rewrites
WY=$FF before VBlank exits. The LY 153→0 wrap briefly raises wy_match and
SARY captures 1 — but REPU still holds REJO at 0. The WY=$FF write drops
wy_match; SARY recaptures 0 on its next TALU edge, ~1.4 ns before mode1
falls. At VBlank exit SARY=0, REJO stays 0, and the window never fires all
frame. The whole family's hardware-pass outcomes follow from this SARY/REPU
edge order.
Mid-Mode-3 fresh arming (gambatte late_wy_FFto2 sub-cluster at SCX ∈
{0, 2, 3}, dmg-sim measurement): the WY=$02 write lands at LY=2
AVAP+15.495 dots. wy_match rises +0.080 dots later. The next TALU edge at
+16.992 captures it, and REJO sets at +16.997. These offsets are identical
across all three SCX variants. Whether the same-scanline cascade fires
then depends on whether the one-dot PX==WX window is still open when REJO
arms:
| SCX | PX==7 window (dots from AVAP) | REJO↑ at | Same-line outcome |
|---|---|---|---|
| 0 | [13.03, 14.03] | +16.997 | window closed 2.97 dots earlier — no fire |
| 2 | [15.03, 16.03] | +16.997 | closed 0.97 dots earlier — no fire |
| 3 | [16.03, 17.03] | +16.997 | REJO arms inside the window — a ~8 ns combinational NUKO glitch that spans no PYCO capture edge |
In all three, LY=2 renders without a window and LY=3 fires the standard cascade (REJO holds across H-Blank). PX is monotonic within a scanline: once past WX, no same-line re-fire is possible without a WX rewrite. The general rule: the cascade fires same-line only if REJO's set (write dot + 0.083 + TALU race) lands before the PX==WX window closes. Increasing SCX widens that budget dot-for-dot, because the window shifts with the fine-scroll delay while TALU's phase does not.
The 10-step activation sequence
flowchart LR
PX["PX == WX"] -- "NUKO<br>(gated by REJO)" --> PYCO
PYCO -- "ROCO↑<br>(needs POKY=1)" --> NUNU
NUNU -- "MEHE↑" --> PYNU["PYNU<br>window-armed latch"]
PYNU -- "∧ nopa_n" --> NUNY["NUNY<br>win_start"]
NUNY --> MOSU["MOSU → NYXU<br>fetch-pipeline reset"]
NUNY --> PUKU
PUKU --> RYDY["RYDY<br>window hit"]
RYDY -- "self-latch" --> PUKU
PYNU -- "ALET↑" --> NOPA["NOPA<br>window mode"]
NOPA -- "closes NUNY" --> NUNY
PORY -- "clears" --> RYDY
With WIN_EN=1, REJO=1, and the fetcher idle (POKY=1):
- PX reaches WX → NOGY drops → NUKO rises.
- PYCO captures NUKO on the next ROCO edge.
- NUNU captures PYCO on the next MEHE edge.
- PYNU sets (window armed).
- NUNY = AND2(PYNU,
nopa_n) rises →win_start→ MOSU rises. - PUKU drops; RYDY = NOR3(0,0,0) rises (window hit).
- NOPA captures PYNU on the next ALET edge (window mode active).
- NUNY falls → MOSU falls.
- PUKU stays low — RYDY's own feedback holds it.
- RYDY remains latched until PORY rises during the restarted BG fetch's cascade — the SUZU falling edge that tells the fetcher to load window tile data.
RYDY's fan-out (each via the shared SYLO/TOMU triple-inversion): SOCY halts CLKPIPE (BG pipeline), TUKU blocks the sprite trigger (sprite pipeline), and SUZU (via TUXY) drives TEVO's window-restart arm.
MOSU drives the same 7-dot pipeline reset as AVAP. Measured on a window-firing scanline (dmg-sim measurement):
| Event | Value |
|---|---|
| MOSU↑ | +6.989 dots from AVAP |
| NYXU↓ | 441 ps after MOSU↑ |
| MOSU pulse width | 0.494 dots |
| NYXU pulse width | 0.503 dots |
| Cascade restart (NYKA↑) | +5.492 dots after MOSU |
| First window pixel | +7.037 dots after MOSU |
MOSU, AVAP, and TEVO each drive NYXU independently through the NOR3 — they do not combine or extend each other.
One interaction: at WX=0 with SCX & 7 = S > 0, the first window pixel
lands +S later still (+10.037 at SCX=3). MOSU fires at the POKY floor
before the line's fine scroll has been paid, and the post-restart pixel
start absorbs it; at WX ≥ 1 the pipe is already running and +7.037 holds
at any SCX.
The per-WX timing model
Measured across the full gbmicrotest window sweep (dmg-sim measurement,
win[0-15]_a ROMs; all values sim-dots):
- MOSU↑ from AVAP = 6.989 + WX for WX ≥ 1 — one dot per WX step.
- WX=0 fires at the same +6.989 floor: the POKY gate on PYCO's clock blocks anything earlier (PX==0 is true from reset, but no capture clock runs until the first fetch completes).
- Window penalty: +6.000 dots, invariant across WX ∈ [0, 15] — exactly one BG-fetch restart.
The joint (WX, SCX) generalisation: SCX enters only through the PX==WX
match dot, because PX advances on SACU, which starts (SCX & 7) dots
later. The capture chain downstream of NUKO has no SCX dependence, and the
WX=0 POKY floor is SCX-invariant:
MOSU↑(WX, SCX) = 6.989 if WX = 0 // POKY floor, SCX-invariant
= 6.989 + WX + (SCX & 7) if WX ≥ 1 // PX-driven
Measured joint anchors: (WX=0, SCX=3) → +6.989 (floor confirmed SCX-invariant);
(WX=10, SCX=0) → +16.989; (WX=10, SCX=3) → +19.989 — the formula is exact at
all three (dmg-sim measurement, gbmicrotest win{0,10}_scx3_a ROMs, 1,290 firing
scanlines each, zero variance).
Sprite-coincident MOSU delay. A sprite fetch overlapping the capture
window freezes ROCO (PYCO's capture clock) with the rest of CLKPIPE. NUKO
stays high, since PX is frozen too. The capture fires on the first ROCO
edge after TAKA clears, so the whole cascade shifts 6.000 dots later.
Measured at the collision band of Mealybug m3_lcdc_win_map_change (WX=7,
sprite X=7 at LY=56–63): MOSU↑ at +19.989 vs the +13.989 control — the
5.515-dot TAKA window plus post-freeze ROCO alignment (dmg-sim
measurement, zero variance across 56 collision scanlines).
Whether the sprite cost appears as "MOSU later" or "Mode 3 longer at the tail" depends only on whether TAKA overlaps the capture window; total Mode 3 picks up the same +6 dots either way.