Introduction
This book describes the timing behaviour of the original Game Boy's SoC — the DMG-CPU B — at the gate and signal level: the clock tree, the PPU's rendering pipeline and mode machinery, the CPU's view of every register boundary, the timer and interrupt-dispatch circuits, and the APU's per-channel counters.
It exists because no current documentation reaches these circuits at this layer:
- gb-ctr — excellent where it reaches, but its coverage is not yet thorough at the time of writing;
- Pan Docs — the console at register granularity;
- pixel-FIFO write-ups — the PPU as an algorithm.
None of them can answer: on which clock edge does Mode 3 actually end? What does the CPU read from STAT during the M-cycle that straddles that edge? Which edge sets IF when TIMA wraps? Emulator authors chasing the last mile of accuracy need answers at exactly that resolution — which flip-flop captures which signal on which edge — and that is the layer this book documents, for every subsystem on the die.
Every claim here is tied to a named gate from the DMG-CPU B netlist. The names (WODU, XYMU, CUPA, MOBA, CARU…) come from the reverse-engineering effort, not the die's designers — but they are stable, globally unique, and shared by the netlist and the simulation: anyone with either can look a cell up and check the claim independently. The dynamic evidence is gate-level simulation, corroborated by test ROMs whose expected results are hardware-verified — no figure here comes from direct electrical measurement of a real unit. Sources and Methodology draws that boundary precisely. Appendix A is the lookup table — every signal named in this book, its role, its connections, and where it is described.
What this book is not
- Not a general Game Boy reference. The console's general hardware and programming model are covered in detail by gb-ctr and Pan Docs; this book does not duplicate them. Where a subsystem's behavioural surface is already well documented (the timer is the clearest example), the relevant chapter summarises it briefly, cites the existing reference, and spends its pages on the gate-level structure underneath.
- Not derived from emulators. No emulator's implementation was used as a source of hardware truth; see Sources and Methodology.
- DMG-CPU B only. Later revisions and the Game Boy Color have no public netlist; their deltas are out of scope.
The shape of the book
- Foundations — the clock tree and the counters and latches that give every other chapter its vocabulary: dots, ALET/MYVO edges, LX/LY, the XYMU rendering-mode latch.
- CPU–PPU Interface — how CPU reads and writes actually reach PPU registers, OAM, and VRAM: the combinational read path, the CUPA write strobe, and OAM DMA's bus arbitration.
- The Rendering Pipeline — Mode 2 OAM scan, the Mode 3 BG/sprite/window machinery, LCD output, mode transitions, STAT interrupts, full scanline and frame timing, and what the CPU observes at every mode boundary.
- CPU-Side Subsystems — the timer, interrupt dispatch, HALT/EI semantics, and the IF register, at the same edge-level resolution.
- Audio Processing Unit — the APU's clock tree, frame sequencer, and per-channel counter cells.
- System State — the complete hardware state at the moment the boot ROM hands control to the cartridge.
The PPU chapters follow one pipeline — the path every rendering scanline runs through Mode 3:
AVAP → NYXU → BG fetch counter → SACU → PX counter → WODU → VOGA → WEGO → XYMU
OAM scan completion (AVAP) starts rendering, the SACU pixel clock pumps pixels through the PX counter, and the terminal-count condition (WODU) propagates through a one-DFF pipeline (VOGA) to set the "not rendering" latch (XYMU). The STAT interrupt cascade hangs off WODU and the LY-comparison match; the CPU reaches PPU registers through a combinational read path and the CUPA write strobe. Every stage of that path has a chapter.
The CPU-side and APU chapters need no shared roadmap — each subsystem is self-contained: the timer is a ripple counter and a two-M-cycle reload sequence, dispatch is a trigger/capture pair against the IF latches, and each APU channel is a divider plus a handful of tick counters.
Reading paths
Few readers need this book cover to cover. Three curated routes:
The emulator-implementor path — building or improving a cycle-accurate core. Read Notation and Conventions, then The Clock Tree and Rendering Mode Control for vocabulary, then go straight to the subsystem you are implementing. The "For implementors" callouts and Appendix B's constants are written for you; finish with Post-Boot State if you skip the boot ROM.
The boundary-behaviour path — chasing a failing timing test. Start at CPU-Visible Mode Boundaries for the PRE/POST framework, then the boundary's owning chapter (Mode transitions, STAT interrupts, Interrupt dispatch, or HALT and EI), then Races if the divergence is sub-dot.
The APU path — self-contained: APU clock tree and frame sequencer first (the re-lock rule and the Δ = 3 boot seed underpin everything), then the channel chapters in any order, then the APU rows of Post-Boot State.
If you are new to gate-level material, read Notation and Conventions first — it is short, and the rest of the book assumes it.
Sources and Methodology
This book was written with substantial AI assistance, under direction and with the verification discipline described in this chapter. The full account is in the project's README.
Everything in this book derives from three primary sources, used in this order:
-
The DMG-CPU B netlist — msinger/dmg-schematics, a transistor-level reverse engineering of the DMG-CPU B die. The netlist supplies the ground truth this book is written in: cell names, cell types, and connectivity. When this book says "WODU = AND2(XENA, XANO)", that is a netlist fact, checkable by opening the schematics.
-
Gate-level simulation — msinger/dmg-sim, a SystemVerilog model generated from that netlist and run under Icarus Verilog — the same logic as the silicon, cell by cell, with modelled propagation delays (tunable parameters, not values extracted from silicon). Every dynamic timing claim in this book was measured from its waveform captures. Test ROMs run under a minimal quickboot stub that reproduces the post-boot state; the real boot ROM was used where boot alignment or residual boot-time state matters, and to validate quickboot itself.
-
Static propagation-delay analysis — gb-propagation-delay-analysis, graph analysis over the netlist (4,102 nodes, 9,458 edges): combinational depth measured in gate equivalents (ge), race-pair identification between converging paths, and critical-path inventories. This is the source for the races chapter and for depth figures quoted elsewhere.
Behavioural documentation — gb-ctr, Pan Docs, TCAGBD, the Mealybug Tearoom PPU notes — is used for cross-checking and for high-level framing, and is cited where it carries a section's behavioural summary. No emulator implementation was used as a source of hardware truth. Hardware test ROMs with hardware-verified expected results corroborate behavioural conclusions but never ground a gate-level claim. Annotations are trusted but not infallible — where a ROM's expected value disagreed with the simulation, the ROM was re-run on real hardware and excluded if the expectation did not hold there. Every cited ROM was measured from a verified local build.
How measurements were made
The measurement workflow behind every "dmg-sim measurement" citation:
- A small test ROM establishes a precisely-timed scenario — for example, a
HALT/NOP loop synchronised to the H-Blank interrupt with SCX=0. Citations
name the scenario and its suite (e.g. gbmicrotest
int_hblank_nops_scx0), or mark it purpose-built, so a measurement can be reproduced by reconstructing the scenario. - dmg-sim runs the ROM and dumps an FST waveform of the full cell-level state (1 ps timescale).
- Named-signal transitions are read from the waveform with picosecond timestamps, and converted to dots using the simulation's measured dot period.
- Single observations are never trusted: values are checked for invariance across consecutive scanlines, across scenario variants (different instruction preludes, different SCX values), and against the static netlist structure.
How to read the numbers
Two kinds of numeric values appear in this book, and they carry different weight:
- Dot-denominated figures ("Mode 3 lasts 173.481 dots", "the sprite penalty is exactly 6 dots") are discrete observations: counts of clock edges between two events, with no precision limit. Given comfortable timing margins, the count is set by the clocking structure, not the delay values; the test-ROM corpus corroborates the counts at the resolution it observes (typically the M-cycle). One dot ≈ 238.4 ns on hardware.
- Picosecond figures ("CATU's Q output moves 988 ps after its clock edge") are model propagation values — the simulation's dot period is 244,000 ps against the real ≈238,418 ps. Read them as edge-ordering and phase evidence, not as nanosecond claims about a physical chip.
Fractional-dot figures such as "WODU→XYMU = 0.436 dots" read against the half-dot clock grid. Events in these chains sit at small propagation offsets from clock edges, so an interval's fraction reveals which edge its endpoint belongs to: VOGA captures WODU on the same-dot ALET rising edge, 0.435 dots after WODU↑, and the WEGO→XYMU gate adds the final 0.001 — so XYMU's set lands just after that ALET rising edge. The prose always names the edges; the fraction is how the waveform shows them.
The evidence boundary. Every dynamic measurement here is a simulation measurement. Hardware enters only as test-ROM pass/fail expectations validated on real units and as reference photographs — never as direct electrical measurement of a real DMG. Real units also vary (process, voltage, temperature), so "zero variance" describes the deterministic model, and a small-margin ps ordering is the model's prediction of the likely silicon ordering, not a guarantee. Where unit variation could plausibly flip an ordering, the text says so.
Calibration pin. The dominant delay-tuning constant is kR_nmos_ref
in dmg-sim's timing-default.sv. Every ps and fractional-dot figure in
this book is stamped to kR_nmos_ref = 3.6e4 (upstream commit
e0c8774, the current calibration as of June 2026). Re-tuning that
constant rescales the ps figures near-uniformly and leaves edge identities
in place — verified across a 3.6× historical re-tuning, with one
margin-sensitive exception flagged where it appears
(pixel clock). If a build disagrees with
this book's ps values by a uniform factor, check that constant first.
Integer dot counts are calibration-independent: dot figures are expressed
in the simulation's own dot period, so known edge counts (the 4-dot TALU
period, the 6-dot sprite fetch) read as exact integers.
Verification
Every numeric figure was re-traced to its measurement evidence during preparation for publication and checked for consistency against every other place it appears; the gate-level measurement always grounds the claim. Nothing unverified is stated silently: residual unknowns are tagged Open question inline and collected in Appendix C.
Some signals are documented structurally but lack a derived semantic role; their concordance entries leave the role cell blank rather than invent one.
Notation and Conventions
This chapter defines the units, naming rules, and recurring table formats used throughout the book. It is reference material — skim it once, then return when a notation needs pinning down.
Timing units
| Unit | Definition |
|---|---|
| dot | One full master-clock cycle: one rising plus one falling edge of the 4.194304 MHz crystal clock. ≈238.4 ns on hardware. Synonymous with T-cycle. |
| M-cycle | One CPU machine cycle = 4 dots. |
| ATAL half-cycle | Half a dot — the interval between two consecutive master-clock edges. Named for ATAL, the buffered 4 MHz clock at the root of the PPU clock tree. |
| ge | Gate equivalent — the unit of combinational depth used by the static netlist analysis (see races). |
| ps | Simulation picoseconds — a model unit, not hardware time: one dot = 244,000 ps in dmg-sim's default timing mode. See Sources and Methodology. |
There is no inherent "start" edge within a dot: the master clock is a continuous alternation of edges, and circuits trigger on whichever edge they are wired to. Statements like "X happens 0.436 dots after Y" locate X's edge relative to Y's edge; the prose names both.
Writing positions. Dots measure length; cycle labels name place:
| To express | Convention | Example |
|---|---|---|
| A duration or offset | dots, fractional where needed | "0.436 dots after WODU↑" |
| A segment of an M-cycle | T-cycles T1–T4 | "CUPA spans T3–T4" |
| An exact point in an M-cycle | + dots from the M-cycle's start | "+3.995 dots — the tail of T4" |
| An instruction's machine cycles | M1, M2, … (M1 = fetch) | "dispatch M1–M4" |
| A counter position | the literal register value, from 0 | "LY 0–153", "PX terminal count 167" |
T-cycle and machine-cycle numbering follow gb-ctr. Pan Docs uses dots purely as durations, with the same zero-based counter values.
Signal names
- Gate names come from the DMG-CPU B netlist and are the book's canonical identifiers, UPPERCASE in prose: WODU, XYMU, AVAP. The names are the reverse-engineering effort's, not the designers' — but they are stable and globally unique, so every claim is greppable.
- Lowercase backtick forms refer to netlist cells or simulation wires where the
distinction matters:
`wodu`(the cell),`byba_n`(a specific output pin). - A gate's Boolean function is written in netlist style:
WODU = AND2(XENA, XANO),NYXU = NOR3(AVAP, MOSU, TEVO). - A few signals are simulation wire names rather than netlist cells (e.g.
`ck1_ck2`, the crystal input); these stay lowercase throughout.
Roles vs names. Each gate has a one-phrase semantic role ("scan-done flag", "Mode 3 ending condition") introduced at first mention in a chapter; after that, the gate name alone is used. The role phrasing is identical everywhere a gate appears, and Appendix A holds the canonical phrasing.
Polarity. Many signals are active-low, and several latches hold inverted
semantics — the most important being XYMU, the rendering-mode latch, which reads
0 during Mode 3 (its role phrase is "not rendering"). Polarity quirks are
stated explicitly wherever they bite. DFF complementary outputs are written
q/q_n; when a consumer takes the q_n pin, the text says so.
Edges
↑= rising edge,↓= falling edge: WODU↑ is WODU rising.- The clock tree produces named edge vocabulary used everywhere: "captures on ALET rising", "XUPY falling". Complementary clock pairs (ALET/MYVO, TALU/SONO, XOTA/XYFY) mean every half-dot boundary has two equivalent names — "MYVO rising" is "ALET falling" — and chapters use whichever matches the flip-flop's clock pin.
Signal paths
Cascades are written as an arrow chain in a blockquote, followed by prose:
WODU → TARU → SUKO → TUVA → VOTY → LALU
The Mode 0 condition (WODU) combines with the STAT enable bit through TARU, joins the other STAT sources at the combining gate (SUKO), and propagates through two inverters to clock the STAT interrupt latch (LALU).
The chain names every stage in order, with no skipped intermediates.
Reference blocks
Chapters that discuss individual gates open with a reference block — a table of the gates the chapter covers:
| Gate | Role | Type | Clock / Trigger | Notes |
|---|---|---|---|---|
| BYBA | Scan-done flag | dffr | XUPY (rising) | Captures FETO; feeds AVAP edge detector |
- Type is the netlist cell type (
dffr,nor_latch,and2,dlatch_ee…). - Clock / Trigger gives clock-and-edge for registered cells, the input list for combinational gates, and set/reset inputs for latches.
- Arrays of identically-clocked cells (shift-register planes, the sprite store) use a family row with a Count column; individual cell names follow in prose.
- A signal whose full description lives in another chapter gets a brief row pointing there. One chapter is primary for every signal; Appendix A records which.
Callout boxes
Seven kinds of callout carry the book's recurring content types:
| Callout | Carries |
|---|---|
| At a glance | A chapter's headline facts — the numbers and the one rule to remember |
| In this part | The first chapter of each part sketches its siblings |
| Rule | A behavioural rule an emulator must honour, stated in its sharpest form |
| Measured | A self-contained measured result worth pulling out of the prose |
| For implementors | Modelling advice — how to structure an implementation so the rule falls out |
| Pitfall | A known wrong model, naming trap, or test-harness trap |
| Open question | A behaviour not yet pinned down by primary-source evidence — collected in Appendix C |
Inline (dmg-sim measurement, …) tags remain the provenance convention —
a Measured callout features a result but never replaces them.
Two markers flag the edges of current knowledge: the Open question callout, and a blank role cell in Appendix A — connectivity documented, semantic role not yet derived, never an invented description.
The Clock Tree
The three Foundations chapters carry everything else: this chapter names the clocks and their phase relationships; Line counters builds the LX/LY counters and the LINE_END pulse on them; and Mode control derives the four PPU modes from a handful of latches.
Every timing claim in this book ultimately reduces to "this cell captures on that edge of this clock". This chapter names the clocks. It follows the master clock from the crystal input through the buffer chain to the PPU's per-dot clock pair (ALET/MYVO), down the video divider chain to the per-M-cycle pair (TALU/SONO), and establishes the phase relationships the rest of the book leans on.
- One dot = one full
ck1_ck2cycle. ALET tracks the crystal in phase (16.3 ge deep); MYVO = NOT(ALET). "ALET rising / falling" is the book's per-dot edge vocabulary. - XUPY-clocked DFFs capture on ALET falling — the opposite edge from ALET-clocked DFFs. BYBA→DOBA measures 0.487 dots apart.
- TALU/SONO are the complementary 1 MHz line-level pair; a boundary TALU↑ leads the same M-cycle's CLK9↑ by ~0.496 dots.
- All PPU dividers are held at 0 under VID_RST and count from zero on release.
The master oscillator
The DMG-CPU B has a single crystal oscillator at 4.194304 MHz connected through
the top-level input ck1_ck2 — the master clock from which all on-chip
timing derives. Wherever this book says "the master clock" (including the
propagation-delay origin used in Races), the referent is ck1_ck2.
One dot covers both edges of the master clock. ALET, the PPU's main 4 MHz
clock, tracks the master clock in phase: ALET rising corresponds to
ck1_ck2 rising after ~16.3 ge of buffer delay. MYVO = NOT(ALET) is the
complementary clock — DFFs clocked by ALET capture on one edge, those clocked
by MYVO on the other, both within a single dot. "One ALET half-cycle" and "one
ATAL half-cycle" both mean half a dot.
Dot counts in this book are edge counts — independent of the simulation's delay model. Picosecond values come from the gate-level simulation's 244,000 ps dot period — see How to read the numbers.
Crystal to PPU clocks
The crystal feeds a NAND feedback pair (ANOS/AVET) that produces ATAL (the 4 MHz enable) and its complement ADEH. From ATAL the PPU clock tree branches through a buffer chain (AZOF → ZAXY → ZEME) to ALET (4 MHz PPU main clock) and its complements (MYVO, MOXE). A parallel branch (XYVA → XOTA) feeds the WUVU/VENA/WOSU video divider chain; VENA drives TALU (1 MHz LX-counter clock) and its complement SONO. The tree diagram shows the propagation hierarchy; the reference block catalogues each named cell.
| Gate | Role | Type | Clock / Trigger | Notes |
|---|---|---|---|---|
ck1_ck2 | Master clock (crystal oscillator input, 4.194304 MHz) | top-level input | — (external crystal) | Tree root. One dot = one full ck1_ck2 cycle (one rising + one falling edge). Conventional propagation-delay origin (see Races). Not an on-chip cell — no netlist cell type. |
| ANOS | Crystal feedback NAND | nand2 | Input: ck1_ck2 | Pairs with AVET |
| AVET | Crystal feedback NAND | nand2 | Input: ck1_ck2 | Pairs with ANOS |
| ATAL | 4 MHz buffered enable | not_x2 | Input: ANOS/AVET output | Master PPU enable. Also drives the CPU phase-generator ring AFUR/ALEF/APUK/ADYK paired with ADEH (Register writes). ATAL's edges define the "ATAL half-cycle" timing unit. |
| ADEH | ATAL complement | not_x1 | Input: ATAL | Pairs with ATAL as the complementary 4 MHz enables driving the CPU phase-generator ring. |
| AZOF | PPU clock buffer | not_x6 | Input: ATAL | Fan-out buffer |
| ZAXY | Buffer stage | not_x2 | Input: AZOF | — |
| ZEME | Buffer stage | not_x4 | Input: ZAXY | Fan-out point: drives ALET (main PPU clock) and XYVA (parallel divider branch). |
| ALET | 4 MHz PPU main clock | not_x2 | Input: ZEME | Depth 16.3 ge from crystal. Rising and falling edges are the book's canonical per-dot timing-event vocabulary ("ALET rising" / "ALET falling"); see the pipeline pairs below and Races. |
| MYVO | ALET complement | not_x1 | Input: ALET | Depth 22.2 ge (5.9 ge more than ALET) |
| MOXE | ALET complement (fine-scroll path) | not_x1 | Input: ALET | Same phase as MYVO, different buffer. Depth 16.7 ge. Dedicated single-consumer inverter isolates the fine-scroll path's clock loading (drives NYZE only) from MYVO's high-fan-out path. |
| XYVA | Parallel divider branch | not_x1 | Input: ZEME | Single-stage inverter feeding XOTA. Same phase as ALET (both compute NOT(ZEME)) on a physically separate routing path; no downstream consumers beyond XOTA. |
| XOTA | WUVU clock | not_x1 | Input: XYVA | Drives the WUVU DFF. "XOTA rising = ALET falling" is the canonical derived-edge framing used throughout the book's WUVU/XUPY divider timing descriptions. |
| WUVU | 2 MHz toggle divider | dffr | XOTA | ~Q → D feedback (toggle). Halves XOTA frequency; drives VENA (via ~Q) and feeds the XUPY inverter, WOSU, and the LCD-control-phase NORs. |
| VENA | 1 MHz toggle divider | dffr | WUVU.~Q | ~Q → D feedback. Halves WUVU.~Q frequency, producing the 1 MHz cadence that TALU inverts and drives into the LX counter / LINE_END pipeline. |
| WOSU | WUVU sampler | dffr | XYFY | Captures WUVU on XYFY rising, producing a half-dot-phase-shifted WUVU replica; feeds WOJO (LCD output) as the phase-differentiated input of the NOR2 LCD-control-phase decode. |
| TALU | 1 MHz LX counter clock | not_x4 | Input: VENA | Drives the LX counter (SAXO chain), NYPE (LINE_END redistribution), ROPO (LY==LYC latch), and related scanline-level DFFs. |
| SONO | 1 MHz LINE_END capture clock (TALU complement) | not_x1 | Input: TALU | Clocks RUTU (line counters) — captures SANU (LINE_END decode) on the SONO edge, half an M-cycle after the LX increment. |
| XYFY | XOTA complement | not_x1 | Input: XOTA | Clocks WOSU. Primary XOTA complement (WOSU is its only consumer; no fan-out alternative). |
ck1_ck2 (4.194 MHz crystal)
└── ANOS (nand2) / AVET (nand2) — feedback pair
└── ATAL (not_x2) — 4 MHz buffered enable
├── ADEH = NOT(ATAL) — complement
├── [CPU phase generators: AFUR/ALEF/APUK/ADYK ring]
└── AZOF (not_x6) = NOT(ATAL) — PPU clock buffer
└── ZAXY (not_x2) = NOT(AZOF)
└── ZEME (not_x4) = NOT(ZAXY)
├── ALET (not_x2) = NOT(ZEME) — 4 MHz PPU main clock
│ ├── MYVO (not_x1) = NOT(ALET) — complementary to ALET
│ └── MOXE (not_x1) = NOT(ALET) — same phase as MYVO
└── XYVA (not_x1) = NOT(ZEME)
└── XOTA (not_x1) = NOT(XYVA)
├── WUVU (dffr) — toggle, 2 MHz [clk=XOTA]
│ ├── VENA (dffr) — toggle, 1 MHz [clk=WUVU.~Q]
│ │ ├── TALU (not_x4) = NOT(vena_n) = VENA.q — LX counter clock, 1 MHz
│ │ │ └── SONO (not_x1) = NOT(TALU) = vena_n
│ │ └── [used as clock for per-dot DFFs]
│ └── WOSU (dffr) — samples WUVU on opposite edge [clk=XYFY]
└── XYFY (not_x1) = NOT(XOTA) — opposite edge
Phase relationships
These five facts carry most of the book's edge reasoning:
- ALET rising =
ck1_ck2rising (in phase, not inverted). The XTAL pad inverts once (clk_in_n= NOT(ck1_ck2)); the ANOS/AVET feedback pair restores ATAL to crystal phase; four more inversions through AZOF → ZAXY → ZEME → ALET land in phase at 16.3 ge total delay. ADEH = NOT(ATAL) pairs with ATAL in the CPU phase-generator ring. - ALET and MYVO = NOT(ALET) are complementary clocks at the 4 MHz master rate. ALET is 16.3 ge deep from the crystal; MYVO is 22.2 ge deep (5.9 ge additional from the inverter and wire routing). DFFs clocked by ALET capture on one edge; DFFs clocked by MYVO capture on the opposite edge. This pair drives most per-dot PPU timing.
- TALU and SONO are complementary clocks at 1 MHz (derived from VENA), driving line-level timing.
- WUVU and VENA are toggle DFFs (internal ~Q → D feedback) forming the video clock divider chain. WOSU is a sampler DFF (D input = WUVU) that captures WUVU on XYFY rising to produce a phase-differentiated replica for the LCD-control decode.
- All PPU dividers are held at 0 while VID_RST is asserted (LCD off) and count from zero on deassertion — see Register writes for the deassertion path.
PPU clock domains
Every registered cell in the PPU is clocked by a signal traceable to the crystal. The major clock domains:
| Clock signal | Source | Frequency | What it drives |
|---|---|---|---|
sacu (CLKPIPE) | combinational, via alet chain | per-dot, variable arrival | Pixel shift registers, X match, PX counter — 52 cells |
alet | crystal via buffer chain | 4 MHz | BG fetch cycle DFFs (NYKA, LYZU, PYGO, RENE), sprite fetch control (DOBA), window mode (NOPA), H-Blank capture (VOGA) |
myvo = NOT(alet) | crystal via buffer chain | 4 MHz (opposite edge) | PORY (fetch cycle counter), VRAM timing |
lebo = NAND2(moce, alet) | gated alet | conditional per-dot | LAXU (fetch counter bit 0) — stops when MOCE goes low |
laxu → mesu → nyva | ripple from LAXU | per-dot, delayed | Fetch counter bits 1, 2 (tile fetch cycle counting) |
talu = NOT(vena_n) (= VENA.q) | 1 MHz from VENA | 1 MHz | LX counter (SAXO chain), NYPE, ROPO |
sono = NOT(talu) (= vena_n) | complementary to TALU | 1 MHz | RUTU (LINE_END), SYGU |
xota | buffered 4 MHz | 4 MHz | WUVU (video clock divider) |
xyfy = NOT(xota) | opposite to XOTA | 4 MHz (opposite edge) | WOSU |
wuvu | toggle at 2 MHz | 2 MHz | VENA (via ~Q) |
xupy = NOT(wuvu_n) (= WUVU.q) | 2 MHz | 2 MHz | BYBA (OAM scan complete capture), CENO (OAM phase control) |
nype | from RUTU (LINE_END) | per-line | POPU (VBlank), MEDA (LY==0), MYTA (FRAME_END) |
popu | VBlank flag | per-frame | NAPO |
muwy (LY bit 0) → ripple | per-line from RUTU | per-line | LY counter chain, sprite store clocks — 181 registers via gated paths |
sabe = NAND2(lape, tame) | gated NOT(alet) | conditional per-dot | TOXE (sprite fetch counter bit 0) — advances when ALET rises, stops at count 5 |
toxe → tuly → tese | ripple from TOXE | per-dot, delayed | Sprite fetch counter bits 1, 2 |
seba / sprite fetch | gated per-line | per-line/per-sprite | Sprite Y comparator, store writes, priority |
roxo = NOT(segu) | from CLKPIPE buffer | per-dot | PUXA (fine scroll match capture) |
moxe = NOT(alet) | same phase as MYVO | 4 MHz (opposite edge) | NYZE (fine scroll match second stage) |
pecu = NAND2(roze, roxo) | gated CLKPIPE inverse | conditional | RYKU (fine scroll counter bit 0) |
Complementary pairs and pipeline relationships
A DFF clocked by A feeding a DFF clocked by NOT(A) forms a half-period pipeline: the downstream cell captures, half a period later, what the upstream cell just captured. Three pairs recur:
| Pair | Upstream captures on | Downstream captures on | Pipeline |
|---|---|---|---|
| ALET / MYVO | ALET↑ — NYKA | MYVO↑ (= ALET↓) — PORY | half a dot; the book's prototypical example (Races) |
| TALU / SONO | TALU↑ — the LX counter (SAXO) | SONO↑ — RUTU captures SANU | half an M-cycle, LX → LINE_END |
| XOTA / XYFY | XOTA↑ — WUVU | XYFY↑ — WOSU samples WUVU | half a dot; the divider's phase-shifted replica |
Worked through for ALET/MYVO: on ALET rising, the ALET-clocked DFFs (NYKA, LYZU, PYGO, RENE, DOBA, NOPA) capture. On ALET falling — MYVO rising — PORY captures NYKA's new output. PORY therefore always holds the value NYKA captured half a dot earlier.
XUPY-clocked DFFs capture on ALET falling — the opposite edge from the ALET-clocked group. The chain: XOTA is ALET's complement; XOTA clocks WUVU; XUPY = WUVU.q. So BYBA — the scan-done capture (OAM scan) — moves half a dot before DOBA, its ALET-clocked partner in the AVAP edge detector.
BYBA's Q moves 2,566 ps after an ALET falling edge; DOBA's Q moves 884 ps
after an ALET rising edge; their separation is 118,888 ps = 0.487 dots,
constant across 1,721 scanlines (dmg-sim measurement, gbmicrotest
int_hblank_nops_scx0).
TALU / ck1_ck2 half-edge alignment
TALU rises align with ck1_ck2 falling edges (modulo divider-startup
phase): WUVU toggles on XOTA↑ = ALET↓ = ck1_ck2↓, VENA captures the toggle
on the same edge family, and TALU inverts VENA — so each TALU↑ traces to a
ck1_ck2↓ through the divider chain's buffer delays. Measured directly:
TALU↑ lands 9,350 ps after its ck1_ck2↓ edge, constant across 51,489
edges (dmg-sim measurement).
Steady-state TALU↑ vs CLK9↑ within one M-cycle
The TALU↑ that opens an M-cycle (clocking NYPE, SAXO, and ROPO) leads the
CLK9↑ that opens the same M-cycle (Interrupt
dispatch) by 121,110 ps ≈ 0.496 dots (dmg-sim
measurement, constant across 51,489 M-cycles). The half-dot offset is
structural: the TALU edge comes from the ck1_ck2↓ preceding the boundary
(+9,350 ps), the CLK9 edge from T1's ck1_ck2↑ (+8,440 ps):
½ dot − 9,350 + 8,440 = 121,090 ps, matching the measured value to within
the edge-asymmetry residual.
The practical reading: "TALU↑ at the M-cycle boundary" fires roughly half a dot before that M-cycle's CLK9↑. The two edges open the same M-cycle but are not co-located.
Line Counters
LX and LY are the PPU's scanline-position and scanline-number counters. Both run on every scanline regardless of mode and are consumed everywhere: Mode 2's sprite Y compare uses LY, Mode 3's window WY/WX compares use LY and the pixel pipeline, and the mode-transition machinery keys off both (LX=113 triggers LINE_END; LY≥144 enters VBlank). LINE_END's distribution via NYPE splits across two TALU edges, driving POPU (Mode 1 capture), MYTA (FRAME_END), and MEDA (LCD vertical sync).
- LX counts 0–113 on TALU (114 M-cycles = 456 dots per scanline); the LX=113 decode (SANU) is captured into RUTU on the complementary SONO edge.
- The RUTU pulse is one full TALU cycle (4 dots); the TALU edge inside it does not advance LX — 114 TALU edges per line, 113 increments.
- LY clocks on RUTU, with three decodes: XYVO (VBlank, ≥144), NOKO (FRAME_END, =153), NERU (=0 — LCD vertical sync only).
- NYPE distributes LINE_END across two complementary TALU edges: POPU first; MYTA and MEDA one TALU period later.
The LX counter
LX is a 7-bit ripple counter clocked by TALU (1 MHz, = VENA.q), counting 0–113: 114 M-cycles × 4 dots = 456 dots per scanline. Each stage's clock comes from the previous stage's ~Q output (the same convention as the BG fetch counter). LINE_END is detected combinationally (SANU) and captured into RUTU on the complementary SONO clock edge.
| Gate | Role | Type | Clock / Trigger | Notes |
|---|---|---|---|---|
| SAXO | LX bit 0 | dffr | TALU | Reset by MUDE |
| TYPO | LX bit 1 | dffr | SAXO (~Q ripple) | Reset by MUDE |
| VYZO | LX bit 2 | dffr | TYPO (~Q ripple) | Reset by MUDE |
| TELU | LX bit 3 | dffr | VYZO (~Q ripple) | Reset by MUDE |
| SUDE | LX bit 4 | dffr | TELU (~Q ripple) | Reset by MUDE |
| TAHA | LX bit 5 | dffr | SUDE (~Q ripple) | Reset by MUDE |
| TYRY | LX bit 6 | dffr | TAHA (~Q ripple) | Reset by MUDE |
| MUDE | LX reset | nor2 | Inputs: LYHA, RUTU | LINE_END or video reset |
| SANU | LINE_END decode | and4 | Inputs: SAXO, SUDE, TAHA, TYRY | Fires at LX=113 (0b1110001) |
| RUTU | LINE_END capture | dffr | SONO | Data: SANU; half an M-cycle later than SANU settle |
TALU (1 MHz)
├── SAXO (dffr) [clk=TALU] — LX bit 0
│ └── TYPO (dffr) [clk=SAXO.~Q] — LX bit 1 (ripple)
│ └── VYZO (dffr) [clk=TYPO.~Q] — LX bit 2
│ └── TELU (dffr) [clk=VYZO.~Q] — LX bit 3
│ └── SUDE (dffr) [clk=TELU.~Q] — LX bit 4
│ └── TAHA (dffr) [clk=SUDE.~Q] — LX bit 5
│ └── TYRY (dffr) [clk=TAHA.~Q] — LX bit 6
Reset: MUDE = NOR2(LYHA, RUTU) — LINE_END (RUTU=1) or video reset (LYHA) drives MUDE low, resetting the counter. LX counts from 0 to 113, where LINE_END fires.
LINE_END detection
LX[0..6] → SANU → RUTU
SANU = AND4(SAXO, SUDE, TAHA, TYRY) = AND(LX[0], LX[4], LX[5], LX[6]) decodes LX = 113 (binary 1110001). The decode is partial — it ignores bits 1, 2, 3 — but exact within the counter's range: 113 is the smallest value with bits 0, 4, 5, 6 all set, so no earlier count can fire it.
RUTU (dffr, clk=SONO) captures SANU on the SONO edge (= TALU falling). The complementary pair does the settling work: LX increments on TALU rising, RUTU samples on SONO rising — half an M-cycle for SANU to settle (complementary pairs).
The RUTU pulse
RUTU.Q is high for one full TALU cycle = 4 dots: it captures SANU=1 on a SONO rising edge, MUDE = NOR2(LYHA, RUTU) holds the LX counter at 0 (keeping SANU=0), and the next SONO rising edge captures SANU=0, ending the pulse. The pulse is bounded by successive TALU-falling edges.
Measured across 36 consecutive scanline boundaries: pulse width 4.000 dots (one TALU cycle) and inter-rise interval 456 dots (the scanline period), both zero-variance (dmg-sim measurement).
One consequence: the TALU rising edge inside the RUTU pulse does not advance LX (the counter is held at 0), so a steady-state scanline sees 114 TALU rising edges of which 113 increment LX. This matters for the post-LCD-on the first scanline, which has no leading RUTU pulse — see LCD-on power-up.
The LY counter
LY is an 8-bit ripple counter clocked by RUTU. Three combinational decodes detect scanline phase transitions: XYVO (VBlank, LY ≥ 144), NOKO (FRAME_END, LY = 153), and NERU (LY = 0). Each decode is captured by a DFF clocked by NYPE (see LINE_END distribution below).
| Gate | Role | Type | Clock / Trigger | Notes |
|---|---|---|---|---|
| MUWY | LY bit 0 | dffr | RUTU | Reset by LAMA |
| MYRO | LY bit 1 | dffr | MUWY (~Q ripple) | Reset by LAMA |
| LEXA | LY bit 2 | dffr | MYRO (~Q ripple) | Reset by LAMA |
| LYDO | LY bit 3 | dffr | LEXA (~Q ripple) | Reset by LAMA |
| LOVU | LY bit 4 | dffr | LYDO (~Q ripple) | Reset by LAMA |
| LEMA | LY bit 5 | dffr | LOVU (~Q ripple) | Reset by LAMA |
| MATO | LY bit 6 | dffr | LEMA (~Q ripple) | Reset by LAMA |
| LAFO | LY bit 7 | dffr | MATO (~Q ripple) | Reset by LAMA |
| LAMA | LY reset | nor2 | Inputs: LYHA, MYTA | FRAME_END or video reset |
| XYVO | VBlank decode | and2 | Inputs: LOVU, LAFO | Fires at LY ≥ 144 |
| POPU | VBlank capture | dffr | nype (NYPE rising) | Data: XYVO |
| NOKO | FRAME_END decode | and4 | Inputs: LYDO, LOVU, LAFO, MUWY | Fires at LY = 153 |
| MYTA | FRAME_END capture | dffr | nype_n (NYPE falling; one TALU period after POPU) | Data: NOKO; drives LY reset via LAMA |
| NERU | LY=0 decode | nor8 | Inputs: all LY bits | Fires when LY = 0 |
| MEDA | LCD vertical-sync source | dffr | nype_n (NYPE falling) | Data: NERU. Drives the LCD s_pad via the mure inverter (LCD output); not an OAM-scan consumer. |
RUTU (LINE_END)
├── MUWY (dffr) [clk=RUTU] — LY bit 0
│ └── MYRO (dffr) [clk=MUWY.~Q] — LY bit 1 (ripple)
│ └── LEXA (dffr) [clk=MYRO.~Q] — LY bit 2
│ └── LYDO (dffr) [clk=LEXA.~Q] — LY bit 3
│ └── LOVU (dffr) [clk=LYDO.~Q] — LY bit 4
│ └── LEMA (dffr) [clk=LOVU.~Q] — LY bit 5
│ └── MATO (dffr) [clk=LEMA.~Q] — LY bit 6
│ └── LAFO (dffr) [clk=MATO.~Q] — LY bit 7
Reset: LAMA = NOR2(LYHA, MYTA) — FRAME_END or video reset triggers the LY reset.
The three decodes, side by side:
| Decode | Function | Fires at | Captured by | On | Consequence |
|---|---|---|---|---|---|
| XYVO | AND(LY[4], LY[7]) | LY ≥ 144 | POPU | nype↑ | Mode 1 (VBlank) begins |
| NOKO | AND(LY[0], LY[3], LY[4], LY[7]) | LY = 153 | MYTA | nype_n↑ — one TALU period after POPU | LAMA goes low; LY resets to 0 |
| NERU | NOR8(all LY bits) | LY = 0 | MEDA | nype_n↑ | drives only the LCD vertical sync (s_pad, via mure) |
The two AND decodes use SANU's partial-decode trick: 144 and 153 are the smallest values with their bit sets, so the decode is exact within range. MEDA touches no scan or mode-control state — see LCD output for the pad driver and LCD-on power-up for why the first frame after LCD-on emits no pulse.
LINE_END distribution via NYPE
RUTU drives NYPE (dffr, clk=TALU), and NYPE's two outputs split the distribution across two complementary TALU edges: Q clocks POPU; Q_n clocks MYTA and MEDA one TALU period later (the capture table above).
The stagger carries two consequences:
- The LY 153→0 race window. The one-TALU-period gap between POPU and MYTA is when ROPO captures the pre-reset LYC match at the 153→0 boundary — STAT interrupts.
- A modelling constraint — the pitfall below.
For implementations, collapsing the NYPE distribution to a single edge-hook shifts MYTA's timing by one TALU period relative to hardware — and with it the LY 153→0 boundary behaviour.
Rendering Mode Control
XYMU is the PPU's single mode-state handle — a NOR latch whose polarity (Q=0 during Mode 3, Q=1 elsewhere) drives 23 downstream consumers across fetch control, VRAM/OAM access, STAT mode bits, sprite comparators, and fine-scroll gating. This chapter describes XYMU's structure and the mode lifecycle; Mode transitions describes what drives XYMU's transitions, edge by edge.
- XYMU is the PPU's single mode-state handle: a NOR latch with Q=0 during Mode 3 ("not rendering" polarity) and 23 consumers.
- Mode 3 starts when AVAP resets XYMU directly; it ends through WODU → VOGA (same-dot ALET↑ capture) → WEGO → XYMU — 0.436 dots total.
- The mode-control web splits into async latches (XYMU, BESU, ROXY, POKY, LONY — combinational delay only) and clocked DFFs (VOGA, RUTU, POPU, MYTA) — most one-dot timing subtleties come from that split.
Mode lifecycle
The PPU mode is determined by combinational logic from several registered signals:
| Mode | Condition | Duration |
|---|---|---|
| Mode 2 (OAM Scan) | XYMU=1, CENO active, OAM scan in progress | 80 dots (40 OAM entries × 2 dots) |
| Mode 3 (Pixel Transfer) | XYMU=0 (rendering active) | 173 + (SCX & 7) + sprite penalty + window penalty dots |
| Mode 0 (H-Blank) | XYMU=1, not VBlank | remainder of the 456-dot line |
| Mode 1 (VBlank) | POPU set (LY ≥ 144) | 10 lines × 456 dots |
This table is the behavioural surface that register-level documentation already describes; everything below is the machinery underneath it.
The XYMU latch
XYMU is the PPU's "not rendering" signal. The transition out of Mode 3 propagates through a WODU → VOGA → WEGO → XYMU chain (Mode transitions carries the full chain timing table; this section introduces the signals).
| Gate | Role | Type | Clock / Trigger | Notes |
|---|---|---|---|---|
| XYMU | Rendering-mode latch (active-low Mode 3 indicator) | nor_latch | Set: WEGO; Reset: AVAP | Primary here; 23 downstream consumers |
| AVAP | Mode 2→3 trigger | not_x2 | Input: BEBU | See OAM scan for the detection chain |
| WODU | Mode 0 condition | and2 | Inputs: XENA, XANO | See STAT interrupts for the full breakdown |
| VOGA | H-Blank capture DFF | dffr | ALET | Data: WODU; reset: TADY |
| WEGO | XYMU set driver | or2 | Inputs: TOFU, VOGA | Drives XYMU's set input |
| TOFU | Video reset | not | Input: XAPO | Reset from LCDC disable / system reset |
| TADY | VOGA + PX-counter reset | nor2 | Inputs: TOFU, ATEJ | One gate drives both the VOGA H-Blank-capture reset and the PX counter reset; ATEJ shared with the scan counter reset chain (Mode transitions) |
Transitions:
- XYMU↓ (Mode 3 starts) at AVAP↑. AVAP drives XYMU's reset input.
- XYMU↑ (Mode 3 ends) at WEGO↑. WEGO drives XYMU's set input.
Signal path (Mode 3 end): WODU → VOGA → WEGO → XYMU
The WODU condition propagates through VOGA (an ALET-clocked DFF capturing WODU), WEGO (an OR2 combining VOGA with the TOFU video reset), and onto XYMU's set pin — 0.436 dots from WODU↑ to XYMU↑ (dmg-sim measurement): VOGA captures at 0.435, and the WEGO → XYMU gate adds the final 0.001.
Two chain members carry timing weight:
- WODU = AND2(XENA, XANO) — no sprite match active AND the pixel counter at terminal count: the H-Blank condition.
- VOGA captures WODU on the same-dot ALET rising edge: WODU rises during the ALET-low phase, and the rising edge within that same dot latches it (0.435 dots after WODU↑). Mode 3 therefore ends within the dot WODU fires.
WEGO's TOFU arm means an LCD disable also sets XYMU; TADY's ATEJ arm resets VOGA at every scanline boundary.
XYMU's 23 consumers
XYMU is an asynchronous NOR latch — no clock; its consumers see transitions after combinational delay only. The complete consumer set (netlist out-edges):
- Fetch cycle resets — LYZU, PYGO, RENE, PAHO, PUXA, RYFA (XYMU as reset input).
- Fine-scroll match reset — NYZE (XYMU as reset input).
- Rendering-gated paths — LOBY, PAHA (both =1 outside Mode 3; see BG pipeline for the PAHA polarity convention).
- VRAM access control — ROPY, XANE.
- OAM access control — AJON, ASAM, AZEM, BUZA.
- STAT mode bits — SADU, XATY.
- Sprite comparator resets — SEBA, TOBU, VONU.
- Fetch control — LURY, SUVU.
- Sprite Y-compare enable — TEPA.
Signal types in the mode-control web
The mode-control web mixes asynchronous latches with synchronous DFFs, and the distinction carries timing weight:
| Signal | Type | Async? | Notes |
|---|---|---|---|
| XYMU | NOR latch | Yes | Reset by AVAP (Mode 3 starts, XYMU↓); set by WEGO (Mode 3 ends, XYMU↑). |
| BESU (scan-active) | NOR latch | Yes | Set by start_oam_parsing at Mode 2 start; reset by ASEN (= OR2(ATAR, AVAP)) at Mode 2 end or video reset. Q=1 during Mode 2. |
| ROXY (fine scroll gate) | NOR latch | Yes | Set (suppress CLKPIPE) by PAHA; reset (enable CLKPIPE) by POVA |
| POKY (pixel pipe data ready) | NOR latch | Yes | Set (data ready) by PYGO; reset by LOBY |
| LONY (tile fetching) | NAND latch | Yes | Set by LURY, reset by NYXU |
| VOGA (H-Blank) | DFF (clk=ALET) | No | Synchronous capture of WODU on the same-dot ALET rising edge |
| RUTU (LINE_END) | DFF (clk=SONO) | No | Synchronous capture of SANU |
| POPU (VBlank) | DFF (clk=NYPE) | No | Synchronous capture of XYVO |
| MYTA (FRAME_END) | DFF (clk=NYPE falling) | No | Synchronous capture of NOKO |
The latches respond to their inputs with combinational delay only — they do not wait for a clock edge. The DFFs capture on specific edges, introducing pipeline delay. Most one-dot timing subtleties in the mode-transition chapters come down to exactly this split.
Registers
Three chapters cover the CPU's view of the PPU: this one (the register file — how reads and writes reach it, the palette latches, and LCDC's consumer fan-out), OAM and VRAM access (the lock logic), and OAM DMA (bus arbitration and conflicts).
The PPU's CPU-facing registers (LCDC, STAT, SCX, SCY, LY, LYC, BGP, OBP0, OBP1, WX, WY) are accessed via distinct paths for reads and writes:
- Reads: purely combinational — address decode plus the ungated
ppu_rdenables tri-state drivers from the underlying DFF Q outputs onto the bus. There is no read strobe; the CPU latches the bus on its own schedule. - Writes: PPU-side level-sensitive latches, transparent during the CUPA strobe — which fires once per M-cycle at a deterministic position, making every writable register transparent at the same time.
The asymmetry reflects which side is the receiver. Reads just drive the bus combinationally while the CPU requests the address (the CPU is the receiver); writes must strobe the PPU-side register latches closed on specific edges (the PPU is the receiver).
What's where
| Page | Covers |
|---|---|
| Reads | The combinational read path, the full per-register driver map, and the STAT driver-family split |
| Writes | The CUPA strobe, the phase generator behind it, and what a mid-rendering write affects |
| Palette latches | BGP/OBP0/OBP1's latch cells, the mid-Mode-3 write race, and the BGP OR-overlap |
| LCDC structure | LCDC's storage cells and the per-bit consumer index |
Reads
CPU reads of PPU registers use a purely combinational path from the underlying
DFF Q outputs to the CPU data bus, gated by address decode and the CPU's
ungated read signal ppu_rd. There is no PPU-side read strobe analogous to
CUPA (writes); the CPU latches the bus on its own schedule via
the SM83-internal data_phase signal.
- Reads are purely combinational — no PPU-side strobe, no latch
between a register's DFFs and the bus; the CPU samples at its own
data_phase. - Every $FF4x register reads through the same decode → AND2(
ppu_rd, select) → tri-state chain, netlist-enumerated for all twelve $FF4x registers (the eleven PPU registers plus DMA $FF46): ten on NOT_IF0 drivers, DMA on NOT_IF1, STAT split across both.
The read path
| Gate | Role | Type | Clock / Trigger | Notes |
|---|---|---|---|---|
ppu_rd | CPU read assert | not_x4 (buffered) | combinational | Buffer-chained from cpu_rd: ajas = NOT(cpu_rd); ppu_rd = NOT(ajas). No phase gating; asserts throughout the CPU's read cycle. |
ff4x | $FF4x range high-byte decode | not_x1 | combinational | Active-high when address[15:4] matches $FF4. Shared input to every $FF4x register address decoder. |
wyle | $FF44 address decode | nand5 | combinational | Inputs: ff4x + low-nibble inversions (xola, xeno, walo, xera). Active-low when address = $FF44. |
wofa | $FF41 address decode | nand5 | combinational | Inputs: ff4x + low-nibble inversions (wado, xeno, xusy, xera). Active-low when address = $FF41. Orthogonal to wyle on address bit 0 (wado = a0; xola = NOT(a0)). |
ff44 | $FF44 active-high | not_x2 | combinational | = NOT(wyle). Drives wafu. |
ff41 | $FF41 active-high | not_x1 | combinational | = NOT(wofa). Drives tobe. |
wafu | $FF44 read enable | and2 | combinational | Inputs: ppu_rd, ff44. Inverted by VARO for the drivers' active-low ena_n: 8 NOT_IF0 drivers from the LY DFFs (MUWY…LAFO) to CPU data bus d[0..7]. |
tobe | $FF41 read enable | and2 | combinational | Inputs: ppu_rd, ff41. Enables the NOT_IF1 drivers for STAT bits 0–2; its complement VAVE gates the NOT_IF0 drivers for bits 3–6 (the family split below). |
The $FF44 (LY) read, representative of the pattern:
- The CPU asserts
cpu_rdwith address $FF44 on its port. ppu_rdfollowscpu_rdthrough two inverter stages; no phase gating.- The address decoders fire:
ff4x(high-byte match) combines with the low-nibble decodewyleto driveff44active-high. wafu= AND2(ppu_rd,ff44) enables the 8 tri-state drivers from the LY DFFs (through VARO, the drivers' active-lowena_n).- Each NOT_IF0 driver presents its bit's current Q output onto the CPU data bus d[0..7] — a single NOT gate plus tri-state driver, all combinational.
- The CPU samples
cpu_port_d(connected to d[] via transmission gates) at the SM83-internaldata_phase— which is the phase generator's NOT(ATYP) window, clock-enable gated: BELU = NOR2(ATYP,clk_ena_n), inverted by BYRY and rebuffered by BEVA. It speaks the same AFUR-ring vocabulary as the write path'sclk_t4(writes); its falling edge — the CPU's capture point — is the ATYP rise at the M-cycle boundary.
The STAT family split
$FF41 (STAT) splits across both driver families (netlist-derived): the
live status bits read through NOT_IF1 drivers (enabled by
TOBE = AND2(ppu_rd, ff41)), the interrupt-enable bits through NOT_IF0
drivers (enabled by VAVE = NOT(TOBE)):
| Bit | Driver | Source | Family |
|---|---|---|---|
| 0 | TEBY | SADU | NOT_IF1 (TOBE) |
| 1 | WUGA | XATY | NOT_IF1 (TOBE) |
| 2 | SEGO | rupo_n | NOT_IF1 (TOBE) |
| 3 | PUZO | roxe_n | NOT_IF0 (VAVE) |
| 4 | POFO | rufo_n | NOT_IF0 (VAVE) |
| 5 | SASY | refe_n | NOT_IF0 (VAVE) |
| 6 | POTE | rugu_n | NOT_IF0 (VAVE) |
The split matches the driver-cell distinction: the transition-prone status bits carry the bus-settling x-window; the per-frame-stable enable bits read like LY, with none.
The full register read map
Every $FF4x PPU register follows the same chain, netlist-enumerated for
all twelve. Per register: a nand5
address decode (ff4x plus the four low-nibble address lines, true or
inverted to match the digit) feeds a not_x2 to make the active-high
select; an AND2 with ppu_rd forms the read enable; a not_x1 inverts it
for the drivers' active-low ena_n; eight NOT_IF0 drivers put the
register on the bus. Each driver's input is the latch's inverted output,
so the bus receives true polarity. The selects are shared with the write
path — the same cell feeds the write enable's AND2 with CUPA
(writes).
| Register | Decode → select | Enable | ena_n | Drivers d0 → d7 |
|---|---|---|---|---|
| LCDC $FF40 | WORU → VOCA | VYRE | WYCE | WYPO XERO WYJU WUKA VOKE VATO VAHA XEBU |
| STAT $FF41 | WOFA → VARY | TOBE | VAVE (bits 3–6) | TEBY WUGA SEGO PUZO POFO SASY POTE (split, no bit 7) |
| SCY $FF42 | WEBU → XARO | ANYP | BUWY | WARE GOBA GONU GODO CUSA GYZO GUNE GYZA |
| SCX $FF43 | WAVU → XAVY | AVOG | BEBA | EDOS EKOB CUGA WONY CEDU CATA DOXE CASY |
| LY $FF44 | WYLE → XOGY | WAFU | VARO | VEGA WUVA LYCO WOJY VYNE WAMA WAVO WEZE |
| LYC $FF45 | WETY → XAYU | XYLY | WEKU | RETU VOJO RAZU REDY RACE VAZU VAFE PUFY |
| DMA $FF46 | WATE → XEDA | MOLU | NYGO → PUSY (ena) | POLY ROFO REMA PANE PARE RALY RESU NUVY |
| BGP $FF47 | WYBO → WERA | VUSO | TEPY | RARO PABA REDO LOBE LACE LYKA LODY LARY |
| OBP0 $FF48 | WETA → XAYO | XUFY | XOZY | XARY XOKE XUNO XUBY XAJU XOBO XAXA XAWO |
| OBP1 $FF49 | VAMA → TEGO | MUMY | LOTE | LAJU LEPA LODE LYZA LUKY LUGA LEBA LELU |
| WY $FF4A | WYVO → VYGA | WAXU | VOMY | PUNU PODA PYGU LOKA MEGA PELA POLO MERA |
| WX $FF4B | WAGE → VUMY | WYZE | VYCU | LOVA MUKA MOKO LOLE MELE MUFE MULY MARA |
Two exceptions to the NOT_IF0 rule:
- STAT splits across both families (above).
- DMA ($FF46) reads through eight NOT_IF1 drivers — the same
active-high-enable family as STAT's live bits — via a double inversion
(MOLU → NYGO → PUSY) and fed by
dma_a15…a8, the DMA source-page register. The family membership has no observable consequence: those DFFs change only on an FF46 write, which cannot land inside an FF46 read M-cycle, so no transition can straddle a read. The x-window machinery stays exclusive to STAT bits 0–2.
Timing implications
- No within-dot ordering for reads. Unlike writes — which land at a
specific dot position gated by CUPA — reads are continuously enabled while
ppu_rdis asserted and the address decoder matches. A read is not an ordered event in the PPU's within-dot sequence. - No PPU-side sample point. What the CPU reads is the current DFF state at
the instant its
data_phasefires. Any update to the underlying DFFs (an LY reset via LAMA, a ROPO capture on TALU rising) propagates combinationally through the inverter and tri-state driver within gate delay — well under a dot. - CPU-side sampling phase.
data_phasegates the CPU's internal capture ofcpu_port_d. What the CPU observes when a PPU transition lands inside the read M-cycle is worked through, case by case, in CPU-visible timing at mode boundaries.
The structural takeaway: there are no latches or DFFs between a PPU
register's DFFs and the CPU data bus on the read side — the PPU drives the
bus combinationally while ppu_rd is asserted, and the CPU latches at its own
data_phase.
Writes
Reads drive the bus combinationally (reads); writes work the other way around — the PPU is the receiver, so the write must strobe the PPU-side register latches closed on specific edges. Every writable PPU register (LCDC, STAT, SCX, SCY, LYC, BGP, OBP0, OBP1, WX, WY) is a level-sensitive latch, transparent during the CUPA strobe — which fires once per M-cycle at a deterministic position, making every writable register transparent at the same time.
- One shared strobe: every writable PPU register is a level-sensitive latch enabled by AND(address decode, CUPA).
- CUPA rises at the start of T3 and falls mid-T4 — 1.493 dots wide; the latch is transparent for the whole pulse.
- What a mid-rendering write affects is decided by the bus-settle instant (~17 ns into the pulse), not by the window edges.
- An LCDC.7=1 write releases VID_RST 3,173 ps after CUPA↑ — purely combinational, no synchroniser — and the dividers count cleanly from zero, which is why the first scanline runs ≈454 dots.
| Gate | Role | Type | Clock / Trigger | Notes |
|---|---|---|---|---|
| CUPA | Shared PPU register write strobe | combinational (derived from phase gen) | — | Primary here; fires once per M-cycle, 1.493 dots wide. Its output wire is named ppu_wr in the simulation — ppu_wr elsewhere in this book is this strobe |
| DYKY | CUPA driver stage 1 | not | Input: TAPU | |
| TAPU | CUPA driver stage 2 | not | Input: UBAL | |
| UBAL | CUPA driver stage 3 | muxi | Test gate | |
| APOV | CUPA driver stage 4 | not | Input: AREV | |
| AREV | CUPA driver stage 5 | nand2 | Inputs: cpu_write, AFAS | |
| AFAS | Ring decode driving CUPA | nor2 | Inputs: ADAR, ATYP | High three ATAL half-cycles per M-cycle (1.504 dots measured) |
| AFUR | Phase generator ring stage 1 | dff | Driven by ATAL/ADEH | 4-stage ring (AFUR/ALEF/APUK/ADYK) |
| ALEF | Phase generator ring stage 2 | dff | Driven by ATAL/ADEH | |
| APUK | Phase generator ring stage 3 | dff | Driven by ATAL/ADEH | |
| ADYK | Phase generator ring stage 4 | dff | Driven by ATAL/ADEH | |
| WARU | LCDC write enable | and2 | Inputs: VOCA, CUPA | Enables XUBO/XURE |
| SEPA | STAT write enable | and2 | Inputs: VARY, CUPA | Enables RYVE/PUPU |
| VELY | BGP write enable | and2 | Inputs: WERA, CUPA | Enables TEPO/LYFA (palette latches) |
| ARUR | SCX write enable | and2 | Inputs: XAVY, CUPA | — |
| BEDY | SCY write enable | and2 | Inputs: XARO, CUPA | — |
| XODO | VID_RST signal | combinational | Input: XONA | Deasserts 3,173 ps after CUPA↑ on an LCDC.7=1 write |
| XONA | LCDC.7 storage latch | drlatch_ee | d: d7; ena: XURE/XUBO | q (ff40_d7) drives XODO/VID_RST on an LCDC.7 write |
The shared CUPA strobe
All CPU-writable PPU registers are level-sensitive latches (drlatch_ee or
dlatch_ee) whose enable is AND(address decode, CUPA):
- LCDC: WARU = AND2(VOCA, CUPA) → XUBO/XURE
- STAT: SEPA = AND2(VARY, CUPA) → RYVE/PUPU
- BGP: VELY = AND2(WERA, CUPA) → TEPO/LYFA
- SCX: ARUR = AND2(XAVY, CUPA)
- SCY: BEDY = AND2(XARO, CUPA)
- …and so on: every writable register shares CUPA, with per-register address decode.
Phase generator derivation
ADAR/ATYP ring decode → AFAS → AREV (with
cpu_write) → APOV → UBAL → TAPU → DYKY → CUPA
The phase generator is a 4-stage ring (AFUR/ALEF/APUK/ADYK) driven by ATAL/ADEH, the complementary 4 MHz enables from the clock tree. Each stage output is high for four consecutive ATAL half-cycles — 2 dots — per M-cycle (measured: AFUR 487,713 ps, ADYK 488,188 ps). The AFAS decode is high for three half-cycles per M-cycle — 367,065 ps = 1.504 dots, from the ALET rising edge of dot 2 to the ALET falling edge of dot 3 (dmg-sim measurement) — and CUPA follows it through the five-stage drive chain at 1.493 dots.
CUPA pulse position
Measured across three independent CUPA pulses with identical alignment (dmg-sim measurement):
- Rises at the start of T3, 4,848 ps after T3's ALET rising edge.
- Falls mid-T4, 1,716 ps after its ALET falling edge.
- Width: 364,298 ps = 1.493 dots.
The register latch is transparent for the entire pulse; bus data propagates through combinationally.
Alignment to TALU/SONO. One TALU cycle = one M-cycle = 4 dots. CUPA fires at the start of T3; the LX counter increments on TALU rising. That alignment decides which LX count a write lands on.
Behaviour on the write dot
While CUPA is high (T3–T4): the latch is transparent — combinational consumers and sampling DFFs see the new value as soon as the bus carries it. When CUPA falls (T4's ALET falling edge): the latch closes and holds.
The bus, not the strobe, sets the commit instant. The latch is open for
the whole 1.493-dot pulse, but the captured value follows the CPU data
bus — which settles ~17 ns in, just past the write-dot SACU↓. The palette
write shows the consequence: the BGP dlatch updates ~15.6 ns after that
pixel edge, so the pixel shifted on the write dot uses the OLD palette
value (palette latches). What a mid-rendering
write affects is decided by the bus-settle instant, not by the window
edges. (clk_t4, the CPU's bus-drive clock, is the
BAPY = NOR3(clk_ena_n, AROV, ATYP) decode — the same ring vocabulary as
CUPA.)
VID_RST deassertion
After a CPU write sets LCDC.7=1, the VID_RST signal (XODO) deasserts through a purely combinational chain (dmg-sim measurement):
- CUPA↑ → XONA↑ (+1,003 ps; the LCDC.7 bit captures) → XODO↓ (+2,170 ps further)
- Total CUPA↑ to XODO↓: 3,173 ps (~0.013 dots)
At the instant XODO deasserts, the divider toggle DFFs are in reset (q=0):
WUVU = VENA = BOGA = 0, with ATAL = ALET = 1. The combinational complements
settle within gate delays of the just-reset q_n outputs: TALU = NOT(vena_n)
= 0, XUPY = NOT(wuvu_n) = 0, SONO = NOT(TALU) = 1.
The dividers then count cleanly from zero (dmg-sim measurement):
| Event | Time after XODO↓ | Dots |
|---|---|---|
| WUVU.q↑ (toggle DFF) | +114,938 ps | +0.471 |
XUPY↑ (combinational from wuvu_n, +2,135 ps after WUVU.q↑) | +117,073 ps | +0.480 |
VENA.q↑ (toggle DFF, captures wuvu_n's first rise) | +360,224 ps | +1.476 |
TALU↑ (combinational from vena_n through the not_x4, +1,628 ps after VENA.q↑) | +361,852 ps | +1.483 |
VID_RST therefore clears within a single dot of the CPU's write; there is no multi-cycle synchroniser delay.
First-scanline consequence
The write's T3 landing plus the divider startup phase make the post-LCD-on first scanline run ≈454 dots instead of 456, the deficit entirely in its Mode 0 — Mode 2 and Mode 3 match steady state. The decomposition, the direct Mode-0-span measurement, and the phase-conservation proof live in LCD-on power-up.
Palette Latches
BGP, OBP0, and OBP1 commit through the same CUPA strobe as every PPU register (writes) — but a palette write landing mid-Mode-3 interacts with the pixel clock, and repeated writes additionally exhibit a below-netlist overlay at the glass (measured on BGP). This page holds the latch cells, the measured write race, and the OR-overlap rule.
- A mid-Mode-3 palette write reaches the latches after the write-dot SACU↓ (the data bus settles ~17 ns into the CUPA pulse): the pixel shifted on the write dot uses the old palette value.
- Second-or-later mid-Mode-3 BGP writes fire a one-column
new | oldOR-overlap at the glass — a below-netlist behaviour with a BESU-reset recovery state and a visible-emission clause. - The OR overlap is measured on BGP; no available OBP transition can discriminate, so its scope across the other palettes is open.
The latch cells
24 registered cells (8 per palette: BGP, OBP0, OBP1) hold the palette
mappings. Each is a dlatch_ee (level-sensitive latch) that captures CPU
data-bus writes via the shared CUPA strobe
(register writes); the write enable is a
per-register AND of address decode and CUPA — for BGP, VELY = AND2(WERA,
CUPA). During normal rendering with no active CPU write, palette values are
stable and contribute no per-dot races.
| Gate | Role | Type | Clock / Trigger | Notes |
|---|---|---|---|---|
| BGP (8 × dlatch_ee) | BG palette | dlatch_ee | Enable: VELY = AND2(WERA, CUPA) | Per-bit cells not individually named here |
| OBP0 (8 × dlatch_ee) | Sprite palette 0 | dlatch_ee | Enable: per-register AND with CUPA | — |
| OBP1 (8 × dlatch_ee) | Sprite palette 1 | dlatch_ee | Enable: per-register AND with CUPA | — |
| TEPO | BGP write enable sub-signal | combinational | Inputs: VELY chain | Fires during a CUPA write to BGP |
| PERO | Palette-output multiplexer signal | combinational | Inputs: palette outputs | Feeds the LCD-output path (LCD output) |
The mid-Mode-3 palette-write race
Measured across 7,621 mid-Mode-3 BGP writes (dmg-sim measurement,
purpose-built bgp_steady ROM; zero variance per write direction). Event ordering relative to the CUPA rising edge (the
dot's ALET rising edge plus 4,848 ps of phase-generator propagation):
| Δ from CUPA↑ (ps) | Event |
|---|---|
| +0 | CUPA↑ (bus write strobe begins) |
| +1,306 | TEPO↓ (BGP write enable opens the latches) |
| +1,320 | SACU↓ (the write dot's pixel clock falls) |
| +16,870 / +17,060 | BGP dlatch output changes (per write direction) — the CPU data bus settles here, past the pixel edge |
| +18,510 | PERO changes |
| +19,464 | REMY (LD0) changes (RAVO/LD1 likewise when the transition flips it) |
| +129,254 | Next SACU↑ |
The dlatch output lands ~15.6 ns after the write-dot SACU↓ — not a marginal race: the latch opens early (TEPO↓), but the captured value follows the CPU data bus, which settles in the second half of the CUPA window. The pixel being shifted out on the write dot therefore uses the OLD palette value, structurally; the new value first affects the pixel shifted on the next SACU rising edge, +129,254 ps after CUPA↑.
The LCD-side pixel-emission edge that captures each pixel — cp_pad↑,
which lags SACU↑ by 1,019 ps combinationally
(LCD output) — frames the same fact from the glass side:
the OLD-palette pixel emits at the cp_pad↑ 0.466 dots before the write
CUPA↑, and the NEW-palette pixel emits at the cp_pad↑ 0.534 dots after
it, within the same ALET cycle as the write. No further latching exists
between PERO/RAVO/REMY and the pad.
Outside Mode 3 (LCD off, H-Blank, VBlank, Mode 2), SACU is static and palette changes propagate combinationally with no pixel-clock interaction — the write is immediately visible on the next pixel when rendering resumes.
The BGP OR-overlap rule
The single-write race above is the netlist-level story, and the gate-level simulation reproduces it exactly for isolated mid-Mode-3 writes. At the LCD interface, however, BGP writes additionally exhibit an OR-overlap behaviour that is below the netlist's modelling scope — it was characterised against hardware reference photographs, not by gate-level measurement. The behavioural rule, with its empirically-tightened scope:
When a BGP CUPA↑ satisfies BOTH (a) it is not the first BGP CUPA since the most recent BESU↑ AND (b) at least one visible
cp_pad↑ has elapsed since that prior BGP CUPA, the firstcp_pad↑ after the new CUPA (the same-dot late-half emission edge at +0.534 dots after CUPA↑) samples a palette value equivalent toBGP_new | BGP_oldrather thanBGP_newalone. All subsequentcp_pad↑ events sampleBGP_newdirectly. When either clause fails — the CUPA is the first BGP write since BESU↑, or no visiblecp_pad↑ has occurred since the prior write — the firstcp_pad↑ after the new CUPA samplesBGP_newcleanly.A visible
cp_pad↑ fires during ST_pad=0 — it shifts a real LCD column. The POVA-driven pulse at AVAP+6.5 dots and the fine-scroll startup pulses fire under ST_pad=1 (LCD output); neither engages the rule.The prior-write tracking resets at each BESU↑ (Mode 2 entry, OAM scan) and persists through H-Blank and VBlank — a late-frame write stays primed until the next frame's first Mode 2.
When NEW ⊇ OLD bitwise, OR = NEW and the rule is invisible. When the
write clears bits, the first post-write column emits with OLD's cleared bits
re-added — a one-column transient before the new palette takes effect.
What the rule predicts
The rule reproduces every hardware reference image across three suites —
DMG photographs from the Mealybug Tearoom suite, Gambatte's
dmgpalette_during_m3 family, and AGE's m3-bg-bgp (the last
hardware-verified on a DMG-CPU C):
Per-test validation
Per-test validation
| Test | Write structure | Outcome under the rule |
|---|---|---|
Mealybug m3_bgp_change, LY ≥ 1 | W1 in Mode 2; W2…W7 in Mode 3 | W1 clean; W2…W7 fire OR (both clauses hold). Matches the reference's 13-column transients: W3/W5/W7 clear bits — visible; W2/W4/W6 OR ≡ NEW — invisible |
Gambatte dmgpalette_during_m3_{3,4,5} | Setup writes pre-LY=0 (no BESU↑ between), then two Mode-3 writes per scanline | LY=0's BESU↑ resets the state: each scanline's first write is clean (clause (a) fails), the second fires OR — discriminating both the every-CUPA and the no-BESU-reset scopes |
AGE m3-bg-bgp, LY=9 (SCX=1) | W1 pre-ST_pad↓ (AVAP+3.50); W2 at AVAP+15.50 — 707 ps (0.003 dots) after ST_pad↓, the column-0 emission dot | W1 clean. W2 satisfies (a) but fails (b): zero visible cp_pad↑ between W1 and W2 (the whole window lies in ST_pad=1). W2 samples NEW; reference column 0 = shade 0 |
AGE m3-bg-bgp, LY=21 (SCX=5) | Same, shifted +4 dots (one extra NOP; SCX=5 adds 4 dots of fine-scroll gating) | Same: W2 fails (b); column 0 = shade 0 |
AGE m3-bg-bgp, LY ∈ {8, 10–20, 22–79} | W1 pre-column-0; W2 after the first visible cp_pad↑ | W1 clean; W2 fires OR post-column-0. Reference: column 0 = shade 3 (bare W1), then the one-column transient |
Mealybug m3_obp0_change | Single OBP0 write, OR ≡ NEW ($04 → $0C) | Cannot discriminate — OR ≡ NEW either way; the OBP scope question stays open (below) |
Evidence
Primary evidence
Primary evidence
- dmg-sim measurement pins the netlist-level invariants: 12.000-dot
CUPA-to-CUPA spacing (Mealybug
m3_bgp_change), write-invariant CUPA→dlatch latency (16,870/17,060 ps — the bus-settle instant, not the enable), uniform +0.534-dot CUPA→cp_pad↑ latency, and no per-dot race in the BGP-to-cp_padchain (all 24 palette dlatch races are per-frame in the static analysis). Isolated-write tests match at LCD-column resolution. The OR overlap is structurally absent from the netlist at every timing calibration — which is what places the mechanism beyond the die model. - The reset is BESU↑ (Mode 2 entry), not Mode-3 entry (AVAP) or Mode-3
exit (WODU): Mealybug
m3_bgp_changeLY ≥ 1 fires the OR overlap on a W1→W2 pair that spans Mode 2 → Mode 3 (an AVAP reset would miss it), and the Gambatte_3/_4/_5setup writes persist through VBlank into LY=0's first Mode-3 write (a WODU reset would clear them). BESU↑ satisfies both. - The overlap also requires a visible
cp_pad↑ between the two writes (AGEm3-bg-bgp): the per-scanline SCX=LY−X handler shifts W2 by one dot per SCX step, and at LY=9 and LY=21 — where W2 lands on the column-0 emission dot with no visiblecp_pad↑ between W1 and W2 — the hardware reference shows clean NEW (so the condition is the interveningcp_pad↑, not merely "second-or-later since BESU↑"). Edge timing: W2 CUPA↑ at AVAP+15.4962 dots, ST_pad↓ at +15.4933, first visiblecp_pad↑ at +16.0444,ld_pad[1:0]= 0b00 (shade 0) at the column-0 sample edge.
Mechanism
The BESU↑ reset — not every CUPA, not Mode 3 exit — extends the recovery
duration to roughly a scanline including H-Blank. That is too long for
intra-cell driver-vs-keeper contention at the BGP dlatch, which would settle
within nanoseconds. The state is therefore best placed downstream of the
die, at the LCD glass's column shift register: while the pipe actively
shifts a cp_pad↑ each dot, the downstream state stays primed for
OR-overlap; H-Blank silences the shift activity; the next BESU↑ marks
resumption with fresh state.
The AGE evidence sharpens this: the primed state engages on actual
sampling of a value into a visible LCD column, not on the CUPA write
itself — cp_pad↑ events during ST_pad=1 do not drive the glass and do not
engage it. The LD0/LD1 pad drivers are ruled out as the home of the state:
the pad path is combinational and measured stateless. Discriminating the
mechanism fully needs hardware capture of LD0/LD1 and CP across BGP writes
spanning H-Blank; the behavioural rule above is sufficient for emulation
regardless.
Per-palette scope
The OR overlap is measured on BGP. Whether it extends to OBP0/OBP1 is untested: the only available mid-Mode-3 OBP transition has OR ≡ NEW, which cannot discriminate. Apply the rule to BGP, and treat OBP0/OBP1 as following the bare single-write race until a discriminating test exists — noting that the working mechanism hypothesis (glass-side state, above) contains nothing BGP-specific.
LCDC Structure
LCDC's 8 bits are stored as drlatch_ee cells sharing the CUPA write
strobe (writes). Each bit has a distinct consumer path in a
distinct mode; this page is the index. LCDC storage lives here —
consumer-path detail lives with the mode that consumes the bit.
- Every LCDC consumer path is combinational from the latch — there is no clocked stage between an LCDC cell and its consumer.
- The static race analysis classifies every LCDC cell as per-frame domain (diff = 59.6 ge from the CPU data bus through the enable chain): stable absent CPU writes, no per-dot races.
| LCDC bit | Gate / wire name | Consumer | Chapter |
|---|---|---|---|
| 0 (BG_AND_WIN_EN) | VYXE / LCDC_BGENp | BG/window pixel plane gate (RAJY/TADE); mid-Mode-3 toggle behaviour | LCD output |
| 1 (OBJ_EN) | XYLO / LCDC_SPENp | Sprite trigger (AROR → per-sprite NAND3 decode) | Sprite pipeline |
| 1 (OBJ_EN) | XYLO / LCDC_SPENp | Pixel MUX gates (XULA, WOXA) | Sprite pipeline |
| 2 (OBJ_SIZE) | XYMO / LCDC_SPSIZEp | Y-compare decoder (GOVU) — Mode 2 | OAM scan |
| 2 (OBJ_SIZE) | XYMO / LCDC_SPSIZEp | Sprite tile VRAM ma4 (GEJY) — Mode 3 | Sprite pipeline |
| 3 (BG_MAP) | XAFO / LCDC_BGMAPp | BG tilemap address during fetch counter=0 | BG pipeline |
| 4 (TILE_SEL) | WEXU / LCDC_BGTILEp | BG tile-data address at counter=2/4 | BG pipeline |
| 5 (WIN_EN) | WYMO / LCDC_WINENp | Window enable (XOFO → PYNU.r) | Window control |
| 6 (WIN_MAP) | WOKY / LCDC_WINMAPp | Window tilemap address at counter=0 | BG pipeline |
| 7 (LCDEN) | XONA / LCDC_LCDENp | VID_RST gate (LCDC.7 → XONA → XODO) | Register writes |
| 7 (LCDEN) | XONA / LCDC_LCDENp | CPL/FR audio/PPU-clock mux (KAHE, KUPA AO22) — all modes including LCD-off | LCD output |
A CUPA write mid-rendering transitions the latch output within the CUPA pulse window (1.493 dots spanning T3–T4), and consumer effects propagate combinationally. Per-bit sampling cadence within a fetch cycle, and mid-Mode-3 write behaviour, are detailed in the consumer chapters.
LCDC.7 has two distinct consumer paths: the VID_RST gate (reset-and-enable for the PPU's internal machinery) and the CPL/FR mux selector — the AO22 mux that keeps the LCD's column-alternator pins toggling on APU clocks while the LCD is off, protecting the glass from DC bias.
OAM and VRAM Access
The registers chapter covered CPU writes to PPU-internal register latches — all sharing the CUPA strobe. CPU writes to OAM ($FE00–$FE9F) and VRAM ($8000–$9FFF) follow distinct paths, because the storage is not in the PPU register file: OAM is an on-chip dual-port SRAM with its own bus arbitration, and VRAM is off-chip, with address/data/strobe pads driven by the PPU through tri-state enables. Both paths gate the write enable on the rendering-mode signals so that CPU writes during the corresponding lock window never reach the storage.
Which windows are locked is well-trodden behavioural ground (Pan Docs covers the accessibility rules); this chapter documents the gates that enforce them, and what those gates imply at the window boundaries.
- The two locks are structurally different: OAM gates the write strobe on-chip (AJUJ → AMAB → WYJA); VRAM tri-states the off-chip address pads and write strobe (ROPY/XANE) — there is no missed-write latch on either side.
- The OAM write window is the CUPA pulse — the
ppu_wrnet is the CUPA cell's output — gated by the AJUJ/AMAB permit; only VRAM's strobe is assembled separately, from the SM83 bus-phase signals. - Lock transitions are M-cycle-quantised against CPU writes — a write window is fully open or fully closed; outcomes flip exactly one M-cycle apart, never partially.
- A mode transition inside the write window is the real boundary case: all three OAM-side straddles are characterised (CPU-visible boundaries).
- Any CPU bus cycle addressing $FExx during Mode 2 suppresses one OAM bitline-precharge beat — the next row's wordline opens onto the previous row's charge: the OAM corruption bug, measured as an exact 8-byte row copy.
What's where
| Page | Covers |
|---|---|
| OAM writes | The on-chip write-strobe chain (AJUJ → AMAB → WYJA), the CUPA-pulse window, and the per-byte strobes |
| VRAM writes | The off-chip pad path, the Mode-3 tri-state lock, and the SM83 bus-phase strobe |
| Lock boundaries | M-cycle-quantised lock transitions, and the straddle cases at a mode edge |
| OAM corruption | The suppressed-precharge bug — a $FExx access in Mode 2, measured as an exact 8-byte row copy |
OAM Writes
A CPU write to $FExx with cpu_wr=1 reaches OAM through an on-chip
arbitration chain combining the address decode fexx, the rendering-mode
signals (dma_run, mode 2, mode 3), and the DMA write-path arm POWU. The
combined enable WYJA drives per-byte write strobes — YNYC (B side) and YLYC
(A side) — which become the active-low pad outputs oam_b_wr_n /
oam_a_wr_n to the OAM SRAM cells.
| Gate | Role | Type | Inputs | Notes |
|---|---|---|---|---|
| FEXX | $FExx address-range decode | not_x2 | = NOT(ROPE) | Active-high while cpu_port_a is in $FE00–$FEFF |
| AJON | Mode 3 + non-DMA | and2 | mode3, BOGE | BOGE = NOT(dma_run). AJON=1 only during Mode 3 with DMA inactive |
| AJUJ | OAM-access permit | nor3 | dma_run, mode2, AJON | AJUJ=1 only when DMA is inactive AND neither Mode 2 nor Mode 3 is active |
| AMAB | OAM CPU-write address gate | and2 | fexx, AJUJ | AMAB=1 only when the CPU addresses $FExx during a permitted window |
| WYJA | OAM write enable (combined) | ao21 | AMAB, ppu_wr, POWU | WYJA = (AMAB AND ppu_wr) OR POWU — ppu_wr is the CUPA cell's output net, so the CPU arm's window is the CUPA pulse; the DMA arm bypasses CPU arbitration |
| YLYC / YNYC | OAM A-side / B-side write strobe drivers | per-byte selector | WYJA + per-byte address decode | Drive the active-low oam_a_wr_n / oam_b_wr_n pad outputs |
Lock-window behaviour. The permit AJUJ decides the outcome:
| State | Permit | Outcome |
|---|---|---|
| Outside Mode 2/3, no DMA | AJUJ=1, AMAB=1 | WYJA passes ppu_wr — write lands |
| Mode 2 | mode2=1 → AJUJ=0 | blocked (BESU gates OAM — OAM scan) |
| Mode 3 | XYMU.q=0 → AJON=1 → AJUJ=0 | blocked |
| DMA | dma_run=1 → AJUJ=0 | CPU blocked; POWU drives WYJA with DMA-source data (DMA) |
CUPA-vs-mode-edge alignment. A write landing in a stable open window
(mode2 = mode3 = 0 across the M-cycle) passes ppu_wr's pulse through WYJA
cleanly; one landing in a stable locked window is blocked (AJUJ=0 holds
WYJA=0). The narrower case — a mode-bit transition inside the write
window — is real and measured: the Mode 2→3 straddle opens the AJUJ permit
for a 6,502 ps window during the AVAP cascade, worked through in
CPU-visible timing at mode boundaries.
VRAM Writes
VRAM is off-chip; the CPU's address, data, and write-strobe outputs reach the
VRAM SRAM through dedicated pads (ma_pad[12:0], md_pad[7:0], mwr_n_pad,
mrd_n_pad, mcs_n_pad) whose tri-state enables are gated on mode 3. There
is no on-chip "VRAM write enable latch" analogous to OAM's WYJA; the Mode 3
lock is enforced by tri-stating the address bus and write strobe so the
off-chip SRAM never sees a valid write cycle.
| Gate | Role | Type | Inputs | Notes |
|---|---|---|---|---|
| ROPY | Mode 3 inverter for VRAM enables | not_x1 | mode3 | ROPY = NOT(mode3); high outside Mode 3 |
| XANE | VRAM address tri-state-disable source | nor2 | vram_to_oam, mode3 | XANE=1 when neither VRAM-sourced DMA nor Mode 3 holds — the CPU may drive the address pads |
| XEDU | VRAM address tri-state enable | not_x2 | XANE | XEDU = NOT(XANE); drives the not_if0 ena_n of the ma_pad[*] address pads |
| TOLE | CPU data-bus enable stage 1 | combinational | CPU bus enables | Carries CPU-side write-window timing |
| SERE | CPU data-bus enable stage 2 | and2 | TOLE, ROPY | Active only when CPU drive is enabled AND outside Mode 3 |
| ROCY / RAHU / SAZO / REVO | Per-bit data-pad tri-state enable chain | combinational | SERE + per-bit fan-out | Drive not_if0 ena_n on md_pad[*] |
| SOHY | Write-strobe driver | nand2 | TYJY (CPU bus-phase write enable), SERE | Drives mwr_n_pad low when both arms hold |
| TAXY / SYSY / RACO / TUTO | CPU-bus-phase strobe assembly | combinational | T1T2_n / SOTO_n / TYJY chain | Generate the CPU-side write window feeding SOHY |
mwr_n_pad | Off-chip write strobe | tri-state pad output | SOHY | Active-low; visible to the off-chip VRAM SRAM |
ma_pad[12:0] | Off-chip address pads | tri-state pad outputs | XEDU enable; CPU bus address | Driven when XANE=1 |
md_pad[7:0] | Off-chip data pads | tri-state pad outputs | SERE chain enable; CPU bus data | Driven when SERE=1 |
Lock-window behaviour.
- Outside Mode 3 — ROPY=1, XANE=1, XEDU=1 → address pads driven from
the CPU bus; SERE=1 (when CPU drive is active) → data pads driven and
SOHY may pulse
mwr_n_padlow. The off-chip SRAM sees a valid write cycle. - During Mode 3 — ROPY=0 → SERE=0 (data pads tri-stated) and SOHY held high (no strobe); independently XANE=0 → XEDU=0 (address pads tri-stated). The SRAM never sees the write: both address and strobe are absent, so there is no "missed write" latch to need — the strobe simply never asserts.
CUPA is not involved here. Unlike the OAM path — whose write window is
the CUPA pulse via ppu_wr — the VRAM-write window is set by the SM83
bus-phase signals (T1T2_n, SOTO_n, TYJY) feeding TAXY/SYSY/SOHY. The mode-3-gated tri-state enables are
the lock. If they are stable across the CPU's write window, the strobe
propagates cleanly; if mode 3 transitions during the window, the address
pads and strobe tri-state mid-cycle, truncating the write at the pins.
What the external SRAM does with a truncated cycle is off-die and out of
scope.
Read Locks
While the PPU owns OAM or VRAM, a CPU read of the locked region returns 0xFF — not the stored byte. The lock disables the array's bus drivers, so the read cycle still fires but nothing drives the bus; it settles to its keeper value. The 0xFF is a floated bus, not a driven constant.
- OAM reads return 0xFF in Mode 2 and Mode 3; VRAM reads return 0xFF in Mode 3. Mode 0 and VBlank read the stored byte.
- The lock acts on the address / data tri-state drivers, not the read
strobe —
asam(OAM) andropy= NOT(mode3) (VRAM) gate the drivers off while the CPU read decode still pulses every cycle. - The VRAM lock engages exactly at the Mode 2→3 boundary (
mode3↑).
What a locked read returns
| Read | Mode 0 / VBlank | Mode 2 | Mode 3 |
|---|---|---|---|
OAM ($FE00–$FE9F) | stored byte | 0xFF | 0xFF |
VRAM ($8000–$9FFF) | stored byte | stored byte | 0xFF |
Measured on the full-SRAM model (dmg-sim, purpose-built read_lock ROM:
OAM pre-filled 0x42, VRAM 0x24). Every Mode-2 and Mode-3 OAM read and every
Mode-3 VRAM read returned 0xFF — never the pre-fill — while a Mode-2 VRAM
read returned 0x24. The VRAM byte flips 0x24→0xFF across the single read
step straddling mode3↑: the lock engages on the Mode-2→3 edge. Mode-0 and
VBlank reads return the stored byte because the same gates go inactive
there (below).
Disabled drivers, not a blocked strobe
The CPU read strobe fires on every read regardless of mode (cpu_oam_rd_n
= NAND3(~clk_t4, FEXX, ppu_rd); the VRAM read-enable toggles each
clk_t4). What the lock removes is the drive onto the bus:
| Gate | Role | Type | Logic |
|---|---|---|---|
| ASAM | OAM CPU-address disable | or3 | oam_addr_cpu_n = OR(mode2, mode3, dma_run) — high disables the eight OAM-address tri-state drivers |
| WAME | OAM output-enable inverter | not_x2 | oam_oe_n = NOT(oam_oe); OAM's output enable is steered to the PPU parser during Mode 2/3 |
| ROPY | Mode 3 inverter for VRAM enables | not_x1 | = NOT(mode3); high outside Mode 3, low in Mode 3 — closes the VRAM read latch via SERE → SEBY |
| SEBY | VRAM read-latch enable | not_x1 | = AND(SERE, cpu_vram_oam_rd), SERE = AND(tole, ROPY); gated low in Mode 3, so the VRAM byte is never latched onto the internal bus |
With the OAM-address drivers disabled (ASAM high) the CPU address never
reaches the OAM array; with the VRAM read latch closed in Mode 3 (ROPY low →
SEBY low) the VRAM byte never reaches the CPU bus. Either way the bus holds
its keeper value and the CPU reads 0xFF. In Mode 0 and VBlank asam goes
inactive and ropy goes high, the drivers turn back on, and the read
returns the stored byte — the mirror of the write lock, where the same
windows block the strobe (lock boundaries).
Lock Boundaries
Away from mode boundaries both enable chains are stable, so a CPU write
sees a fully-open or fully-closed window — never a partial one. Outcomes
flip exactly one M-cycle apart across a lock transition (dmg-sim
measurement, gbmicrotest oam_write_l0_* / vram_write_l[01]_*
families).
The exception is a write whose strobe straddles a boundary transition. On the OAM side all three boundaries are characterised in CPU-visible mode boundaries: a Mode 2→3 straddle lands through the ~6.5 ns permit gap, a Mode 3→0 straddle lands the moment the permit opens, and Mode 0→2 closes cleanly — the write is blocked. The VRAM-side straddle reaches the off-chip SRAM as a partial cycle, outside the netlist's scope (above; Appendix C).
OAM Corruption
The widely documented bug: a CPU bus cycle whose address lands in $FE00–$FEFF during Mode 2 (classically a 16-bit INC/DEC, whose IDU cycle drives the register pair onto the bus) corrupts the OAM row under scan. The mechanism is a suppressed bitline precharge, measured end to end on the full-SRAM simulation model.
FEXX → CUFE → BYCU → COTA → WOVU →
oam_bl_precharge_nThe $FExx address decode (FEXX) forces CUFE low for the bus-phase half of the M-cycle; BYCU sticks high, COTA sticks low — and COTA's complement WOVU is the OAM bitline precharge. The beat that should separate two scan rows never fires.
| Gate | Role | Type | Logic |
|---|---|---|---|
| FEXX | $FExx address-range decode | not_x2 | = NOT(ROPE); ROPE = NAND2(SOHA, RYCU) |
| CUFE | $FExx/DMA bus-cycle term | oai21 | = NOT((FEXX OR dma_run) AND dma_phi_n) — low for the bus-phase half-M-cycle of any $FExx access |
| BYCU | COTA driver | nand3 | Inputs: CUFE, XUJY, AVER |
| COTA | Stage-2 capture enable / bitline-precharge source | not_x1 | = NOT(BYCU); gates the Stage-2 latches (OAM scan) |
| WOVU | OAM bitline precharge driver | not_x2 | oam_bl_precharge_n = NOT(COTA) |
| WAHE / WUJY | OAM wordline-driver precharge | not_x3 / nand2 | WUJY = NAND2(WAZO, oam_bl_precharge_n) — not suppressed by the bug |
Measured timeline (dmg-sim measurement, purpose-built oam_corrupt_sweep
ROM on the full dmg_generic_sram OAM model; INC HL with HL = $FE00
landing in LY=40's Mode 2; t = 0 at the INC's bus cycle):
| t (ns) | Event |
|---|---|
| ~5 | FEXX↑ — $FE00 decodes |
| ~106 | the in-flight precharge beat completes normally; row 11's second wordline epoch opens |
| ~347 | CUFE↓ (the dma_phi_n phase gate arrives) |
| ~470 / ~590 | AVER pulses for the next scan step — BYCU and COTA do not respond; no precharge beat fires |
| ~593 | row 12's wordline opens onto bitlines still carrying row 11's read charge — the cells are overwritten |
| ~835 | CUFE↑ (bus phase ends, exactly 2 dots low) |
| ~961 | precharge beats resume; row 12's second epoch is clean — the damage is already committed |
Three structural facts fall out:
- No write strobe is involved —
oam_a_wr2/oam_b_wr2never assert. An open wordline with defined (un-precharged) bitline charge overwrites the cells; that is SRAM physics, faithfully modelled. - The wordline side is untouched — WAHE keeps its cadence and exactly one wordline is enabled at every instant (no multi-wordline epochs). Only the bitline precharge is gated through COTA.
- The row granularity is 8 bytes by construction: one wordline row spans 4 columns in each of the two banks.
The corrupted row is the one whose first wordline epoch falls inside the 2-dot CUFE-low window; the source data is whatever the bitlines last carried — the previous row's read. Measured across a 6-frame ladder stepping the INC one M-cycle per frame: each frame's VBlank readback shows exactly one full 8-byte row copy (row 12 ← row 11, then 13 ← 12, … 17 ← 16), with every other OAM byte untouched.
Outside Mode 2 there are no scan epochs to corrupt — AVER carries the scan-phase term, and the same CUFE gating during DMA coincides with the scan being disabled entirely (DMA).
Community documentation catalogues further corruption at other bus/scan
alignments — first-word AND/OR mixes rather than the clean copy above — and
the blargg oam_bug suite pins them on hardware. These are the same
suppressed-precharge mechanism: where a fully-settled read resolves as a clean
copy, a partially-driven bitline resolves bitwise. Sweeping the row
alignment, the full-SRAM model reproduces the clean copy (single-access) and
the multi-row case (two reads, e.g. POP), but never the bitwise mix — a
digital SRAM cell cannot partially retain its old value. That mix is analog
bitline physics below the netlist's reach, so the hardware-validated community
formulas remain its reference.
Model the measured case — a $FExx-addressed CPU bus cycle in Mode 2 copies the previous row's 8 bytes over the row whose scan begins during that cycle — and defer to the community first-word formulas for the alignment-dependent mixes, which capture analog behaviour the digital model does not.
OAM DMA
OAM DMA copies 160 bytes from a source page to OAM at one byte per M-cycle. The behavioural surface — the source-region quirks (gb-ctr covers the address decoding), and the transfer timing and "CPU should stay in HRAM" rule (Pan Docs) — is documented elsewhere. What this chapter adds is the arbitration machinery: who owns each bus during DMA, what the PPU's scan and fetch hardware does in the meantime, what happens at the engage/release boundaries to the dot, and what the CPU actually observes when it touches a bus DMA is driving.
- During DMA the OAM address bus has exactly one driver: DMA. The PPU's scan and fetch machinery keeps running — only its bus drivers tri-state — which is the strikethrough mechanism.
- The scan chain's capture enable is fully gated off during a DMA + Mode-2 overlap: Stage 1 holds the pre-DMA byte-pair, producing phantom sprites when the held Y matches.
- DMA pauses during HALT (its clock is pinned) but
dma_runkeeps owning the buses for the whole span. - Engage takes the bus on the same master-clock edge as
dma_run↑; release waits at most one scan-cadence period (≤2 dots). - A CPU access to a DMA-driven bus has its address replaced at the pads; HRAM is exempt. Sources $FE/$FF read WRAM — the echo region extended one page.
What's where
| Page | Covers |
|---|---|
| The OAM bus under DMA | Four-source address arbitration, the per-T-cycle byte transfer, the strikethrough effect, the two-bank SRAM, and the scan-chain capture hold |
| Engage, release, and HALT | The FF46→dma_run latency, DMA-start and DMA-end cadences against Mode 2, the dot-80 visibility boundary, and the HALT pause |
| The source bus and conflicts | Address-pad MUXing, CPU read/write conflicts byte by byte, the HRAM exemption, and the $FE/$FF source fold into WRAM |
The OAM Bus
Four sources can drive the OAM address bus; DMA preempts the other three without ever contending the bus. Meanwhile the PPU's scan machinery keeps running blind — and what it holds (not captures) during the overlap is the mechanism behind the strikethrough and phantom-sprite artefacts.
- Arbitration is symmetric and clean: across every PPU-mode ×
dma_runcombination, exactly one of the four drive-enables is asserted. - Both PPU-side drivers tri-state via the same upstream gate
(BOGE = NOT(
dma_run)); the CPU path is gated in ASAM's OR3. - OAM is a two-bank SRAM: every read presents an aligned (even, odd) byte-pair; a0 matters only for writes.
- While DMA holds the bus, no capture path can fire — the Stage-1 latches hold the pre-DMA byte-pair while the scan counter runs, filling the sprite store with phantom copies if the held Y matches. A DMA that ends mid-scan lets the entries scanned afterward capture fresh OAM.
OAM address-bus arbitration
Four sources drive the bus through active-low tri-state enables; at any instant exactly one is asserted.
| Source | Drive-enable signal | Driver gate | Asserted when |
|---|---|---|---|
| CPU port | oam_addr_cpu_n | ASAM (or3) | OR3(mode2, mode3, dma_run) = 0 (= AJUJ = 1, lock open) |
| DMA | oam_addr_dma_n | DUGA (not_x2) | dma_run = 1 |
| OAM scan (Mode 2) | oam_addr_parse_n | APAR (not_x2) | mode2 = 1 |
| Sprite fetch (Mode 3) | oam_addr_render_n | BETE (not_x1) | AJON = 1 |
The CPU enable carries all three PPU-state conditions in ASAM's OR3; the
parse and render enables terminate in their mode-nets, themselves gated
through BOGE = NOT(dma_run) upstream:
- Parse: mode2 = AND2(BOGE,
oam_parsing) (ACYL) — DMA forces mode2 = 0 regardless of BESU; APAR tri-states the parse driver. - Render: AJON = AND2(mode3, BOGE) — DMA forces AJON = 0 regardless of XYMU; BETE tri-states the render driver.
| PPU mode | dma_run | CPU | DMA | Parse | Render | Bus owner |
|---|---|---|---|---|---|---|
| Mode 0 / VBlank | 0 | drive | — | — | — | CPU |
| Mode 0 / VBlank | 1 | — | drive | — | — | DMA |
| Mode 2 | 0 | — | — | drive | — | Parse |
| Mode 2 | 1 | — | drive | — | — | DMA |
| Mode 3 | 0 | — | — | — | drive | Render |
| Mode 3 | 1 | — | drive | — | — | DMA |
Every cell has exactly one driver — the bus is never contended (dmg-sim
measurement, Hacktix strikethrough ROM: with dma_run=1 and BESU.q=1
mid-scan, mode2 = 0, only the DMA driver active).
Per-T-cycle structure of the byte transfer
flowchart LR
UVYT["UVYT<br>dma_phi = NOT(data_phase)"] -- "clk" --> MATU["MATU<br>dma_run + byte counter"]
MAKA["MAKA<br>dffr · ppu_4mhz"] --> NAXY["NAXY<br>NOR(dma_phi, MAKA)"]
UVYT --> NAXY
NAXY --> POWU["POWU<br>AND(dma_run, NAXY)"]
MATU --> POWU
POWU --> WYJA["WYJA → YLYC / YNYC<br>OAM write strobes"]
dma_run (the M-cycle-stable active flag) is the Q of the MATU DFF, clocked
by dma_phi. Within each 4-T-cycle byte transfer:
dma_phi= NOT(data_phase) (gate UVYT) — high during the address-phase half (T1–T2) and low during the data-phase half (T3–T4) of each M-cycle.- MAKA (DFF,
ppu_4mhz) carries the byte-transfer state; withdma_phiit gates the read-then-write structure. - NAXY = NOR2(
dma_phi, MAKA) rises only when both are 0, identifying the sub-window within each byte where the write strobe fires. - POWU = AND2(
dma_run, NAXY) asserts the OAM write arm during that sub-window; the resulting WYJA → YLYC/YNYC strobe chain (OAM writes) commits the byte.
Each byte transfer therefore has two sub-phases with the same address-bus owner (DMA) but different OAM-bus activity:
- Read-phase sub-window — DMA address asserted, no write strobe; the SRAM presents the prior contents of DMA's destination cell.
- Write-phase sub-window — POWU arms WYJA, the strobe fires, and the SRAM commits DMA's source byte into the destination cell.
The strikethrough effect: the PPU keeps running
DMA gating tri-states the PPU-side bus drivers only — the scan and fetch machinery keeps running: BESU still sets on Mode-2 entry (CATU.q), the scan counter clocks, and the Y comparator, sprite store, and fetch state machine all keep operating. They just sample the OAM data bus while the SRAM is decoding DMA's destination address.
This is the mechanism behind the Hacktix strikethrough.gb "+" overlay:
the sprite store on affected scanlines fills with the Y/X byte-pair the
Stage-1 latches held when DMA began — captures are gated off for the whole
overlap, so no freshly-written OAM byte reaches the store.
The two-bank SRAM
OAM is two parallel 80-byte banks:
| Bank | Stores | Write strobe |
|---|---|---|
| A | odd addresses (X position, attribute) | oam_a_wr2, selected when a0=1 |
| B | even addresses (Y position, tile index) | oam_b_wr2, selected when a0=0 |
Both banks share the address inputs (row decode from a3..a7 — the wordline
row, an 8-byte sprite pair; column mux from a1..a2 — the column within the
row) and the output enable. (Logically that same address is sprite index
a2..a7, byte-within-sprite a0:a1.) Reads ignore a0: on every read, bank B presents OAM[addr & ~1]
and bank A presents OAM[addr | 1] — the bus is effectively 16 bits wide,
always carrying an aligned (even, odd) byte-pair. a0 matters only for writes.
The scan-chain capture hold
Stage-1 capture: one shared enable for 16 dlatches. Each output-bus
bit is captured by a single dlatch clocked by oam_data_latch (ODL):
- X side (bank A) — XYKY, YRUM, YSEX, YVEL, WYNO, CYRA, ZUVE, ECED;
- Y side (bank B) — YDYV, YCEB, ZUCA, WONE, ZAXE, XAFU, YSES, ZECA.
All 16 share the same enable — the Y and X operands always update together as a byte-pair.
The ODL source. oam_data_latch = NOT(AND3(AJEP, XUJA, cpu_oam_rd_n))
(gate BODE). Three contributing paths:
- Mode-2 scan tick — AJEP = NAND2(mode2, XOCE) falls once per 2-dot scan step while mode2=1 (XOCE toggles at the scan cadence — OAM scan). One byte-pair per sprite entry.
- Mode-3 sprite-fetch capture — XUJA = 0 requires mode3=1 plus sprite-fetch counter state (TUVO decode); one pulse per sprite fetch.
- CPU OAM read —
cpu_oam_rd_n= 0 during a CPU OAM read.
During DMA + Mode 2 overlap, no path can fire. DMA forces mode2 = 0
(path 1), mode3 = 0 in Mode 2 kills the TUVO decode (path 2), and CPU OAM
reads are blocked (path 3). Measured across an overlapped Mode 2's 80
dots: zero ODL pulses (dmg-sim measurement, Hacktix strikethrough ROM).
The Stage-1 latches hold whatever byte-pair they captured before DMA
started, for the whole overlap:
- the last Mode-2 scan tick of the prior line holds (sprite 39 Y, sprite 39 X);
- a Mode-3 sprite fetch after that holds the last fetched sprite's (tile index, attribute);
- a CPU OAM read during H-Blank holds whatever pair the CPU read.
Whichever fired most recently before dma_run↑ pins the held pair.
Meanwhile the scan counter advances through all 40 entries (its clock is not DMA-gated, only its bus driver), the Y comparator evaluates the held Y against LY every step, and — on a match — the store fills successive slots with the same held X byte: phantom sprites.
A long DMA window confirms the hold directly: across one full DMA the 16
Stage-1 dlatches show zero transitions, a Mode-2 entry lands inside the hold
span without effect, and the first post-DMA scan tick reflects the
freshly-written OAM (dmg-sim measurement, gambatte late_sp00x_2
re-trigger stream).
Stage-2 latches. Between Stage 1 and the comparator/X-storage chain sits
a second dlatch_ee rank (Y side: XUSO and siblings; X side: YLOR and
siblings), enabled by COTA = NOT(BYCU), BYCU = NAND3(CUFE, XUJY, AVER) — a
net AND3. During DMA with the CPU off $FExx, CUFE reduces to dma_phi, so
Stage 2 is transparent during T1–T2 and holds during T3–T4 of each DMA
M-cycle. Since Stage 1 holds throughout the overlap, Stage 2's windows
merely re-affirm the held value.
The SRAM forces the aligned-pair capture model — two plausible alternatives do not hold:
| Model | Verdict |
|---|---|
| (i) Aligned-pair: latch (OAM[addr & ~1], OAM[addr | 1]) regardless of a0 | Structurally correct — the two-bank SRAM forces this on every read |
(ii) Bus-following per-dot: each latch samples whatever single byte dma_a addresses | Incorrect — the 16 dlatches share one enable; they never alternate per dot |
| (iii) Same-byte-twice on both operands | Incorrect — the banks store different physical bytes |
Model (i) is correct at the SRAM level, but the scan chain does not exercise it during a DMA + Mode-2 overlap because ODL is gated off — a per-dot aligned-pair model over-counts the captures by ~40× versus hardware's zero.
The DMA-end escape. The held-byte gating lasts only as long as the DMA
holds the bus. A DMA that ends part-way through Mode 2 releases the OAM bus
mid-scan, and every entry the counter has not yet visited is then read from
the freshly-written OAM — so a sprite DMA'd into a not-yet-scanned slot is
captured normally. This is the mechanism behind the diverging
oamdma_late_sp* outcomes: not a path outside the held-pair rule, but its
boundary — captures resume the instant DMA releases the bus.
A purpose-built ROM (carttype-0x00 DMG; OAM pre-filled uniform $A0 with one
real sprite DMA'd into slot 20 — scanned at dot 40 of Mode 2 — and the
DMA-end swept across that read) pins it: the sprite is captured in exactly
the frames whose DMA ends before dot 40 (save_sprite_num0 fires; Mode 3
lengthens from the 173.481 baseline to 184.5 dots, the +11-dot single-sprite
penalty), and never in the frames whose DMA is still running when slot 20 is
read. The capture/no-capture boundary sits exactly at the slot's scan-read
(dmg-sim measurement).
Engage, Release, and HALT
A transfer has three boundaries: the engage after an FF46 store, the release back to the scan when the last byte commits, and the pause across HALT. Each interacts with the Mode-2 scan differently — engage is edge-immediate, release waits for the scan cadence, and HALT freezes the byte counter without surrendering the bus.
- FF46-store →
dma_run↑ is a fixed engine constant: 1.495 M-cycles ≈ 6 dots, M-cycle-phase quantised by MATU's clock. - Engage is immediate — the lock takes the bus and suppresses captures
on the same master-clock edge as
dma_run↑. - Release waits for the scan cadence — the first post-DMA capture fires within 2 dots, and only if BESU is still set (before dot 80).
- The dot-80 boundary is sharp: DMA ending after AVAP defers OAM visibility one full line.
- HALT pauses DMA structurally —
dma_phiis pinned, the byte counter freezes, butdma_runkeeps owning the buses for the whole span.
DMA-start during Mode 2: engage cadence
A dma_run↑ inside Mode 2 freezes the scan partway through the line. Two
things fix where it cuts off: the FF46-store → dma_run↑ latency, and the
engage cascade's alignment to the scan cadence.
FF46-store → dma_run↑ latency. The arm chain:
LAVY → NOLO → MYTE → LARA/LOKY → MATU
LAVY = AND2(
ppu_wr, ff46) detects the store; MYTE captures it ondma_phi_n; the LARA/LOKY latch pair arms; MATU (clocked bydma_phi) raisesdma_run.
Because MATU is clocked by dma_phi, dma_run↑ is M-cycle-phase
quantised: measured LAVY↑ → dma_run↑ is a fixed engine constant of
1,459,029 ps = 1.495 M-cycles ≈ 6 dots, identical across all 1,154
DMAs in the capture, always landing at dma_phi = 1. A one-M-cycle shift
of the FF46 store shifts dma_run↑ by exactly one M-cycle.
Engage cascade. Opposite polarity to the release chain (next section), and — unlike release — with no XOCE wait:
| Stage | Δ from matu.q↑ | Source |
|---|---|---|
| BOGE↓ | +2,741 ps | NOT(dma_run) |
| mode2↓ | +3,688 ps total | AND2(BOGE, oam_parsing) → 0 |
oam_addr_parse_n↑ | +4,507 ps | APAR — parse driver tri-states; DMA takes the bus |
| AJEP | already high — no edge | NAND2(mode2, XOCE); the quantised engage always lands at XOCE=0 |
| ODL held low | while AJEP = 1 | BODE |
The +4,507 ps is constant across every mode2=1 engage in the stream
(dmg-sim measurement, gambatte late_sp00x_2). The lock takes the bus and
suppresses captures on the same master-clock edge as matu.q↑ —
engage is immediate, unlike release.
The last captured entry. The scan captures one byte-pair per
XOCE↑ with mode2=1; entry 0 is captured at BESU↑ + 0.49 dots. The scan
tick at dot 2k captures sprite k, so with dma_run↑ at Mode-2 dot D,
sprites 0..⌊D/2⌋ are captured this line and ⌊D/2⌋+1..39 are blocked,
holding the pre-engage Stage-1 pair. A 4-dot shift of the FF46 store moves
the boundary by ±2 sprites — the structure the late_sp* partner ROMs
bracket.
Sprite 0 is always captured — the early-engage sub-case is unreachable. The engage instant is phase-locked twice over (dmg-sim measurement):
dma_run↑ sits at a fixed phase of the CPU clock grid (+973,143 ps pastclk_1mhz↑; single population, n=1,154);- the line boundary CATU↑ sits at its own fixed phase (+610,111 ps, n=5,468), and the PPU grid's phase against the CPU clock is itself pinned by the M-cycle-quantised LCD-on write.
The only reachable engage positions are therefore Mode-2 dots 1.488 + 4k: no program can place a lock inside the first ~0.5 dots of Mode 2, and the entry-0 capture (BESU↑ + 0.49 dots) always completes a full dot before the earliest possible lock.
DMA-end during Mode 2: release cadence
A matu.q↓ (DMA end) inside Mode 2 re-enables the scan capture — but not
on the same edge; the next ODL pulse waits on the scan cadence:
| Stage | Δ from matu.q↓ | Source |
|---|---|---|
| BOGE↑ | +1,941 ps | NOT(dma_run) |
| mode2↑ | +3,723 ps total | AND2(BOGE, oam_parsing) — requires BESU.q=1 |
| next XOCE↑ edge | 0–2 dots | free-running scan cadence |
| AJEP↓ | +105 ps after that XOCE↑ | NAND2(mode2, XOCE) |
| ODL↑ | +2,547 ps after AJEP↓ | BODE |
Every stage is a few nanoseconds; the only multi-dot wait is for the next XOCE edge. Upper bound: the first post-DMA capture fires within 2 dots of DMA end.
The interlock that matters is BESU: the release requires BESU.q = 1 at the
matu.q↓ edge. BESU's set (CATU, at the scanline boundary) is independent
of DMA, and its reset (AVAP) fires at scan completion — the line's dot
80 — regardless of DMA, because the scan counter free-runs through the
overlap. So:
- DMA ends before dot 80: the chain re-enables at the next XOCE↑, and the remaining sprites of this line's scan capture from the freshly-written OAM. Earlier entries hold the pre-DMA Stage-1 pair.
- DMA ends after dot 80 (post-AVAP): BESU is already clear, mode2 stays 0, and no captures occur for the rest of this line — new OAM contents first become visible at the next line's Mode 2.
The boundary is sharp at dot 80, with at most one XOCE period (2 dots) of
resolution. Both regimes are directly captured in the DMA re-trigger
stream (dmg-sim measurement, gambatte late_sp00x_2): a release with
BESU.q=1 resumes captures within ~1 dot; a release ~280 dots after AVAP
produces zero captures for the remainder of the line.
This boundary is exactly what the gambatte late_sp* partner-test pairs
bracket: variants whose DMA ends ≤ dot 80 extend Mode 3 via the
newly-captured sprites; the 4-dot-later partners miss the window and defer
visibility one full line, landing the test's STAT sample in a different
mode. Emulators that defer OAM visibility to "the next line" unconditionally
— or fail to advance the scan counter during DMA — get the in-time partner
wrong; switching the PPU's OAM view atomically at DMA end reproduces both.
DMA pause during HALT
The DMA byte counter (MATU) is clocked by dma_phi = NOT(data_phase),
and data_phase is held LOW throughout HALT
(HALT and EI). The consequence is structural: while
halted, dma_phi is held HIGH, MATU has no clock edges, and the byte
counter does not advance. Any DMA in flight at HALT entry pauses on the
byte being fetched and resumes on the first dma_phi↑ after HALT exit
(~+0.5 dots after the wake CLK9↑). The same holds for any state that pins
data_phase low (reset, STOP).
dma_run does not fall during the pause — both its set and reset
transitions require dma_phi clocks — so all dma_run-gated arbitration
(OAM bus and source bus alike) continues to assert "DMA owns the bus" for
the entire HALT span, even though no bytes move.
End to end (dmg-sim measurement, gambatte
oamdmasrc80_halt_lycirq_read8000 ROM): a VRAM-source DMA advances 13
bytes, HALT entry freezes dma_a at 0x0D, the counter stays frozen across
a ~17 ms HALT (≈17,900 M-cycles — zero advance), and on wake it resumes at
one byte per M-cycle. The post-HALT read of the source page lands at byte
index 0x1F = source[$801F] = $81, matching the hardware-verified out81
expectation. The HALT span contributes exactly zero to the byte index —
which is why the _lycirq (17 ms HALT) and _m2irq (~1 ms HALT) variants
share the same expected value.
The Source Bus
The OAM bus is one side of DMA; the other is the source bus DMA reads from — the external bus (cart ROM/RAM, WRAM) or the VRAM bus. The question here is what the CPU observes when its own read or write targets the same physical bus DMA is driving.
- The address pads are MUXed to DMA; the data pads and CPU strobes are not gated at all — every conflict behaviour falls out of that one asymmetry.
- CPU read on the DMA bus: the CPU latches source[
dma_a] — the byte DMA is fetching that M-cycle. - CPU write on the DMA bus: OAM gets the CPU's byte, and the cell at DMA's source address takes a spurious write of it.
- HRAM is on-chip, on neither bus: CPU access during DMA is uncontended.
- Sources $FE/$FF fold into WRAM:
work_ram[(src − $E000) & $1FFF]— the echo region extended one page.
Source-side bus arbitration
One asymmetry drives every conflict on this page: during DMA the address pads are MUXed to DMA, but the data pads and the CPU's read/write strobes are not gated at all.
| Gate | Role | Type | Logic |
|---|---|---|---|
| LOGO | DMA bus-read window | not_x1 | NOT(RORU) — per-T-cycle DMA address window |
| MORY | DMA-vs-CPU address MUX select source | nand2 | NAND2(dma_run, LOGO) |
| LUMA | dma_addr_ext driver | not_x1 | dma_addr_ext = AND2(dma_run, LOGO) |
| per-bit address MUXes | address pad source | mux | sel = dma_addr_ext: DMA address vs CPU address |
| CEDE | inverted dma_addr_ext | not_x2 | gates CPU-side address-pad drivers off during DMA |
So whatever the CPU's instruction intended, the address reaching the bus during DMA is DMA's:
| CPU target | dma_addr_ext during the CPU strobe | Address pads carry |
|---|---|---|
| external bus, DMA source on external bus | 1 | DMA's address — the CPU's intended address never reaches the pads |
| VRAM bus, DMA source on VRAM | 1 | DMA's address |
| HRAM ($FF80–$FFFE) | irrelevant | HRAM is on-chip, on neither bus; CPU access proceeds normally |
An emulator cannot model the conflict as "did the CPU's address collide with DMA's?" — the CPU's address has already been replaced at the pads.
CPU read conflict
The CPU's read strobe asserts on its normal cadence; the address pads carry
dma_a; the memory drives source[dma_a] onto the data pads; the CPU
latches it. The CPU's register receives the same byte DMA is fetching this
M-cycle — equivalently OAM[dma_dest] after this M-cycle's commit.
Anchored end to end (dmg-sim measurement, gambatte
oamdma_src0000_busyread0000_1 ROM): at the conflict M-cycle the pads show
DMA's address $009E, the CPU's sample edge latches 0x04 = source[$009E],
and the DMA OAM commit of the same byte fires co-temporally on a distinct
strobe chain — A = 0x04, matching the hardware-verified out4.
CPU write conflict
The CPU's write strobe and data drivers assert on their normal cadence, overriding DMA's read-direction pad state — so when DMA's write arm (POWU → WYJA) latches the data pad into OAM, the pad carries the CPU's data. Three things follow:
- OAM[
dma_dest] receives the CPU's byte. - the off-chip cell at DMA's source address takes a spurious write of the same byte (the strobe asserts with DMA's address on the pads);
- the CPU's intended target never sees the write.
Anchored: the pad sequence shows the CPU's 0x00 winning the data bus
mid-settle, the OAM commit strobe latching it, and OAM[$9E] = 0x00 after
the transfer — matching the hardware-verified out0 (dmg-sim measurement,
gambatte oamdma_src8000_busywrite8000 ROM).
Both conflicts resolve deterministically — CPU and DMA share the master clock, so the CPU's data-phase and DMA's write-arm sub-window are co-aligned the same way every M-cycle. There is no sub-dot race here.
HRAM exemption. The HRAM chip-select chain derives from the CPU bus
phase and the $FFxx decode; dma_run is not an input. CPU access to HRAM
during DMA is uncontended (120 successful HRAM reads inside one DMA window
in the measurement above) — the basis of every HRAM-stub DMA routine.
Stack accesses are ordinary bus cycles at SP; the rules above apply unchanged. Stack in HRAM is unaffected; stack in WRAM during a same-bus DMA redirects identically.
DMA sources $FE and $FF
For source pages $FE/$FF the address MUX behaves as for any other page:
dma_a goes onto the cart address pads. The difference is the cart
chip-select MUX (TYHO, sel = dma_addr_ext): during DMA it takes DMA's
a15 directly, bypassing the CPU-side decode (TOZA) that would
normally de-assert /CS for $FE–$FF. So the cart bus is driven with
$FE00–$FE9F / $FF00–$FF9F addresses — even though on the CPU side those
map to on-die OAM/IO/HRAM.
The package bus is now driven with an OAM/IO-range address; one device on it answers:
| Candidate | Responds? | Reason |
|---|---|---|
| On-die OAM SRAM | No | OAM uses its own internal address bus, not the external pads; its output enable stays 0 throughout (measured) |
| On-die HRAM | No | The HRAM enable chain rides the CPU-internal address bus, not the external pads |
| On-package WRAM die | Yes | Selects on cs_n_pad + a14=1 alone (see below) |
| Cart devices | Moot | WRAM has already driven the bus; MBC RAM /CS requires a14=0 anyway |
The WRAM die in the DMG-CPU-B package selects on cs_n_pad and a14=1
alone — no a13 qualifier and no $FE/$FF exclusion, because those
exclusions live in the CPU die's internal decode, which TYHO has
bypassed. It drives the data pads with work_ram[a[12:0]], so the
address wraps: $FExx → $1Exx, $FFxx → $1Fxx.
The behaviour is the documented echo region extended one page: DMA from
$FE/$FF fetches work_ram[(src − $E000) & $1FFF] — hardware-verified as
work_ram[$1E00+i] / work_ram[$1F00+i] per byte index.
A pre-filled-WRAM ROM family confirms 8-of-8 byte matches at the fold
offsets (dmg-sim measurement, gambatte oamdma_src{FE,FF}00_busypop*
ROMs), and both bank commit strobes fire exactly 80 times per transfer —
every byte index commits, with no per-index suppression. $FE and $FF
sources differ only in offset — work_ram[$1Exx] vs work_ram[$1Fxx] —
through the identical mechanism; there is no per-page behavioural
difference between them.
Treat $FE/$FF sources as the echo region extended one page — the single mechanism covers the read-conflict and pop-conflict ROM families alike.
Mode 2: OAM Scan
The Rendering Pipeline chapters walk a scanline in machine order: the OAM scan (this chapter), the BG fetch and pixel output (BG pipeline), sprite fetch and mixing (sprite pipeline), window control, the LCD interface, then the cross-cutting views — mode transitions, STAT interrupts, scanline/frame timing, CPU-visible boundaries, and races.
Mode 2 is the first 80 dots (20 M-cycles) of each non-VBlank scanline. The PPU scans the 40 OAM entries (2 dots each), comparing each sprite's Y position against LY and capturing the X position of matching sprites into a 10-slot sprite store for Mode 3 rendering. At scan completion, AVAP rises and triggers the Mode 2→3 transition. This chapter follows the signal chain: scan counter → scan-active latch → Y comparator → sprite store → AVAP generation.
- Mode 2 decomposes exactly: 2 dots (CATU↑ → first tick) + 76 (counter 1→39) + 2 (FETO → BYBA capture) = 80 dots; boundary-to-AVAP is 81.
- The Y compare is combinational (an 8-stage carry chain) — no DFF captures the match; it gates the per-slot store write-enables directly.
- The store's write enable is the PPU's deepest per-line path (71.2 ge) while its data arrives at depth 0 — the source of a family of races.
- AVAP is a half-dot pulse from the BYBA/DOBA edge detector: BYBA captures scan-done on XUPY (ALET falling), DOBA catches up on ALET rising.
- On the first scanline after LCD-on, BESU never sets — no scan, no Mode 2 STAT bit, no sprite store.
What's where
| Page | Covers |
|---|---|
| The counter and BESU | The 6-bit ripple counter, its clocking and reset, the scan-active latch and its XUPY-phased copy |
| The Y comparator | The 8-stage carry chain, the WOTA decoder, LCDC.2 masking, and capture durability |
| The sprite store | 10-of-40 priority, the slot counter and write enables, reset domains, and the LCDC.1 trigger branch |
| AVAP and the 80 dots | The BYBA/DOBA edge detector, the measured counter-39 → AVAP cascade, and Mode 2's exact decomposition |
The Counter and BESU
The scan's bookkeeping: a 6-bit ripple counter walks the 40 OAM entries, and the BESU latch (with its XUPY-phased copy CENO) marks the scan as active.
The scan counter
The 6-bit OAM scan counter is a ripple chain clocked by GAVA = OR2(XUPY, FETO), counting 0–39 across the 40 OAM entries. Each stage's output clocks the next (the same ~Q convention as the LX and BG fetch counters). All six cells share ANOM as reset, driven by the CATU/ANEL scanline-boundary chain (Mode transitions).
| Bit | Signal | Type | Clocked by | Notes |
|---|---|---|---|---|
| 0 (LSB) | YFEL | dffr | GAVA | Reset: ANOM |
| 1 | WEWY | dffr | YFEL (~Q ripple) | Reset: ANOM |
| 2 | GOSO | dffr | WEWY (~Q ripple) | Reset: ANOM |
| 3 | ELYN | dffr | GOSO (~Q ripple) | Reset: ANOM |
| 4 | FAHA | dffr | ELYN (~Q ripple) | Reset: ANOM |
| 5 (MSB) | FONY | dffr | FAHA (~Q ripple) | Reset: ANOM |
The terminal value 39 is 0b100111 — YFEL=WEWY=GOSO=FONY=1, ELYN=FAHA=0 (1+2+4+32 = 39) — the value visible in the boot-ROM handoff snapshot (Post-boot state).
Clocking and reset
| Gate | Role | Type | Clock / Trigger | Notes |
|---|---|---|---|---|
| GAVA | Scan counter clock | or2 | Inputs: XUPY, FETO | Drives the YFEL→FONY ripple chain |
| ANOM | Scan counter reset | nor2 | Inputs: ATEJ, ATAR | Output is oam_parse_reset_n; see the CATU/ANEL chain (Mode transitions) |
| FETO | Scan-done decode | and4 | Inputs: oam_parse_a2, a3, a4, a7 | Wire name last_sprite. Fires at counter = 39. Also feeds BYBA's D input |
| ASEN | BESU reset driver | or2 | Inputs: ATAR, AVAP | Rising edge clears the scan-active latch — at scan completion or video reset |
| ATAR | VID_RST active-high | not | Input: XAPO | Drives ANOM and ASEN |
XUPY = NOT(wuvu_n) = WUVU.q is the 2 MHz scan clock from the
clock tree. FETO's AND4 over a2, a3, a4, a7 is another
partial decode that is exact within range: in [0..39], only 39 sets all four
of those bits.
OAM bus address. The counter drives OAM address bits oam_parse_a2–a7
(the entry index); bits a0–a1 come from a separate byte-cycle counter
advancing within each entry's 4-byte record. The address reaches the OAM SRAM
through the APAR tri-state driver — which DMA tri-states without stopping the
counter (DMA).
XUPY buffer copies. Two signals appear as OAM-scan clock sources for per-byte-bit DFFs: CYKE = NOT(XUPY) and WUDA = NOT(CYKE) = XUPY. They are physical buffer copies, not new signals — XUPY edges by another name.
COTA, the composite Stage-2 enable. COTA = NOT(BYCU) gates the Stage-2 capture latches (XUSO and siblings, below); BYCU = NAND3(CUFE, XUJY, AVER) is a net AND3 of three condition terms:
- AVER = NAND2(mode2, XYSO) — scan phase;
- CUFE — CPU/DMA $FExx access;
- XUJY — sprite-fetch counter.
Its cadence therefore depends on CPU/DMA activity and mode state; during Mode 3 steady-state with CPU and DMA idle it sits at 1 — Stage 2 transparent (dmg-sim measurement). COTA's complement WOVU drives the OAM bitline precharge — the lever behind the OAM corruption bug (OAM corruption).
The scan-active latch (BESU) and its phased copy (CENO)
BESU tracks whether OAM parsing is in progress; CENO is BESU re-captured on XUPY rising, giving downstream consumers an XUPY-edge-aligned copy.
| Gate | Role | Type | Clock / Trigger | Notes |
|---|---|---|---|---|
| BESU | Scan-active latch (Q=1 during Mode 2) | nor_latch | Set: CATU.Q at Mode 2 start; Reset: ASEN | Wire name oam_parsing. Drives the OAM-bus lock directly: mode2 = AND2(BOGE, BESU.Q) |
| CENO | XUPY-phased scan-active copy | dffr | XUPY rising | Data: BESU. Drives oam_rendering = AND2(ceno_n, mode3) and CEHA = NOT(ceno_n) feeding CARE |
CATU captures RUTU AND NOT(vblank) on the XUPY rising edge after each
scanline boundary; its Q both sets BESU and drives the scan-counter reset
chain. At scan completion, AVAP propagates through ASEN = OR2(ATAR, AVAP) and
the rising ASEN clears BESU.
The OAM-bus lock is BESU-direct — mode2 takes BESU.Q straight, not through CENO. CENO's two consumers are:
- The Mode 3 rendering gate —
oam_rendering= AND2(ceno_n, mode3). - The sprite-store save enable — CEHA = NOT(
ceno_n) feeds CARE = AND3(XOCE, CEHA,sprite_y_match). CENO's one-XUPY-cycle lag behind BESU gives the save window precise XUPY-edge alignment — and is the source of the first-entry staleness race catalogued in Races: the CARE-gated window has not fully opened when the very first OAM entry's compare result arrives.
Reset behaviour. BESU has no reset net of its own; it starts at 0 at LCD-on because ASEN includes ATAR, which is 1 throughout LCD-off.
On the first scanline of the first frame after LCD-on, BESU never sets — the LCD-on Mode-2-entry path bypasses CATU (Scanline and frame timing). Consequences:
- the STAT Mode 2 bit reads 0 through the nominal scan window;
- the Mode 2 interrupt does not fire;
- the CARE chain never arms;
- the sprite store is not populated by the first scanline's scan.
The Y Comparator
During the scan, the hardware compares each sprite's Y position against LY.
The comparison is an 8-stage full-adder carry chain with a NAND6 decoder, and
the match result is combinational — there is no DFF capture of the match
during the scan. sprite_y_match gates the per-slot sprite-store
write-enables directly via the CARE chain.
| Gate | Role | Type | Clock / Trigger | Notes |
|---|---|---|---|---|
| GESE | sprite_y_match driver | not_x1 | combinational (input: WOTA) | sprite_y_match = NOT(WOTA) |
| WOTA | Y-compare decoder | nand6 | combinational | Inputs: GACE, GUVU, GYDA, GEWY, wuhu_c, GOVU. Low when the Y-in-range condition holds |
| GYKY | Carry-chain bit 3 | full_add | combinational | See the chain table |
| GOVU | LCDC.2 mask | or2 | combinational | GOVU = OR2(ff40_d2, GYKY) — masks the bit-3 comparison for 8×16 sprites |
| XUSO | OAM Y-offset bit 0 latch (Mode 2) / tile-index bit 0 latch (Mode 3) | dlatch_ee | Enable: COTA | Data: YDYV (Stage 1). xuso_n feeds carry-chain bit 0 operand B in Mode 2 and the sprite tile VRAM row-bit derivation in Mode 3 (Sprite pipeline) |
| Y-side Stage-1 register | 8 × dlatch on the bank-B bus | dlatch | Enable: oam_data_latch (shared) | YDYV, YCEB, ZUCA, WONE, ZAXE, XAFU, YSES, ZECA — capture the even byte of the addressed OAM byte-pair (DMA for the byte-pair architecture) |
The chain computes LY − spriteY in two's-complement form:
- Operand A — LY, complemented: each a-input is a
not_x1on a V-counter bit (EBOS…GUSU = NOT(v0…v7)). - Operand B — the OAM Y byte, complemented: the Stage-2 latches' q̄ outputs (XUSO and its seven siblings, each holding the Stage-1 byte through the COTA-gated rank).
- Stage-0 carry-in — constant 0.
| Stage | Gate | a input (NOT LY bit) | b input (OAM-Y latch q̄) | cin | cout | sum |
|---|---|---|---|---|---|---|
| 0 | ERUC | EBOS | xuso_n | const0 | eruc_c | ERUC |
| 1 | ENEF | DASA | xegu_n | eruc_c | enef_c | ENEF |
| 2 | FECO | FUKY | yjex_n | enef_c | feco_c | FECO |
| 3 | GYKY | FUVE | xyju_n | feco_c | gyky_c | GYKY |
| 4 | GOPU | FEPU | ybog_n | gyky_c | gopu_c | GOPU |
| 5 | FUWA | FOFA | wyso_n | gopu_c | fuwa_c | FUWA |
| 6 | GOJU | FEMO | xote_n | fuwa_c | goju_c | GOJU |
| 7 | WUHU | GUSU | yzab_n | goju_c | wuhu_c | WUHU |
The decoder is WOTA = NAND6(GACE, GUVU, GYDA, GEWY, wuhu_c, GOVU). Its
six inputs:
- GACE, GUVU, GYDA, GEWY = NOT(GOPU, FUWA, GOJU, WUHU) — the
upper-nibble sums, inverted (
not_x1cells, netlist-derived); wuhu_c— the top-stage carry-out, the sign of the difference;- GOVU — the LCDC.2 size mask (below).
WOTA asserts (low) exactly when sum bits 4–7 are all zero, the sign carry is set, and the size mask holds — i.e. the difference lies in the sprite-height range.
LCDC.2 (OBJ_SIZE) in the comparator. GOVU = OR2(XYMO, GYKY): with XYMO=1 (8×16 sprites) the OR masks one bit of the range comparison, accepting a 16-row match; with XYMO=0 it passes GYKY unchanged for 8 rows. Because the compare is combinational, a mid-scan CUPA write to LCDC.2 splits the scan — entries before the write use the old range, entries after use the new — so hybrid sprite-store populations are possible. XYMO's other consumer (the sprite tile VRAM address bit, Mode 3) lives in the sprite pipeline.
Capture durability. XUSO's enable (COTA) is high through most of Mode 3
steady-state, so XUSO is transparent there — its held Mode-2 value survives
only because the Stage-1 latch upstream (YDYV) holds when oam_data_latch
stops pulsing. On scanlines with Mode-3 sprite fetches, each fetch pulses
oam_data_latch once and overwrites the Stage-1 byte-pair with the fetched
sprite's (tile index, attribute) — the Mode-2 Y bits do not survive past the
first fetch. This breaks nothing: the Y-compare chain is only consumed during
Mode 2.
The Sprite Store
The store captures up to 10 sprites' X positions (plus per-slot state)
into dedicated latch arrays — the first 10 Y-matching sprites in OAM-index
order, the widely documented DMG behaviour
(Pan Docs covers the rule; this
page covers the machinery). Each slot has its own write enable
(save_sprite_numN, N ∈ [0..9]): a 4-bit slot counter and a 10-way
one-hot decoder make exactly one enable fire per matching sprite.
| Gate family | Role | Type | Clock / Trigger | Count | Notes |
|---|---|---|---|---|---|
| Sprite store slots | Per-slot X-position latches | drlatch_ee | Enable: save_sprite_numN | 80 (10 slots × 8) | Data: OAM bus (depth 0) |
| EBEB | Slot-3 write-enable inverter | not_x1 | combinational | 1 | drives save_sprite_num3 (logic NOR2(sprite_save_en_n, FOCO)). Depth 71.2 ge from MUWY via the Y-compare chain — the per-line critical path |
| CARE | Global sprite-save enable | and3 | combinational | 1 | Inputs: XOCE, CEHA, sprite_y_match; sprite_save_en_n = NOT(CARE) |
| FOCO | Slot-3 selector | nand4 | combinational | 1 | Decodes slot counter = 3; one of 10 per-slot selectors |
| BESE / CUXY / BEGO / DYBE | Slot counter bits 0–3 | dffr (toggle) | CAKE, then ~Q ripple | 4 | Reset: AZYB |
| AZYB | Slot counter reset driver | not_x1 | combinational | 1 | AZYB = NOT(ATEJ) — same start-of-line reset as the scan counter |
The priority mechanism:
sprite_y_matchis combinationally established per OAM entry.- When it holds AND XOCE=1 AND CEHA=1, CARE=1 releases the write enables.
- The slot counter (matched sprites so far) selects which slot's selector fires — FOCO decodes counter = 0b0011, via FYCU = BESE, FONE = CUXY, CAPE = NOT(BEGO), CAXU = NOT(DYBE).
save_sprite_numN= NOR2(sprite_save_en_n, selector_N) fires only when enabled AND targeted.- CAKE pulses once per stored sprite, advancing the counter.
- At counter = 10 no decoder matches; remaining matching sprites pass through without a write.
The late write enable. The store's data input is the OAM bus (depth 0); its write enable arrives through the deepest per-line combinational path in the PPU (71.2 ge from MUWY). The enable arrives very late relative to data — the family of races this creates is catalogued in Races.
X-side Stage-1 register. The per-slot latches store
sprite_x0_n..x7_n, derived combinationally (via YLOR/ZAGO) from the
X-side Stage-1 capture register: XYKY, YRUM, YSEX, YVEL, WYNO, CYRA, ZUVE,
ECED on the bank-A bus, sharing the oam_data_latch enable with the Y
side. DMA covers the byte-pair semantics and the hold
under DMA.
Reset domains. Each of the store's 80 X-position latches resets via
its slot's sprite_rstN = NOR2(ABAK, fetched flag):
- ABAK = OR2(ATEJ, AMYG) — AMYG carries LCD-off (every slot held at 0 while the LCD is off); ATEJ pulses at each scanline boundary, including every VBlank line (RUTU keeps firing through LY 144–153).
- The per-slot fetched flag (EBOJ/CEDY/FONO family, clocked by WUTY) holds a fetched slot in reset until the next line's ATEJ.
Cross-frame state at the first scanline. From the second frame on, the
first scanline's store is cleared by the VBlank ATEJ pulses and populated by
its own scan. On the first frame (LCD-on) it is cleared but never
populated, twice over:
BESU never sets (CARE never arms, the slot counter never clocks), and
oam_data_latch is mode2-gated (the comparator operands are never
refreshed from OAM).
LCDC.1 (OBJ_EN): the sprite-trigger branch
LCDC bit 1 (XYLO) drives four netlist consumers, all combinational (the full set is in LCDC.1/.2 paths). The trigger-side branch:
XYLO → AROR → per-slot match NAND3s → FEFY/FOVE → FEPO
AROR = AND2(XYLO, AZEM), with AZEM = AND2(mode3, BYJO); the ten match NAND3s reduce through the two NAND5s to FEPO, the "sprite match active" aggregate.
With XYLO=0, AROR=0: every NAND3 settles high, FEPO stays 0, TEKY never fires — no sprite fetch starts at all, SACU is never frozen, and no penalty dots are added. The pixel-mux consumer paths (what happens to already-fetched sprite pixels) live in the sprite pipeline.
AVAP and the 80 Dots
At scan completion, a rising-edge detector formed by BYBA and DOBA produces AVAP — a ~0.483-dot pulse that triggers the Mode 2→3 transition.
| Gate | Role | Type | Clock / Trigger | Notes |
|---|---|---|---|---|
| AVAP | Mode 2→3 trigger | not_x2 | Input: BEBU | Wire name stop_oam_parsing. Primary here; consumed in Mode transitions |
| BEBU | Scan-complete aggregate | or3 | Inputs: DOBA, BALU, byba_n | The third input is BYBA's Q_n pin, not Q |
| BYBA | Scan-done flag | dffr | XUPY rising | D: FETO. Reset: BAGY (active-low) |
| DOBA | ALET-clocked scan-done-prev | dffr | ALET rising | D: BYBA.Q. Reset: BAGY |
| BALU | OAM-parse reset (active-high) | not_x1 | Input: ANOM | |
| BAGY | BYBA/DOBA reset driver | not_x1 | Input: BALU | ANOM buffered through two inverters |
AVAP = NOT(BEBU) fires when BYBA has just captured scan-done (byba_n=0) AND
DOBA hasn't yet caught up (DOBA=0) AND the parse reset is released (BALU=0).
BYBA captures on XUPY rising (= ALET falling); DOBA captures BYBA half a dot
later on ALET rising. The AVAP pulse runs 0.483 dots — just under the
0.487-dot BYBA.Q↑→DOBA.Q↑ capture span it tracks (the small skew is the
BEBU/AVAP gate delay).
BALU is structurally dominated. When BALU rises, BAGY falls and forces
BYBA into reset, driving byba_n to 1 within 864 ps (dmg-sim measurement).
BALU is therefore never the sole BEBU contributor, and the OR3 reduces
observationally to AVAP = BYBA AND NOT(DOBA) (given BYBA's clearing at
the scanline-boundary reset).
Measured cascade: counter=39 → AVAP
From the XUPY edge that advances the counter 38→39 (dmg-sim measurement,
gbmicrotest int_hblank_nops_scx0, zero variance across 1,721 scanlines):
| Stage | Δ (ps) | Dots | Type | Mechanism |
|---|---|---|---|---|
| Counter 38→39 (XUPY rising N) | 0 | 0.000 | Clocked | Only YFEL toggles; upper bits hold |
| YFEL.Q settled | +296 | 0.001 | Combinational | DFF Q delay |
| FETO↑ | +2,602 | 0.011 | Combinational | AND4 of settled ripple Qs |
| XUPY rising N+1 | +488,000 | 2.000 | Clocked | Next XUPY edge |
| BYBA.Q↑ | +488,902 | 2.004 | Clocked | Captures FETO=1, 902 ps after the edge |
| BEBU↓ | +489,558 | 2.006 | Combinational | byba_n↓ collapses the OR3 |
| AVAP↑ | +490,679 | 2.011 | Combinational | Mode 2→3 trigger |
| BESU↓ | +491,385 | 2.014 | Latch reset | ASEN clears the scan-active latch |
| ALET rising | +606,906 | 2.487 | Clocked | First ALET↑ after BYBA↑ |
| DOBA.Q↑ | +607,790 | 2.491 | Clocked | Captures BYBA=1 |
| BEBU↑ | +608,036 | 2.492 | Combinational | DOBA restores BEBU |
| AVAP↓ | +608,613 | 2.494 | Combinational | Pulse ends — width 117,934 ps = 0.483 dots |
The one-XUPY-cycle latency from counter=39 to BYBA's capture is structural
(BYBA is a clocked DFF, not a combinational path): FETO settles 2,602 ps
after edge N — a comfortable margin against the 488,000 ps period — and the
static race analysis tags the whole BESU/CATU/BYBA/YFEL/FETO family
static accordingly.
Supporting pulse measurements (same scenario, zero variance): BALU pulse 1.008 dots per scanline at an exactly-456-dot period; BYBA Q delay 902 ps after XUPY; DOBA Q delay 884 ps after ALET.
Where the 80 dots come from
Mode 2's duration (BESU=1, CATU↑ to AVAP↑) decomposes exactly:
2 dots (CATU↑ → first counter tick) + 76 dots (counter 1→39 over 38 XUPY cycles) + 2 dots (FETO → BYBA capture) = 80 dots
The preceding RUTU↑ → CATU↑ capture latency is one dot — measured at 243,059 ps (0.996 dots), picosecond-identical across 431 steady-state boundaries (dmg-sim measurement) — and sits before Mode 2: total scanline boundary (RUTU↑) to AVAP↑ is 81 dots.
Mode 3: The BG Pipeline
Mode 3 is the pixel-transfer phase — typically 173 dots plus fine-scroll, sprite, and window penalties. Two machines run in parallel: the tile fetcher pulls BG/window tile data from VRAM on an 8-dot cadence, and the pixel clock (CLKPIPE / SACU) shifts pixels out of the BG shift registers toward the LCD. This chapter covers the BG side across four pages; the sprite pipeline, window control, and LCD output have their own chapters.
- Mode 3 baseline is 173.481 dots = a 173.045-dot AVAP→WODU span (a 7.026-dot startup cascade, then 167 pixel-clock edges) + a 0.436-dot WODU→XYMU handoff tail — before fine-scroll, sprite, and window penalties.
- 167 SACU edges per Mode 3, invariant with SCX — fine scroll is clock
suppression (1.000 dot per
SCX & 7step), never push-and-discard. - Tile fetches run an 8-dot cadence: 6 counting dots plus a 2-dot drain; the fetch-counter reset NYXU fires 22 times per scanline at SCX=0, 23 at SCX&7 ∈ {1..7}.
- The pixel clock is the deepest routinely-toggling signal in the PPU (63.8 ge) — which is why shifter data is always stable before the edge that shifts it.
- LCDC.3/4/6 reach VRAM addressing through level-sensitive paths; TILE_SEL is sampled twice per fetch, enabling hybrid fetches.
What's where
| Page | Covers |
|---|---|
| The tile fetcher | The 3-bit fetch counter, the 8-dot tile cycle and drain cascade, VRAM addressing, and the live LCDC.3/4/6 fetch-address sampling |
| The pixel clock | The PX counter, terminal count 167, the CLKPIPE/SACU gating tree, and the four halt conditions |
| Startup and fine scroll | The AVAP→first-SACU cascade, the fine-scroll counter and discard, late SCX writes, the tail-of-Mode-3 drain, and the invariance measurements |
| The BG shift registers | The shifter planes, temp latches, parallel load via NYXU, and the two startup loads |
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.
- 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.)
| Gate | Role | Type | Clock / Trigger | Notes |
|---|---|---|---|---|
| LEBO | Fetch-counter gated clock | nand2 | Inputs: MOCE, ALET | When MOCE=0 (terminal count), LEBO freezes at 1; the counter stops |
| LAXU | Fetch counter bit 0 | dffr | LEBO | Async reset by NYXU |
| MESU | Fetch counter bit 1 | dffr | LAXU (~Q ripple) | Async reset by NYXU |
| NYVA | Fetch counter bit 2 | dffr | MESU (~Q ripple) | Async reset by NYXU |
| MOCE | Self-stop condition | nand3 | Inputs: LAXU, NYVA, NYXU | Drives LEBO low at count 5 when NYXU=1 |
| NYXU | BG fetch counter reset | nor3 | Inputs: MOSU, TEVO, AVAP | Async 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}:
| Source | Count | Fires at | Role |
|---|---|---|---|
| AVAP | 1 | Mode 2→3 transition | Starts Mode 3, before any tile fetch |
| TAVE → TEVO | 1 | +5.996 dots from AVAP | Closes tile 0 (the startup fetch) |
| SEKO → TEVO | 20 | 8-dot boundaries from +13.997 dots | Closes tiles 1–20 (the 20 visible BG tiles) |
| SEKO → TEVO (tail) | 0 or 1 | +173.997 dots, fixed | Closes 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.
| Gate | Role | Type | Clock / Trigger | Notes |
|---|---|---|---|---|
| LYRY | "Fetch complete" combinational | not_x1 | Input: MOCE | High when the counter sits at terminal with no reset — drives NYKA's data input |
| NYKA | Fetch-complete capture | dffr | ALET | Data: LYRY |
| PORY | NYKA pipeline stage | dffr | MYVO | Half-dot-later capture of NYKA |
| LYZU | Fetch counter bit 0 sample | dffr | ALET | Data: LAXU |
| PYGO | PORY pipeline stage | dffr | ALET | Feeds POKY |
| RENE | Drain-detect stage 2 | dffr | ALET | Data: RYFA |
| RYFA | Drain-detect stage 1 | dffr | SEGU | Data: PANY |
| TEVO | Tile-boundary / restart trigger | or3 | Inputs: SEKO, SUZU, TAVE | Drives NYXU per tile, per window trigger, per startup |
| SEKO | Tile-boundary drain detector | nor2 | Inputs: RENE, RYFA | Fires when the pipeline drains (both 0) |
| SUZU | Window-activation trigger | not | Input: TUXY | See Window control |
| TAVE | Startup / window-restart trigger | not | Input: SUVU | SUVU = NAND4(NYKA, PORY, ROMO, mode3) |
The 2-dot hold, stage by stage:
- 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.
- Hold-dot 1: RENE captures RYFA=1 — SEKO stays 0.
- 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).
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:
| Counter | What happens | VRAM address |
|---|---|---|
| 0 | Tilemap fetch: address set | Tilemap address |
| 1 | Tilemap data arrives | — |
| 2 | Tile data low: address set | Tile-data address (low byte) |
| 3 | Tile data low arrives | — |
| 4 | Tile data high: address set | Tile-data address (high byte) |
| 5 | Tile 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 bit | Cell | Wire name | Drives | Path |
|---|---|---|---|---|
| 3 (BG_MAP) | XAFO | LCDC_BGMAPp | ~ma10 during BG tilemap reads | XAFO → AMUV (not_if0, enable BAFY) |
| 4 (TILE_SEL) | WEXU | LCDC_BGTILEp | ~ma12 during tile-data reads | WEXU → VUZA (nor2 with tile-index bit 7 PYJU) → VURY (not_if1, enable bp_cy) |
| 6 (WIN_MAP) | WOKY | LCDC_WINMAPp | ~ma10 during window tilemap reads | WOKY → 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:
| Counter | Stage | LCDC bits sampled |
|---|---|---|
| 0 | Tilemap address | LCDC.3 or LCDC.6 (BAFY/WUKO arbitration) |
| 1 | Tilemap data | — (tile index captured) |
| 2 | Tile-data low address | LCDC.4 |
| 3 | — | — |
| 4 | Tile-data high address | LCDC.4 (second sample) |
| 5 | — | — |
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.
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.
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 input | Drives | Stage(s) sampled | Mid-fetch write effect |
|---|---|---|---|
| SCX, map column | tilemap column bits | counter=0 only | tile-aligned 8-px shift (like BG_MAP); no hybrid |
SCX, fine scroll (SCX & 7) | startup pixel-clock suppression | startup fine scroll only | separate mechanism; never a tile-data address |
| SCY, map row | tilemap row bits | counter=0 only | tile-aligned 8-px shift (like BG_MAP) |
| SCY, fine-Y | tile-data row offset | counter=2 and counter=4 | hybrid 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.
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.
The Pixel Clock
Two tightly coupled circuits meter every pixel of every scanline: the PX counter tracks horizontal position, and CLKPIPE (SACU) — the pixel pump — decides on each dot whether the pipeline shifts at all. Every Mode 3 penalty in this book is, mechanically, a CLKPIPE halt.
- PX is an 8-bit hybrid synchronous/ripple counter clocked by SACU; it always starts at 0 — fine scroll never masks PX.
- Terminal count is 167, decoded by XUGU from five bits; with no sprite match this raises WODU, the H-Blank condition.
- SACU = OR2(ROXY, SEGU) — a 63.8-ge-deep gated clock with four halt conditions: fine scroll, data-not-ready, window activation, and sprite-match/H-Blank.
- 167 SACU rising edges per Mode 3, invariant with SCX.
The pixel counter (PX)
PX is an 8-bit hybrid synchronous/ripple counter clocked by SACU. The lower nibble (XEHO, SAVY, XODU, XYDO) is clocked directly by SACU with XOR carry logic — all four bits update on the same SACU rising edge. The upper nibble (TUHU, TUKY, TAKO, SYBE) clocks on TOCA = NOT(XYDO), rippling off bit 3.
| Gate | Role | Type | Clock | Notes |
|---|---|---|---|---|
| XEHO | PX bit 0 | dffr | SACU | Toggle |
| SAVY | PX bit 1 | dffr | SACU | D = XOR(SAVY, XEHO) |
| XODU | PX bit 2 | dffr | SACU | D = XOR(XUKE, XODU); XUKE = AND2(SAVY, XEHO) |
| XYDO | PX bit 3 | dffr | SACU | D = XOR(XYDO, XYLE); XYLE = AND2(XUKE, XODU) |
| TOCA | Upper-nibble clock | not | XYDO | |
| TUHU / TUKY / TAKO / SYBE | PX bits 4–7 | dffr | TOCA | Toggle / XOR carry, mirroring the lower nibble |
| XUGU | Terminal count decode | nand5 | SYBE, SAVY, TUKY, XEHO, XODU | Fires at PX = 167 |
| TADY | VOGA + PX-counter reset | nor2 | TOFU, ATEJ | Shared with VOGA's reset and (via ATEJ) the scan-counter chain |
PX is held in reset during Mode 2 and video reset and released when Mode 3 begins. PX starts at 0 regardless of fine scroll — fine scroll is pure clock suppression, never PX masking.
Terminal count 167. XUGU monitors PX bits {0, 1, 2, 5, 7}: all five high = 0b10100111 = 167 (bits 3, 4, 6 are 0 at that count, so the partial decode is exact at the terminal). XUGU=0 → XANO=1, and with no sprite match (XENA=1), WODU goes high — the H-Blank condition (STAT interrupts).
Fine scroll delays the first SACU; it never changes the count (dmg-sim measurement). Of the 168 PX states, 160 drive visible pixels: CP is gated by PX ≥ 9 through TOBA, so the first 8 CLKPIPE cycles shift through the register without reaching the glass, CP fires 159 times at PX 9–167, and the POVA path captures the 160th pixel at end-of-line (LCD output).
CLKPIPE / SACU
SACU — the pixel pump — is an OR2 at depth 63.8 ge with fan-out 53, clocking
52 cells across the shifters, the sprite attribute pipes, and PX. Its gate
chain (SACU ← SEGU ← TYFA ← VYBO) and the four halt inputs are below.
| Gate | Role | Type | Inputs | Notes |
|---|---|---|---|---|
| SACU | Pixel pipe shift clock (CLKPIPE) | or2 | ROXY, SEGU | Depth 63.8 ge; fan-out 53 |
| ROXY | Fine-scroll gate | nor_latch | Set: PAHA; Reset: POVA | See Startup and fine scroll |
| SEGU | Buffered CLKPIPE | not_x4 | TYFA | Depth 49.4 ge |
| TYFA | CLKPIPE gate | and3 | POKY, SOCY, VYBO | Depth 48.5 ge |
| POKY | Pixel-pipe data ready | nor_latch | Set: PYGO; Reset: LOBY | LOBY = NOT(mode3): clears POKY outside Mode 3 |
| SOCY | Window-halt gate | not_x1 | TOMU | SOCY = NOT(RYDY) via triple inversion (TOMU, SYLO) — drops during window activation |
| VYBO | Operational per-dot gate | nor3 | MYVO, FEPO, WODU | Depth 46.9 ge |
| FEPO | Sprite X priority aggregate | or2 | FOVE, FEFY | Depth 41.9 ge |
Four halt conditions — any one stalls CLKPIPE (SACU stuck high):
| Halt | Cause | Mechanism |
|---|---|---|
| ROXY=1 | Fine scroll active | Forces SACU=1 directly |
| POKY=0 | Data not ready (outside Mode 3, or first tile not yet fetched) | TYFA=0 → SEGU=1 |
| SOCY=0 | Window activation (RYDY=1) | TYFA=0 → SEGU=1 |
| VYBO=0 | Sprite match (FEPO=1) or H-Blank (WODU=1) | TYFA=0 → SEGU=1 |
When free-running, SACU is simply MYVO's alternation pushed through the tree: SACU is low while ALET is high and high while ALET is low — one rising edge per dot, on ALET falling. The shift registers advance on SACU rising only.
Why CLKPIPE arrives late.
Crystal → ALET (16.3 ge) → MYVO (22.2) → VYBO (46.9, dominated by WODU's 45.5-ge input) → TYFA (48.5) → SEGU (49.4) → SACU (63.8)
The pixel clock is the deepest routinely-toggling signal in the PPU — which is exactly why data-vs-clock ordering works out: shifter data settles at depth 0–8 ge, the parallel-load enable at 16.2 ge, and the shift clock last of all. Data is always stable long before the edge that shifts it.
Sprite penalty interaction
A sprite X match sets FEPO=1, halting CLKPIPE for the fetch core (TEKY↑ → TAKA↓, 6.010 dots; zero SACU transitions; SACU resumes 1.024 dots after TAKA clears — dmg-sim measurement; the plateau total is exactly +11.000 dots, penalty model). The resume edge is the book's one margin-sensitive edge assignment — its exact dot is delay-calibration-dependent to within half a dot, a model prediction rather than a hardware fact; the totals (the +11.000 plateau, the 6-dot core) are calibration-independent. The freeze also pins the drain-detect cascade (the tile fetcher), so no tile boundary fires during a sprite fetch.
VYBO sees FEPO, not TAKA. A mid-fetch LCDC.1 ← 0 write drops FEPO early
via AROR (a short combinational chain), and SACU resumes mid-fetch while the
fetch state machine runs on unaffected. The Mode-3-end dot count is set by
the FEPO↓ edge, not TAKA↓ (sprite pipeline).
H-Blank
When PX hits terminal with no sprite match, WODU=1 halts CLKPIPE the same way; VOGA captures WODU on the same-dot ALET rising edge and XYMU sets 0.436 dots after WODU↑ (mode transitions). When XYMU flips, the PAHA/LOBY/ROXY/POKY chain switches to its non-Mode-3 state combinationally — POKY disarms, ROXY re-arms for the next scanline, the Mode-3-only DFFs acquire their reset hold, and VRAM/OAM access control releases. No clock edge is waited for.
Startup and Fine Scroll
Every scanline's Mode 3 opens with the same deterministic cascade from the
scan-complete pulse (AVAP) to the first pixel clock — and fine scroll rides
on it, holding the pixel clock off for exactly SCX & 7 extra dots. This
page covers the fine-scroll counter, the startup pipeline stage by stage,
and the measurements that pin both.
- AVAP↑ → first SACU↑ = 7.026 dots at SCX=0, every stage at a fixed offset, zero variance across scanlines.
- Fine scroll is clock suppression: ROXY holds SACU high until the fine
counter matches
SCX & 7— exactly +1.000 dot per step. Mode 3 core duration is 173.045 + (SCX & 7) dots. - The SACU count stays 167 at every SCX — there is no push-and-discard.
- SCX is read live: whatever
ff43holds at the count=N ROXO↑ capture edge decides the discard, measured to a 0.014-dot bracket.
The fine scroll counter
A 3-bit ripple counter that discards SCX & 7 pixels at Mode 3 startup by
delaying SACU's enable. The counter runs at CLKPIPE cadence (via ROXO =
NOT(SEGU)) while ROXY holds SACU high; a two-stage match pipeline (PUXA on
ROXO, NYZE on MOXE) captures the match, and POVA then clears ROXY.
| Gate | Role | Type | Clock / Trigger | Notes |
|---|---|---|---|---|
| PECU | Fine-counter gated clock | nand2 | Inputs: ROZE, ROXO | Frozen when ROZE=0 (counter at 7) |
| ROZE | Fine-counter self-stop | nand3 | Inputs: RUBU, ROGA, RYKU | 0 only at count 7 — MOCE's analogue |
| RYKU | Fine count bit 0 | dffr | PECU | Async reset by PASO |
| ROGA | Fine count bit 1 | dffr | RYKU (~Q ripple) | Async reset by PASO |
| RUBU | Fine count bit 2 | dffr | ROGA (~Q ripple) | Async reset by PASO |
| PASO | Fine-counter reset | nor2 | Inputs: PAHA, TEVO | Asserted outside Mode 3 or at tile boundaries |
| SUHA | Per-bit SCX match | xnor | ff43_d0 vs RYKU | 1 when bit 0 matches |
| SYBY | Fine-count bit 1 match | xnor | ff43_d1 vs ROGA | 1 when bit 1 matches |
| SOZU | Fine-count bit 2 match | xnor | ff43_d2 vs RUBU | 1 when bit 2 matches |
| RONE | Drain-gated match aggregate | nand4 | Inputs: ROXY, SUHA, SYBY, SOZU | |
| POHU | Match comparator | not_x1 | Input: RONE | POHU = AND4(ROXY, SUHA, SYBY, SOZU) — self-terminating via the ROXY term |
| PUXA | Match capture (odd) | dffr | ROXO | Data: POHU |
| NYZE | Match capture (even) | dffr | MOXE | Data: PUXA |
| POVA | Match pulse | and2 | Inputs: NYZE.Q_n, PUXA | Single-dot pulse; drives ROXY's reset |
| ROXY | Fine-scroll gate | nor_latch | Set: PAHA; Reset: POVA | The SACU suppressor |
PAHA = NOT(mode3) where mode3 = XYMU.Q_n. The double inversion means PAHA
follows XYMU.Q — PAHA=1 outside Mode 3, 0 during it.
The match pipeline:
- Per-bit XNORs read
ff43(SCX) live — there is no SCX snapshot at Mode 3 entry. - POHU = AND4(ROXY, match bits): the ROXY term makes the comparator self-terminating — it collapses as soon as POVA clears ROXY.
- PUXA latches POHU on ROXO↑; NYZE mirrors PUXA on the next MOXE↑; POVA = AND2(NYZE.Q_n, PUXA) is high exactly between those two edges — a single-dot pulse, by construction unable to re-fire within the scanline (PUXA/NYZE reset to 0 outside Mode 3, guaranteeing NYZE.Q_n=1 at entry).
The discard terminates on whatever ff43 holds at the count=N ROXO↑
capture edge. A CPU SCX write reaches the comparison only if its CUPA
transparency window has made the value visible by that edge; a write
arriving even fractionally later is missed, and the pre-write SCX decides
the discard — measured to a 0.014-dot bracket
(late SCX writes).
The startup pipeline: AVAP to first SACU
Each scanline's Mode 3 begins with a 7-dot cascade from AVAP↑ to the first
SACU rising edge — 7.026 dots at SCX=0, with every stage at a deterministic
offset, zero variance across scanlines (dmg-sim measurement, purpose-built
scx0_steady
ROM, n=1,612):
| Stage | Δ from AVAP (dots) | Type | Mechanism |
|---|---|---|---|
| AVAP↑ | 0.000 | Combinational | OAM scan complete |
| XYMU↓ | 0.000 | Latch | AVAP drives XYMU.r directly |
| NYXU↓ | 0.002 | Combinational | NOR3 delay (439 ps) |
| LAXU/MESU/NYVA reset | 0.002 | Async reset | Fetch counter cleared |
| LAXU↑ | 0.986 | Clocked | First counter toggle after NYXU releases |
| NYKA↑ | 5.481 | DFF (ALET↑) | Fetch-complete captures after state 5 |
| PORY↑ | 5.991 | DFF (MYVO↑) | Half-dot later |
| SUVU↓, TAVE↑ | 5.996 | Combinational | First TEVO of the scanline fires |
| PYGO↑ | 6.480 | DFF (ALET↑) | |
| POKY↑ | 6.481 | NOR latch | Data-ready latches (+342 ps) |
| ROXY↓ | 6.490 | NOR latch | Cleared through the fine-scroll match chain (SCX=0: immediate match) |
| SACU↓ (glitch) | 6.511 | Combinational | ROXY opens the OR before the first proper pulse |
| SACU↑ | 7.026 | — | First pixel clock |
| XEHO↑ | 7.026 | Clocked | PX 0→1, within the rounding of the same edge |
| RYFA↑ | 13.991 | DFF | Drain-detect chain reaches the first tile boundary |
The "AVAP at 0.000" row is the dot-count reference, not an ALET-edge alignment — AVAP transitions on the ALET falling edge preceding the table (it derives from BYBA's XUPY-clocked capture); the downstream DFFs are ALET/MYVO-edge aligned. The cascade is identical on every scanline including the first (whose 2-dot shortening lives entirely in Mode 0 — see Register writes). The window's MOSU trigger feeds NYXU through the same NOR3 and produces the same 7-dot pipeline (Window control). The NYXU pulse itself runs 0.492 dots when AVAP drives it and 0.495 dots from TEVO (0.503 from MOSU).
Fine-scroll startup suppression
ROXY holds SACU high until the fine counter matches SCX & 7, POVA clears
ROXY, and the pipe starts shifting. No pixels enter the FIFO during the
gated window — the register is frozen, not loaded-and-discarded. The SACU
count stays 167 (a push-and-discard model would predict 167 + (SCX & 7)).
The cascade (NYKA → PORY → PYGO → POKY) fires at the same offsets regardless
of SCX; only ROXY's release moves. Measured at three SCX values (dmg-sim
measurements, purpose-built scx{0,3,7}_steady ROMs, n=1,151–1,612 scanlines each, zero variance):
| SCX | Mode 3 core, AVAP → WODU (dots) | SACU count | First SACU (dots from AVAP) |
|---|---|---|---|
| 0 | 173.045 | 167 | 7.026 |
| 3 | 176.045 | 167 | 10.026 |
| 7 | 180.045 | 167 | 14.026 |
The model is exact: 1.000 dot per SCX & 7 step; first SACU at
7 + (N & 7) + 0.026; Mode 3 core duration 173 + (N & 7) + 0.045.
Edge-by-edge startup waveform
The internal transitions, measured at SCX ∈ {0, 3, 7} (dmg-sim measurement,
purpose-built scx{0,3,7}_steady ROMs; AVAP↑ = 0.0000; PASO re-arms for good shortly
after ROXO's first rise):
Common cascade (SCX-independent):
| Δ dots | Signal | Mechanism |
|---|---|---|
| +0.0349 | PAHA↓ | NOT(mode3) — XYMU.Q_n rose on the AVAP reset just before |
| +0.0390 | PASO↑ | Fine-counter reset released |
| +5.4805 | NYKA↑ | Fetch state 5 complete |
| +5.9910 | PORY↑ | |
| +5.9963 | TEVO↑ | Startup tile pulse |
| +5.9973 | PASO↓ | Briefly re-asserted by TEVO |
| +6.4800 | PYGO↑ | |
| +6.4814 | POKY↑ | Data-ready |
| +6.4827 | SEGU↓ | First CLKPIPE enable |
| +6.4838 | TEVO↓ | |
| +6.4846 | ROXO↑ | Clocks PUXA and the fine counter |
Per-SCX match pipeline:
| Stage | SCX=0 | SCX=3 | SCX=7 |
|---|---|---|---|
| Counter ticks (one per ROXO↑) | 0 | 3 (+6.993 … +8.993) | 7 (+6.993 … +12.993) |
| POHU↑ | already 1 at reset | +8.9949 | +12.9949 |
| PUXA↑ | +6.4885 | +9.4864 | +13.4864 |
| POVA↑ | +6.4897 | +9.4876 | +13.4876 |
| ROXY↓ | +6.4902 | +9.4881 | +13.4881 |
| POHU↓ (self-termination) | +6.4906 | +9.4886 | +13.4886 |
| SACU↓ (glitch) | +6.5111 | +9.5090 | +13.5090 |
| NYZE↑ (NYZE.Q_n↓) | +6.9825 | +9.9825 | +13.9825 |
| POVA↓ | +6.9881 | +9.9881 | +13.9881 |
| SACU↑ | +7.0259 | +10.0259 | +14.0259 |
Each SCX increment shifts the block by exactly 1.0000 dots with identical internal shape. The counter self-stop at SCX=7 does not suppress ROXO (PUXA's clock path is independent of PECU), so the SCX=7 match fires with the same geometry.
The tail-of-Mode-3 drain
The drain detector fires on a fixed 8-dot cadence at +13.997, +21.997, …, +165.997, +173.997 dots from AVAP — independent of SCX. Mode 3 ends at +(173.045 + N). The 21st drain edge at +173.997 therefore lands:
- at SCX=0: after Mode 3 ends (by 0.952 dots) — suppressed: the VOGA/WEGO transition and the mode3 reset on PYGO/RENE/RYFA clear the cascade first;
- at SCX&7 ≥ 1: inside Mode 3 — SEKO fires, TEVO → NYXU → a final parallel-load of the in-flight fetch's tile data.
The 21st load completes within the closing dot but produces no observable
LD output — the SACU count is already at 167, so the freshly loaded content
never meets another CP-gated edge before H-Blank. Both planes load from the
same tile index — there is no hybrid-fetch path here (dmg-sim measurement,
AGE m3-bg-scx-nocgb ROM: post-load shifter content 0b11111111 on both
planes where tilemap column 21 holds an all-$FF tile).
Late SCX writes during the discard
The live-SCX rule, measured to its edge (dmg-sim measurement, gambatte
ly0_late_scx7_m3stat_scx3_{1,2} ROMs — SCX=3 held, late SCX=7 write during
the first-scanline discard):
| ROM | SCX=7 CUPA↑ | count=3 ROXO↑ | SCX seen at capture | Discard ends | First WODU↑ |
|---|---|---|---|---|---|
_1 (hardware: mode 3) | +5.496 | +9.483 | 7 (write in time) | +13.488, count=7 | +180.045 (= 173+7) |
_2 (hardware: mode 0) | +9.496 | +9.483 | 3 (write 0.014 dots late) | +9.488, count=3 | +176.045 (= 173+3) |
In _2 the write becomes visible 0.025 dots after the capture edge — and
the already-cleared ROXY cannot re-open (POHU collapsed at ROXY↓). The rule
is purely "the ff43 value at the count=N ROXO↑ wins"; both hardware
outcomes follow from it.
Startup invariance under stacked sprites
The whole cascade is configuration-invariant: with 5+5 sprites stacked at
two X positions chosen so (X + SCX) mod 8 == 0 (the wilbertpol
intr_2_mode0_timing_sprites_scx1_nops worst case), every BG-fetch and cascade
transition between AVAP↑ and first SACU↑ lands at an identical sub-dot
offset across SCX ∈ {0..4}, and the sprite-chain edges (FEPO/TEKY/SOBU/
RYCE/TAKA/WUTY/SUDA) are likewise SCX-invariant (dmg-sim measurement). The
transitions nearest integer-dot boundaries sit 83–469 ps clear of them,
deterministically — real-silicon process variance cannot produce
per-configuration selectivity from a uniform per-die offset.
The BG Shift Registers
The shifters are where fetched tile data becomes a pixel stream: two 8-bit planes shifting under SACU, refilled in parallel every 8 dots from a bank of temporary latches. The interesting timing is all in the load path — when it fires, and why loaded data can never race the shift clock.
- Two 8-bit planes of dffsr cells, clocked by SACU; tile data staged through 8 temporary latches off the VRAM bus.
- The parallel load is level-sensitive, fired by NYXU via LOZE — independent of the shift clock.
- Load enable settles at 16.2 ge against SACU's 63.8 — loaded data is always stable before any shift edge.
- Startup fires two loads: stale content at AVAP, fresh at +5.996 dots; the stale load never reaches the glass.
The cells
Two 8-bit shift registers (planes A and B), each an 8-stage chain of dffsr cells clocked by SACU. Tile data arrives via an 8-bit temporary latch group whose enable chain hangs off fetch-counter bit 2.
| Gate family | Role | Type | Clock / Trigger | Count | Notes |
|---|---|---|---|---|---|
| BG shifter plane A | Tile data plane 0 | dffsr | SACU | 8 | MYDE–PYBO range |
| BG shifter plane B | Tile data plane 1 | dffsr | SACU | 8 | TOMY–SOHU range |
| Temp data latches | VRAM bus capture | dlatch_ee_q | Enable: LUNA/LOMA | 8 | LEGU, LUZO, MEGU, MUKU, MYJY, NASA, NEFO, NUDU |
| LUNA / LOMA / METE / NYDY / NOFU | Temp-latch enable chain | not / nand3 | from NYVA | 5 | Depth 16.2 ge at LUNA |
The 16 shifter cells: MACU, MODU, MOJU, MYDE, NEDA, NEPO, NOZO, PYBO, RALU,
RYSA, SADY, SETU, SOBO, SOHU, TACA, TOMY — chain order determined by dffsr
d-pin connectivity.
Parallel load via NYXU
Each cell has a NAND2 pair driving its s_n / r_n, gated by LOZE =
NOT(NYXU): s_n = NAND2(LOZE, temp bit), r_n = NAND2(LOZE, temp bit
complement). When NYXU pulses low, LOZE pulses high and every cell loads its
temp-latch bit; when NYXU is idle, both NANDs output 1 and the cells shift
normally under SACU. The load is level-sensitive and independent of the
shift clock — and since the load enable settles at 16.2 ge against SACU's
63.8, loaded data is stable before any shift edge.
Two parallel loads per Mode 3 startup
NYXU fires twice during the startup cascade:
- Load #1 at AVAP (dot 0). The temp latches still hold pre-Mode-3 content — the previous scanline's last tile, or power-on state on the very first frame. The shifters load this stale content.
- Load #2 at the first TEVO (dot 5.996). By now the fetch counter has run 0–5 and the temp latches hold this scanline's first tile. The shifters load the fresh content, overwriting load #1.
The first SACU edge lands at 7.026 — after load #2 — and CP is gated by PX ≥ 9, so load #1's stale content never reaches the glass. This matters cross-frame: on the first frame vs a steady-state frame the stale content differs, but load #2 overwrites it before any CP sample either way, so LD output is identical. At mid-Mode-3 tile boundaries only the TEVO-driven load fires — 20 more per scanline, 21 at SCX&7 ≥ 1 (the tail drain).
Mode 3: The Sprite Pipeline
Sprite rendering interleaves with BG fetching: when a sprite's X position matches the pixel counter, a trigger chain freezes the pixel clock while the sprite fetcher reads OAM attributes and VRAM tile data, then merges the fetched pixels into dedicated shift registers with hardware-level priority. This chapter covers that machinery across four pages.
- The fetch core costs 6 dots (the TEKY↑ → TAKA↓ freeze — 6.010 dots at gate resolution); the documented "6–11 dots per sprite" is this fixed core plus 0–5 dots of BG-fetcher alignment overhead.
- Sprite-to-sprite priority is implemented in the shift register: the parallel load is transparency-gated per stage, so the first-fetched (lowest-OAM-index) sprite's opaque pixels survive.
- N sprites stacked at one X cost the first sprite's penalty plus (N − 1) × 6.000 dots — the inter-fetch gap is combinational (gate delays).
- The CLKPIPE freeze is FEPO-driven, not TAKA-driven: a mid-fetch
LCDC.1 ← 0write resumes the pixel clock early while the fetch machine runs on. - The fetcher has no dedicated OAM DFFs — it reuses Mode 2's capture chain; under DMA it captures the byte-pair at DMA's current address.
What's where
| Page | Covers |
|---|---|
| Sprite storage and X matching | The sprite shift registers, the transparency-gated load, the per-slot store and X comparators, FEPO, and the attribute pipes |
| The fetch state machine | The TEKY→SOBU→RYCE→TAKA trigger chain, the fetch counter, the BG-cascade freeze, OAM byte capture (including under DMA), and TAKA's line-end behaviour |
| The penalty model | Measured penalties by SPRITEX, the (K + SCX) mod 8 plateau, stacked-sprite composition, the K=167 right edge, and sprites in the prelude |
| LCDC.1 and LCDC.2 paths | OBJ_EN's trigger-vs-output split, mid-fetch disable writes, the OFF single-pixel transient, and OBJ_SIZE's live VRAM row-bit path |
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.
- 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 family | Role | Type | Clock / Trigger | Count | Notes |
|---|---|---|---|---|---|
| Sprite shifter plane A | Sprite tile data plane 0 | dffsr | SACU | 8 | NYLU–VUPY range |
| Sprite shifter plane B | Sprite tile data plane 1 | dffsr | SACU | 8 | NURO–WUFY range |
| Sprite temp A | VRAM bus capture, plane 0 | dlatch_ee_q | Enable: latch_sp_bp_a | 8 | PEFO, ROKA, MYTU, RAMU, SELE, SUTO, RAMA, RYDU |
| Sprite temp B | VRAM bus capture, plane 1 | dlatch_ee_q | Enable: latch_sp_bp_b | 8 | REWO, PEBA, MOFO, PUDU, SAJA, SUNY, SEMO, SEGA |
| Per-stage load gate | OR3 per stage | or3 | combinational | 8 | MEFU, MEVE, MYZO, RUDA, VOTO, VYSA, TORY, WOPE = OR3(XEFY, sprite_px_aN, sprite_px_bN) |
| Per-stage load enable | sprite_onN driver | not | per-stage OR3 | 8 | LESY, 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.
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.
| Aspect | BG shifter | Sprite shifter |
|---|---|---|
| Load enable | Broadcast LOZE = NOT(NYXU) | Per-stage sprite_onN |
| Timing | AVAP / TEVO pulses | WUTY pulse per fetch |
| Scope | All 8 stages, unconditional | Transparent stages only |
| Role | Replace tile | Overlay 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 family | Role | Type | Clock | Count | Notes |
|---|---|---|---|---|---|
| Per-slot X store | Holds NOT(X) — captures the inverted OAM X byte on the slot's save strobe | drlatch_ee | Enable: per-slot save strobe | 8 × 10 | Slot 0: XEPE, YLAH, ZOLA, ZULU, WELO, XUNY, WOTE, XAKO; save strobe FUXU (store-counter = 0 decode); r_n = per-slot reset |
| Per-slot comparator | XOR per bit vs NOT(PX), NOR4 per nibble | xor + nor4 | combinational | 10 × 10 | Slot 0: ZOGY, ZEBA, ZAHA, ZOKY / WOJU, YFUN, WYZA, YPUK → ZAKO / XEBA |
| Per-slot match | NAND3(AROR, low nibble, high nibble), active-low | nand3 | combinational | 10 | FEFY group: YDUG (slot 0), DYDU, DEGO, YLOZ, XAGE; FOVE group: EGOM, YBEZ, DYKA, EFYL, YGEM |
| FEFY / FOVE | Match aggregates (slots 0–4 / 5–9) | nand5 | — | 2 | |
| FEPO | Sprite X priority aggregate | or2 | FEFY, FOVE | 1 | Feeds VYBO — the CLKPIPE freeze (the pixel clock) |
| Per-slot reset | OR2(line reset, slot fetch-done) | or2 + dffr | — | 10 | Slot 0: DYWE = OR2(DYBA, EBOJ); EBOJ = DFF(d = GUVA, clk = WUTY) — sets when slot 0's fetch completes |
| Sprite attribute pipes | Per-pixel palette select + BG-over-OBJ priority | dffsr | CLKPIPE | 16 | LYME (output: sprite_px_palette), MODA, NUKE, PALU, ROSA, RUGO, SATA, SOMY, VOSA, VUMO, WODA, WURU, WYFU, XETE + VEZO → VAVA |
| VAVA / VEZO | Priority mask pipe MSB / LSB | dffsr | CLKPIPE | 2 | Load 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.
The Fetch State Machine
The sprite fetcher is a one-shot trigger chain, a 3-bit counter on the opposite clock edge from the BG fetcher, and a running latch (TAKA) whose lifetime extends well beyond the fetch itself.
- Trigger: TEKY → SOBU (DFF) → RYCE → TAKA, a one-shot closed by SUDA; the counter runs 0–5 on ALET rising — the opposite edge from the BG fetcher.
- TEKY's AND4 requires FEPO (X match), TUKU (no window activation), LYRY (BG fetch complete), and SOWO (no fetch already running).
- WUTY ends the fetch at +6.010 dots from TEKY↑, clears TAKA, and gates the shifter load.
- The BG drain cascade is pinned throughout — no NYXU fires during a sprite fetch.
- TAKA is high for most of every scanline: ATEJ re-asserts it at each line end, and the startup TAVE clears the carry-over.
The gates
| Gate | Role | Type | Clock / Trigger | Notes |
|---|---|---|---|---|
| TOXE / TULY / TESE | Fetch counter bits 0–2 | dffr | SABE, then ~Q ripple | Reset by TAKA↑ |
| SABE | Gated fetch clock | nand2 | LAPE, TAME | Advances on ALET rising — opposite edge from the BG fetcher |
| TAME | Self-stop | nand2 | TESE, TOXE | Freezes SABE at count 5 |
| TEKY | X-match trigger | and4 | FEPO, TUKU, LYRY, SOWO | TUKU = NOT(RYDY) blocks during window activation; LYRY requires BG fetch complete; SOWO = NOT(TAKA) blocks re-trigger |
| SOBU | Fetch request | dffr | TAVA (clk5 = ALET rising) | Captures TEKY — the one DFF in the trigger chain. No reset domain (r_n tied high) |
| SUDA | Trigger one-shot partner | dffr | LAPE (clk4 = ALET falling) | Captures SOBU ~1 dot later; suda_n closes RYCE. No reset domain |
| RYCE | One-shot fire | and2 | SOBU, suda_n | High from SOBU's capture until SUDA mirrors it |
| SECA | TAKA set net | nor3 | RYCE, ROSY, ATEJ | Three set arms: Mode-3 trigger, LCD-off, and the line-end pulse |
| TAKA | Fetch-running latch | nand_latch | Set: SECA; Reset: VEKU = NOR2(WUTY, TAVE) | Freezes SACU via FEPO's persistence; WUTY = normal end-of-fetch, TAVE = startup carry-over clear |
| WUTY | Fetch done | not | VUSA | Clears TAKA; clocks the per-slot fetched-flag DFFs |
| VUSA | Fetch-done aggregate | or2 | TYNO, tyfo_n | Two parallel decode branches |
| TYNO | Done decode A | nand3 | SEBA, TOXE, VONU | |
| TYFO | Done decode B | dffr | LAPE | Captures TOXE; no reset domain |
| TOBU / VONU / SEBA | TULY capture chain | dffr | clk5 / clk5 / clk4 | Mode-3-only (r_n = mode3); feed TYNO |
The trigger chain
TEKY → SOBU (DFF) → RYCE → TAKA, with SUDA closing the one-shot
Measured with zero variance (dmg-sim measurement, purpose-built
sprite_steady ROM —
one sprite at SPRITEX 80, 64 firing scanlines):
| Stage | Δ from TEKY↑ (dots) |
|---|---|
| TEKY rises (ALET falling edge) | 0.000 |
| SOBU captures on the next ALET rising | +0.493 |
| RYCE fires (a gate delay from SOBU) | +0.493 |
| TAKA fires | +0.496 |
| Counter resets to 0 | ~+0.50 |
| SUDA captures, closing RYCE | +0.997 |
The counter then counts 0→5 over five dots (one tick per ALET rising via SABE); at count 5 WUTY fires and clears TAKA at +6.010 dots from TEKY↑.
The net penalty is 6 dots (six suppressed SACU edges; the TEKY↑ → TAKA↓ freeze is 6.010 dots gate-resolved), zero SACU transitions inside the window, SACU resuming 1.024 dots after TAKA falls (the next ALET falling edge) — developed with its alignment overhead in the penalty model.
A brief TEKY glitch pulse (~0.016 dots ≈ 3.9 ns) appears as TAKA clears and FEPO deasserts (combinational race); it is too narrow for SOBU's capture and has no pipeline effect.
The BG cascade freezes too
Throughout each TAKA-high window, the BG drain-detect cascade is pinned:
SEGU stuck at 1 gives RYFA no clock edges (frozen at 1 — the pre-freeze
capture of PANY=1 is guaranteed, because LYRY=1 was required for TEKY);
RENE keeps re-capturing RYFA=1; SEKO stays 0; TEVO and NYXU never fire. The
BG counter holds at 5 by the ordinary MOCE self-stop, and no BG-shifter
parallel load occurs during a sprite fetch. Verified across 30 TAKA-high
windows with zero NYXU/RENE/RYFA/SEGU/SEKO transitions inside any of them,
while the same scanlines show the normal 22-pulse NYXU cadence outside the
cluster (dmg-sim measurement, the gambatte 10spritesPrLine stacked-sprite
ROM).
OAM byte capture during the fetch
The fetcher has no dedicated tile/attribute DFFs — it reuses the two-stage capture chain that Mode 2 uses for (Y, X), with the byte roles following the address:
| Stage | Cells | Mode 2 (scan) | Mode 3 (fetch) |
|---|---|---|---|
| 1, bank B | YDYV…ZECA | Y byte (OAM byte 0) | tile index (byte 2) |
| 1, bank A | XYKY…ECED | X byte (byte 1) | attribute (byte 3) |
| 2, Y-side | XUSO, XEGU, YJEX, XYJU, YBOG, WYSO, XOTE, YZAB | Y-compare operand B | sprite tile VRAM row bits |
| 2, X-side | YLOR, ZYTY, ZYVE, ZEZY, GOMO, BAXO, YZOS, DEPO | X-store fan-out | attribute bits: GOMO = palette, BAXO = X-flip, YZOS = Y-flip, DEPO = priority (bits 0–3 unused on DMG) |
In Mode 3 the capture enable reduces to oam_data_latch = WEFY =
AND2(TUVO, tyfo_n), with TUVO high while the fetch counter ∈ {0, 1} — one
pulse per fetch, near TAKA-rise, capturing the byte-pair at the
fetcher-driven address (sprite index × 4 + 2, + 3). Stage 2 is transparent
through most of Mode 3, so the bytes flow straight through.
Under DMA, the fetcher's bus driver is tri-stated but neither capture
enable is gated — each fetch's pulse captures the byte-pair at DMA's
current address instead: garbage tile index to the VRAM path,
DMA-source-byte attribute bits. Measured: 10 capture pulses across one
DMA + Mode 3 overlap, each at TAKA=1/counter=0, each overwriting Stage 1
with the byte-pair at the then-current dma_a (dmg-sim measurement,
Hacktix strikethrough ROM).
TAKA outside Mode 3
TAKA's set net SECA = NOR3(RYCE, ROSY, ATEJ) includes the line-end pulse: ATEJ fires at every scanline boundary (including VBlank lines) and re-asserts TAKA during H-Blank. TAKA is therefore high for most of every scanline — low only between the startup carry-over clear (TAVE↑ at AVAP+5.996; TAKA↓ at +5.996) and the line's first real fetch, and between fetch completions. The carry-over is cleared each scanline by TAVE through VEKU's second reset arm.
Measured per-scanline waveform (zero variance, six scanlines): ATEJ↑ at AVAP+376.000 dots sets TAKA (= the scanline boundary: 456 − 80 dots of Mode 2); ATEJ pulse width 1.001 dots (one half-XUPY cycle); TAKA holds through H-Blank until the next line's TAVE.
A side effect of the carry-over clear: SOWO = NOT(TAKA) rising at +5.999 briefly satisfies TEKY's AND4 — a glitch pulse too narrow for SOBU to capture.
The Penalty Model
What sprite fetches cost, measured: a fixed 6-dot core, an alignment
overhead set by (K + SCX) mod 8, an exact composition rule for stacked
sprites, and two edge regimes — the prelude and the right edge.
- Every fetch core costs exactly 6.000 dots; the documented "6–11 dots per sprite" is the core plus 0–5 dots of BG-fetcher alignment.
- The maximum +11.000 plateau occurs at (K + SCX) mod 8 = 0.
- N sprites at one X: first sprite's penalty + (N − 1) × 6.000 dots — measured exact through N=10.
- SPRITEX < 8 hides the fetch in the startup prelude — same total cost, different shape.
- The K=167 right edge is not special: the single-sprite cost floors at the 6-dot core and the same composition rule applies per effective sprite.
Penalty by SPRITEX
Single-sprite and four-sprite penalty by SPRITEX (= OAM X; displayed X + 8), SCX=0 (dmg-sim measurement, gbmicrotest sprite ROMs; firing-scanline median minus non-firing median):
| SPRITEX | Single sprite (dots) | Four sprites (dots) |
|---|---|---|
| 0 | +11.000 (prelude-absorbed) | — |
| 8 | +11.000 | +45.030 |
| 9 | +10.000 | +40.937 |
| 10 | — | +36.843 |
| 11 | — | +32.749 |
| 12 | — | +27.646 |
| 13 | — | +24.562 |
| 14 | — | +27.895 (tile-boundary anomaly) |
| 15 | — | +38.643 (tile-boundary anomaly) |
The single-sprite cost decrements exactly 1 dot per +1 SPRITEX over [8, 9]; the four-sprite penalty decrements a clean ~4.094 dots per +1 SPRITEX over the first three steps of [8, 13] (≈1 dot of alignment per sprite × 4 sprites). The SPRITEX=12 row (+27.646) carries a sub-dot wobble — its non-firing baseline itself drifts ~1 dot by tile-boundary alignment, so the 11→12 and 12→13 steps read −5.103 / −3.084 rather than the clean −4.094. At SPRITEX ∈ {14, 15} the sprites straddle a BG tile boundary and per-sprite alignment overhead re-enters in a form the linear model does not capture (dmg-sim measurement).
The +11.000 value reproduces at (K, S) = (8, 0), (7, 1), and (167, 1) — all with (K + S) mod 8 = 0 — with zero variance across 72 firing scanlines per ROM (dmg-sim measurement).
Stacked sprites at one X: the chain collapses to combinational
When N sprites share an OAM X byte, each runs its full fetch core but the inter-fetch gap collapses to gate delays: WUTY↑ᵢ → VEKU → TAKA↓ᵢ → SOWO↑ → TEKY↑ᵢ₊₁ with no clocked element on the path. SACU never resumes between fetches, the BG counter stays at 5, LYRY stays high — which is exactly what keeps the AND4 re-fire chain armed. Each completed fetch's per-slot fetched-flag DFF (EBOJ/CEDY/FONO family, clocked by WUTY) clears that slot's X latch, so the next unfetched same-X slot drives FEPO for the re-fire; the chain ends when no unfetched same-X slot remains.
Measured cadence (zero variance, 7 firing scanlines per N, the gambatte
10spritesPrLine K=167 chain):
| Stage | Δ from TEKY↑ᵢ (dots) |
|---|---|
| SOBU↑ᵢ | +0.481 |
| TAKA↑ᵢ | +0.484 |
| WUTY↑ᵢ (counter=5) | +5.998 |
| TAKA↓ᵢ | +5.999 |
| TEKY↑ᵢ₊₁ (same dot) | +5.999 |
mode3_ext(N, K, S) = single_sprite_penalty(K, S) + (N − 1) × 6.000 dots
The rule's measured anchors:
| Configuration | Predicted | Measured |
|---|---|---|
| N=1, K=0 (prelude-absorbed) | 11.000 | 11.000 |
| N=2, K=0 | 17.000 | 17.000 |
| N=3, K=0 | 23.000 | 23.000 |
| 5 × K=7 + 5 × K=167, SCX=1 | 70.000 | 70.000 |
The Mooneye intr_2_mode0_timing_sprites calibration table (extras of
2/4/5/7/8/10/11/13/14/16 M-cycles for N = 1..10) is M-cycle rounding of
this uniform 6-dots-per-sprite extension — there is no hardware
shared-fetch alternation, and the BG fetcher plays no per-sprite role
(it is frozen throughout the chain). Multi-group configurations compose
per X-group: when PX passes from group A to group B, FEPO drops, SACU
resumes, the BG fetcher advances normally, and group B re-pays its own
first-sprite alignment.
The right edge: K = 167
At K = 167 the X match fires at the dot WODU would otherwise fire. The
cost is not right-edge-special: (167 + 0) mod 8 = 7 puts the single
sprite at the alignment model's 6-dot floor, and the stacked-composition
rule applies unchanged — ext = 6.000 + (N − 1) × 6.000 per effective
sprite. Measured (dmg-sim measurement, gambatte 10spritesPrLine_10xposA7
ROM — nine on-screen sprites — and N ∈ {1,2,3,4} patched variants, zero
variance, 7 firing lines each):
| Effective N | Mode 3 extension (dots) | Integer part |
|---|---|---|
| 1 | +5.978 | 6 = 1 × 6 |
| 2 | +11.975 (two ROM variants) | 12 = 2 × 6 |
| 3 | +17.978 | 18 = 3 × 6 |
| 9 | +53.977 | 54 = 9 × 6 |
The ~0.02-dot residual against exact N × 6 is the end-of-chain WODU rise arriving through the FEPO↓/XENA arm rather than the PX-decode arm. Per-sprite TEKY spacing in the long chain: 5.947 dots between #1 and #2, then exactly 6.000 between each subsequent pair, ending in a final TEKY glitch pulse too narrow for SOBU's capture as FEPO falls.
Sprites at SPRITEX < 8: the fetch hides in the prelude
When the X match fires before the first SACU↑, the whole fetch lands inside the AVAP→first-SACU prelude instead of between SACU pulses — same total cost, different shape. Measured at SPRITEX=0 (zero variance, 23 firing scanlines): the real TEKY fires at +11.255 dots from AVAP (the second LYRY-high event — the first is consumed by the TAKA carry-over clear), the fetch runs +11.766 → +17.399, the first SACU lands at +18.430 (vs 7.026 baseline), and total Mode 3 duration is identical to the SPRITEX=8 case.
LCDC.1 and LCDC.2: The Sprite Consumer Paths
Two LCDC bits act directly inside the sprite pipeline. OBJ_EN (LCDC.1) splits into a trigger path and an output path that a mid-Mode-3 write can divorce; OBJ_SIZE (LCDC.2) feeds the VRAM row-bit mux live while sprite visibility stays scan-latched.
- XYLO (OBJ_EN) has two pipeline consumers: AROR gates the trigger, XULA/WOXA gate the output mux — a mid-Mode-3 flip between fetch and emission splits them.
- A mid-fetch OFF write drops FEPO within gate delays of CUPA↑; SACU resumes mid-fetch and Mode 3's end is set by the FEPO↓ edge.
- On hardware, the OFF direction emits one shade-3 column — a below-netlist behaviour pinned by reference photographs.
- XYMO (OBJ_SIZE) is live in the VRAM row-bit path, but visibility was latched at scan time — mid-Mode-3 writes produce hybrid artefacts.
LCDC.1 (OBJ_EN): the pixel-mux paths
XYLO has four netlist consumers: AROR (the trigger gate — OAM scan), XULA and WOXA (this section), and XERO (CPU read-back).
| Gate | Role | Type | Inputs | Notes |
|---|---|---|---|---|
| XULA | Plane-B pixel-output gate | and2 | XYLO, sprite_px_b7 (WUFY) | |
| WOXA | Plane-A pixel-output gate | and2 | XYLO, sprite_px_a7 (VUPY) | |
| NULY | Sprite-visibility mask | nor2 | WOXA, XULA | 1 ⇔ sprite transparent or XYLO=0; drives the output-MUX combiner POKA (LCD output) |
Trigger vs output divergence. Steady-state XYLO is redundant across the two paths, but a mid-Mode-3 flip between a sprite's fetch and its emission splits them: data in the shifter but gated at output, or vice versa. The outcome of a write depends on where the CUPA window lands relative to the trigger's capture edge:
| CUPA window | Outcome |
|---|---|
| Entirely before TEKY's ALET edge | AROR=0 kills the trigger — no fetch, no penalty |
| Entirely after SOBU's capture | Fetch completes; XULA/WOXA gate the output (sprite suppressed at the mux) |
| Straddling the edge | Sub-dot race between the level-sensitive XYLO latch and the edge-triggered SOBU — not resolvable from topology alone |
The mid-fetch OFF write and Mode 3's end. In the "after SOBU" row, the
same write also drops FEPO within gate delays of CUPA↑ via AROR (TAKA is not on
the SACU halt path — VYBO watches FEPO). SACU resumes mid-fetch from the
AROR↓ dot; the fetch machine runs on; Mode 3's end is set by the FEPO↓
edge. Measured across the four gambatte
sprites_late_late_disable_spx{18,19,1A,1B} ROMs (bit-identical AVAP, all
four firing the write inside the freeze window), the closed form
mode3_end(SPRITEX, AROR↓) = floor(AROR↓ dot) + 167 − SPRITEX + 0.436
predicts the measured XYMU↑ to <0.001 dots in all four cases (dmg-sim measurement). The +3-dot step in the data between SPX=0x19 and 0x1A is a test-harness artifact — the +1-NOP CUPA shift (+4 dots) net of the −1-dot SPRITEX step — not a tile-boundary effect; the formula holds across it.
The OFF-CUPA single-pixel transient
Like the BGP OR-overlap (palette latches), LCDC.1 OFF transitions exhibit a behaviour below the netlist's modelling scope, characterised against hardware reference photographs:
When a CUPA write transitions LCDC.1 from 1 to 0 inside Mode 3, the first
cp_pad↑ after that CUPA↑ (+0.534 dots) emits one anomalous LCD column on real DMG hardware — shade 3 in the Mealybug reference image — before the sprites-off state takes hold. Subsequent columns follow the normal mux path. The 0→1 RESTORE direction produces no transient.
The gate-level simulation shows zero transitions on NULY/POKA/PATY across
the affected scanlines and white output at every column — the netlist does
not produce the symptom at any available timing calibration. The reference
images show the transient walking columns x=1..7 across LY≈24..125 (the
walking reflects per-LY cp_pad cadence variation from sprite-stall
positions; the write's AVAP-offset is constant within each test branch),
with verified column agreement at five sampled LYs.
Two further empirical facts:
- some LYs at frame edges and a few scattered rows do not fire the transient at all despite ps-identical netlist state (verified across all 144 LYs — no signal differs between firing and non-firing rows);
- multi-pixel rows in the reference carry additional earlier columns that are ordinary gate-level shifter content, not part of this rule.
Modelling the rule as an asymmetric consumption — the OBJ-mux popper reads
the pre-transition XYLO for one cp_pad while the trigger chain sees the
live value — reproduces the column-precise behaviour with no per-LY
exception list.
Whether the silicon mechanism is a popper-side sub-dot ordering, pad-driver residue, or LCD-glass sample-and-hold requires hardware measurement; the per-LY firing scope likewise. See Appendix C.
LCDC.2 (OBJ_SIZE): the VRAM address path
XYMO's second consumer (the first is the Y comparator — OAM scan) selects the sprite tile-data row bit during the fetch:
| Gate | Role | Type | Inputs | Notes |
|---|---|---|---|---|
| FUFO | XYMO complement | not_x1 | XYMO | |
| GEJY | OBJ_SIZE mux | ao22 | xuso_n, FUFO, ff40_d2, WAGO | 8×8: passes xuso_n (Y-offset bit 0); 8×16: passes WAGO |
| WAGO | 8×16 half-select | xor | sprite_y_store3, WUKY | Which half of the 16-row sprite |
| WUKY | Y-flip term | not_x1 | YZOS | YZOS latches OAM attribute bit 6 |
| FAMU | ~ma4 tri-state driver | not_if0 | in = GEJY, ena_n = ABON | One of 6 ~ma4 drivers; the sprite-fetch arm |
| ABON | FAMU enable | not_x2 chain | from TULY/VONU/mode3 | Active (low) exactly during the tile-data stages of the sprite fetch |
Mid-Mode-3 OBJ_SIZE writes. Sprite visibility was decided at scan
time and is latched; XYMO writes cannot change it. The VRAM row-bit path,
however, is live: any in-progress or subsequent fetch uses the new XYMO
combinationally through GEJY. The result is a hybrid — visibility under the
old size, tile-data row under the new — producing the small rectangular
artefacts the Mealybug m3_lcdc_obj_size_change tests exercise.
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 (PYCO → NUNU → PYNU → NUNY, three capture stages feeding the combinational NUNY) gated by LCDC.5, a hit-and-latch condition (RYDY), a window-mode DFF (NOPA), and the window-trigger pulse (MOSU) that 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 |
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 — identically 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 — and increasing SCX widens that budget dot-for-dot, since 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 (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 (PX is frozen too), and the capture fires on the first ROCO edge
after TAKA clears — 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.
Edge cases
Three self-contained edge-case studies — the right-edge inert zone, multiple window activations on one scanline, and the NUKO/PANY one-dot slip — are collected in Window edge cases.
Window Edge Cases
Three edge cases of the window trigger model (window control): the inert right-edge zone, multiple activations on one scanline, and the NUKO/PANY one-dot slip.
The right edge
WX = 167 never fires: PX terminates at 167 on the same dot XUGU fires, and TADY resets PX before any further advance — no NUKO rise is possible.
WX ∈ [~162, 166]: the cascade fires but is observationally inert.
Measured at WX=166 (dmg-sim measurement, patched gbmicrotest win0_a ROM;
zero variance
across 368 firing scanlines, all values sim-dots from AVAP↑):
| Stage | Δ (dots) |
|---|---|
| NUKO↑ (PX = 166) | +172.038 |
| PYCO↑ / NUNU↑ / PYNU↑ | +172.481 / +172.982 / +172.985 |
| NUNY↑ / MOSU↑ | +172.988 / +172.989 |
| NUKO↓ / XUGU↓ (PX = 167) | +173.040 / +173.042 |
| WODU↑ | +173.044 |
| XYMU↑ (Mode 3 ends) | +173.481 |
| NOPA captures → MOSU↓ | +173.482 |
Mode 3 length is identical to the no-window baseline on the same trace. The MOSU-driven NYXU pulse executes (BG counter resets, steps 1–7 of the sequence all fire, NOPA captures), but XYMU sets ~0.5 dots after MOSU↑ and the combinational POKY-clear switches the pipeline off before any SACU edge can advance the fresh fetch — structurally executed, observationally inert. The +6-dot penalty term simply does not apply once MOSU lands inside the [WODU↑, XYMU↑] closing window.
Right-edge window + sprite collision (WX=166, sprite at OAM X=167,
gambatte m2int_wxA6_spxA7 ROM, dmg-sim measurement): on steady-state
firing scanlines the window cascade adds nothing (inert as above) and the
sprite adds exactly +6.000 dots — the right-edge collapse of the
single-sprite penalty (sprite pipeline). The one-off
first firing scanline (LCDC.5 just enabled, NOPA still 0) measures +17.0
dots — the standard 11-dot first-sprite penalty plus the 6-dot core, induced
by the MOSU↑ NYXU pulse re-firing the fetch on the cascade's maiden
activation only.
Multi-activation within one scanline
A mid-Mode-3 LCDC.5 1→0 write raises XOFO and clears PYNU on the same dot; NOPA captures the cleared PYNU one ALET edge later, re-opening NUNY's gate. PYCO and NUNU are not reset by XOFO — they keep propagating any NUKO pulse regardless of LCDC.5. PYNU's nor_latch inputs are level-sensitive, so what happens at a subsequent LCDC.5 0→1 restore depends on NUNU's state at the XOFO↓ edge:
- NUNU=0 at restore — the match fired long before the disable and has
drained. PYNU stays 0; a new activation needs a fresh NUKO rise, which
under fixed WX is impossible same-line (PX is monotonic) — the CPU must
rewrite WX ahead of PX. This is the structurally-new "multi-fire" case:
Mealybug
m3_lcdc_win_en_change_multiple(WX rewritten 24 → 120 between the toggles) produces two independent MOSU pulses per scanline (dmg-sim measurement: second activation at NUKO↑ +132.013, PYNU↑ +132.996). - NUNU=1 at restore — the match fired inside the disabled window
and the restore lands within the ~1-dot NUNU-high pulse
(≈ [NUKO↑+1.0, NUKO↑+2.0]). PYNU sets
combinationally at XOFO↓ — a deferred completion of the
first activation, not a new one. Mealybug
m3_lcdc_win_en_change_multiple_wxexercises exactly this: on LY=16 and LY=44 the restore lands inside the NUNU pulse and MOSU fires within a gate delay of XOFO↓; on LY=22 the match falls between the off-pulses and the standard sequence runs untouched (dmg-sim measurement).
The window line counter (VYNO → VUJO ripple) clocks on PYNU falling edges. A scanline with N intra-line LCDC.5 1→0 transitions advances it N+1 times (once per disable, once at the end-of-line ATEJ). Under WX-only rewrites with LCDC.5 held high it advances exactly once per scanline: PYNU's set input saturates (already 1) and NUNY stays closed (NOPA holds), so a second NUNY/MOSU pulse is impossible without a PYNU fall.
The NUKO/PANY slip
NUKO has exactly two netlist consumers: PYCO (the capture chain above) and PANY — the BG drain-detector input (BG pipeline). The second consumer produces the subtlest window behaviour on the DMG.
PANY pulses high for ~1 dot at every tile-boundary drain. A NUKO pulse
landing inside that window splits it across RYFA's capture edge: PANY is
forced low at NUKO↑, released at NUKO↓, and then falls naturally one dot
late — so RYFA captures the late half, and the entire SEKO → TEVO → NYXU
chain slips by one dot for that boundary. The slip delays both the
BG-shifter parallel load (unconditionally — this branch has no window
gating) and the window tile-X increment (only when in_window=1).
- WX-rewrite form (Mealybug
m3_wx_4_changefamily): a mid-Mode-3 WX rewrite that lands PX==WX inside a PANY window produces the documented pair-shuffle pattern at recurring tile boundaries. Measured: three perturbed scanlines per batch with NUKO arriving in the last sub-dot of the PANY window; pulses outside the window do nothing; and the simulated render is bit-identical to the hardware reference — the mechanism is fully on-die (dmg-sim measurement). - Armed-but-disabled form (no rewrite needed): NUKO is gated by REJO but
not by LCDC.5. With the window armed then disabled (REJO=1, LCDC.5=0)
and WX ≡ 7 (mod 8) placing PX==WX at a tile boundary, the natural sweep
fires the same slip — the BG shifts one pixel right from that column to
the line's end, every scanline until VBlank clears REJO, while the window
itself never renders (PYNU is held reset, so the
wx_clkbranch stays silent). Measured end to end: the boundary's PANY/RYFA/SEKO/TEVO/NYXU all slip exactly +1.000 dot (dmg-sim measurement).
A model that keys the slip on "window active" misses the armed-but-disabled form entirely — REJO alone arms NUKO. Disabling LCDC.5 does not disarm the coupling; only VBlank's REPU does.
LCD Output
The LCD output path combines the BG and sprite pixel MSBs through a
pixel-multiplexer pipeline gated by LCDC.0 and the sprite priority pipe,
then drives eight physical pads to the LCD glass. This chapter covers the
pixel mux, the per-pixel emission edge (cp_pad) and its interaction with
mid-Mode-3 register writes, the horizontal-sync driver, and the full pin
inventory.
cp_pad↑ — the glass's pixel-capture edge — lags SACU↑ by +1,019 ps, purely combinationally: no latch sits between the pixel mux and the pads.- For a mid-Mode-3 write at CUPA↑, the OLD/NEW boundary falls between the emissions at −0.466 and +0.534 dots around the write.
- CPL/FR keep alternating during LCD-off on borrowed APU clocks — glass protection; LD0/LD1/CP/CPG hold at 0 under VID_RST.
- The LCDC.0 overlay is the third below-netlist LCD-interface behaviour: every mid-Mode-3 write defers one column, bidirectionally.
- The first frame after LCD-on emits no VSYNC pulse.
The pixel multiplexer
| Gate | Role | Type | Inputs | Notes |
|---|---|---|---|---|
| TADE | BG plane B output (LCDC.0 gated) | and2 | SOHU (bg_px_b), VYXE (ff40_d0) | LCDC.0=0 forces plane B to 0 |
| RAJY | BG plane A output (LCDC.0 gated) | and2 | PYBO (bg_px_a), VYXE | Same gating, plane A |
| RYFU | BG plane A gated by sprite priority | and2 | RAJY, VAVA | VAVA from the priority pipe (sprite pipeline) |
| RUTA | BG plane B gated by sprite priority | and2 | TADE, sprite_px_priority | Plane-B analogue of RYFU |
| POKA | BG-pixel combine | nor3 | NULY, RUTA, RYFU | bgpx = NOT(POKA); feeds the MOKA/NURA/WUFU combiners (sprite pipeline drives NULY here) |
| MOKA | OBP1 palette AO2222 combiner | ao2222 | bgpx vs sprite selection | |
| NURA | BGP palette AO2222 combiner | ao2222 | bgpx vs sprite selection | |
| WUFU | OBP0 palette AO2222 combiner | ao2222 | bgpx vs sprite selection | |
| PATY | Pixel-mux output | or3 | MOKA, NURA, WUFU | |
| RAVO / REMY | LD1 / LD0 drivers | not | PATY / PERO |
CPL and FR: the column alternators
cpl and fr alternate the LCD column drivers. Their sources are AO22
muxes selected by LCDC.7:
- KAHE = AO22(
ff40_d7, KASA, KEDY, UMOB) — CPL source - KUPA = AO22(
ff40_d7, KEBO, KEDY, USEC) — FR source - KEDY = NOT(LCDC.7) selects the second arm
With the LCD on, KAHE follows KASA = RUTU.Q (once-per-scanline LINE_END cadence) and KUPA follows KEBO = NOT(MECO). With the LCD off, both pins are muxed onto buffered APU audio dividers — UMOB (~8 kHz) for CPL, USEC (~4 kHz) for FR.
The rationale is glass protection: LCD column cells must see a net-zero
average voltage, so CPL/FR must keep alternating even with the LCD off —
the silicon borrows APU clocks to do it. Measured across a full pre-LCD-on
boot window: cpl_pad alternates at ~8 kHz and fr_pad at ~4 kHz
throughout, then both switch to per-scanline cadence at the LCD-on edge
without a glitch (dmg-sim measurement).
The other four pins (cp, cpg, ld0, ld1) are not audio-muxed —
they hold at 0 during LCD-off. Keep the two mechanisms apart: LCDC.7=0
holds the whole rendering machinery in VID_RST (the pixel pipeline never
advances), whereas LCDC.0=0 merely gates the BG planes at RAJY/TADE during
active rendering.
cp_pad: the pixel-emission edge
cp_pad is the pixel clock the LCD glass uses to capture LD0/LD1 — one
pixel per rising edge. The drive path from SACU is purely combinational:
SACU↑ → TOBA↑ (= AND2(PX9, CLKPIPE)) → SEMU↑ (= OR2(TOBA, POVA)) → RYPO↓ →
cp_pad↑
Net delay SACU↑ → cp_pad↑: +1,019 ps; the falling edge lags SACU↓ by
+1,711 ps — identical across 68,256 rises (dmg-sim measurement), with the
once-per-line POVA-arm capture at its own +4,493 ps offset. TOBA's PX ≥ 9
gate is why the first 8 CLKPIPE cycles never reach
the glass, and the POVA arm of SEMU supplies the end-of-line 160th-pixel
capture (BG pipeline).
The palette-write timeline, glass-side
For a palette write whose CUPA↑ lands at dot N, the emission edges bracket the on-die race (palette latches) like this:
| Δ from CUPA↑ | Event |
|---|---|
| −0.466 dots | Prior cp_pad↑ — the LCD captures dot N−1's pixel. LD0/LD1 still hold the OLD palette result |
| 0 | CUPA↑ |
| +0.005 / +0.076 / +0.080 dots | SACU↓ / PERO / LD-driver transition to the NEW result |
| +0.534 dots | Next cp_pad↑ — the LCD captures the NEW-palette pixel |
No latching exists between the pixel-mux output and the pad.
The OLD/NEW boundary falls between the columns emitted at −0.466 and +0.534 dots around the write — a model that adds any further one-dot lag between mux output and LCD column mis-places the boundary one column right.
The LCDC.0 mid-Mode-3 overlay
At netlist resolution, a mid-Mode-3 LCDC.0 write behaves exactly like the
palette write above: VYXE settles within 41–69 ps of CUPA↑, the
combinational chain follows, and the cp_pad↑ at +0.534 dots samples the
NEW value (dmg-sim measurement across five anchor scanlines; the static
race analysis lists no per-dot entries anywhere in the
VYXE→RAJY/TADE→NURA→PATY chain).
On real hardware there is one more wrinkle — third in the family of below-netlist LCD-interface overlays (with the BGP OR-overlap and the LCDC.1 OFF transient):
When a CUPA write transitions LCDC.0 inside Mode 3, the first
cp_pad↑ after that CUPA↑ emits a pixel computed using the OLD VYXE state; the NEW state takes effect from the secondcp_pad↑ onward. The rule is bidirectional (both 1→0 and 0→1) and applies to every Mode-3 write — no first-write-of-scanline exception.
Characterised against the Mealybug m3_lcdc_bg_en_change hardware
reference, which shows each transition deferred by exactly one LCD column
relative to the netlist prediction, in both directions, across the
affected scanline range.
The three family members have distinct scopes but share one signature:
| Member | Scope |
|---|---|
| LCDC.0 | bidirectional, every write |
| LCDC.1 | OFF direction only |
| BGP | second-or-later write |
In every case the netlist captures CUPA-edge propagation cleanly, and the hardware exhibits a +1-column overlay at the LCD-pipeline boundary. The silicon mechanism (glass-side sample-and-hold vs pad-driver residue) is a shared open question (Appendix C).
The ST (horizontal sync) driver
ST is driven by a three-cell combinational feedback loop gated by a PX-bit-3 tracker and synchronised per scanline by AVAP:
| Gate | Role | Type | Inputs | Notes |
|---|---|---|---|---|
| POME | Loop AVAP gate | nor2 | AVAP, POFY | |
| RUJU | Loop OR combiner | or3 | PAHO, POME, TOFU | TOFU is the video-reset arm |
| POFY | Loop inverter + feedback | not_x1 | RUJU | Feeds POME and the pad buffer |
| PAHO | PX-bit-3 capture | dffr | ROXO | Data: XYDO (PX bit 3); reset by XYMU outside Mode 3 |
| RUZE | ST pad buffer | not_x3 | POFY |
The loop dynamics: during the AVAP pulse, POME is forced low and POFY initialises to NOT(PAHO) = 1 (PAHO was reset throughout Mode 2). During Mode 3, PAHO tracks XYDO on each CLKPIPE cycle — PX-bit-3-aligned transitions — and the first PAHO=1 event with POFY=1 drops POFY to 0, where the loop self-holds until the next scanline's AVAP.
ST is one high pulse per scanline: [AVAP+0.008, AVAP+14.493] dots, width 14.485 dots at SCX=0 — POFY arms at AVAP+0.006, the first PAHO=1 capture (PX bit 3 first high, ≈PX 8–9 through ROXO) drops it at +14.489, and the pin follows each edge ~490 ps later. Zero variance across sampled scanlines (dmg-sim measurement). The rising edge is AVAP-anchored; the falling edge tracks the first PX-bit-3 capture and therefore shifts with fine scroll.
LCD-off and release. During LCD-off, TOFU=1 forces POFY=0 and the pad
is static; PAHO is additionally held by XYMU. At the LCD-on edge TOFU
drops combinationally, the loop stays quiescent, and the first AVAP
produces an ST edge on the first scanline — HSYNC is normal from the
outset. Measured: first st_pad↑ inside that scanline's Mode 3, the
second exactly one 454-dot first-scanline period later, and per-scanline
cadence thereafter — with the second scanline onward at the steady 456-dot period
(dmg-sim measurement; the 2-dot first-scanline shortening sits in Mode 0 and
shifts the cadence once without altering pulse shape).
The pulse shape above is measured; what remains open is the interpretation at the glass — pulse-width expectations and the row-driver response rest on community pin-role references, not measurement. See Appendix C.
Pin inventory
Eight pads connect the PPU to the glass. Each pad is driven through a
single inverting driver stage into an inverting pad cell (pad = !o_n) —
two inversions, so each pin tracks its source signal in phase (measured:
the st pin follows POFY with ~490 ps of buffer delay):
| Pad | Driver cell | Source | Primary description |
|---|---|---|---|
s (VSYNC) | MURE (not_x1) | MEDA | Line counters (the LY=0 capture) |
st (HSYNC) | RUZE (not_x3) | POFY | this chapter |
cp (pixel clock) | RYPO (not_x1) | SEMU = OR2(TOBA, POVA) | this chapter |
cpg (clock pulse gate) | POGU (not_x1) | RYNO = OR2(SYGU, VCLK); VCLK = RUTU.Q | per-scanline gate |
cpl | KYMO (not_x1) | KAHE (AO22 on LCDC.7) | this chapter |
fr | KOFO (not_x1) | KUPA (AO22 on LCDC.7) | this chapter |
ld0 | REMY | PERO | pixel plane 0 |
ld1 | RAVO | PATY | pixel plane 1 |
The VSYNC path. MEDA (the NYPE-clocked LY=0 capture) drives s_pad
through the MURE inverter — one gate delay end to end. MEDA's first 0→1
transition after LCD-on lands at the LY 153→0 boundary ending the first
frame, so the first frame emits no VSYNC pulse — part of the LCD-on startup
transient (LCD-on power-up).
Community pin-role documentation describes pin S as the input to the LCD's
Y-driver row shift register (one row per pulse); that interpretation is
not netlist-derivable.
The pads are stateless. The LD0/LD1 pad cells have no clock and no stateful element; their propagation is fixed at the netlist level, independent of pixel index or upstream pipeline state. Measured across both branches of a palette-write iteration family: the CUPA-relative offsets at the chip pin are picosecond-identical within each branch, and a known one-M-cycle upstream differential reproduces at the pin without compression (dmg-sim measurement). Whatever produces the LCD-interface overlay family above, it is not a stateful pad driver on the die.
Mode Transitions
Mode transitions are the state-machine edges between Modes 2, 3, 0, and 1. Each is driven by a specific signal event: AVAP↑ starts Mode 3; the WODU → VOGA → WEGO chain sets XYMU to end it; the RUTU → NYPE distribution ends the scanline into Mode 2 or Mode 1; the CATU/ANEL chain re-initialises the Mode 2 machinery at each boundary; MYTA fires at frame end. This chapter walks each edge.
- Mode 2→3: AVAP↑ resets XYMU directly — a 0.483-dot pulse that is not ALET-rising-aligned (it reacts to the falling edge).
- Mode 3→0: WODU → VOGA → WEGO → XYMU in 0.436 dots — Mode 3 ends within the same dot WODU fires; baseline total 173.481 dots.
- Scanline end: RUTU → NYPE splits across two TALU edges — POPU (Mode 1) first, MYTA (FRAME_END) and MEDA one period later.
- The CATU/ANEL chain turns RUTU into the ATEJ line-end pulse feeding five subsystems — and the LCD-on path bypasses it entirely, which is why the first scanline has no Mode 2 STAT phase.
Mode 2 → Mode 3
AVAP — the scan-complete pulse (OAM scan) — drives XYMU's reset directly: AVAP↑ clears XYMU to Q=0 and Mode 3 begins. AVAP's four netlist consumers:
| Cell | Type | Role |
|---|---|---|
| ASEN | or2 | OR2(ATAR, AVAP) — clears the scan-active latch BESU |
| NYXU | nor3 | NOR3(AVAP, MOSU, TEVO) — BG fetch counter reset (BG pipeline) |
| POME | nor2 | NOR2(AVAP, POFY) — per-scanline synchroniser of the ST sync loop (LCD output) |
| XYMU | nor_latch | reset pin — Mode 3 starts |
It rises ~0.018 dots after an ALET falling edge (BYBA's XUPY-clocked capture) and falls ~0.483 dots later when DOBA captures on the subsequent ALET rising edge — the 0.483-dot pulse straddles one ALET rising edge. Snapping AVAP to the nearest ALET rising edge produces half-dot bookkeeping errors; the reactive edge is the falling one.
The transition lands at dot 80 of the scanline (Mode 2's 80-dot decomposition is in OAM scan), and the 7.026-dot startup cascade to the first pixel — zero variance across scanlines, no first-scanline transient — is in the BG pipeline.
Mode 3 → Mode 0
WODU → VOGA → WEGO → XYMU set
| Gate | Role | Type | Clock / Trigger |
|---|---|---|---|
| WODU | Mode 0 condition | and2 | XENA, XANO (STAT interrupts) |
| VOGA | H-Blank capture DFF | dffr | ALET rising — primary here |
| WEGO | XYMU set driver | or2 | TOFU, VOGA |
| XYMU | Rendering-mode latch (active-low Mode 3 indicator) | nor_latch | Set: WEGO; Reset: AVAP |
The sequence: PX reaches terminal count (XUGU decode), no sprite match (FEPO=0), WODU = AND2(XENA, XANO) rises — in the ALET-low phase, 0.063 dots after the ALET falling edge. The same-dot ALET rising edge, 0.435 dots after WODU↑, latches WODU into VOGA; WEGO rises 315 ps later and XYMU sets simultaneously (dmg-sim measurement):
| Event | Δ from WODU↑ (dots) | Δ (ps) |
|---|---|---|
| WODU↑ | 0.000 | 0 |
| VOGA↑ | +0.435 | +106,107 |
| WEGO↑ | +0.436 | +106,422 |
| XYMU↑ (Mode 3 ends) | +0.436 | +106,422 |
Mode 3 ends within the same dot WODU fires — a half-dot pipeline delay set by WODU's ALET-low-phase rise and VOGA's same-dot capture. The baseline duration decomposes exactly: AVAP → WODU = 173.045 dots (167 pixels through the pipe after the 7-dot startup), WODU → XYMU = 0.436, total 173.481 dots at SCX=0 with no sprites or window.
Mode 0 → Mode 2 / Mode 1
The scanline ends through LINE_END (RUTU) and its NYPE redistribution (line counters carries the cell detail):
- LX reaches 113; SANU fires.
- RUTU captures on the SONO edge and holds for one full TALU cycle.
- LY increments (MUWY toggles on RUTU's rise).
- NYPE captures RUTU on the next TALU rising edge — half an M-cycle after RUTU's capture. NYPE's Q clocks POPU; NYPE's Q_n clocks MYTA and MEDA — splitting the distribution across two TALU edges one period apart.
- POPU captures XYVO on NYPE's rise: LY < 144 → Mode 2 begins; LY ≥ 144 → Mode 1 (VBlank).
- One TALU period later, MYTA captures NOKO (FRAME_END) and MEDA captures NERU (LY=0). MYTA's later edge is the source of the LYC=153 race window (STAT interrupts).
In parallel, RUTU drives the scan-counter reset chain below.
The CATU/ANEL chain
The OAM scan counter is reset at each scanline boundary by a two-DFF-plus-combinational chain transforming RUTU into the ANOM reset pulse:
flowchart LR
RUTU["RUTU<br>LINE_END"] -- "∧ NOT(vblank)" --> CATU["CATU<br>dffr · XUPY"]
CATU -- "Q sets" --> BESU["BESU<br>scan-active latch"]
CATU -- "Q" --> ANEL["ANEL<br>dffr · NOT(XUPY)"]
CATU -- "ABAF = NOT(CATU)" --> BYHA["BYHA<br>oa21"]
ANEL --> BYHA
BYHA --> ATEJ["ATEJ<br>line-end pulse"]
ATEJ --> ANOM["ANOM<br>scan-counter reset"]
ATEJ --> TADY["TADY<br>PX reset"]
ATEJ --> SECA["SECA<br>TAKA set"]
ATEJ --> AZYB["AZYB<br>slot-counter reset"]
ATEJ --> ABAK["ABAK<br>store resets"]
| Stage | Cell | Type | Clock | Inputs | Drives |
|---|---|---|---|---|---|
| 1 | CATU | dffr | XUPY | D = AND2(SELA, ALES); reset = ABEZ | ABAF, ANEL, BESU |
| 2 | ANEL | dffr | AWOH = NOT(XUPY) | D = CATU; reset = ABEZ | BYHA |
| 3 | BYHA | oa21 | — | ABAF (= NOT(CATU)), ANEL, ABEZ | ATEJ |
| 4 | ATEJ | not_x2 | — | BYHA | ANOM + 5 more consumers |
| 5 | ANOM | nor2 | — | ATEJ, ATAR | scan counter resets, BALU |
Only CATU and ANEL are clocked — one half-XUPY pipeline stage between them;
the rest is combinational. CATU's data decomposes to
RUTU AND NOT(vblank) (SELA is RUTU through two buffers; ALES =
NOT(XYVO)) — the chain fires only outside VBlank. Both DFFs are held in
reset while the LCD is off.
The steady-state sequence: RUTU rises mid-XUPY-cycle; CATU captures it one dot later (243,059 ps = 0.996 dots, picosecond-identical across 431 boundaries — dmg-sim measurement); ANEL captures CATU half an XUPY cycle after that; BYHA/ATEJ/ANOM pulse the counter reset between the two captures. Total: CATU's capture asserts the reset on its own edge (via the ABAF arm), ANEL's capture releases it (via the second BYHA arm), and the counter ticks 0→1 on the next XUPY rising — one XUPY cycle after CATU.
CATU's clk-to-Q is directly measured: 988 ps after the XUPY edge, zero variance across 860 scanline boundaries (dmg-sim measurement) — slightly slower than BYBA's 902 ps, matching the two cells' relative load parameters.
ATEJ's other consumers matter. Beyond ANOM, the line-end pulse drives TADY (the PX-counter reset — BG pipeline), SECA (the TAKA set net — the H-Blank TAKA re-assert in the sprite pipeline), AZYB (the sprite-store slot-counter reset), and ABAK (the per-slot store resets). One pulse, five subsystems.
ANOM's BALU arm. While the reset pulse is asserted, BALU = NOT(ANOM) goes high and forces BEBU=1 — masking AVAP during the boundary transition. The full interaction lives with the AVAP detector in OAM scan.
The LCD-on bypass
When LCDC.7 goes 0→1, the first scanline enters its scan by a different mechanism — CATU and ANEL are not involved:
- During LCD-off, ATAR=1 holds ANOM=0 (counter in reset) and ABEZ=0 holds CATU/ANEL at 0; RUTU is 0.
- At the LCD-on edge, ATAR falls and ANOM releases immediately — combinationally, no chain propagation. CATU's data is still 0 (no RUTU pulse exists yet), so the chain stays silent.
- The counter simply starts advancing on the next XUPY edge.
Consequences on the first post-LCD-on scanline:
- BESU never sets (its set input is CATU.Q) — the STAT mode bits read Mode 0 through the nominal scan window and the Mode 2 interrupt does not fire;
- the scan itself runs — counter 0→39, FETO, BYBA, AVAP, Mode 3 all normal;
- the sprite store is never populated (CARE stays unarmed).
From the second scanline on the full chain operates — RUTU fires at the first LINE_END and everything proceeds steady-state. The first-frame observability consequences are catalogued in LCD-on → first WODU.
Mode 1 → Mode 2 (frame start)
- LY reaches 153: NOKO=1; MYTA captures it on NYPE's falling-edge distribution.
- LAMA = NOR2(LYHA, MYTA) goes low — LY resets to 0.
- NERU (the LY=0 NOR8) rises.
- MEDA captures NERU on the same NYPE_n edge family — driving only the LCD vertical-sync pad (LCD output); MEDA touches no scan-state or mode-control logic.
- The first visible line's OAM scan begins through the normal RUTU/CATU machinery at the next scanline boundary.
The LY=153→0 wrap's CPU-visible fine structure ("LY reads 153 for only a few dots") is timed edge-by-edge in CPU-visible timing at mode boundaries.
STAT Interrupts
The STAT interrupt (IF bit 1) is set by LALU, a DFF whose clock is the combined STAT condition: SUKO, an AO2222 that OR-ANDs the four sources with their enable bits. There is no periodic clock anywhere in this chain — LALU fires when SUKO's output rises. Everything subtle about DMG STAT interrupts ("blocking", the write glitch, the boundary races) falls out of that one fact plus per-leg gate depths.
- LALU (IF bit 1) is clocked by the condition itself: it fires only on a genuine SUKO through-zero — "STAT blocking" is emergent, not a rule.
- The Mode 0 leg reaches the interrupt in 0.011 dots — 0.425 dots before the CPU-visible mode change.
- Leg swaps are decided by gate depth: ROPO (LYC) is the fastest leg, PARU (Mode 1) next, TARU/TAPA (Mode 0/2) slowest — VBlank entry and exit glitch (Cases 1/4); same-line swaps stay covered (Cases 2/3).
- The "DMG STAT-write glitch" is ordinary transparent-latch bus settling — whether it fires depends on SUKO's through-zero, nothing else.
- ROPO survives LCD-off (its reset is system reset, not VID_RST).
| Gate | Role | Type | Clock / Trigger | Notes |
|---|---|---|---|---|
| LALU | STAT interrupt latch (IF bit 1) | dffsr | Clock: VOTY; D tied high | Edge-triggered by the SUKO pulse |
| SUKO | Condition combining gate | ao2222 | (ROXE·TARU) + (RUFO·PARU) + (REFE·TAPA) + (RUGU·ROPO) | |
| TUVA / VOTY | Inverter pair | not | SUKO → LALU clock | |
| TARU | Mode 0 condition | and2 | WODU, TOLU | TOLU = NOT(mode1) — non-VBlank gate |
| PARU | Mode 1 condition | not_x1 | popu_n | = POPU.Q; also feeds TOLU |
| TAPA | Mode 2 condition | and2 | TOLU, SELA | SELA ≡ RUTU buffered — the line-end pulse |
| ROPO | LY==LYC synced match | dff17 | TALU rising | Captures PALY |
| ROXE / RUFO / REFE / RUGU | STAT.3/.4/.5/.6 enables | drlatch_ee | CUPA strobe | |
| WODU | Mode 0 condition | and2 | XENA, XANO | Full breakdown below; mode-control is the primary chapter |
| PALY | LY==LYC comparator | not_x1 | combinational | Per-bit XNORs → SUBO/SOVU → RAPE → PALY |
| RUPO | STAT bit 2 visible latch | nor_latch | s = ROPO.Q; r = PAGO | Transparent in normal operation (below) |
The enable bits and the FF41-write transient
The four enables are level-sensitive latches sharing the CUPA strobe — and
that transparency is the netlist origin of the "DMG STAT-write glitch". During a CPU FF41 write, the data bus settles bit by bit across
the write window, and the transparent latches follow it. Measured
(dmg-sim measurement, gambatte enable_after_lyc_during ROM, write
$40 → $08): the bits being cleared walk through a transient where all
four enables read 1 simultaneously for ≈ 15.8 ns before settling to the
written value.
Whether that transient fires an interrupt depends entirely on SUKO's through-zero behaviour. A transiently-set enable matters only if its paired condition is high — and if some other leg already held SUKO high, no edge occurs.
In the measured scenario (WODU high mid-H-Blank, LYC leg high then dropping), SUKO transitions exactly zero times: the Mode 0 leg takes over as the LYC leg drops, the output never dips, LALU never clocks. The glitch is ordinary transparent-latch bus-settling: SUKO = OR(EN·COND) settles per-leg, and LALU clocks only on a genuine through-zero.
Single-step models miss the transient and its consequences consistently (usually correct); two-pass models that raise-then-drop "all enables on" without per-leg condition tracking over-fire.
WODU: the Mode 0 condition
WODU = AND2(XENA, XANO)
├── XENA = NOT(FEPO) — no sprite fetch active
└── XANO = NOT(XUGU) — pixel counter at terminal count (PX = 167)
One signal, two consumers with very different latencies: the mode transition (WODU → VOGA → XYMU, 0.436 dots — mode transitions) and the STAT chain (WODU → TARU → SUKO → TUVA → VOTY → LALU, 0.011 dots). The interrupt fires 0.425 dots before the CPU-visible mode changes.
The terminal-count WODU pulse
WODU's inputs settle at different depths. On the advance onto PX=167, XANO (a shallow NAND5 decode) rises while the deep sprite-match aggregate FEPO is still low for the new count. With a sprite stacked at X=167, this ordering produces a brief WODU pulse before the terminal sprite's match suppresses it (dmg-sim measurement):
| Signal | Δ from XANO↑ (ps) | Event |
|---|---|---|
| XANO↑ | 0 | PX=167 decode asserts |
| WODU↑ (early pulse) | +483 | XENA still high |
| FEPO↑ | +3,141 | terminal sprite match |
| XENA↓ | +3,205 | |
| WODU↓ | +3,494 | pulse ends — width ≈ 3 ns |
The two consumers treat it asymmetrically: the combinational STAT chain catches the pulse and sets IF bit 1 at the early edge — ahead of the visible Mode 3 end by the whole terminal-sprite fetch extension — while VOGA samples on the ALET edge after FEPO has settled and misses it entirely; the mode bits flip only at the sustained WODU rise after the last terminal sprite. A sprite at X=166 or below produces no pulse (it is fetched before the terminal count).
Propagation: measured leg latencies
Mode 0 path (dmg-sim measurement, 1,289 assertions, ps-identical):
| Signal | Δ from WODU↑ (ps) |
|---|---|
| TARU↑ | +801 |
| SUKO↑ | +1,671 |
| TUVA↓ | +2,064 |
| VOTY↑ = LALU clock | +2,637 |
| LALU.q↑ (IF bit 1) | +2,637 |
Mode 2 path (dmg-sim measurement, 1,289 assertions):
| Signal | Δ from RUTU↑ (ps) |
|---|---|
| SELA↑ | +2,860 |
| TAPA↑ | +3,011 |
| SUKO↑ | +3,881 |
| VOTY↑ = LALU clock | +4,847 |
The 2,210 ps differential vs Mode 0 is entirely SELA's double-inverter; the shared SUKO→VOTY tail (966 ps) and the AO2222 input delay (870 ps) match the Mode 0 leg to ps resolution. The static analysis gives LALU a tight-but-safe per-line race (diff 5.8 ge, deepest input VOTY at 64.8 ge).
The Mode 0 condition (TARU) drops in two structurally different situations.
On VBlank entry WODU stays high and TARU drops via TOLU as mode1 rises
(Case 3), a cascade riding the boundary TALU↑ — TARU↓ = TALU↑ + 4,038 ps
(dmg-sim measurement).
At a line start the trigger is WODU itself, de-asserting as the pixel counter resets — ≈ 1 dot after RUTU↑ (WODU↓ at +1.008 dots, TARU↓ at +1.010), quantifying Case 2's "further dot to drop". With only the Mode 0 leg enabled that SUKO drop is uncovered, exposing the whole falling leg (dmg-sim measurement, three consecutive boundaries ps-identical):
| Signal | Δ from WODU↓ (ps) |
|---|---|
| TARU↓ | +449 |
| SUKO↓ | +1,465 |
| TUVA↑ | +2,246 |
| VOTY↓ = LALU clock | +2,542 |
The AO2222 input delay (1,016 ps) and the SUKO→VOTY tail (1,077 ps) match the Mode 1 / Mode 2 legs' falling cascade to the picosecond — the tail is leg-independent, the falling mirror of the rising 870 / 966 ps. LALU clocks on VOTY's rise, so this VOTY↓ sets no interrupt; it only readies the clock for the next line's Mode 0 rise.
The VBlank-entry fall rides the boundary TALU↑; the line-start fall lands a full dot later, a single AND2 after WODU's de-assert. They are different events on different reference edges — one Mode 0 falling arrival cannot time both.
Edge-triggered behaviour and the leg-swap races
Because LALU clocks on SUKO's rise, overlapping conditions produce one interrupt, not two — the "STAT IRQ blocking" behaviour emerges naturally. The interesting cases are leg swaps: one source leg falls and another rises on the same boundary. Whether SUKO dips (→ fresh interrupt) or stays covered (→ nothing) is decided by gate-prop depth, and all four configurations have been measured (dmg-sim measurements, gambatte interrupt-precedence ROM family):
Case 1 — VBlank entry, LYC + Mode 1 enabled: glitch fires. On the TALU edge ending scanline 143, the LYC arm drops via ROPO (one TALU-clocked stage — fast) while the Mode 1 arm rises via NYPE → POPU → PARU (buffered, ~1.9 ns slower). All four arms sit at 0 for 1,802 ps; SUKO dips; LALU fires ~4.4 ns after POPU's capture. IF bit 1 sets even though "the LYC interrupt ended and the VBlank interrupt began".
Case 2 — line boundary with all four enables: covered. The Mode 2 arm rises ~1 ns after RUTU while the Mode 0 arm takes a further dot to drop (WODU's deep pipeline-end conditions). At every instant some arm is high — measured across 143 consecutive boundaries: zero SUKO transitions, zero spurious interrupts. The structural ordering (Mode 2 rises fast, Mode 0 drops slowly) is intrinsic to the netlist.
Case 3 — VBlank entry, Mode 0 + Mode 1 enabled: covered. Both arms cascade off the same POPU chain: the Mode 1 arm rises at PARU (2 stages from POPU.q), the Mode 0 arm drops at TARU (4 stages, via TOLU). The rising leg arrives 1,216 ps before the falling one — no dip.
Case 4 — VBlank exit (LY 153→0), Mode 1 + Mode 2 enabled: glitch fires. The symmetric counterpart: the Mode 1 arm drops at PARU (2 stages) before the Mode 2 arm can rise at TAPA (4 stages). SUKO dips for 1,524 ps; LALU fires ~5.5 ns after POPU's fall. Measured identically on two independent ROMs.
The general structure, from the shared propagation tree:
POPU.q → popu_n → PARU (mode1) — 2 stages: the FAST leg
└→ TOLU → TARU (mode0) — 4 stages
└→ TAPA (mode2) — 4 stages
ROPO (LYC) — 1 TALU-clocked stage: the only leg FASTER than PARU
SUKO glitches low only when, between the falling leg's transition and the rising leg's, no other (EN·COND) pair is true. On VBlank entry with Mode 1 enabled, PARU's early rise covers everything except the still-faster LYC drop (Case 1 fires, Case 3 doesn't). On VBlank exit, PARU's early fall uncovers everything (Case 4 fires). Emulators must apply each leg's transition on its own hardware edge and fire only on a genuine through-zero: collapsing the POPU-driven arms into one update step misses Cases 1/4; firing on every leg-rise breaks Cases 2/3.
One ROM (gambatte m2enable/late_m1disable_ly0_3) expects IF[1]=0 at a read that
dmg-sim places after a Case 4 glitch with no intervening clear — the
simulation says 1. An equivalent-cascade ROM with the clear on the other
side of the boundary is netlist-consistent. The disagreement is
margin-sensitive: Case 4's dip is 1,524 ps wide, while the narrowest
dip that hardware-verified expectations require to fire is Case 1's
1,802 ps — a 278 ps gap, inside the scale where gate delays are model
predictions rather than hardware facts
(methodology). A real unit
whose LALU clock threshold sits between the two would match the
annotation and the simulated Case 1 — the annotation may simply be
correct for the tested unit. Resolving it needs a hardware measurement.
See Appendix C.
The LYC-match pipeline
LY bits + LYC bits → per-bit XNORs → SUBO/SOVU → RAPE → PALY (combinational)
→ ROPO (dff17, clk = TALU rising) — the only registered stage
├→ SUKO's LYC arm (with RUGU)
└→ RUPO (nor_latch) → tri-state → STAT bit 2
RUPO is transparent in normal operation. Its reset-side driver PAGO is static-1 outside hardware reset, so the NOR latch continuously re-evaluates and STAT bit 2 tracks ROPO.Q with gate delay only. A true latch-hold requires hardware reset and a concurrent FF41 write — irrelevant to normal operation.
LCD on/off does not reset the pipeline. ROPO's reset traces to the system-level hardware reset, not VID_RST. Across an LCD-off span, ROPO holds whatever it captured at the last TALU edge before the LCD went off; after LCD-on, the first TALU edge (+1.483 dots after VID_RST deasserts — register writes) recaptures (LY=0 == LYC). STAT bit 2 in the brief window between LCD-on and that first TALU edge reads the pre-LCD-off match value.
The LYC=153 boundary race
At the 152→153 boundary, MYTA captures FRAME_END one TALU period after POPU (line counters) and async-resets LY to 0. The reset propagates to PALY through six gate stages; ROPO's capture on the same TALU edge completes through four shorter internal stages — ROPO wins the race and captures pre-reset PALY, the LY=153 comparison.
Across three consecutive TALU rises at the boundary:
| TALU rise | Event | ROPO captures |
|---|---|---|
| 1 | POPU's edge; LY already 153 | (153 == LYC) |
| 2 | MYTA fires; LY resets after the capture | (153 == LYC) — pre-reset, via the race |
| 3 | LY stably 0 | (0 == LYC) |
For LYC=153 the match window is therefore two TALU periods; for LYC=0 the match onsets at rise 3 — one TALU period after MYTA's edge. The sub-phase window between PALY's combinational transition and ROPO's next capture — where the comparator and the visible bit disagree — interacts with CPU read sampling and is worked through in CPU-visible timing at mode boundaries. Implementations that fire POPU and MYTA on a single collapsed edge place MYTA one TALU period early and shift all of this downstream.
Scanline and Frame Timing
This chapter frames the per-subsystem behaviour at scanline and frame level: one scanline's dot structure, and the physical ordering of events inside a single dot. The complete power-up story — the shortened first scanline, the startup transient at the pins, and the LCD-on → first-WODU composition — has its own pages.
- One scanline = 456 dots = 114 M-cycles; the Mode 2 / Mode 3 / Mode 0 split is set by the pixel pipeline, not a fixed schedule.
- Inside a dot, events unfold by propagation depth: registered outputs first (0–8 ge), ALET captures mid (8–22), CLKPIPE last (63.8).
The scanline
One scanline = 456 dots = 114 M-cycles (LX counts 0–113):
| Phase | Dots | Duration | What happens |
|---|---|---|---|
| Mode 2 (OAM scan) | 0–79 | 80 dots | 40 OAM entries, 2 dots each; sprite store builds |
| Mode 3 (pixel transfer) | 80 – ~253.5+ | 173.481 + (SCX & 7) + penalties | Fetch and shift; CLKPIPE drives output |
| Mode 0 (H-Blank) | remainder | 456 − the rest | CPU may access VRAM/OAM |
The Mode 3 budget, consolidated (all developed in their own chapters):
- Baseline 173.481 dots: 7.026 startup + 166.019 dots through the pipe (167 SACU edges, 166 steps after the first) + the 0.436-dot VOGA tail.
- Fine scroll: exactly 1 dot per
SCX & 7step, applied at startup via ROXY; the SACU count never changes. - Sprites: 6-dot core + 0–5 dots alignment per sprite. Single sprite at SPRITEX 8/9 → +11/+10 dots; four sprites step ~4 dots per +1 SPRITEX over [8, 13]; tile-boundary anomalies at {14, 15}; stacked same-X sprites add exactly 6 dots each (sprite pipeline).
- Window: +6.000 dots, invariant across WX — one fetch-pipeline restart (window control).
- The 21 TEVO tile boundaries are internal to the fetcher and cost nothing.
Inside one dot
By propagation depth, each master-clock edge unfolds in three bands:
- Early (0–8 ge) — registered outputs and the VRAM bus settle.
- Mid (8–22 ge) — ALET arrives and ALET-clocked DFFs capture; MYVO/LEBO arrive on the opposite edge.
- Late (22–64 ge) — sprite matches, FEPO, WODU, VYBO, and finally CLKPIPE at 63.8 ge.
The physical ordering on silicon:
ALET rising: the ALET-clocked DFFs capture (NYKA, LYZU, PYGO, RENE, DOBA, NOPA, VOGA); the sprite fetch clock SABE rises; the NOR/NAND latches (XYMU, ROXY, POKY, LONY, BESU) respond combinationally; addresses and enables settle.
ALET falling (= MYVO rising): PORY captures NYKA's fresh output; the BG fetch clock LEBO rises and the LAXU→MESU→NYVA ripple advances; CUPA is high in this window once per M-cycle, making the CPU-written register latches transparent.
Delayed, same falling edge: SACU's rising edge is the ALET falling edge, arriving 41.6 ge later through VYBO/TYFA/SEGU — the delay is what guarantees DFF captures and counter advances settle before the pixel pipe shifts. CPU register reads are not ordered events at all — the read path is continuously-enabled combinational logic, sampled on the CPU's own schedule.
At the 1 MHz tier, TALU/SONO distribute LX counting and LINE_END capture across M-cycle boundaries (line counters).
The LCD-on story
The power-up sequence has two pages:
| Page | Covers |
|---|---|
| LCD-on power-up | The 454-dot first scanline, the pin-level startup transient, and the reset-domain taxonomy of every PPU DFF |
| LCD-on → first WODU | The end-to-end path from the LCD-enabling write to the first H-Blank, and what the CPU observes |
LCD-on Power-up
What happens on the first frame after LCDC.7 goes high: the shortened first scanline, the startup transient at the LCD pins, and which PPU flip-flops are reset by what (scanline and frame timing).
The first scanline: the 454-dot line
Immediately post-LCD-on, the first scanline measures 453.5 ≈ 454 dots instead of 456, with the deficit entirely in Mode 0 — Mode 2 and Mode 3 match steady-state. The gate-level decomposition (dmg-sim measurement):
| Component | First scanline | Steady-state | Diff |
|---|---|---|---|
| Line-start reference → first LX-incrementing TALU↑ | 2.021 (XODO↓ phase) + 1.483 (divider startup) = 3.504 | 6.000 (first TALU↑ lands inside the RUTU pulse and doesn't count) | −2.496 |
| 112 further LX increments | 448.000 | 448.000 | 0 |
| LX=113 → SANU → RUTU | ≈2.0 | ≈2.0 | ≈0 |
| Total | 453.500 | 456.000 | −2.5 ≈ −2 |
The structural sum lands at −2.5 while the directly-measured Mode-0 deficit (below) is exactly −2.0 dots (488,000 ps): the ~0.5-dot difference is sub-dot TALU phase, conserved through the LX chain and never realised as a whole-dot change in line length, so the line runs an integer 454 dots and the Mode-0 deficit is exactly 2.
The deficit's location is measured directly: the first scanline's Mode 3 + Mode 0 span (XYMU↓ → RUTU↑) is 48,680,932 ps (199.512 dots) against 49,168,932 ps (201.512) on scanlines 1–5 — shorter by exactly 488,000 ps = 2 dots — and the pure-Mode-0 interval (XYMU↑ → RUTU↑) shows the same 488,000 ps gap, placing the whole deficit in Mode 0 (dmg-sim measurement).
Three structural facts compose the deficit — and none of them is "drift":
- No leading RUTU pulse. A steady-state line spends one TALU period inside the RUTU pulse where LX is held at 0; the first scanline starts counting immediately. Saves 6 dots against the reference.
- The LCD-on write lands in T3 of its M-cycle (the CUPA position, +2.020 dots); VID_RST deasserts a gate delay later at XODO↓ (+2.021), so the chain's first effect is 2.021 dots late against the M-cycle boundary.
- The divider stack starts mid-phase: the first TALU↑ lands 1.483 dots after VID_RST deasserts — 3.504 dots past the M-cycle boundary instead of the 6.000 a steady line spends reaching its first counting rise.
That phase is then conserved exactly: the write sets the divider phase for the whole epoch, and every TALU↑ thereafter — all 113 on the first scanline and every steady-state rise after — lands 854,890 ps past its M-cycle boundary with zero variance (155,000+ edges, dmg-sim measurement). No per-line adjustment exists anywhere in the chain. The LX chain is a delay-conserving propagator, not an amplifier.
Reproduce the three structural inputs (write phase, divider phase, no leading RUTU pulse) and let the trajectory fall out — patching the LX==113 detector misframes a mechanism that contains no scanline-specific gating.
The LCD-on startup transient at the pins
| Pin | Pre-LCD-on | First edge after LCD-on | Cadence |
|---|---|---|---|
s (VSYNC) | static 0 | +17.1 ms — the LY 153→0 boundary ending the first frame | once per frame |
st (HSYNC) | static 0 | +19.2 µs (first-scanline Mode 3) | per scanline |
cp | static 0 | +20.7 µs | half-dot period during Mode 3 |
cpg | static 0 | +6.7 µs | per scanline |
cpl / fr | alternating at ~8/~4 kHz (audio mux) | seamless | per scanline |
ld0 / ld1 | static 0 | first Mode 3 pixel | pixel rate |
Seven of eight pins are normal from the first scanline. VSYNC is silent for all of the first frame: MEDA can only capture the LY=0 decode at an LY 153→0 wrap, and the first such wrap is the one ending the first frame. From the second frame on it fires once per frame; the first frame is distinguished by the absence of a pulse, not an altered one.
The community-documented consequence — the first frame never reaches the glass ("first frame is blank") — rests on pin-role references for the LCD's Y-driver, not on a measured glass-side behaviour; the PPU-side mechanism (MEDA's reset domain) is netlist-derived and measured. The 2-dot first-scanline shortening changes no pixels — it only moves where the mode and LY transitions fall — so it has no display consequence, but it is fully observable to the CPU: STAT mode reads, LY reads, and per-scanline interrupt timing all see it.
VBlank IF is not gated by the first frame. The IF[0] latch (LOPE) has no LCD-off reset path; POPU resumes normally after LCD-on, and the first frame's VBlank entry raises IF[0] with the same edge structure as any steady-state frame (dmg-sim measurement) — there is no first-frame interrupt suppression of any kind.
Pipeline state at LCD-on: the reset-domain taxonomy
Every PPU DFF belongs to one of five reset classes, and the whole cross-frame story follows from the classification:
| Class | Reset condition | Members | Behaviour |
|---|---|---|---|
| Direct LCD-off | ppu_reset_n / ppu_reset2_n | dividers WUVU/VENA/WOSU; RUTU, NYPE, POPU, MYTA, MEDA; NOPA | Held at 0 while LCDC.7=0; released at the VID_RST edge |
| Indirect LCD-off | combinational nets containing VID_RST terms | LX, LY, PX, VOGA, scan counter, fine-scroll counter, BG fetch counter, AVAP generator, sprite fetch counter, sprite store | Held at 0 during LCD-off; reset at their per-scanline/per-tile boundaries during rendering |
| Mode-3-only | the mode3 net | LYZU, PUXA, NYZE, PYGO, RENE, RYFA, PAHO, SEBA, TOBU, VONU | Zero at every Mode 3 entry, on every scanline, every frame — identical by construction |
| No-reset persistence | tied-high r_n or data-driven set/reset only | BG + sprite shift registers (32 cells), tile temp latches, SOBU/SUDA/TYFO, XYMU, ROXY | Hold state indefinitely across LCD-off |
| CPU-register | CPU-write/system-reset only | LCDC, SCX/SCY, WX/WY, palettes, LYC, STAT enables, ROPO | Survive LCDC.7 toggles; palettes have no reset at all (power-on state undefined on silicon) |
What matters is whether any persisted stale state reaches LD0/LD1. For the BG plane, no — the two-parallel-loads mechanism (BG pipeline) propagates stale content at load #1 (AVAP) and overwrites it at load #2 (first TEVO, dot 5.996) before the first CLKPIPE shift at 7.026, and CP is PX≥9-gated besides. For the sprite plane, stale shifter content is mux-gated until a real fetch fires. That leaves exactly two pieces of cross-frame state to examine at the first scanline; only the first reaches LD0/LD1:
-
The sprite store is empty on the first frame — the LCD-on bypass never sets BESU, CARE never arms, and
oam_data_latchnever pulses (mode transitions). Consequences by OAM content:Case OAM at LY=0 Divergence on LD0/LD1? 1 no Y-visible sprite none — both frames render spriteless. Anchored across 777 Y-non-matching scanlines (zero TEKY/FEPO, zero variance) and 842 scanlines of DMA overlap across all three regimes: DMA cannot convert an empty-store line into an extended one 2 Y-visible sprite, on-screen X yes — the first frame misses the sprite; steady-state frames render it 3 Y-visible sprite at OAM X=0 timing only — a steady-state frame's save-written X=0 fires the X match and its fetch penalty; the first frame's reset-held store cannot fire (the store holds NOT(X); reset decodes as X = 0xFF — sprite pipeline) -
The window's REJO latch. REPU clears it on every VBlank entry (window control), so it carries nothing across frames — no divergence from this path.
Everything else either lands outside the pixel path (MEDA's silent first-frame VSYNC; BESU's missing Mode 2 status/IRQ on the first scanline of the first frame) or is overwritten before it can be sampled.
One cold-boot footnote: ROPO (the LYC-match capture) resets only at system reset, so STAT bit 2 between LCD-on and the first TALU edge briefly exposes the pre-LCD-off match value — and on the very first boot, the power-on zero.
LCD-on → First WODU
The end-to-end path from the CPU's LCD-enabling write to the first H-Blank, and how it maps onto CPU-observable timing (scanline and frame timing).
The end-to-end path
The full path from ff40_d7↑ (the LCD-enabling write's effect) to the
first H-Blank condition on the first scanline — SCX=0, no sprites, no window
(dmg-sim measurement):
| Stage | Δ from ff40_d7↑ (dots) |
|---|---|
| → XODO↓ (VID_RST deasserts) | +0.001 |
| → first XUPY↑ (scan counter starts) | +0.480 |
| → first AVAP↑ (Mode 2 → 3) | +78.491 |
| → first SACU↑ | +85.5 (= AVAP + 7.026) |
| → first WODU↑ | +251.536 (= AVAP + 173.045) |
With two dot-of-M-cycle invariants: the write's ff40_d7↑ lands at
+2.020 dots into its M-cycle (the CUPA position), and the first WODU↑
lands at +1.556 dots into its M-cycle. Both are CPU-prelude-invariant —
ps-identical across five LCD-on events in three radically different
instruction streams (quickboot's mid-boot enable, a test ROM's own
enable, and a post-HALT interrupt-handler enable, twice) — the
instruction stream between the write and the first WODU is irrelevant to
the PPU cascade.
The first-scanline Mode 2 measured from the write spans ~78.5 dots rather than 80 — not a Mode 2 shortening, but the dot-0 reference shifted by the write phase (+2.020) and divider startup (+0.480); each subsequent Mode 2 is exactly 80 dots.
SCX extension (measured at SCX ∈ {0,1,2,3,7}): the entire +1-dot-per-
SCX&7 shift lands in the startup pipeline; every other stage is
SCX-invariant to picosecond precision. Total: 251.536 + (SCX & 7) dots,
giving a first-WODU M-cycle phase of (1.556 + (SCX & 7)) mod 4.
Mapping to CPU observables
For the canonical LDH (LCDC),a; xor a; inc a; inc a; … prelude, walking
the wall-clock M-cells from the write pins everything: 63 CLK9 edges
separate the write's M-cycle from WODU's, placing the first WODU at dot
1.556 of cell M65 with inc#61 in flight — handler-entry A = 61, matching
the hardware-verified ROM expectation.
A walk that charges 4 dots per instruction after the write's M3 comes
out 4 dots short, because the cell after a memory write hosts the writer's
internal M4 and the next instruction's fetch — the SM83 fetch overlap.
An emulator without that overlap cell computes inc#62 in flight and
A = 62 at handler entry.
Per-SCX, the in-flight instruction and one further mechanism — a one-M-cycle
dispatch slip when the WODU-driven int_pending settles past the
dispatch-trigger setup boundary (first-WODU phases 2.556/3.556 — see
interrupt dispatch) — combine into the
hardware-verified expectations:
| SCX | WODU M-cycle | phase | in-flight | slip | handler A |
|---|---|---|---|---|---|
| 0 | M65 | 1.556 | inc#61 | no | 61 |
| 1 | M65 | 2.556 | inc#61 | +1 | 62 |
| 2 | M65 | 3.556 | inc#61 | +1 | 62 |
| 3 | M66 | 0.556 | inc#62 | no | 62 |
| 4 | M66 | 1.556 | inc#62 | no | 62 |
| 5 | M66 | 2.556 | inc#62 | +1 | 63 |
| 6 | M66 | 3.556 | inc#62 | +1 | 63 |
| 7 | M67 | 0.556 | inc#63 | no | 63 |
Skip-boot note
The first-scanline shortening happens once, in the first frame after the boot ROM's LCD-on; by PC=0x0100 (hundreds of frames later) it is baked into the machine's phase. Skip-boot emulators must adopt the post-boot state values jointly — LX=98 with the divider states as given — and must not synthesise a "pre-shortening" LX and re-apply the shortening at an LCD-on edge that never happens on the skip-boot path.
CPU-Visible Mode Boundaries
What does a CPU read latch when it lands on a PPU transition? This chapter
answers that question edge-by-edge for the four STAT-readout boundaries and
the LY 153→0 wrap, plus the write-side analogue (an OAM write straddling the
Mode 2→3 boundary). The recurring structure: a PPU trigger fires at some
sub-dot phase; a gate cascade settles in nanoseconds; the bus driver takes
far longer; and the CPU latches at data_phase_n↑, +974,856 ps (+3.995 dots — the tail
of T4) after its M-cycle's CLK9↑. Which of those windows the read M-cycle straddles
decides PRE vs POST — and the proximate mechanism differs per transition.
- The CPU latches the bus at the tail of T4 (+3.995 dots into its read M-cycle) — late; what matters is the bus state then, not the gate cascade.
- STAT mode-bit reads on a trigger M-cycle latch PRE: the
not_if1bus drivers stay unresolved through the rest of the M-cycle (the x-window). LY reads have no such window (not_if0drivers). - Mode 3→0 has no torn middle at single speed: VOGA quantises XYMU↑ onto the ALET grid — every read is provably fully-PRE or fully-POST.
- The Mode 2→3 cascade opens a measured ~6.5 ns OAM write permit between Mode 2 closing and Mode 3 locking — a straddling write's strobe chain completes with ~4.6 ns to spare.
The LYC stale window
Between PALY's combinational transition (the comparator seeing the new LY)
and ROPO's next TALU-edge capture, the comparator and the visible STAT bit 2
disagree (STAT interrupts). The CPU never sees the
stale value on a normal read: its data_phase sample lands late enough in
the read M-cycle that ROPO's transition has propagated to cpu_port_d
first.
Measured on a read M-cycle containing the transition (dmg-sim
measurement, gbmicrotest line_153_lyc153_stat_timing ROM family): the bus flips
0xC5 → 0xC1 at the instant rupo_n transitions, ≈117 ns before the CPU's
latch point (−116,838/−116,911 ps across two variants) — the CPU captures
0xC1, matching hardware. Emulators that latch CPU-visible reads early in
the M-cycle report the stale value instead.
Mode 2 → 3: the AVAP dot
Two paths fan out from AVAP↑ to the STAT mode-bit gates SADU = NOR2(mode3, mode1) and XATY = NOR2(mode2, mode3): the BESU branch clears mode2 in ~2.2 ns; the buffered XYMU branch raises mode3 at ~8.5 ns. In between, XATY sees NOR(0,0) — a ~6.3 ns transient "Mode 0" glitch on bit 1 (dmg-sim measurement, zero variance across 1,720 scanlines):
| Window from AVAP↑ | STAT[1:0] |
|---|---|
| before | 10 (Mode 2) |
| ~2.2 ns … ~8.5 ns | 00 (transient) |
| after ~8.5 ns | 11 (Mode 3) |
The glitch closes ~350 ns before any plausible CPU latch — never directly
observable. What is observable is the bus: the mode-bit drivers are
not_if1 tri-state cells whose output transition plus contention
holds the transitioning bit unresolved through the rest of the trigger
M-cycle. Measured on a read whose M-cycle contains AVAP↑ (dmg-sim
measurement, gbmicrotest lcdon_to_stat3_c): AVAP fires mid-T3 (+2.5 dots), the
gate cascade settles in ~8.5 ns, the bus bit starts transitioning at
~69 ns — and is still unresolved at the CPU's latch, ~360 ns later (it
resolves ~21 ns after).
Hardware latches the PRE-transition value (Mode 2 — the bus voltage hasn't crossed the input threshold), matching the hardware-verified expectation; the sibling ROM one M-cycle later reads Mode 3. The threshold is not the ~3 ns cascade — it is the bus-driver settling window.
Mode 0 → 2: the BESU-set boundary
The same bus-driver mechanism with a faster cascade: BESU.q rises
combinationally from CATU at the scanline boundary, mode2 rises at ~3.4 ns
(2.5× faster than the AVAP cascade — one combinational stage closer), only
bit 1 transitions. Measured on the trigger M-cycle (dmg-sim measurement,
gbmicrotest lcdon_to_stat2_c): gate settle at ~3.4 ns, bus bit unresolved from ~61 ns
through the CPU latch (it resolves ~20 ns after) — hardware latches PRE
(Mode 0); one M-cycle later reads Mode 2.
Mode 3 → 0: the WODU cascade
This transition differs structurally: the cascade contains a DFF (VOGA, ALET-clocked), so it takes ~106 ns rather than ~8.5 ns, and WODU↑'s phase within the read M-cycle varies with Mode 3's duration (set by SCX, sprites, and window). Both mode bits transition. Five measured anchors span the regime structure (dmg-sim measurements; all five ROMs are gbmicrotest; parenthesised offsets are dots from the M-cycle's start):
| Anchor | WODU↑ position | XYMU↑ vs CPU latch | Latches | Mechanism |
|---|---|---|---|---|
lcdon_to_stat0_c | read M-cycle, T4 (+3.556) | +0.006 dots after | PRE (0x83) | cascade misses the latch |
sprite_0_a | next M-cycle, T3 (+2.556) | +3.00 dots after | PRE (0x83) | cascade entirely after the read |
sprite_0_b | read M-cycle, T3 (+2.556) | −1.00 dots before | POST (0x80) | cascade flips the already-settled bus |
win0_b | read M-cycle, T2 (+1.556) | −2.00 dots before | POST (0x80) | cascade beats the read driver; bus settles directly to POST |
lcdon_to_stat0_d | prior M-cycle, T4 (+3.556) | −4.00 dots before | POST (0x80) | bus settled a full M-cycle earlier |
All five share a bit-identical gate cascade; only the alignment against the read M-cycle differs. Every expectation matches hardware-verified ROM results.
The ambiguous middle does not exist at single speed. VOGA quantises XYMU↑ onto the ALET grid — one possible position per dot. The WODU→XYMU delay is not a constant ("next ALET edge minus WODU, plus a DFF delay" — measured shrinking exactly as WODU↑ slides toward the capture edge); the grid position is the invariant. For the canonical FF41-read M-cycle, the nearest grid points land +0.006 dots after the CPU latch and −1.00 before it — nothing in between is reachable, so every single-speed read is provably fully-PRE or fully-POST.
The residual metastability sits at VOGA's capture: a WODU↑ inside the setup/hold window flips the outcome between two clean values a full dot apart — whole-mode selection, not a torn read.
One scoping caveat from the anchor family: "PPU timing is prelude-invariant" holds per-scanline only when that scanline's Mode 3 is not coupled to in-flight OAM DMA. A sibling ROM pair whose preludes pause DMA differently across HALT showed three mid-frame scanlines differing by one 11-dot sprite penalty — because DMA's byte 1 (a sprite X write) landed in one ROM's scan window and not the other's (DMA).
LY 153 → 0: the MYTA reset
The LY wrap is the clean counter-example to the STAT cases. MYTA fires on a
TALU edge at the start of an M-cycle (T1); the LAMA reset cascade to
the LY DFFs and the FF44 read drivers is purely combinational (~3–4 gate
stages); and crucially the LY drivers are not_if0 cells — single
tri-state inverters with no companion-driver contention, so no
hundreds-of-ns unresolved window exists.
- A read whose M-cycle starts on MYTA's edge latches 0x00 — the bus has been at the post-reset value for nearly the whole M-cycle.
- A read one M-cycle earlier latches 0x99 (153).
Measured anchors place two hardware-verified ROMs exactly one M-cycle
apart across this boundary (latch points 975,927 ps apart), with the
LCD-on→first-MYTA interval matching the analytic frame structure
(454 + 152 × 456 + 4 dots) to within 0.13 M-cycle (dmg-sim measurement,
gbmicrotest line_153_lyc153_stat_timing family). The same boundary M-cycle read
on FF41 catches the LYC stale window with bit 2 mid-transition, resolving
to the post-clear value per the first section above.
not_if1 readouts (STAT mode bits) have trigger-M-cycle bus-settling
windows; not_if0 readouts (LY) do not. When a transition lands inside
a read M-cycle, the driver cell type decides whether the read resolves PRE
(x-window) or simply follows the settled bus. The driver map is
netlist-enumerated for every register
(register reads): besides STAT's live bits, only
DMA reads through not_if1 — and its source register changes only on an
FF46 write, which cannot land inside an FF46 read M-cycle. STAT bits 0–2
remain the only x-window-capable readout.
The write-side straddle: AJUJ's ~6.5 ns window
The Mode 2→3 cascade's mode2/mode3 gap exists on the write-permit path
too: AJUJ = NOR3(dma_run, mode2, AJON) sees mode2 fall at +1,951 ps and
AJON rise at +8,759 ps — the permit is open from +2,318 to +8,820 ps
after AVAP↑, a 6,502 ps window between Mode 2 closing and Mode 3
locking (dmg-sim measurement, 1,720 scanlines, zero variance).
A CPU OAM write whose CUPA strobe spans this window fires the full WYJA → strobe chain. Measured in situ on the landing anchor (dmg-sim):
| Event | Δ |
|---|---|
| AVAP↑ | +122,925 ps into the write strobe |
| AJUJ opens (permit) | +2,318 ps after AVAP↑ |
| WYJA fires | +1,349 ps after the permit |
| YNYC byte strobe | +513 ps after WYJA |
That is 1,862 ps from permit to strobe, completing 4,640 ps before the
window closes. The OAM SRAM cell — a transparent latch with no
setup/hold edge — captures the bus value during the strobe-high residue.
The hardware-verified anchor is gbmicrotest oam_write_l1_c (write
lands on DMG), bracketed by one-NOP siblings whose writes are blocked:
blocked / LANDS / blocked.
The structurally parallel boundaries behave differently: Mode 0→2 has no transient gap (mode2 rises directly; AJUJ closes cleanly — write blocked), and Mode 3→0 simply opens the permit: AJON releases at WODU↑ +111,054 ps and AJUJ opens at +111,421 ps — 4,999 ps after the mode-bit change (dmg-sim measurement, 1,721 scanlines, zero variance).
A straddling write across this opening is demonstrated directly with the
purpose-built oam_write_m3end sweep, with SCX = 3 phasing the boundary
into the strobe — fine scroll shifts Mode 3's end in 1-dot steps. The
landing position, from the CUPA strobe's rise (dmg-sim):
| Event | Δ from strobe rise |
|---|---|
| Strobe begins (permit still closed) | 0 |
| AJUJ opens (permit) | +245,254 ps (1.005 dots) |
| WYJA fires | +1,349 ps after the permit |
| YNYC byte strobe | +1,414 ps after WYJA |
That leaves 116,281 ps of strobe-high residue for the transparent OAM cell to capture, and the WYJA latency matches the Mode 2→3 case. Ladder positions either side bracket it: writes wholly inside Mode 3 never fire WYJA (blocked); writes after the opening land 396 ps from the strobe's rise. Blocked / LANDS / clean — the write-side mirror of the Mode 2→3 straddle.
Races
This chapter consolidates the static race analysis: which signal pairs race, which orderings are structural, and — just as importantly — where propagation delay does not matter. The source is graph analysis over the netlist (depths in gate equivalents from the master clock; see Methodology).
- Only 13 per-dot races exist, and none is a data contention: 5 are clock-vs-settled-data skew, 8 are the BG temp-latch late enable — which is what makes the capture correct.
- The sprite store is the deepest per-line path (71.2 ge, negative slack at worst case) — the source of the first-entry staleness window.
- The palette/SACU pair is not a race: a mid-Mode-3 write's latch update lands ~15.6 ns after the write-dot SACU↓ (data-bus settling).
- Most register paths are per-frame: stable absent CPU writes.
Same-clock synchronous paths
DFFs sharing a clock capture on the same edge with the full period as settling budget — race analysis does not apply:
| Clock | DFFs |
|---|---|
alet | NYKA, LYZU, PYGO, RENE, DOBA, NOPA, VOGA |
myvo | PORY |
sacu | PX bits 0–3 + 16 BG shifter + 16 sprite shifter + 16 attribute pipe (52 total) |
talu | SAXO, NYPE |
sono | RUTU, SYGU |
Complementary-clock pipelines
A DFF on clock A feeding a DFF on NOT(A) forms a structural half-period pipeline:
- ALET → MYVO (NYKA → PORY): PORY always reflects what NYKA captured on the most recent ALET edge — a half-dot lag, not a full dot.
- TALU → SONO (LX → LINE_END): the LX counter increments on TALU rising; RUTU captures the SANU decode on SONO rising — RUTU sees the LX value from one TALU edge ago.
- SACU → TOCA (PX lower → upper nibble): PX bits 4–7 clock on TOCA = NOT(XYDO), one propagation delay after bit 3 — a small skew inside the dot that the XUGU terminal decode (which reads bits from both groups) absorbs.
Cross-domain paths: the 13 per-dot races
PPU Cycles group (5 races). Clock-vs-registered-data skew, not data contention: the data inputs are DFF Q outputs (depth 0) that settled on the previous edge, so data is always stable before the clock arrives.
| DFF(s) | Clock | Clock depth | Data inputs |
|---|---|---|---|
| PORY | MYVO | 22.2 ge (tightest) | NYKA, NAFY (6.2 ge) |
| LYZU, PYGO, RENE | ALET | 16.3 ge | LAXU, PORY, RYFA |
| NYZE | MOXE | 16.7 ge | PUXA |
BG FIFO data-latch group (8 races, all diff = 16.2 ge). The temp-latch enable propagates from fetch-counter bit 2 through NYVA → NOFU → NYDY → METE → LOMA → LUNA, arriving 16.2 ge after the counter changes — after the VRAM bus has stabilised with the current fetch's data. The hardware ordering is therefore: bus stable → counter advances → enable fires late → already-stable data captured. The late enable is what makes the capture correct.
Palette/SACU ordering (3 registers). A mid-Mode-3 palette dlatch update lands ~15.6 ns after the write-dot SACU↓ (palette latches) — the latch enable opens early but the captured value follows the CPU data bus, which settles past the pixel edge. The old-palette-on-the-write-dot outcome is structural: a ~15 ns ordering, not a marginal race.
Per-line races
Sprite store (100 races, diff 70.5–71.2 ge). The deepest per-line path in the PPU: MUWY (LY bit 0) → the Y-compare carry chain → slot decode → EBEB (the store write enable) at 71.2 ge, against OAM data arriving at depth 0. At the worst-case gate-delay estimate the path misses its 1-M-cycle deadline — negative slack. The hardware effect: on the very first OAM entry after LY increments, the Y-comparator result can be one cycle stale; subsequent entries settle normally. This is the first-entry-staleness window noted in OAM scan (CENO's one-XUPY-cycle lag is the other half of that story).
Sprite FIFO / X match (44 races, diff 30.7–63.8 ge). Sourced from CENO. These settle during Mode 2's 80 dots, long before CLKPIPE starts toggling — ample slack for the rest of the line.
Sprite Y-compare captures (TOBU/VONU/SEBA, diff 25.8–27.3 ge). Technically per-dot but only fire during the scan's 2-dot-per-entry window, which comfortably covers them.
Where propagation delay does not matter
"Per-frame" below means the race's critical source only changes on CPU writes — the writes themselves land mid-frame at the dot the register-write chapter describes.
| Area | Domain | Notes |
|---|---|---|
| Palette registers | Per-frame | Stable absent writes; the mid-Mode-3 write ordering is the structural case above |
| SCX | Per-frame | Mid-scanline writes propagate through the VRAM address adder (66.2 ge). SCX feeds only the counter=0 tilemap stage (the map column) — the tile-data stages use the cached tile index, and SCX's fine bits drive only the startup fine scroll (pixel-clock suppression, startup and fine scroll), never a tile-data address, so there is no LCDC.4-style hybrid-fetch path for SCX: a write landing between counter=2 and counter=4 cannot mix bitplanes. Writes before/after counter=0 split exactly like LCDC.3/.6 sampling (BG pipeline) |
| SCY | Per-frame | Same adder, but SCY feeds two fetch stages: the map row at counter=0 (once, like BG_MAP) and the fine-Y tile-data row offset at counter=2/4 (twice, once per bitplane — like TILE_SEL). So SCY does have an LCDC.4-style hybrid path: a write landing between counter=2 and counter=4 fetches the low bitplane from the old fine-Y row and the high bitplane from the new one (BG pipeline) |
| WX, WY | Per-frame | WY compared once per line; WX per-dot but the compare is shallow |
| LCDC | Per-frame | All consumer sampling is combinational from the latches; the interesting cases are the per-bit mid-write behaviours covered in their consumer chapters |
| STAT enables | Per-frame | CUPA-strobed; the FF41 bus-settle transient is in STAT interrupts |
| LY == LYC | Per-frame | Full line to settle (boundary races excepted — STAT interrupts) |
| LCD output path | Static | Pixel data is combinational from shifter MSBs |
| External cartridge address pads | Per-line | CPU-vs-DMA contention on a0..a10, uniformly resolved in DMA's favour by the address MUX; the observable conflict is on the data side (DMA) |
| Internal OAM address bus | Per-line | Four mutually-exclusive tri-state enables — exactly one driver in every (mode, DMA) combination; not a deadline race at all, but it shapes what the scan latches during DMA (DMA) |
The Timer
Four chapters cover the SM83-side subsystems: the timer (this chapter), interrupt dispatch, HALT, EI, and the IME pipeline, and the IF register.
The timer's behavioural surface — DIV, TIMA/TMA/TAC semantics, the reload quirks, the write-during-reload edge cases — is documented in Pan Docs, and this book does not restate it. (gb-ctr defines the four timer registers in its memory-map tables but does not yet cover their behaviour.) What this chapter adds is the gate inventory underneath: the actual counter cells, the exact register-to-counter bit mapping, and the picosecond-resolution edge sequence of the reload cycle.
- DIV and TIMA share one free-running 16-bit ripple counter that
increments per M-cycle; FF04 reads
reg_div16[13:6]. - TIMA increments on the falling edge of the TAC-selected bit through a NOR gate — which is why TAC disables and DIV writes can themselves increment it.
- IF[2] and the TMA load both fire at the start of the reload M-cycle — the "one-M-cycle delay" is wrap-late-in-one-cell to IF-early-in-the-next.
- MEXU holds the TIMA cells transparent for the whole reload M-cycle — the window where the documented TIMA/TMA write interactions live.
| Gate | Role | Type | Clock / Trigger | Notes |
|---|---|---|---|---|
| UKUP … UPOF | reg_div16 bits 0–15 | dff | M-cycle boundary, then ~Q ripple | The free-running divider |
reset_div_n | Divider reset net | nor | UCOB (DIV write), reset, TAPE | Resets all 16 DFFs |
| REGA … NUGA | TIMA bits 0–7 | tffnl (toggle FF with load) | LSB by SOGU; ripple by lower bit's q | mexu↑ switches all 8 to level-sensitive load |
| UKAP / TEKO / TECY | TAC mux chain | muxi | — | Select the reg_div16 bit driving TIMA |
| SOGU | TIMA clock gate | nor2 | TECY, sabo_n | TAC.2 low ties the TIMA clock high |
| NYDU | NUGA pre-wrap capture | dffr | CLK9↑ | Holds NUGA's previous M-cycle value |
| MERY | Wrap detect | nor2 | NUGA, nydu_n | Rises when the MSB just fell |
| MOBA | Wrap capture | dffr | CLK9↑ | Q = int_timer |
| MEKE / MEXU | Reload enable chain | not / nand3 | — | MEXU drives the tffnl load pins |
| NYBO | IF[2] capture | dffsr | clocked by int_timer | IF register |
The 16-bit counter
DIV and TIMA share one free-running 16-bit ripple counter (reg_div16):
bit 0 toggles at the M-cycle boundary; each higher bit clocks from the bit
below. It runs unconditionally outside STOP. After a reset deassert it
starts cleanly from 0 — the first M-cycle boundary tick reads 0x0001, with
no reset artefacts on any bit (dmg-sim measurement).
The advance edge, measured by ps-bisect: a 976,000 ps period exactly. Bit 0 toggles ~1.2 ns after the master-clock edge separating consecutive M-cycles, and each carry stage adds ~1.3 ns of ripple — a boundary carrying through k bits settles ~(1.2 + 1.3k) ns in.
DIV is bits [13:6], not the top byte. The FF04 register reads
reg_div16[13:6] combinationally (verified directly in the netlist
model). Because the counter increments per M-cycle rather than per T-cycle,
bit 6 toggles every 64 M-cycles = 256 T-cycles — yielding exactly the
documented 16,384 Hz DIV rate. The bit numbering differs from Pan Docs's
T-cycle-scale convention; the observable register matches. A DIV write
clears the whole counter through reset_div_n.
The TIMA path
TIMA is eight toggle cells (REGA → POVY → PERU → RATE → RUBY → RAGE →
PEDA → NUGA), the LSB clocked by SOGU and each higher bit by the lower
bit's q — so bit N+1 toggles on bit N's falling edge. The TAC selection,
in reg_div16 terms:
| TAC select | Frequency | reg_div16 bit feeding TIMA |
|---|---|---|
| 00 | 4,096 Hz | bit 7 |
| 01 | 262,144 Hz | bit 1 |
| 10 | 65,536 Hz | bit 3 |
| 11 | 16,384 Hz | bit 5 |
TIMA increments on the falling edge of the selected bit, gated by TAC.2 through SOGU — which is why a TAC disable (or a DIV write that drops the selected bit) can itself produce an increment: the gate output falls. (The behavioural consequences are documented in Pan Docs; the gate explains them all.)
The reload cycle, edge by edge
flowchart LR
NUGA["NUGA↓<br>TIMA MSB falls"] --> MERY["MERY<br>wrap detect"]
NYDU["NYDU<br>pre-wrap capture"] -- "q_n" --> MERY
MERY -- "CLK9↑ (next M-cycle)" --> MOBA["MOBA<br>int_timer"]
MOBA --> NYBO["NYBO<br>IF[2] set"]
MOBA --> MEKE["MEKE"]
MEKE --> MEXU["MEXU<br>TMA load enable"]
MEXU -- "tffnl cells transparent" --> TIMA["TIMA ← TMA"]
NUGA's falling edge is the only TIMA transition that distinguishes the 0xFF → 0x00 wrap. The wrap-detect and reload sequence spans exactly two M-cycles — the wrap M-cycle (TIMA ripples to 0x00 late in it) and the reload M-cycle (IF[2] and the TMA load fire at its start). Measured (dmg-sim measurement; offsets from each M-cycle's opening CLK9↑):
| Event | Δ | Notes |
|---|---|---|
| Wrap M-cycle | TIMA = 0xFF entering | |
| SOGU↓ (increment edge) | +2,998 ps | selected bit falls |
| TIMA ripples 0xFF → 0x00 | +4,600 → +15,500 ps | ~1,550 ps per stage, NUGA last |
| MERY↑ (wrap detect) | +15,700 ps | 173 ps after NUGA |
| Reload M-cycle | opens at the next CLK9↑ | |
MOBA.q↑ (= int_timer) | +0 ps | DFF capture on the boundary edge |
| NYBO.q↑ (IF[2] set) | +931 ps | |
| MEKE↓ | +951 ps | |
| MERY↓ | +1,032 ps | NYDU has caught up |
| MEXU↑ (TMA load begins) | +2,005 ps | all 8 tffnl cells go transparent |
| TIMA visibly = TMA | ≈ +2,000 ps | level-sensitive load |
| MEXU↓ | +978,463 ps | 2,463 ps into the following M-cycle |
The key phase fact: both the interrupt flag and the TMA load fire at the start of the reload M-cycle — the CLK9 edge that closes the wrap M-cycle — not at its end. The documented "one-M-cycle delay" between overflow and interrupt is the gap from the wrap late in one M-cycle (~+15.7 ns in) to the IF rise early in the next (+931 ps in). MEXU stays high through the whole reload M-cycle — the window where the documented TIMA/TMA write interactions play out — and releases 2,463 ps into the next.
The dispatch-side continuation of int_timer — through the IF latch and
into the CPU's interrupt chain — is in
the IF register and
interrupt dispatch.
Interrupt Dispatch
When an unmasked interrupt is latched into IF with IME=1, the CPU dispatches to its handler through a fixed 5-M-cycle sequence. The behavioural shape — five M-cycles, two pushes, the vector table — is standard reference material (Pan Docs); this chapter documents the machinery: the CLK9-cadence capture DFF, the precharged-evaluate priority chain, the instruction-boundary gate, the per-edge write-back rule, and the measured slip regimes.
- The IF & IE latch is transparent through T1–T2 and opaque during the data phase — that gating decides every slip regime.
- Dispatch latency = (body M-cycles remaining) + (1 if no-overlap) + 4; overlap-capable instructions dispatch at the body's closing CLK9↑.
- Write-back and dispatch capture on the same CLK9 edge — the in-flight instruction always retires.
- The dispatch sequence applies a universal IDU −1 to PC — which is also why RETI re-enters HALT.
- The IE push bug's race window is exactly M3's data phase.
| Semantic name | Cell / wire | Role | Type | Notes |
|---|---|---|---|---|
irq_latched | YOII (g42) | Pending-interrupt capture | DFF on CLK9 | Samples IF & IE once per M-cycle; forks into the running-CPU and halt-release chains |
irq_pending | wire int_pending | Dispatch eligibility | combinational | Per-bit IF & IE through the priority chain |
dispatch_trigger | wire int_take | Dispatch trigger | combinational | Pulses in the eval phase (T4) of fetch M-cycles |
dispatch_active | ZACW, wire int_entry | Dispatch capture | DFF on CLK9 | Q↑ starts the 5-M-cycle sequence |
| CLK9 | — | CPU M-cycle clock | — | One rising edge per M-cycle = dot-0 ALET↑ + 2,983 ps; period 976,000 ps, zero jitter |
CLK9, pinned. CLK9↑ is physically the master-clock rising edge that opens each M-cycle — the dot-0 ALET↑, reached through the CPU clock-tree buffers 2,983 ps later. Every "M-cycle boundary" in this part of the book is this edge.
The capture chain, cell by cell
flowchart LR
IF["IF ∧ IE<br>per-bit enabled latch<br>(transparent T1–T2)"] --> PRIO["irq_prio_bit<br>precharged wired-NAND"]
PRIO -- "eval in T4" --> TRIG["dispatch_trigger"]
TRIG --> ZAOC["ZAOC<br>∧ XOGS<br>instruction boundary"]
ZZOM["ZZOM<br>DI data-phase block"] --> ZAIJ
ZAOC --> ZAIJ["ZAIJ<br>NOR with ime_n,<br>data_phase"]
ZAIJ --> ZKOG["ZKOG / ZLOZ<br>set / hold"]
ZKOG --> ZFEX["ZFEX<br>merge"]
ZFEX -- "CLK9↑" --> ZACW["ZACW<br>dispatch_active"]
From the per-bit IF latches to dispatch_active:
- Per-bit IF & IE latch (
irq_latch<0..4>) — an enabled D-latch, not a DFF: transparent whiledata_phase_n=1 (dots −0.022…1.978 of each M-cycle) and opaque during the data phase (+1.978…+3.978 dots). This gating is load-bearing for half the timing behaviour in this part. - Priority chain (
irq_prio_bit<0..4>) — a precharged wired-NAND bus: precharged high through T1–T3, evaluated whenwrite_phaserises in T4. The highest-priority pending bit wins. dispatch_trigger= NOT of the bus — rises at the T4 eval phase, 240,200 ps before the next CLK9↑. The FST-observed ~729 ns from a mid-M-cycleirq_pending↑ todispatch_trigger↑ is the wait for the eval phase, not gate propagation.- Boundary and blocking gates — ZAOC combines the trigger with the
instruction-boundary signal XOGS; ZZOM blocks during DI's data phase;
ZAIJ = their NOR with
ime_nanddata_phase— the set condition. - Set / hold SR latches (ZKOG, ZLOZ) and the merge (ZFEX) feed ZACW, which captures on CLK9↑. ZKOG initiates; ZLOZ holds through dispatch M2–M5.
The instruction-boundary signal XOGS = (data_phase ∧ ctl_fetch ∧
¬CB-prefix) ∨ idle. ctl_fetch asserts during the data phase of whichever
M-cycle performs the next opcode fetch — and that is where instruction
classes split:
- Overlap-capable instructions (NOP, ALU r, INC r, JR, POP's last cycle, EI/DI, HALT, …— 21 sequencer states): the terminal body M-cycle has no bus traffic of its own and carries the next fetch. Dispatch can fire at the body's closing CLK9↑.
- No-overlap instructions (memory writes, indirect reads, pushes, and a
few internal-arithmetic terminals): the bus is busy in the terminal body
M-cycle, so the sequencer inserts a dedicated post-body fetch M-cycle,
and dispatch fires one M-cycle later than the body. Two counter states
carry it:
- m6 — the indirect-read-into-register subclass (six states, including the dispatch sequence's own vector fetch).
- m7 — the memory-write/push/internal subclass (twenty states); also the force-path parking state during HALT, reset, and condition-failed branches.
The latency formula: (body M-cycles remaining) + (1 if no-overlap) + 4.
The dispatch sequence
| M-cycle | Bus | State change |
|---|---|---|
| 1 | internal | IME cleared; the IDU −1 step on PC (below) |
| 2 | internal | — |
| 3 | write PC-high to SP−1 | SP decremented |
| 4 | write PC-low to SP−2 | SP decremented; vector resolved |
| 5 | opcode fetch from vector | PC = $40/$48/$50/$58/$60 |
The universal IDU −1. Dispatch decrements PC once during its internal cycles, on every dispatch path. For ordinary in-flight instructions, PC holds addr+2 at the dispatching edge (fetch + overlap increment) and the −1 yields the correct return address addr+1. For HALT — whose own increment is suppressed — the same −1 yields the HALT address itself, which is why RETI re-enters HALT (HALT and EI).
The IE push bug, gate by gate
When M3's push of PC-high lands on $FFFF, the write updates IE mid-dispatch:
- M3 data phase:
reg_iecaptures the pushed byte; the per-bit NAND re-evaluates combinationally — but the IF&IE latch is opaque (data phase). - At the M3→M4 boundary,
data_phase_n↑ — ALET-pinned, just before the next CLK9↑ (tail of T4; the precise offset is in CPU-visible boundaries) — re-opens the latch, which captures the post-write value. - M4's dot-3 eval re-runs the priority chain on the new state; M5's fetch address derives from it.
The race window is therefore exactly M3's data phase: an IE write there
wins; one in M4's data phase is too late. And if the write clears the
dispatching bit, the priority chain finds nothing — the vector drives
$0000, and the acknowledge (which requires the bit still pending) never
fires, leaving the IF bit set: the canonical mooneye ie_push observation.
Write-back and dispatch share one edge
For an instruction retiring at the dispatching CLK9↑, the register-file
DFFs, the PC DFFs, and ZACW all capture on the same edge — register
write-back settles at +210–417 ps, dispatch_active at +983–1,190 ps.
There is no "dispatch pre-empts write-back" race, and the IF-coincident
M-cycle is not converted to a no-retire cycle.
Verified across four HBlank-STAT anchors at three different WODU dot phases, and generalised across sources:
- LYC — source rise at +3.499 dots (T4) of the prior M-cycle;
- Timer — boundary-aligned at +0.000; the phase that costs a HALT wake an extra M-cycle costs running-CPU dispatch nothing (the trigger has ~972 ns of margin);
- OAM STAT — +1.502 dots (T2).
In every case the in-flight instruction commits its result and the handler-entry register values match the hardware-verified ROM expectations (dmg-sim measurements).
Running-CPU edge timing and the slip regimes
For a mid-M-cycle irq_pending↑ during a one-M-cycle instruction:
| Event | Δ from irq_pending↑ |
|---|---|
dispatch_trigger↑ | +728,721 ps (the dot-3 eval) |
dispatch_active.q↑ | +969,330 ps (the closing CLK9↑) |
| PC = vector | +4,878,688 ps ≈ 5.000 M-cycles |
Steady-state invariance. In a steady scanline loop the WODU dot phase repeats identically, so the ~732 ns spread across the four possible dot phases produces zero spread in the dispatching edge.
The first-WODU slip. The first H-Blank after LCD-on lands at M-cycle
dot phase (1.556 + (SCX & 7)) mod 4, and here the IF-latch gating bites:
LALU.q rising before +1.978 dots propagates combinationally
(irq_pending↑ +12,333 ps — no slip); rising after +1.978 is held
by the opaque latch until it reopens at the next boundary (irq_pending↑
one M-cycle late — dispatch slips +1 M-cycle).
The LALU.q edge rises just after WODU↑, at phases 0.567/1.567/2.567/3.567. Phases 0.567/1.567: no slip; 2.567/3.567: held +356,788/+112,788 ps, both releasing at exactly boundary +0.029 dots (dmg-sim measurement, the SCX sweep — each phase replicated across two SCX values plus the lcdon family). The resulting per-SCX handler-entry values match the hardware-verified ROM directives (LCD-on → first WODU carries the combined table).
Same numerical phase, no slip — the calibrated-loop escape. Test families that re-calibrate their NOP padding per SCX place WODU at +2.556/+3.556 dots of the M-cycle before the fetch M-cycle: the held latch re-opens at that boundary, leaving the full fetch M-cycle for the trigger to settle — identical dispatching edge across all SCX (dmg-sim measurements across the eight-ROM cluster, including an anchor where the fetch M-cycle is an ordinary NOP body). The slip is a property of where the rise falls relative to the latch windows, not of the numerical dot phase.
The boundary values: the IF-latch is transparent in [−0.022, 1.978] dots
of each M-cycle and opaque in [1.978, 3.978] (the open edge measured from
the held-release convergence at boundary +0.029 minus the +12,333 ps
combinational leg); data_phase_n↑ at +965,635 ps; the measured
trigger-to-capturing-CLK9↑ setup in the steady case is 240,200 ps.
Finally, the sequencer state at a measured dispatching edge confirms the
gate story end to end: at WODU↑ the in-flight state reads the fetch
M-cycle (m6 of ld a,(hl) or a NOP body), ime_n=0, the DI-block gate
inactive — and a trailing DI (the next instruction, already fetched)
does not block dispatch, because ZZOM looks at the in-flight instruction
only (dmg-sim measurement).
HALT and EI
HALT and EI both look like one-line behavioural rules ("HALT waits for an interrupt"; "EI takes effect one instruction late") and both decompose into small gate clusters with measurable consequences — per-source wake latencies, the HALT bug, and the exact PC arithmetic of interrupted HALTs.
data_phaseis held LOW throughout HALT: the IF latches go transparent and OAM DMA pauses.- Wake latency splits by source phase at YOII's setup window: timer wakes cost 6 M-cycles, every other source 5 — one missed DFF setup, no "wakeup NOP".
- IME enables mid-EI's own M-cycle; the one-instruction delay lives in the dispatch set gate, not the IME path.
- The HALT bug is IDU-suppression ± dispatch-compensation — there is no separately-gated bug path.
- Whether the halt latch sets is decided at one CLK9 edge, bounded by the IF-latch window at +1.978 dots into the body M-cycle.
HALT: the state and the wake
Three cells carry HALT:
- YOII (
irq_latched) — the same CLK9 capture the dispatch chain uses; - YKUA — an OAI21 whose output is the halt latch's active-low reset;
- YNKW — the halt RS latch; q =
halt, chip-level clock enable = NOT(halt).
When irq_latched captures a pending interrupt, YKUA pulls low
combinationally and the halt latch resets — no M-cycle-aligned gating
anywhere in the release.
data_phase is held LOW throughout HALT. Two big consequences:
- Every per-bit IF latch is transparent for the whole HALT (the enable
is
data_phase_n) — a source rise propagates combinationally toirq_pendingregardless of its dot phase. dma_phi= NOT(data_phase) is held HIGH — any in-flight OAM DMA pauses for the duration (DMA).
Per-source wake latency
The wake latency splits by source phase relative to CLK9, at YOII's setup window (dmg-sim measurements):
| Source | irq_pending↑ phase in M-cycle | Setup at next CLK9↑ | Handler entry from irq_pending↑ |
|---|---|---|---|
| Timer | ~3.6 ns after the boundary (boundary-aligned by construction — MOBA is a CLK9 DFF) | misses — waits a full M-cycle | 5,854,711 ps ≈ 6 M-cycles |
| VBlank / LYC | ~88% into the M-cycle | meets (~117 ns margin) | ≈ 5 M-cycles |
| HBlank STAT | WODU is ALET↓-aligned; ≥122 ns from any CLK9↑ at every SCX | meets | ≈ 5 M-cycles |
| OAM STAT | RUTU at +1.5 dots; chain 4,847 ps | meets (~606 ns margin) | ≈ 5 M-cycles |
The timer's extra M-cycle is one missed DFF setup window — there is no "wakeup NOP" in the sequencer. Everything downstream of YOII is uniform across sources to within ~0.1 ns.
IME=0 wake: one M-cycle faster
With IME=0 and IE & IF ≠ 0, the same release chain runs but no dispatch
fires (the set gate ZAIJ is held low by ime_n). The M-cycle counter was
parked at m7 throughout HALT — and m7 is a fetch state — so the wake
M-cycle is itself the post-HALT opcode fetch: the phase ring restarts,
data_phase rises at +1.999 dots, the bus reads PC mid-data-phase, and the
next instruction's body begins at the following boundary. Measured edge by
edge: halt↓ at +4,226 ps after the wake CLK9↑, fetch read at +2.0 dots, PC
advance at +4.01 dots (dmg-sim measurement, mooneye
halt_ime0_nointr_timing). The IME=1 path spends this M-cycle capturing
dispatch_active instead and discards the wake fetch.
The HALT-wake boundary cases
Two LYC-source anchors pin the subtler structure (dmg-sim measurements, paired HALT and running-CPU ROMs):
- On the LY 153→0 wrap, the IF-driving edge is ROPO's capture one TALU
period after MYTA; HALT's transparent IF latch fires
irq_pending~113 ns earlier than the running CPU's gated latch — and that head start is exactly absorbed by the halt-release chain's extra capture M-cycle. Both paths commitdispatch_activeon the identical wall-clock edge. - On ordinary increments (LY=N→N+1), the pre-capture cascade differs (PALY rises ~2 dots before ROPO's edge instead of ~4 ns after MYTA) but every ROPO-relative anchor is picosecond-identical across iterations — the post-HALT phase-ring shift (~1 ns) settles by the first iteration and never accumulates.
One structural surprise survives into handler code: the first handler instruction with a memory write runs M1→M2→m7 after a HALT wake but M1→m7→M2 after a running-CPU dispatch — a one-M-cycle wall-clock shift in the write's position, uniform across iterations (measured on 8 write pairs). For LCD-column-sensitive handlers (mid-Mode-3 palette writes), HALT-entered handlers land their writes 4 dots earlier than running-CPU-entered ones.
A gate-level decomposition of the ordering difference — and the reconciliation of one reference image whose column shift runs the other way — remains open. See Appendix C.
The EI pipeline
flowchart LR
EI["EI<br>data phase"] -- "sets" --> ZJJE["ZJJE<br>ime_pending"]
DI["DI<br>data phase"] -- "resets" --> ZJJE
DISP["dispatch M1"] -- "clears briefly" --> ZRSY["ZRSY<br>IME-set unblocked"]
ZJJE --> NAND["NAND"]
ZRSY --> NAND
NAND -- "exec_phase↓<br>711 ps before CLK9↑" --> ZIVV["ZIVV<br>q = ime_n"]
The IME path is one DFF (ZIVV, q = ime_n, clocked once per M-cycle on
exec_phase falling — 711 ps before CLK9↑) fed by a NAND of two SR
latches: ZJJE (ime_pending — "EI seen, not yet cancelled") and ZRSY
("IME-set unblocked" — cleared briefly during dispatch so dispatch can
force IME off). There is no DFF chain, and the one-instruction delay is
not in this path:
- IME enables mid-EI's own M-cycle — at ZIVV's capture on that
M-cycle's closing
exec_phase↓ (dmg-sim measurement). - The delay lives in the dispatch set gate: ZAIJ is blocked during
EI/DI's data phase, so
dispatch_activecannot capture until the next instruction's M-cycle. Total EI → handler entry: exactly 6 M-cycles (EI + one successor instruction + dispatch M1–M4), with the successor's boundary — not its opcode — controlling the capture.
DI cancels combinationally. DI's data phase resets ime_pending
through the same gate dispatch uses; IME disables at DI's own capture
edge. EI; DI yields ~1 M-cycle of IME with no dispatch possible in it;
EI; NOP; DI dispatches during the NOP and the DI never executes until
the handler returns to it.
EI; HALT and the HALT bug — one mechanism
HALT's body M-cycle suppresses the normal IDU+1 (the PC-advance gate is masked while the HALT decode flag is set), so PC holds HALT+1 across the boundary. Everything observable follows from composing that suppression with the dispatch sequence's universal −1:
| IME | IF & IE at HALT decode | What happens |
|---|---|---|
| 1 | ≠ 0 | Dispatch fires via the running-CPU path mid-HALT-M-cycle (the halt latch never sets — irq_latched already holds its reset low). PC: HALT+1 − 1 = HALT is pushed; RETI re-enters HALT. Measured end to end on the hardware-verified AGE ei-halt ROM: both anchors push exactly the HALT address |
| 0 | ≠ 0 | No dispatch, no −1, halt latch never sets: the post-HALT M-cycle re-reads PC = HALT+1 — the byte after HALT executes twice (the Pan Docs HALT bug) |
| 0 | = 0 | Halt latch sets; the CPU idles; the wake path above applies |
There is no separately-gated "bug path": the folk model of one is wrong — the behaviour is IDU-suppression ± dispatch-compensation.
The halt-bug detection edge
Whether the halt latch sets at all is decided at one CLK9 edge — the
one opening the M-cycle after HALT's body, which captures both the
HALT-delayed flag and irq_pending (into YOII). The reset-dominates-set
latch then resolves: YOII captured 1 → latch stays unset (halt-bug /
dispatch regime); YOII captured 0 → latch sets (halt-wait regime).
Because the IF latch is data-phase-gated during the body M-cycle, the
boundary between regimes is the latch window: a source rising before dot
1.978 of the body M-cycle reaches YOII in time; one rising after is held
past the edge. Measured across an 8-iteration SCX sweep that walks a
STAT source across the body M-cycle dot by dot: iterations with the rise
at +0.5/+1.5 dots produce the halt-bug outcome, the rise at +2.5 onward
produces halt-wait — a 3-of-8 split exactly matching the
hardware-verified pass signature 02 02 02 01 01 01 01 01 (dmg-sim
measurement, AGE halt-m0-interrupt). No additional gates or
multi-edge windows are required.
The IF Register
IF (FF0F) is five chip-level DFFs — one per source — outside the SM83 core, each with D tied high and the source as its clock. That one structural fact generates all the behaviour: edge-triggered capture, held-high sources that cannot re-set after a clear, and two distinct clear-vs-rise races with opposite outcomes.
- Each IF bit is a DFF with D tied high, clocked by its source — a held-high source cannot re-set a cleared bit.
- FF0F-write clear vs same-cycle rise: rise wins (the clear pulse closes before the mid-M-cycle sources fire).
- Dispatch-acknowledge clear vs same-cycle rise: clear wins (the ack pulse straddles the boundary and absorbs the edge).
- IF survives LCD-off — no first-frame VBlank suppression of any kind.
| Bit | Cell | Clock (source) | Source driver |
|---|---|---|---|
| 0 (VBlank) | LOPE | int_vbl | POPU.q (mode1) through TOLU + VYPU |
| 1 (STAT) | LALU | int_stat | SUKO through TUVA + VOTY (STAT interrupts) |
| 2 (Timer) | NYBO | int_timer | MOBA.q — a CLK9-aligned DFF (timer) |
| 3 (Serial) | UBUL | int_serial | CALY toggle at the transfer clock |
| 4 (Joypad) | ULAK | int_jp | AND2(APUG, BATU) — combinational, no source latch |
Each cell's s_n (FF0F write of 1) and r_n (FF0F write of 0, or dispatch
acknowledge) are active-low overrides. Since D is constant, only the
clock matters: a held-high source cannot re-set a cleared bit — a fresh
rising edge is required. That is the LALU edge-trigger rule generalised to
all five bits.
Source cascades, measured
VBlank (POPU↑ → IF[0]):
POPU↑ → two-NOT buffer (768 ps) →
cpu_irq0(+1,064 ps) →irq_pending(+4,144 ps)
The whole chain is sub-dot. VBlank IF and the Mode 1 STAT condition co-rise on the same dot with nanosecond ordering.
The Mode 1 STAT leg at VBlank entry depends on the active enables:
- LYC leg active — the entry traverses the SUKO glitch (the combining gate dips and recovers); IF[1] sets ~4.4 ns after POPU, ~3.6 ns behind IF[0].
- Mode 0 + Mode 1 enables — the Mode 1 arm covers SUKO before the Mode 0 arm drops, so IF[1] does not change at all at the boundary; the measured handler reads IF[0]=1, IF[1]=0.
(dmg-sim measurements; the case analysis is the leg-swap table in STAT interrupts.)
The boundary into the CPU: the per-bit IF&IE latch inside the SM83 is
an enabled D-latch transparent outside the data phase
(interrupt dispatch) — a mid-M-cycle IF rise
reaches irq_pending with no clock-edge wait; the first CLK9-aligned
capture is YOII. Timer is the one boundary-aligned source (its driver is
itself a CLK9 DFF), which is the entire origin of the source-class split
in HALT wake latencies.
Race 1: CPU FF0F-write-clear vs same-M-cycle source rise
The FF0F write-clear pulse rides the CPU write strobe — measured co-located with CUPA (T3 to mid-T4, width 1.493 dots, matching to sub-ps). The mid-M-cycle PPU sources rise mid-T4 (+3.5 dots) — after the clear pulse closes (measured: the reset releases 1,155 ps before the VBlank clock arrives) — and the boundary-aligned timer source rose in T1, before the write window opened. Either way:
Rise wins. A source firing in the same M-cycle as a CPU clear of its bit leaves the bit set.
(The held-high case is the separate rule above: a clock that rose in an earlier M-cycle and stayed high cannot re-set after the clear.)
Race 2: dispatch-acknowledge clear vs same-M-cycle source rise
The acknowledge pulse is different: it asserts in T4 of the dispatch's
vector-resolution M-cycle and releases only at the next M-cycle
boundary — width ≈1.000 dot, straddling into the successor M-cycle's
opening. A source edge landing inside it — measured with a second STAT
dispatch deliberately aligned so the LY 143→144 SUKO-glitch edge fires
+0.529 dots into the ack pulse — is captured but suppressed: the DFF
sees the clock edge while r_n=0 holds Q at zero, and when the reset
releases the clock is already high, so no later edge exists.
Clear wins. A source edge inside the acknowledge window is absorbed; IF stays 0 (dmg-sim measurement).
| Clear path | Pulse position | Width | vs +3.5-dot source rise |
|---|---|---|---|
| FF0F write | +2.0–3.5 dots (T3–T4) | 1.493 dots | lands after — rise wins |
| Dispatch ack | +3.0–4.0 dots (T4 into next M-cycle) | 1.000 dot | lands inside — clear wins |
Emulators that clear the IF bit as a single step "at the end of dispatch" miss the absorbing behaviour; the faithful model holds the reset active for the full acknowledge window.
Across LCD-off
LOPE (and its siblings) have no LCD-off reset path — their resets are the FF0F-write and acknowledge chains plus system reset only. IF survives LCDC.7 toggles; POPU (which is LCD-off-reset) restarts cleanly and the first post-LCD-on VBlank entry sets IF[0] with the steady-state edge structure — no first-frame gating of any kind (dmg-sim measurement; LCD-on power-up).
APU Clock Tree and Frame Sequencer
Five chapters cover the APU at gate level: the clock tree and frame sequencer (this chapter), the square channels' period divider and duty, the wave channel, the sweep and envelope timers, and the length counters and power-cycle behaviour.
The APU shares the die with the PPU and CPU but is structurally independent:
per-channel prescalers off a dedicated 4 MHz tap, and a frame sequencer
hanging off the timer's reg_div16. This chapter anchors every APU clock
edge against the CPU's T-cycle grid, and pins the frame sequencer's silicon
form — a 3-bit ripple counter with its own reset domain, whose re-lock rule
across an NR52 power-cycle has a measured closed form.
- APU register writes commit on the same dot-2–3.5 window as PPU
register writes —
apu_wrand CUPA are sibling buffers. - The frame sequencer is a 3-bit ripple counter (CARU/BYLU/JYNA),
clocked by the 512 Hz divider tap but reset by
apu_reset— two different reset domains. (reg_div16 >> 11) & 7is the frame-sequencer step only when Δ = 0 — which no release guarantees; the re-lock offset Δ has a measured closed form.- The real boot ROM hands off with Δ = 3 — not the divider phase, and not the quickboot harness's Δ = 7.
The clock tree
atal_4mhz → AZOF → ATAG → apu_4mhz ──┬─→ APUV (CH1 prescaler clock)
├─→ AZEG (CH2 prescaler clock)
└─→ CYBO (CH3 prescaler clock)
cpu_wr → BAFU → apu_wr (APU register-write strobe)
Phase relations:
apuv/azeg/cybo— in phase withatal_4mhz(≈ +1 ns buffer delay);apu_4mhz— the opposite phase;apu_wr— in phase withcpu_wr.
apu_wr and the PPU's CUPA are sibling buffer outputs of the same source,
so APU register writes commit on the same T3–T4 window as PPU register
writes (register writes).
Measured T-cycle anchors (dmg-sim measurement; quickboot phase):
| Signal | Edge | Phase | Δ from atal_4mhz↑ |
|---|---|---|---|
| AJER / ATEP / CERY (prescaler stage 1, all channels) | toggle | start of every T-cycle | +1,900 to +3,200 ps |
apu_wr | rise | start of T3 | +7,500 ps |
apu_wr | fall | mid T4 | +4,700 ps from T4 atal↓ |
ch1_1mhz | ↑ / ↓ | T2 / T4 of every M-cycle | +5,100 / +4,700 ps |
ch3_2mhz | ↑ / ↓ | T1+T3 / T2+T4 | +2,400 ps |
The second prescaler stages are phase-locked at power-on, not
absolutely. CALO/CEMO (and CERY's phase) lock to whatever M-cycle the
apu_reset release (NR52 bit 7 ← 1) landed in, then free-run; the
alternative phase rotation is silicon-equivalent. Skip-boot emulators
should pick one phase deterministically and never re-phase it — the
prescalers ignore register writes and DAC toggles entirely.
Trigger-write timing in T-cycle terms (NR14; NR24 mirrors it on ch2_1mhz): the
write commits at T4-mid of M-cycle N; the NR14 sub-strobe propagates at T1
of M-cycle N+1; and the chN_restart synchroniser samples it at the first
prescaler edge after the strobe — under quickboot phase, T2 of M-cycle N+1.
For CH1/CH2 the capturing edge is the first chN_1mhz↑ after the write
strobe falls; CH3's NR34 instead samples on ch3_2mhz↓, the opposite edge
family (CH3).
The frame-sequencer strobes
The 512/256/128/64 Hz strobes hang off reg_div16
(timer) — but in two structurally different ways:
Family A — reg_div16-direct. horu_512hz is combinational from
reg_div16 bit 10 (through the BURE/FYNE/GALE/GEXY/HORU buffer chain) —
no DFF, no reset. It free-runs unbroken across everything except a DIV
write.
Family B — the apu_reset-reset ripple. The 256/128/64 Hz strobes tap
a 3-bit ripple counter clocked by the 512 Hz BURE and async-reset to 0 by
apu_reset:
BURE (512 Hz) ─clk→ CARU (/2 → 256 Hz) ─→ bufy_256hz (LENGTH clock)
└─→ BYLU (/2 → 128 Hz) ─→ byfe_128hz / cate_128hz (SWEEP clock)
└─→ JYNA (/2 → 64 Hz) ─→ kene (ENVELOPE clock)
(CARU, BYLU, JYNA) is the silicon frame-sequencer step counter.
Measured steady-state phases (dmg-sim measurement): all strobe edges land
at T2 with per-signal offsets of +6,400 to +9,700 ps, exact period ratios
1 : 2 : 4, and zero off-phase edges across hundreds of cycles. kene↓ —
the envelope advance — is the ripple's step-7→0 wrap.
The bare DIV write
A DIV write clears reg_div16 through reset_div_n (UCOB —
timer) but does not assert apu_reset. Unlike a
power-cycle it therefore does not reset the Family-B ripple; it can only
clock it.
CARU's clock BURE rises when reg_div16 bit 10 falls. A DIV write
landing while bit 10 is high forces it 1→0, and the resulting BURE↑
toggles CARU: the ripple steps once. While bit 10 is low the reset
makes no edge and the ripple holds. This is the same gate behaviour as a
DIV write that drops a TAC-selected bit clocking TIMA
(timer).
Measured (dmg-sim, two late_div_write FSTs one sub-step apart): a bare
write at reg_div16 = 0x3FF (bit 10 low) holds the ripple; at 0x400
(bit 10 high) the reset drops bit 10 and CARU steps once. The BURE↑ that
clocks it trails the reset edge by ≈ one T-cycle, so the advance is a
level test on bit 10 at the write M-cycle, not a sub-ns
BURE-vs-reset_div_n race.
The NR52 power-cycle re-lock rule
Because Family B resets at power-off and reg_div16 does not, an in-game
NR52 = 0 → 0x80 cycle desynchronises the two. The re-lock has a measured
closed form (dmg-sim measurement, eight power-cycle FSTs plus the quickboot
release, with the counter bits read directly):
Δ = (S_pon + a − 1) mod 8
S_pon = (reg_div16_pon >> 11) & 7 # coarse 512 Hz step at power-on
a = 1 if (reg_div16_pon & 0x7FF) ≥ 1023 else 0
counter C = ((reg_div16 >> 11) & 7) − Δ (mod 8)
| Strobe (consumer) | fires at | re-lock step |
|---|---|---|
horu_512hz↑ (Family A) | — | no shift — pure reg_div16 |
bufy_256hz↑ (length) | CARU rising (C 0→1) | (Δ + 1) mod 8 |
byfe_128hz↓ (sweep) | BYLU falling (C 3→4 / 7→0) | (Δ + 4) mod 8 |
kene↓ (envelope) | JYNA falling (C 7→0) | Δ |
The sub-step threshold is pinned to a single count — a power-on at sub-step ≤ 1022 catches the in-flight BURE edge (a = 0); at ≥ 1023 it misses by one 256 Hz half-period (a = 1) — bracketed on both sides by measurement. It is one counter with one offset: the three tick strobes are taps of the same ripple, so they share its phase exactly — any per-strobe non-uniformity is an artifact of comparing different counter transitions, not a real difference between the strobes.
(reg_div16 >> 11) & 7is the frame-sequencer step only when Δ = 0 — which no release guarantees. A model deriving the frame sequencer purely from the divider is wrong across any in-game power-cycle. The step counter must be modelled as this separately-reset ripple.- The boot path does not synchronise it either. The real DMG boot
ROM's handoff leaves the ripple at a directly-measured
(0, 1, 0)→ Δ = 3 (dmg-sim measurement with the real boot ROM) — not the divider phase, and not the quickboot harness's Δ = 7. This seed decides real-hardware outcomes for tests that never touch NR52 or DIV (sweep and envelope).
The single off-phase transient at a power-cycle (one strobe edge at the
wrong T-phase from the reg_div16 restart, then steady-state lock) is
measured and single-cycle.
Prescaler vs frame sequencer: the cross-grid phase
Both grids share the single apu_reset↓ anchor, and their sub-M-cycle
relationship is fixed: measured at a power-cycle re-lock (dmg-sim
measurement), the prescaler edge ch2_1mhz↑ and the envelope edge kene↓
land in the same T-cycle (T2), prescaler first by 6,291 ps — and
kene↓'s absolute phase (T2 + 9,597 ps) reproduces the steady-state strobe
anchor exactly. An independent raw-VCD check confirms ch2_1mhz is a
clean ~976 ns square wave (no sub-ns pulses).
Why this matters: the trigger synchroniser's load window opens at the prescaler edge (+~4 ns), so an envelope tick landing ~6 ns later falls inside the load window — the mechanism behind the envelope "+1 quirk" case in sweep and envelope. The cross-grid phase is not a free parameter; it composes directly with the strobe table above.
CH1/CH2: Pulse Channels
The square channels' tone generation is two counters: an 11-bit period
divider that overflows every 0x800 − period ticks, and a 3-bit duty
step counter advanced once per overflow. The behavioural surface is Pan
Docs territory; what the netlist supplies is the load network that makes
the trigger quirks fall out — including the exact silicon meaning of
"the low two bits of the frequency timer are not modified".
- The "frequency timer" is a 13-bit object: a free-running 2-bit prescaler plus the 11-bit divider. Triggers reload only the upper 11 bits.
- Trigger and natural overflow share one load net — there is no trigger-only load path, and the load is level-sensitive.
- A channel-enabling trigger's first overflow comes one
chN_1mhzcycle late; a retrigger of a running channel and every natural overflow run at the steady period (Rule below). - The duty counter clocks on
chN_frstfalling and resets only onapu_reset— fast retriggers starve it rather than resetting it.
The period divider
apu_4mhz → prescaler /2 → prescaler /2 → chN_1mhz ─(gated by chN_fdis)→ toggle clock
(CH1: AJER → CALO; CH2: ATEP → CEMO)
↓
11-bit divider: 11 × tffnl in three ripple sub-chains (4 + 4 + 3),
joined by single inverters; all stages share one load enable:
CH1: epyk = NOR(ch1_frst, ch1_restart) → fume/dega/dako
CH2: duju = NOR(ch2_frst, ch2_restart) → cogu/erog/gypa
| Gate (CH1 / CH2) | Role | Type | Clock / Trigger | Notes |
|---|---|---|---|---|
| AJER / ATEP | Prescaler /2 stage 1 | dffr | apu_4mhz (per-channel buffer) | Toggle; free-running 2 MHz; only reset is apu_reset — never reloaded by triggers |
| CALO / CEMO | Prescaler /2 stage 2 | dffr | stage-1 ripple | Toggle; free-running 1 MHz; output (buffered / directly) is chN_1mhz |
ch1_fdis / ch2_fdis | Channel-disable latch | nand_latch | Set: DAC-off / apu_reset; Reset: trigger (one cycle delayed) | While set, gates the divider toggle clock low |
| FULO→GEKU / CAMA→DOCA | Divider toggle clock | nor2 + not | chN_1mhz gated by chN_fdis | Bit 0 toggles on the active-high rise |
| GAXE HYFE JYTY KYNA / DONE DYNU EZOF CYVO | Divider bits 0–3 | tffnl | bit-to-bit ripple | Load enable fume / cogu |
| JEMA HYKE FEVA EKOV / FUXO GANO GOCA GANE | Divider bits 4–7 | tffnl | ripple via inverter (KYPE / sibling) | Load enable dega / erog |
| EMUS EVAK COPU / HEVY HEPU HERO | Divider bits 8–10 | tffnl | ripple via inverter (DERU / sibling) | MSB q = chN_ftick — the overflow edge |
| CALA + COMY / sibling pair | Overflow detector | not + dffr (/2 toggle) | clk: NOT(chN_ftick) | Produces the one-cycle self-clearing pulse chN_frst; also async-reset by triggers |
ch1_restart / ch2_restart | Trigger synchroniser | dffr | chN_1mhz | Captures the NRx4-bit-7 write; aligns it to the next chN_1mhz↑ |
| EPYK / DUJU | Divider load enable | nor2 | chN_frst, chN_restart | Active-low; fanned through fume/dega/dako / cogu/erog/gypa (fan-out split only — no functional difference between sub-chains) |
Each divider stage is a toggle-with-load cell: l=1 → q ← d
combinationally (level-sensitive); l=0 → toggles on its ripple clock.
AJER/CALO (ATEP/CEMO) have no input from the trigger or the overflow;
their only reset is apu_reset. This is the silicon form of "the low
two bits of the frequency timer are NOT modified on trigger": the
"frequency timer" is the 13-bit concatenation of prescaler + divider, and
triggers reload only the upper 11 bits. Under fast retriggers, the
free-running prescaler keeps carrying into the divider, the duty step
eventually advances — and a model that reloads all 13 bits per trigger
stalls forever.
The remaining structural facts:
- The divider counts up to 0x7FF; the MSB's fall marks overflow. The
one-stage detector (CALA + COMY) produces the one-
chN_1mhz-cycle pulsechN_frstthat opens the reload. - Trigger and overflow share the same load net.
chN_restartandchN_frstare the two NOR inputs — there is no trigger-only load path. - The reload is level-sensitive: during the window the divider tracks the period source combinationally; the captured value is whatever the source held when the load fell.
Period sources. CH2 (and CH3) load directly from the latched NRx3/NRx4
bits. CH1 loads from acc_d0..10 — the sweep shadow accumulator —
which has three write paths: the sweep adder's sum (clocked per sweep
fire), and async per-bit set/reset from NR13/NR14 writes. A trigger
immediately after an NR13/NR14 write therefore loads the just-written
period (the async path has settled long before the synchroniser fires).
A natural overflow reloads the divider to exactly {NR14[2:0], NR13} for
one ch1_1mhz cycle, then resumes counting (dmg-sim measurement,
purpose-built ch1_retrigger ROM).
The reload window, edge by edge
| Edge | What happens |
|---|---|
| Divider MSB falls (overflow) | chN_ftick↓ → CALA↑ → COMY toggles → chN_frst↑ |
chN_frst↑ | Load enable falls → all 11 stages enter level-sensitive load (q ← period source) |
Next chN_1mhz↑ | AND(COMY, chN_1mhz) asserts COMY's async reset → chN_frst↓ |
chN_frst↓ | Load enable rises → stages return to toggle mode holding the loaded value |
| NRx4 trigger write | Async-loads the period source (CH1: acc_d8..10; CH2: NR24 latches); the strobe arms the synchroniser |
Next chN_1mhz↑ after the write | chN_restart↑ — the same load path opens, held while restart is high |
chN_restart_dly↑ (one cycle later) | chN_fdis clears — the channel becomes audible |
Trigger-vs-overflow on the same edge
When the trigger's synchroniser edge coincides with a would-be overflow,
the race resolves at the overflow-capture DFF (COMY): its async reset
(through DYRU = NOR(apu_reset, ch1_restart, DOKA)) asserts ~2 ns
after the edge — before any overflow ripple or load-induced MSB fall can
reach its clock pin (measured: reset at +1,970 ps, earliest possible clock
attempt at +2,830 ps). Across all five sub-cases (divider at 0x7FE/0x7FF/
lower, period MSB set or clear): no chN_frst pulse, no duty advance.
The trigger reloads the divider and opens the channel one cycle later, but
the duty step is preserved (dmg-sim measurement, 41 trigger events).
The one distinct case: a trigger landing while an overflow window is
already active (COMY=1 from the prior edge) forces the same chN_frst
fall the natural self-clear would have produced — the counter advances
once, indistinguishably from the natural path.
A retrigger that merely follows a completed overflow is likewise
unremarkable: the divider counts the chN_1mhz ticks between the
overflow's reload and chN_restart↑ as it would any interval —
period-independent, with no suppression or extra hold (dmg-sim measurement).
Trigger-to-first-overflow: the load-settle cycle
The divider's toggle clock (GEKU / DOCA) is chN_1mhz gated by
chN_fdis, so while the channel is disabled the divider holds its value. A
channel-enabling trigger loads the divider (chN_restart↑, above) while
chN_fdis is still set; the loaded value is held one extra chN_1mhz tick —
the divider cannot count until chN_fdis clears. A trigger of an
already-running channel (chN_fdis already 0) reloads while the toggle clock
is still running and counts on the next tick — no extra hold.
Channel-enabling trigger → first overflow = (0x800 − period) + 1
chN_1mhz cycles. A retrigger of a running channel — like every natural
overflow — takes the steady 0x800 − period. Measured at 0x800 − period
= 63: an enabling trigger's first interval is +64.4 cycles, then exactly
+63; a retrigger's first interval is +63, and toggling the DAC off then on
restores the +1 on the following trigger (dmg-sim measurement).
Apply the two-tick hold (the loaded value held for two consecutive ticks
before the first toggle) only on the trigger that re-enables the channel
(chN_fdis 1→0). Applying it to every trigger makes a retrigger's first
interval one tick too long — the boot chime's second trigger retriggers the
running channel, and the spurious +1 leaves the post-boot CH1 divider at
0x7F8 instead of 0x7F9. Collapsing the two ticks onto one edge makes the
enabling trigger's interval one tick too short — the off-by-one behind
several hardware-verified duty/envelope race outcomes.
The duty step counter
A 3-bit ripple counter clocked by chN_frst falling — the end of each
natural-overflow window. Its only reset is apu_reset: triggers, DAC-off,
and channel-stop never touch it.
| Gate (CH1 / CH2) | Role | Type | Clock / Trigger | Notes |
|---|---|---|---|---|
| DAJO / CULE | Counter clock | not_x1 | NOT(chN_frst) | Rising edge = chN_frst falling |
| ESUT / CANO | Duty counter bit 0 | dffr | DAJO / CULE | Toggle; reset only by apu_reset |
| EROS / CAGY | Duty counter bit 1 | dffr_cc | bit-0 ripple | |
| DAPE / DYVE | Duty counter bit 2 (MSB) | dffr_cc | bit-1 ripple | |
| COSO CAVA CEVU CAXO / DOMO DYTA DOJU DOVE | Duty-pattern selects | nor2 ×4 | — | NRx1[7:6] decode, one per duty value |
| ENEK EZOZ CODO / EGOG DYMU DARE | Counter-state decodes | and2 / not | — | counter=6 / counter ∈ {6,7} / counter ∈ {0..5} |
ch1_pwm / ch2_pwm | Duty waveform output | ao2222 | — | 4:1 mux: OR of four (decode ∧ select) pairs |
| DUWO / DOME | PWM latch | dffr | chN_frst↑ | Captures the pre-advance step; reset to 0 by apu_reset |
| COWE / CYSE | PWM gated by channel-running | and2 | — | ANDed with the envelope to form the 4-bit DAC input |
The duty decode:
| NRx1[7:6] | Pattern (high at step k) | Decode |
|---|---|---|
| 00 | 00000010 (12.5%) | counter = 6 |
| 01 | 00000011 (25%) | counter ∈ {6,7} |
| 10 | 00001111 (50%) | counter bit 2 |
| 11 | 11111100 (75%) | counter ∈ {0..5} — the 25% decode negated |
The capture-on-rise / advance-on-fall split means each overflow plays the pre-advance step.
First-trigger gating, decoded. "Duty clocking is disabled until the
first trigger" is implemented one stage upstream: the chN_fdis disable
latch (set by DAC-off or apu_reset, cleared one cycle after a trigger)
gates the divider's toggle clock — no divider ticks, no overflows, no duty
clocks. "The first duty step plays as if it were 0" is the PWM latch:
reset to 0 by apu_reset and only re-captured at the first real overflow.
Fast retriggers starve the counter rather than resetting it: each trigger reloads the divider before it can overflow, removing the only advance path. The counter freezes at its pre-loop value — Pan Docs' "duty step never advances" with the mechanism made precise.
Post-boot state: the boot chime leaves CH1's duty counter at 2, with the DAC on (NR12 = 0xF3 retained), the running latch set, and the period divider mid-count at 0x7F9. Only NR52 clears the duty counter, so it persists into the handoff. CH2 stays at 0. See post-boot state. This post-boot step is the baseline the fast-retrigger staircase below counts up from.
The fast-retrigger phase staircase
A fast-retrigger loop freezes the counter at its running value, and which value
is a staircase in the trigger phase: while the divider free-runs, the counter
advances once per overflow, so the pinned step is just how many overflows landed
before the loop's first reload caught it — +1 step per overflow interval
(0x800 − period ticks).
Sweeping the pin trigger one M-cycle later across 80 steps (period 0x7F0, a 64-T
overflow interval) walks the pinned counter 0→1→2→3→4→5 — one step per 64 T
of added delay. The boundary is sharp to one chN_1mhz tick: adjacent triggers
4 T apart give a clean one-step change, at counter 0→1 and at the silent→audible
4→5 edge of the 50% pattern (dmg-sim measurement, purpose-built
apu_ch1_duty_phase_sweep ROM).
The emitted bit is the pre-advance step — duwo captures on chN_frst↑, the
counter advances on the following chN_frst↓ — so a counter pinned at k ≥ 1
plays step k − 1 (pinned at 0, having seen no overflow, the latch still holds
its apu_reset 0). The flip is therefore one step past the pattern edge: for the
50% pattern (high at steps 4–7) the bit is 0 for a counter pinned at 0–4 and high
at 5 (measured chN_out 0 → full-scale across 4→5).
A retrigger write lands on an M-cycle (4-T) boundary and chN_restart
re-synchronises it to the next chN_1mhz↑, so the trigger only moves in whole
chN_1mhz ticks — never a sub-tick. The pinned step changes once per overflow
interval; at a step boundary a single 4-T shift carries the pin across that
overflow's chN_frst fall and flips the captured count — one overflow, not a
sub-chN_1mhz parity effect. Which side of the edge a boundary trigger lands on
is the overflow-capture race of Trigger-vs-overflow on the same edge above.
The inter-trigger audible window
With trigger spacing longer than the overflow interval, the full
chain — overflow → PWM capture → mixer — runs between triggers. Measured
across a 7-overflow inter-trigger window (dmg-sim measurement, gambatte
duty3_pattern_pos7 ROM): the duty counter walks 0→7, the PWM latch holds
1 through six overflows of the 75% pattern and captures 0 at the seventh
(counter 6 decodes low), producing a sustained ~375 µs audible pulse and
then silence — every digital gate operating exactly as the cell map says,
with no hidden muting.
A sweep period change rescales the duty capture cadence
The staircase and audible window above hold the period fixed. CH1's period is
the sweep accumulator acc_d, and a sweep fire that commits a new acc_d
changes the rate at which the emitted duty level updates: DUWO re-samples the
duty waveform only at each overflow's chN_frst↑ (The duty step counter
above), so updates are one overflow interval — 0x800 − period ticks — apart,
and a period change rescales that spacing from the next overflow. The counter
goes on climbing; the emitted bit holds its last capture until the new period's
first overflow re-samples it. A re-trigger in the gap does not close it:
chN_restart async-clears COMY through DYRU and so produces no chN_frst pulse
of its own (Trigger-vs-overflow on the same edge above), only reloading the
divider to the new period and leaving the stale capture in place.
At the maximum period 0x7FF the divider overflows every chN_1mhz cycle, so
DUWO follows the climbing duty step cycle by cycle. One decrease-sweep fire
(NR10 = 0x1F: pace 1, decrease, shift 7) cuts the period to 0x7F0; the
every-cycle train stops, and the divider — reloaded to 0x7F0 — now counts a
full 16-tick interval to its next overflow. A re-trigger placed just after the
fire finds the duty counter already at step 4 (high in the 50% pattern) but DUWO
still holding the low capture from the previous overflow; the next chN_frst↑
that would re-sample it lands ≈16 ticks later — past the SameSuite
channel_1_sweep_restart round-1 probe window, which reads silent throughout.
Holding the period at 0x7FF instead (no sweep, otherwise identical) keeps the
train running: an overflow one cycle before the re-trigger captures the
now-high step 4, and the channel is audible. The duty counter advances
identically either way — only the capture cadence differs (dmg-sim measurement,
purpose-built 0x7FF→0x7F0 and constant-0x7FF ROMs).
A sweep fire — or any acc_d write — takes effect at the next overflow's reload
and rescales the overflow interval from there. Because DUWO re-samples only at
an overflow, the channel keeps playing the pre-change level for one full
overflow interval of the new period (0x800 − new_period ticks) afterwards,
wherever the counter has reached. A re-trigger inside that interval does not
shorten it: it produces no chN_frst pulse and reloads the divider to the new
period, so the first level update still waits for that period's first overflow.
The commit, the overflow that first reloads the new period, and the re-trigger
never share one chN_1mhz edge — they fall in separate cycles and never contend
on the divider-load net (EPYK). The commit acts on the duty path purely through
the period it sets, and hence the capture cadence.
CH3: Wave Channel
CH3 reuses the square channels' divider topology with two changes — a
single /2 prescaler (so the divider runs at 2 MHz, halving the overflow
interval: Pan Docs' (2048 − period) × 2 T-cycles) and a 16-byte wave RAM
stepped by a 5-bit position counter in place of the duty machinery. The
distinctive CH3 behaviours — the trigger delay, the locked CPU access, and
the retrigger corruption — all live in the small synchroniser and SRAM
circuits this chapter maps.
- Same 4+4+3 divider topology as CH1/CH2, but clocked at 2 MHz by a
single-prescaler chain — and the trigger synchroniser samples on
ch3_2mhz↓, the opposite edge family from CH1/CH2. - Triggers reset the wave position to 0 (unlike the duty counter), and a wrap interlock dwells the counter at 0 for one extra overflow.
- CPU wave-RAM access while CH3 runs goes wherever CH3 points; writes land unconditionally at CH3's current byte.
- DAC-off freezes, never clears: the wave data latch keeps its last strobed byte through NR30 off/on — only NR52 power-off resets it. A retrigger replays the stale byte until its first strobe.
- Retrigger corruption is a 4-byte row copy on DMG too — the single-byte-on-DMG rule is inaccurate against the gate-level model.
Period divider
Identical 4+4+3 tffnl topology to CH1/CH2
(CH1/CH2), at 2 MHz and with no sweep:
| Gate | Role | Type | Clock / Trigger | Notes |
|---|---|---|---|---|
| CERY | CH3 prescaler /2 stage | dffr | apu_4mhz (via CYBO buffer) | Toggle; free-running; q = ch3_2mhz directly; only reset is apu_reset — never reloaded by triggers |
| GUGU | Channel-disable latch (q_n = ch3_fdis) | nand_latch | Enable: FAJU = NOT(GYRA), the trigger pulse retimed one ch3_2mhz edge behind GYTA; Disable: FUVO = NOR(ch3_amp_en_n, apu_reset) — a held level | While ch3_fdis is high, gates the divider toggle clock low |
| HEFO → JUTY | Divider toggle clock | nor2 + not | ch3_2mhz gated by ch3_fdis | One toggle per T-cycle |
| KUTU KUPE KUNU KEMU | Divider bits 0–3 | tffnl | bit-to-bit ripple | Load enable kyko |
| KYGU KEPA KAFO KENO | Divider bits 4–7 | tffnl | ripple via KYRU inverter | Load enable kaso |
| KEJU KEZA JAPU | Divider bits 8–10 | tffnl | ripple via KESE inverter | MSB q = ch3_ftick — the overflow edge |
| HYFO + HUNO | Overflow detector | not + dffr (/2 toggle) | clk: NOT(ch3_ftick) | One-ch3_2mhz-cycle self-clearing pulse ch3_frst; also async-reset by triggers |
ch3_restart | Trigger synchroniser q | dffr | FABO (= NOT(ch3_2mhz)) | Captures the NR34-bit-7 write on ch3_2mhz↓ — opposite-phase sample edge to CH1/CH2 |
| HERA | CH3 divider load enable (active-low) | nor2 | ch3_frst, ch3_restart | Fanned through jera/kaso/kyko (fan-out split only) |
The period source is the latched NR33/NR34 bits (drlatch_ee cells
written by the apu_wr ∧ ff1d/ff1e strobes) — there is no sweep adder in
the path, and the reload is level-sensitive exactly as on CH1/CH2.
Because GUGU's disable side (FUVO) is a held level, restoring the DAC
(NR30 bit 7 back high) does not restart the divider — only a trigger
clears ch3_fdis, one ch3_2mhz edge after the ch3_restart pulse
(measured) — and while FUVO holds the disable level it pins GUGU's q_n,
so a trigger arriving during a DAC-off window leaves ch3_fdis set
throughout.
Wave-position counter
A 5-bit ripple counter clocked by ch3_frst falling — one step per
overflow. Bit 0 selects the nibble; bits 1–4 drive the wave-RAM byte
address.
| Gate | Role | Type | Clock / Trigger | Notes |
|---|---|---|---|---|
| DERO | CH3 wave-position counter clock | not_x1 | NOT(ch3_frst) | Rising edge = ch3_frst falling |
| EFAR | CH3 wave-position bit 0 (wave_nibble_sel) | dffr | DERO | Toggle; reset by ETAN |
| ERUS / EFUZ / EXEL / EFAL | Bits 1–4 (wave-RAM byte address) | dffr | bit-to-bit ripple | Reset by ETAN; EFAL's q_n clocks FETY |
| ETAN | Wave-position counter reset (active-low) | nor2 | ch3_restart, FETY | Clears the counter on trigger or wrap |
| FETY | CH3 counter-wrap interlock (/2 toggle) | dffr | EFAL↓ (counter wrap 31→0) | Holds the counter at 0 for one extra overflow; cleared by the next ch3_frst↑ |
- Triggers reset it to 0 —
ch3_restartis an input to ETAN. Unlike the duty counter, wave position does not survive a retrigger. (This asymmetry is the netlist's answer to Pan Docs' CH3-vs-pulse retrigger contrast.) - The wrap interlock (FETY) holds the counter at 0 for one extra overflow after wrapping 31→0 before the next count resumes.
The trigger synchroniser and its delay
An NR34 trigger traverses four stages — with a fifth cell shaping the pulse:
| Stage | Cell | Type | Clock / Enable | Role |
|---|---|---|---|---|
| 1 | GAVU | drlatch_ee | enable: apu_wr ∧ ff1e | Write-strobe latch — captures the trigger bit at T3/T4 of the write M-cycle |
| 2 | FOBA | dffr | apu_phi | M-cycle-boundary DFF |
| 3 | GOFY | nor_latch | — | Tag latch |
| 4 | GARA | dffr | ch3_2mhz↓ | q is ch3_restart |
| 5 | GYTA | dffr | one edge behind GARA | Async-resets GARA — shapes the pulse to exactly one ch3_2mhz cycle |
Measured end to end (dmg-sim measurement): write commits at T4-mid;
boundary DFF at T1 of the next M-cycle; ch3_restart↑ on the next
ch3_2mhz↓ (T2 under quickboot phase); cleared two T-cycles later. The
aggregate from write strobe to first overflow at period 0x7FF is 9–10
T-cycles — the synchroniser accounts for 6–8 of them (phase-dependent),
which is the silicon content of the community's "+6 T-cycle trigger
delay". The T2-vs-T4 placement is apu_reset-locked, like every other APU
phase (APU clocks).
Wave RAM: address mux, read strobe, CPU access
The wave RAM's 4-bit address is muxed between the CPU bus and the position
counter's bits 1–4, steered by ch3_active (a DFF re-timed copy of the
trigger-armed FOZU latch — cleared by FYGO = OR3(DAC-off, GEDO length-stop,
power-off)).
CH3's own byte read is strobed by wave_data_latch — ch3_frst pushed
through a three-DFF apu_4mhz synchroniser (BUSA → BANO → AZUS). Measured:
the strobe rises ~1.5 T-cycles after ch3_frst↑ and stays high ~1 T-cycle;
the captured byte holds on eight drlatch_ee cells — CYFO CESY BUDY BEGU
(bits 0–3), CUVO CEVO BORA BEPA (bits 4–7) — until the next strobe.
Playback from the cells is combinational: an inverting nibble mux steered
by wave_nibble_sel, then the NR32 volume shift, then the ch3_active
output gates (BARY BYKA BOPA BELY) driving the channel's 4-bit DAC input.
The data latch's only reset is ACOR = NOT(apu_reset) — an NR52
power-off; BAMA, the strobe chain's reset, is the same signal. Clearing
NR30 bit 7 raises ch3_amp_en_n, whose complete fan-out is FUVO and FYGO
(the divider-freeze and FOZU clears), the global DAC-off term (TACE), and
the DAC power gate — it never reaches the data latch or its strobe chain. Measured across a
stop → NR30-on → retrigger sequence at period $700: the pre-stop byte
survives on the cells throughout (the only zeroing at the DAC input is the
ch3_active gate), and once the retrigger re-arms ch3_active the DAC
input replays the stale nibble for the full first sample period — one
512-T overflow interval plus the strobe synchroniser, measured 257
ch3_2mhz cycles ≈ 514 T-cycles — until the first wave_data_latch
strobe reloads byte 0.
With ch3_active=1, a CPU read or write of FF30–FF3F ignores the CPU's
address entirely — the mux is on the counter side. Reads return CH3's
current byte when the CPU's strobe overlaps the wave_data_latch window
(and an open bus value otherwise); writes land unconditionally at
CH3's current byte (the SRAM accepts them whenever the CPU write strobe is
high — no gating by wave_data_latch).
The write path is pinned by a hardware-verified nine-subtest sweep
(samesuite channel_3_wave_ram_locked_write, dmg-sim measurement): each
NOP of delay slides the landing byte by exactly one position, all nine
landing addresses matching the test's expected pattern. The same sweep
pins a gating subtlety: between subtests the DAC is off, ch3_fdis
freezes the divider, and the read-strobe chain is idle — so the
retrigger corruption (below) does not fire. Emulators that let the divider
free-run through DAC-off windows fire it spuriously.
Retrigger corruption: a 4-byte row copy
Triggering CH3 while it is reading corrupts wave RAM: the netlist's SRAM produces a 4-byte row copy on DMG — not the single byte the widely-quoted rule describes.
When
ch3_restart↑ lands in the window where the SRAM bit-line precharge is off —(AZUS | AZET) = 1, spanning fromwave_data_latch↑ to one T-cycle past its fall (≈ 2 T-cycles per overflow) — then withrow = wave_position >> 3:ram[0..3] ← ram[row*4 .. row*4+3], other bytes unchanged. Row 0 is naturally a no-op (source = destination — no special case).
The mechanism, in the SRAM cells: word lines are sticky until the word-line driver precharges, so the trigger's address snap to 0 leaves the old row's word line high while row 0's rises; the bit lines still hold the old row's data (precharge off); and the write-back loop commits that data to every enabled row — the whole source row lands in row 0. Verified across 12 retrigger events spanning rows 1–3 and both nibble phases (dmg-sim measurements with the full SRAM model).
Evaluate (AZUS | AZET) at the ch3_restart capture edge; if set, do the
row copy. And gate the divider on ch3_fdis, or the window will be open
when hardware has it closed.
Post-boot state
The boot ROM never touches CH3: divider, position counter, overflow chain,
and ch3_restart all at their reset defaults; ch3_fdis=1; DAC off.
Wave RAM itself has no reset path — it survives NR52 power cycles, and
its cold-boot content is undefined on silicon. See
post-boot state.
CH4: Noise Channel
The noise channel's tone generator is a 15-bit LFSR clocked by a two-stage frequency timer: a 3-bit divisor prescaler (reloaded on every trigger) feeding a 14-bit shift divider (never reset by a trigger). What the netlist supplies is the load and gating network behind the trigger quirks — the silicon reason a re-trigger's first sample lands at a different phase than a cold trigger's, and the reason a channel-enabling trigger on a non-zero divisor code is one cycle late.
- The "frequency timer" is two reset domains: a 3-bit divisor prescaler
that a trigger reloads, and a 14-bit shift divider that a trigger never
resets (only
apu_reset). This is the opposite split from the pulse channels, where the prescaler free-runs and the divider reloads. - Because the shift divider is never reset, a re-trigger (or any non- power-on re-enable) starts its first LFSR clock from the divider's retained phase, not from zero — a phase-dependent offset of up to one sample, at every shift (half-sample steps at shift 0, finer above).
- The trigger is also synchronised to
hama_512khz(the 512 kHz APU clock): a NR44 write takes effect on the next rise, so the write→first-clock latency carries that 8 T clock's phase too — a second, divider-independent source of cold-vs-re-trigger variation. - A channel-enabling trigger on divisor code ≥ 1 is one
hama_512khzperiod late: the prescaler count is frozen whilech4_fdisis set. Divisor code 0 pre-loads the prescaler to its terminal count, so the freeze is a no-op (measured below).
The frequency timer
ch4_1mhz ─→ jeso_512k ÷2 ─→ kanu ─(gated by ch4_fdis)→ divisor prescaler ─→ hyno ─→ gary
(4 T) (= hama_512khz, 8 T) jyco→jyre→jyfu (enable)
(loads NR43[2:0]) │ gates
▼
ch4_1mhz ───────────────→ noise_counter_clk = ch4_1mhz & gary ──→ 14-bit shift divider
cexo…esep ─tap(shift)→
ch4_lfsr_clk → LFSR shift
| Gate | Role | Type | Clock / Trigger | Notes |
|---|---|---|---|---|
ch4_1mhz (BAVU) | Noise base clock | not | apu 1 MHz | ~1.048 MHz (4 T); divider and prescaler source |
| JYCO JYRE JYFU | Divisor prescaler bits 0–2 | tffnl | toggle clk kanu (bit 0), ripple | Loaded from NR43[2:0] (ff22_d0..2_n) via l(huce); reloaded on trigger |
| HYNO | Prescaler terminal-count | and3 | — | AND(jyfu,jyre,jyco) — all-ones = terminal |
| GARY | Divider-clock enable latch | dffr | gyba (= NOT ch4_1mhz) | d = hyno; reset guny = NOR(apu_reset, ch4_restart) — trigger clears it; gates the shift divider and reloads the prescaler |
| HUCE | Prescaler load enable | not | — | huce = NOT gofu = ch4_restart OR gary — reloads JYCO/JYRE/JYFU on a trigger and on every terminal count |
| CARY | Shift-divider clock | and2 | — | noise_counter_clk = ch4_1mhz AND gary |
| CEXO … ESEP | 14-bit shift divider | dffr (ripple ÷2) | noise_counter_clk (bit 0), ripple | Reset only apu_reset4_n — never reloaded or reset by a trigger |
| ETYR / ERYF (+ DARY, ELYX) | Divider tap muxes | ao / or | — | NR43 shift code (ff22_d4..d7) one-hot-selects one divider bit |
| FEME | LFSR shift-clock select | mux | sel = ff22_d7 | ch4_lfsr_clk1 = ff22_d7 ? etyr : eryf (high vs low tap bank) |
ch4_fdis (JERY) | Channel-disable latch | nand_latch | Set: DAC-off / apu_reset; Reset: trigger (delayed) | While set, holds the prescaler toggle clock (kanu) — see load-settle |
| JESO | Free-running ÷2 bit | dffr | ch4_1mhz | Reset apu_reset5_n — never trigger-reset; its buffered output is hama_512khz (the 512 kHz timebase) |
| KYKU→KANU | Prescaler toggle clock | or + not | kanu = ch4_fdis OR jeso_512k | kanu is the tclk_n of JYCO; ch4_fdis = 1 freezes the prescaler |
| GONE | ch4_restart synchroniser | dffr | hama_512khz | Captures the NR44-bit-7 write, aligned to the next 512 kHz rise |
| GYSU | ch4_start | dffr | apu_phi | First synchroniser stage; drives the HAZO latch (→ hazo_n) feeding GONE |
Each shift-divider stage is a plain ÷2 toggle (.d = q_n, clocked by the
previous stage's q_n); the divisor prescaler stages are toggle-with-load
cells (l = 1 → q ← NR43 bit; l = 0 → toggle on tclk_n). hama_512khz
is the buffered free-running jeso_512k bit (a ÷2 of ch4_1mhz, ≈ 512 kHz);
the same bit, OR'd with ch4_fdis, is also the prescaler's toggle clock
kanu.
A trigger reloads JYCO/JYRE/JYFU (via huce) and clears gary; it leaves the
14-bit shift divider CEXO…ESEP untouched (its only reset is apu_reset).
This is the silicon form of "the trigger does not reset the noise frequency
divider": the divider keeps its phase across a trigger, so a re-trigger's first
LFSR clock is referenced to wherever the divider already was, not to zero. A
model that reloads the whole frequency timer on every trigger gets the first
cold trigger right (divider sitting at 0) but a re-trigger wrong.
For divisor code 0 the prescaler loads to 111 = terminal immediately, so
gary stays high and the shift divider runs at the full ch4_1mhz rate
(4 T). The shift code taps divider bit shift, so the LFSR clocks every
8 << shift T — bit 0 (CEXO, 8 T) at shift 0, bit 2 (EZEF, 32 T) at shift 2.
Period = divisor << shift T, with divisor 8 for code 0.
The LFSR
A 15-bit shift register (DFFs HENO/HEPA/HEZU/HORY/HYRO, HAPE/JAJU/JAVO/JEPE/
JUXE, KETU/KOMU/KUTA/KUZY/KYWY), all reset by goge = NOT(ch4_restart OR apu_reset) — a trigger returns the LFSR to its reset state (post-boot
gives the value as 0x7FFF). The chain shifts on the ch4_lfsr_clk1 edge,
fanned out as ch4_lfsr_clk2/3 across the 15 cells. The feedback DFF
(JOTO) latches XNOR(HYRO, HEZU) — the two XNOR taps — on NOT ch4_lfsr_clk1,
and HEZU is the output bit: lfsr_out = AND(ch4_active, HEZU), gated into the
envelope at dato. In 7-bit (short) mode (ff22_d3) the AO22 cell KAVU also
injects the feedback into the mid-chain stage JEPE, shortening the active loop
to 7 bits. (The shift/feedback algebra is behavioural Pan Docs territory; it
is not the subject of this chapter.)
Trigger synchronisation and the first-clock latency
A NR44 write with bit 7 set propagates hoga → ch4_start (captured on
apu_phi) → the HAZO latch → ch4_restart (captured on hama_512khz,
8 T). ch4_restart is held high for one full hama_512khz period (8 T);
during it gary is forced low (guny), so the shift divider is frozen, then
resumes when ch4_restart falls.
From ch4_restart↑ to the first LFSR shift, with the divider starting from
count 0 (a cold trigger after power-on):
| period (= 8 << shift) | first LFSR clock after ch4_restart↑ |
|---|---|
| 8 (shift 0) | 12 T |
| 32 (shift 2) | 24 T |
i.e. 8 T (the ch4_restart-high / divider-freeze window) + period/2
(the zeroed divider counting up to the tap bit's first rise, at count
2^shift). Relative to the NR44 write, add the two-stage synchroniser
delay (hoga→ch4_start on apu_phi, then →ch4_restart on hama_512khz),
which depends on the write's phase: measured 5–9 T, giving write→first-clock
17–21 T at period 8 — consistent with the hardware-test-ROM channel_4_delay
figure of period + 3 M-cycles (±1 M), the synchroniser phase being that ±1 M.
Re-trigger: the free-running divider phase
Because the shift divider is never reset, a re-trigger — or any cold re-enable that is not the first trigger after power-on — resumes from the divider's retained count, and the first LFSR clock lands wherever the tapped bit next rises.
First LFSR clock from ch4_restart↑, by the divider's frozen count. At
period 32 (shift 2, the tap spans eight counts):
| frozen count | first LFSR clock |
|---|---|
| 0 (= power-on cold) | 24 T |
| 2 | 16 T |
| 4 (tap bit just rose) | 40 T |
— a spread of nearly a full sample period (16–40 T). The same dependence shows
at shift 0 in half-sample steps: only CEXO is tapped, frozen at 0 or 1 →
12 T or 16 T (in channel_4_lfsr_restart, the power-on cold trigger starts
at count 0 → 12 T; later re-enables resume at CEXO = 1 → 16 T). A trigger that
freezes the divider at count 0 is indistinguishable from a power-on cold
trigger — nothing about a trigger resets the divider.
A second, divider-independent source of variation is the synchroniser itself:
ch4_restart is captured on hama_512khz, so the write→first-clock latency
also carries the write's phase against that 8 T clock. Within one
channel_4_lfsr_restart [cold, re-trigger] pair the cold and re-trigger freeze
the divider at the same count — the same restart→first-clock (12 T in the first
pair, 16 T after) — while their two writes sit a half-hama_512khz offset
apart; the pair's first clocks then differ by exactly the 4 T (half a sample)
synchroniser phase (sync 5 T vs 9 T, consistent across pairs).
A re-trigger therefore differs from a power-on cold trigger through both the
retained divider phase and the synchroniser phase. Sampled at one-period
spacing, that is the one-sample restart delay the hardware tables record
(restart[i] = cold[i−1]); a model that resets the divider on every trigger and
has no synchroniser reproduces neither, and clocks the restart one sample early.
The fdis load-settle (divisor code ≥ 1)
For divisor code ≥ 1 the prescaler loads below terminal and must count up; its
toggle clock kanu = ch4_fdis OR jeso_512k is held while ch4_fdis is set,
so a channel-enabling trigger cannot advance the prescaler until ch4_fdis
clears (one hama_512khz period after the trigger). Measured at divisor code 1
(period 16), same divider phase: a cold trigger's first LFSR clock is 8 T
(one hama_512khz period) later than a re-trigger's (24 T vs 16 T from
ch4_restart↑). For divisor code 0 the prescaler pre-loads to terminal, so
ch4_fdis never gates a count: this load-settle adds nothing at code 0 (the
period-8 cold-vs-re-trigger difference is the divider-phase and synchroniser
effects above, not this path).
This is the CH4 analogue of the pulse channels' chN_fdis load-settle, but it
gates the divisor prescaler (not the main divider) and the extra hold is
one hama_512khz period.
Three independent gate-level effects shape the noise first-clock timing; a faithful model needs all three:
- Do not reset the shift divider on a trigger — only reload the divisor
prescaler and clear
gary. The divider's retained phase sets the re-trigger first-clock at every shift; resetting it makes every re-trigger (and every non-power-on re-enable) look like a count-0 cold. - Synchronise the trigger to
hama_512khz— the NR44 write takes effect on the next 512 kHz edge, so the write→first-clock latency carries that clock's phase: a second, divider-independent contribution to the cold-vs-re-trigger difference. - Gate the prescaler count on
ch4_fdisfor divisor codes ≥ 1 — a channel-enabling trigger's first clock is onehama_512khzperiod late; a re-trigger of a running channel is not. (No effect at divisor code 0.)
Mid-run divisor-code changes (non-trigger)
huce = ch4_restart OR gary carries no NR43-write term, so writing NR43
while the channel plays does not itself reload the prescaler. The new divisor
code waits at the load inputs (ff22_d0..2_n) and is loaded only when huce is
next asserted — the reload that already fires on every terminal count. The
14-bit shift divider is untouched (its only reset is apu_reset), so it keeps
its phase across the change. A mid-run divisor-code change therefore takes effect
at a prescaler reload, not at the write instant.
Divisor code 1 → 2 ($11→$12, shift fixed), with the write phase swept in
4 T steps. At code 1 the prescaler period is 8 T — gary/huce high for ~4 T
after each terminal (the load window), then low while counting:
| write vs the load window | prescaler at the write | new code in effect |
|---|---|---|
| inside the window | reloads to the new code at once | ≈ at the write |
| outside (counting) | finishes its period at the old code | at the next terminal (≈ +3 T) |
The capture point tracks the prescaler terminal, not the write instant: across the sweep the new cadence stays locked to the prescaler grid — its phase set by the terminal that catches the change, never re-phased to the write.
Two corollaries follow. First, a change made while the channel is on divisor
code 0 takes effect at once: code 0 pre-loads the prescaler to terminal,
holding gary/huce high continuously, so the write is always inside the load
window (dmg-sim: $18→$1a, code 0 → 2, switches the shift divider's clock from
4 T to 16 T at the write). Second, the ch4_fdis load-settle is trigger-only:
a non-trigger write, even one into a divisor code ≥ 1, never re-asserts
ch4_fdis (its sole edge across each change is the trigger), so the cold
"+1 hama_512khz" does not apply mid-run. The latch point is set entirely by the
load-window phase and the code-0 case; the huce-driven load has no
divisor-direction term.
Sweep and Envelope
The sweep unit (CH1 only) and the volume envelopes (CH1/CH2) share one
silicon idiom: a small ripple up-counter loaded with ~pace through a
level-sensitive load net, a fire latch that catches the counter's
saturation on a frame-sequencer strobe, and a pace-0 detector that holds
the fire latch in reset. Every documented quirk — "+1 on trigger near a
tick", pace-0 pausing, zombie volume, the saturation stop — is a
consequence of those gates.
- Both timers are
~pace-loaded up-counters that fire on saturation — sweep at 128 Hz, envelope at 64 Hz. - Pace = 0 pauses, not "period 8": the pace-0 decode holds the fire latch in reset (the envelope's is doubly paused).
- The "+1 quirk" is the load window: a frame-sequencer tick landing inside the ~1 µs reload window is lost — first fire one period late.
- The volume step is a two-edge event (fire rise arms, fire fall commits) — every mid-channel NRx2 write subtlety follows from it.
- Saturation is a stop latch, not a clamp — an unstopped 4-bit counter would simply wrap.
The CH1 sweep timer
A 3-bit counter clocked by the frame sequencer's 128 Hz tap and loaded with NOT(NR10[6:4]) by either reload path through one shared net:
| Gate | Role | Type | Clock / Trigger | Notes |
|---|---|---|---|---|
cate_128hz | Sweep toggle clock | not chain | = byfe_128hz, the frame-sequencer 128 Hz tap (APU clocks) | Held low while apu_reset=1 |
| CUPO / CYPU / CAXY | Sweep counter bits 0–2 | tffnl | cate_128hz, then bit-to-bit ripple | Load: q ← NOT(NR10[4..6]) — no reset path at all |
| COZE | Sweep counter-at-max detector | and3 | CUPO, CYPU, CAXY | High at counter = 7; drives BEXA's data |
| BAVE | Sweep pace = 0 detector | and3 | NR10[6:4] complements | Holds BEXA in async reset via BURY |
| BEXA | Sweep-fire latch | dffr | AJER (the 2 MHz CH1 prescaler) | Captures COZE; while high, reloads the counter; falls on the next AJER↑. Held at 0 when pace = 0 |
| DAFA → CYMU | Load enable | nor2 + not | BEXA, ch1_restart | CYMU = OR(BEXA, ch1_restart) drives all three l pins |
The two reload paths:
- Trigger reload — the same
ch1_restartsynchroniser as the period divider; no second synchroniser, no pending-reload latch. - Self-reload on fire — COZE (counter = 7) captured into BEXA on the next AJER edge; BEXA reloads the counter and drops one cycle later.
Fire interval = pace × 1/128 s (Pan Docs' rule, with the table exact for
every pace). Pace = 0 pauses via BAVE holding BEXA in async reset —
the counter still ripples and saturates invisibly.
cate_128hz free-runs; a trigger landing so that the 128 Hz tick falls
inside the ~1 µs load window loses that tick (the l pin overrides the
toggle path) — effective first-fire time (pace + 1) × 1/128 s.
Just-after, just-before, and inside-the-window are the three landing cases;
only the third loses a tick.
On each fire, three independent things happen:
- The counter reloads (above).
- The sweep-adder commit strobes (
ch1_freq_upd1/2) fire — gated by AND3(BEXA, no-overflow, shift ≠ 0). Shift = 0 means the computed period is not written back (Pan Docs' rule, as a literal AND term). - The overflow check runs regardless of shift, but it does not
resolve at the fire — it samples a registered snapshot of the shadow
(below),
shiftM-cycles late. When it resolves, with direction = add and adder carry-out,ch1_sum_ovfl_nfalls, BONE rises, and the channel-stop OR4 clears the running latch — channel disabled, NR52 bit 0 reads 0. Shift = 0 skips the commit (item 2) and resolves in one operand-load cycle: the single calc — 2 × shadow — disables ≈ 1 M-cycle after the fire, so any shadow ≥ 0x400 cuts the channel.
The snapshot is loaded shift M-cycles late because a second counter must
count out the shift first:
| Gate | Role | Type | Clock / Trigger | Notes |
|---|---|---|---|---|
| BYRA / CAJA / COPA | Sweep shift-step counter bits 2–0 | tffnl | ch1_shift_clk, then bit-to-bit ripple | Load ~shift; saturate shift steps after a fresh shadow |
ch1_ld_sum | Sweep-adder operand-load strobe | not_x4 | Pulses on BYRA·CAJA·COPA saturation | Clocks the adder's operand registers — the adder never reads the live shadow |
Because the check lands shift M-cycles after the shadow is presented, a
fire whose first calc passes does not cut the channel: item 2 commits
the result to the shadow, and the next ch1_ld_sum strobe rechecks the
committed value — the double-check. A sweep that overflows only on the
recheck stays audible for shift M-cycles past the fire. A trigger's own
shift ≠ 0 check resolves the same way, shift M-cycles after the write.
From the fire to the channel-disable:
| shift | fire → disable (M-cycles) | event |
|---|---|---|
| 0 | 1.0 | single calc (no double-check) |
| 1 | 1.5 | recheck |
| 7 | 7.5 | recheck |
For the double-check (shift ≥ 1) the recheck lands shift M-cycles +
0.5 — BEXA fires on the 2 MHz AJER grid, half an M-cycle off the step
counter's grid. Shift 0 has no double-check; its single calc disables ≈ 1
M-cycle after the fire (one operand-load cycle). A shift ≠ 0 trigger runs
its check shift M-cycles after the write, but a CPU write's sub-cycle phase
is free: shift 7 lands at trigger + 8.75 (a re-trigger one step earlier in
the grid, + 7.75).
The trigger-reload cascade (restart → CYMU → all three cells loaded in
~1.7 ns; window ≈ 975 ns = one ch1_1mhz cycle), the free-running ripple
(+1 per 128 Hz tick, ripple-binary), a pre-trigger fire on the NR10 write
itself (releasing BEXA's reset with COZE already saturated fires
immediately — harmless when the channel is stopped), the first natural
fire at trigger→first-tick + (pace−1) periods, and steady-state fires at
exactly pace × 7.995 ms (sim time) thereafter (dmg-sim measurement). A hardware-verified NR52
probe between trigger and first fire reads the channel still enabled.
Post-boot: the sweep counter cells have no reset at all — their power-on state is undefined on silicon, and the boot ROM never writes NR10 (pace 0 throughout), so BEXA stays reset and the counter state at handoff is power-on residue. Tests needing a known state must write NR10 and trigger.
The volume envelope (CH2 cells; CH1 mirrors cell-for-cell)
Three pieces: the envelope counter, the fire latch, and the volume up/down counter.
| Gate | Role | Type | Clock / Trigger | Notes |
|---|---|---|---|---|
kene | 64 Hz envelope toggle clock | not chain from JYNA | JYNA = /2 of byfe_128hz | The frame-sequencer ripple's last stage (APU clocks) |
| JORE / JONA / JEVY | Envelope counter bits 0–2 | tffnl | kene, then bit-to-bit ripple | Load: q ← NOT(NRx2[0..2]) via JAKE |
| KYVO | Envelope counter-at-max detector | and3 | JORE, JONA, JEVY | High at counter = 7; drives JOPA's data |
| JUPU | Envelope pace = 0 detector | nor3 | NRx2[2:0] | Feeds both JOPA's reset (via HAFE) and HOFO |
| JOPA | Envelope-fire latch | dffr | horu_512hz | Captures KYVO; while high, reloads the counter; falls on the following 512 Hz tick. Held at 0 when pace = 0 |
| HYLY → JAKE | Load enable | nor2 + not | JOPA, ch2_restart | JAKE = OR(JOPA, ch2_restart) drives all three l pins |
| HOFO | Volume bit-0 toggle clock | or3 | JOPA, JUPU, ch2_eg_stop | High while firing, paced-off, or stopped — the toggle commits on its fall |
| FENO / FETE / FOMY / FENA | Volume counter bits 0–3 | tffnl | HOFO, then direction-muxed ripple | Load: q ← NRx2[7:4] on trigger |
| (per-bit AO22 muxes) | Direction ripple selects | ao22 | NRx2[3] | Up: ripple on carry; down: ripple on borrow |
| EMYR / FYRE → GUFY | Saturation detectors | nor5 / nand5+not / or2 | — | vol 0 + down / vol 15 + up |
| HEPO | Saturation capture | dffr | JOPA↑ | Reset on trigger or apu_reset |
JEME / ch2_eg_stop | Envelope-stop latch | nor_latch | Set: HEPO; Reset: trigger / apu_reset | Pins HOFO high — the saturation lockout |
Fire interval = pace × 1/64 s. Pace = 0 is doubly paused: JUPU both
holds JOPA in reset and pins HOFO high so the volume bit-0 clock can
never complete a pulse. (Pan Docs' "period 0 treated as 8" does not
apply to the DMG envelope — the gates pause it outright.)
HOFO rises at the fire (JOPA↑) and the tffnl slave commits the
toggle only on HOFO's fall (the natural JOPA↓ one 512 Hz cycle
later). Everything subtle about mid-channel NRx2 writes follows from it.
The mid-channel write cases:
- A pace→0 write between fires just freezes the volume (HOFO rises once and never falls — no toggle).
- A pace→0 write racing the fire itself: if the write commits before
the
horu_512hz↑ sample, JOPA is held in reset — tick suppressed; if after, JOPA has fired and the queued toggle completes — tick applied. - A pace→0 write landing inside the (JOPA↑, HOFO↓) window freezes the in-flight toggle — the fire happened but the volume never steps.
- A direction flip mid-channel switches each ripple mux combinationally
and can produce a spurious carry edge into the next bit — measured:
$50→$18on a volume-5 counter lands at 10. One of two silicon paths behind "zombie mode" volume changes; the second — the per-write +1 step — is measured below.
The NRx2 cells are transparent latches, and during each write strobe the
whole data byte transiently reads $FF (~16–19 ns) before settling to
the written value — measured directly: bits written 0 read 1 across the
transient. The pace bits dip JUPU, so HOFO completes one fall→rise pulse
and the volume counter gets exactly one clock; the direction bit
(NRx2[3]) reads 1 through the same transient, so the toggle commits — and the
whole carry ripple completes — while the muxes are still up/carry. The
step is therefore +1 regardless of the direction the value selects: a
decrease-mode write increments. Measured on a running CH2, pace = 0, zero
variance (dmg-sim, the purpose-built zombie_x8_ch2 and decrease-mode
zombie_x0_dec_ch2 ROMs): every $18-over-$18 and every $F0-over-$F0
write steps +1, wrapping 15→0 freely — HEPO and JEME never fire under
pace = 0, so no stop exists (the "repeat to decrement" idiom). A pace 0→1
write still steps +1 (the transient dip still fires); a pace 1→0 return write
completes no HOFO pulse and does not step on its own — though in decrease mode
the transient's direction edges can switch a ripple mux combinationally (the
same spurious carry a deliberate flip produces), so $F0-over-$F1 lands +2
where $18-over-$19 holds.
Saturation stop: the saturated-low/high decodes (volume 0 + down / 15 + up) are captured by HEPO on each fire and latch the envelope-stop (JEME), which pins HOFO high — there is no arithmetic clamp; an unstopped 4-bit counter would simply wrap. The next trigger clears both latches.
The trigger window and the "+1 quirk"
Identical shape to the sweep: the reload window is one ch2_1mhz cycle
(measured ≈ 975 ns, opening ~950 ps after ch2_restart↑), kene
free-runs, and a 64 Hz tick inside the window is skipped — first fire one
full period late. Pan Docs documents this as the envelope's
trigger-near-a-tick reload quirk; the gate content is just the l pin
override.
The three measured regimes
Three measured anchors pin the complete model (dmg-sim measurements,
gambatte ch2_init[_reset]_env_counter_timing ROMs):
- The suppression race (
_3): counter saturates 14.1 ms after the trigger; the ROM's pace→0 write commits 0.18 ms before the next 512 Hz sample; JOPA never fires; volume stays 0 — silent, matching hardware. - The frame-sequencer-phase race (
_1, env1 — a ROM that never touches NR52 or DIV, so itskenephase is the boot ROM's leftover ripple seed, Δ = 3 (APU clocks)). At the real boot phase, the fire lands ~1 ms before the pace→0 write — but the write falls inside the (JOPA↑, HOFO↓) window, freezing the in-flight toggle: volume never steps, channel silent (dmg-sim measurement with the real boot ROM). Under the quickboot harness's wrong seed (Δ = 7) the same ROM comes out audible — a harness artifact, not hardware. The anchor is what pins the post-boot Δ = 3 requirement for skip-boot emulators. - The +1-quirk case (
_11, a_reset_ROM that re-locks the phase deterministically): the 64 Hz tick lands ~1.5 ns inside the ~975 ns load window — measured with the counter provably not advancing across it — first fire one full period late, channel silent.
The unified rule: one load-window test fixes the first-fire time (hold
the counter at ~pace while the load net is open; ignore any tick edge
inside it), and the HOFO toggle-vs-pace-0-write race fixes whether a
fire steps the volume. The two compose; no further mechanism is needed
across the test family.
Post-boot: CH1's envelope is saturated-stopped (volume decayed to 0,
stop latch set, counter holding ~3 from the chime's pace); CH2's is
all-zero with the stop latch clear. See post-boot state.
Length Counters and Power Cycling
The length counters are the APU's third counter family — and the one with no reset path at all. That single fact, plus the polarity of their 256 Hz gate, generates the DMG power-cycle behaviour and the NRx4-write extra-clock quirk.
- The counter cells (
tffnl) have no reset pin — power-off preserves the value and merely stops the clocking. - The counter advances on
bufy_256hz's falling edge (frame-sequencer even steps). - Enabling length via NRx4 is itself a clock opportunity: extra clock ⟺ CARU.q = 0 ∧ not stopped — a plain level test at the enable edge.
The counter cells
CH2's counter (CH1/CH3/CH4 mirror cell-for-cell; CH3's is 8-bit) is a 6-bit ripple up-counter loaded from NR21[5:0] on an NRx1 write and counting on the gated 256 Hz tick:
| Bit | Cell | Load value | Load enable | Count clock | Notes |
|---|---|---|---|---|---|
| 0 | ERYC | NR21[0] | bymo | DYRO (= NOT(DEME), the gated 256 Hz tick) | First stage |
| 1 | CERA | NR21[1] | bymo | ERYC ripple | |
| 2 | CONU | NR21[2] | bymo | CERA ripple | |
| 3 | CAME | NR21[3] | bymo | CONU ripple | |
| 4 | BUVA | NR21[4] | aget | CAME ripple (via BUKO) | Upper bits load via aget — a fan-out split of the same NRx1 strobe |
| 5 | AKYD | NR21[5] | aget | BUVA ripple | q_n = the channel's length-stop flag |
tffnl has no reset pin. Compare the duty counters
(CH1/CH2), which are dffr cells reset by apu_reset:
the two counter families sit on opposite sides of the reset tree, and
that is the entire DMG-vs-power-cycle story below.
The length clock and its gate
The tick is bufy_256hz — the CARU tap of the frame-sequencer ripple
(APU clocks) — gated per channel by a NOR3:
CH2:
deme= NOR(cyre,bufy_256hz, NOT(NR24[6]))
(CH1/CH3/CH4: CAPY/DODA/GEPY with their NRx4 bits.) The counter's clock
pulses only with length-enable set and bufy_256hz low.
bufy_256hz equals CARU.q exactly — verified at 1,266/1,266 sampled
points (dmg-sim measurement) — and the counter advances on
bufy_256hz's falling edge (CARU 1→0, i.e. the frame-sequencer
ripple reaching an even step: the conventional "length clocks on even
steps"). To keep the two edge roles straight: the rising edge is the
re-lock step marker in the power-cycle closed form; the falling edge is
the counting edge.
The NRx4-write extra length clock
Because the gate is a plain NOR, enabling length is itself a clock
opportunity: an NRx4 write taking bit 6 from 0→1 drives the gate's
enable input low, and the gate output rises — one extra count, identical
to a 256 Hz tick — iff bufy_256hz is LOW at that instant (and the
channel isn't already length-stopped).
extra clock ⟺ CARU.q = 0 (frame-sequencer step even) ∧ not stopped — evaluated at the enable edge, nothing more.
After an in-game power-cycle the ripple holds its reset value until the
first 512 Hz edge, so an enabling write landing in that window fires (or
not) according to the power-on sub-step — the a bit of the re-lock
closed form. Measured on the hardware-pinned gambatte
div_write_reset_length_counter_timing ROM (dmg-sim measurement): power-on
at sub-step 14 leaves CARU high, the enabling NR24 write produces no
extra clock, the counter holds its value across the write, and the ROM's
NR52 expectation follows — with the opposite sub-step case predicted to
fire by the same level test.
Across an NR52 power cycle (DMG)
Power-off does two things to a length counter, neither of which is a reset:
- No clear — the cells have no reset path; the value holds.
- No clocking — power-off clears the register bank, so length-enable drops and the gate pins the clock; the counter cannot advance.
Preserved and frozen — exactly the behaviour blargg's
08-len ctr during power pins for DMG ("unaffected, and not clocked").
The phase of the length clock after power-on re-locks per the
frame-sequencer rule in APU clocks; the value is
governed entirely by the two points above.
Post-Boot State
At the first PC=0x0100 instruction fetch after the real DMG boot ROM, the machine is in a single, fully-measured hardware state. This chapter pins that state — to the edge, because "PC=0x0100" is ambiguous without it — and gives skip-boot emulators the exact values to adopt.
- The anchor is the CLK9 edge opening the overlap M-cycle of the boot ROM's terminating LDH — anchoring at the write instead is 3 dots early.
- The handoff lands at LX = 98 of the first scanline's Mode 0; adopt 98 directly — the shortening is baked in.
reg_div16= 0xEAF3 — FF04 alone under-constrains the timer phase.- IME is disabled; IF = 0xE1 with the VBlank bit set.
- The frame-sequencer ripple seed is Δ = 3 — not derivable from the divider; CH1's duty counter holds 2 with the DAC on.
The anchor, precisely. Under SM83 fetch overlap, the boot ROM's
terminating LDH (FF50),A performs its post-body fetch in the same
physical M-cycle as the cartridge instruction's first fetch. The anchor is
the CLK9 edge that opens that M-cycle: cpu_port_a transitions to
0x0100, reg_pc holds 0x0100, the FF50 write committed in the prior
M-cycle, and the M-cycle counter reads m7 (the post-body fetch state —
interrupt dispatch).
Anchoring at LDH's mid-write instead places the CPU 3 dots early and propagates a CPU/PPU phase error through everything downstream (dmg-sim measurement, picosecond-identical across three captures).
The handoff lands deep in the first scanline's Mode 0 — LX = 98 of 113, 15 M-cycles before LINE_END — with the OAM scan long complete (scan counter at its terminal 39).
PPU state
| Signal | Value | Notes |
|---|---|---|
| WUVU / VENA / TALU / XUPY | 0 / 1 / 1 / 0 | Divider phases; VENA=1 with WUVU=0 follows from the toggle timing since LCD-on |
| BOGA / ALET | 1 / 1 | T1 of the M-cycle; ALET rising phase |
| XYMU | 1 | Not in Mode 3 |
| BESU / AVAP | 0 / 0 | Scan complete; no transition in flight |
| LY | 0 | |
| LX | 98 | FST-measured per bit: (SAXO…TYRY) = 0b1100010. 98 × 4 = 392 dots into the 456-dot line. The first-scanline shortening and divider startup are already baked in — adopt 98 directly; do not synthesise a pre-shortening value (LCD-on power-up) |
| Scan counter | 39 | Terminal (0b100111) |
| MEDA | 1 | Mid-frame-pulse (fires at each LY 153→0 wrap, holds through LY=0) |
| POFY / PAHO | 0 / 0 | ST loop settled; PX-bit-3 capture in its XYMU-held reset |
| BG fetch counter | 5 (frozen) | Terminal value held by the MOCE self-stop since the last Mode 3 |
| Fetch cascade DFFs (NYKA, PORY, LYZU, PYGO, RENE, RYFA) | all 0 | Mode-3-only / NAFY reset domains |
| Fine-scroll counter + match pipeline | all 0; ROXY = 1 | PASO-held; SACU gated high awaiting the next Mode 3 |
| CLKPIPE tree | SACU=1, SEGU=1, TYFA=0, VYBO=0, SOCY=1 | All derivable from the non-Mode-3 state; WODU=1 residually (PX still holds the terminal-count bit pattern from the boot ROM's last Mode 3) |
| No-reset shifters/temp latches | boot-ROM residue | Never observable: the two-parallel-loads mechanism overwrites BG content before any pixel samples, and sprite content is mux-gated (BG pipeline) |
CPU-visible registers
| Register | Value | Source |
|---|---|---|
| LCDC | 0x91 | boot ROM |
| STAT | 0x85 | Mode 1 bits + LYC match set (LY=LYC=0) + bit 7 (dmg-sim post-boot readback; the mode register reads 01 at the boot→cart handoff, settling to 0 within a few lines) |
| SCY / SCX / WY / WX / LYC | 0x00 | power-on defaults |
| BGP | 0xFC | boot ROM |
| OBP0 / OBP1 | undefined; use 0xFF | the palette latches have no reset path — true silicon power-on state is undefined; real DMGs consistently read 0xFF |
| DIV (FF04) | 0xAB | see the divider block below |
| TIMA / TMA | 0x00 / 0x00 | |
| TAC | 0xF8 | |
| IF | 0xE1 | bit 0 (VBlank) set — captured at the boot ROM's last line-144 entry; benign (IE=0) but visible to cartridge code |
| IE | 0x00 |
LYC=0 with LY=0 requires STAT bit 2 = 1 at handoff. Pairing LYC=0 with a clear match bit means the LYC pipeline state contradicts the register state.
CPU dispatch chain
Every dispatch-chain cell is idle (measured, picosecond-identical across
three captures): no dispatch in flight, nothing latched, the per-bit IF&IE
latches all reading "not pending", and — the one that matters —
IME is DISABLED (ime_n = 1). Skip-boot initialisers that set IME=1
contradict the measured handoff.
The divider, exactly
reg_div16 = 0xEAF3 at the anchor (settling from 0xEAF2 just after
the boundary edge — bit 0's normal toggle, not a cascade).
DIV = bits [13:6] = 0xAB.
Three subtleties:
- FF04 alone under-constrains the state. The low six bits (0x33) are
software-observable through TIMA-increment phase; an emulator matching
FF04 but not
reg_div16diverges on the timer-phase test family. Pre-load the full 0xEAF3. - The simulation's bit 15 is a harness artifact. The boot-ROM
execution measurement shows
reg_div16 = (0x8000 + M-cycles) mod 2^16because the sim deasserts the divider reset ~32,768 M-cycles before the CPU starts; real hardware would read 0x6AF3. The difference is invisible to all software (no bit-15 consumer), so either value is correct — but know which convention you are matching.
For boot-ROM-executing emulators: the total is 5,860,082 M-cycles from reset deassert to the anchor (dmg-sim measurement with the real boot ROM), with a per-PC checkpoint table dominated by the VRAM clear (~57k), the logo scroll (~4.25M), and the jingle wait (~1.4M). One sampling caveat carries over: several checkpoint PCs are byte-fetch transients of multi-byte instructions, so comparisons must use literal per-byte PC advance, not instruction-boundary semantics.
APU state
The boot ROM powers the APU, configures CH1, triggers it twice for the chime, and lets the envelope decay to silence. Measured at the anchor with the real boot ROM (dmg-sim measurement):
CH1 — the load-bearing block for the init_pos test family:
| Item | Value |
|---|---|
| Duty step counter | 2 |
| Period divider | 0x7F9 (counting toward 0x7FF — six ticks to the next overflow) |
| Prescaler | 1 |
Reload value (acc_d = {NR14[2:0], NR13}) | 0x7C1 |
| Volume | 0 — but the DAC is ON (NR12 = 0xF3 retained) and the running latch is SET |
| Envelope | saturated-stopped (ch1_eg_stop = 1); counter holds ~pace = 0b100 |
| Sweep | inactive (NR10 never written; pace 0 holds the fire latch in reset); the sweep counter cells are no-reset — power-on residue |
A skip-boot emulator that zeroes CH1's internals diverges from hardware on the whole retrigger test family: the first cartridge trigger reloads the divider but preserves the duty counter and the running latch, so audibility depends on duty step 2 advancing (or not) during the test's padding.
CH2/CH3/CH4 — untouched: counters zero (a simulation convention; the no-reset divider cells are undefined on silicon but unreachable before a trigger), DACs off, disable latches set. CH4's LFSR holds its 0x7FFF reset pattern. Wave RAM has no reset — the all-zero reading is a simulator initialisation, not a hardware claim; real post-boot wave RAM is implementation-defined.
The single most commonly mis-modelled value: the step counter is the
apu_reset-reset ripple, measured directly at handoff as
(CARU, BYLU, JYNA) = (0, 1, 0) → Δ = 3 in the re-lock formula
(APU clocks). Do not derive the step from reg_div16
bits — that places envelope/length/sweep ticks ~3 steps off the real
phase, which is exactly the failure mode of the phase-sensitive envelope
tests (sweep and envelope). The ripple's advance
rate is divider-locked; only its seed carries the boot offset.
Appendix A: Signal Concordance
Every netlist gate named in this book — 1019 signals — with its role, cell type, and the chapter that owns its primary description. Gate names come from the DMG-CPU B netlist via msinger/dmg-schematics; each is independently checkable against the schematics and the dmg-sim Verilog. Roles are taken from the owning chapter's reference blocks; gates without a role row appear in prose or chain descriptions, where the linked chapter carries the context.
This index is generated from the book's own text and validated against the netlist sources, so every entry corresponds to a real cell and a real mention.
| Gate | Role | Type | Primary chapter | Also appears in |
|---|---|---|---|---|
| ABAF | Mode Transitions | |||
| ABAK | Per-slot-reset shared gating term | or2 | Mode 2: OAM Scan | Mode Transitions |
| ABEZ | Mode Transitions | |||
| ABON | FAMU enable | not_x2 chain | Mode 3: The Sprite Pipeline | |
| ACOR | Wave data latch reset driver (= NOT(apu_reset)) | not_x1 | CH3: Wave Channel | |
| ACYL | OAM DMA | |||
| ADAR | Register Writes | |||
| ADEH | ATAL complement | not_x1 | The Clock Tree | Register Writes |
| ADYK | Phase generator ring stage 4 | dff | The Clock Tree | Register Writes |
| AFAS | Ring decode driving CUPA (inputs ADAR, ATYP) | nor2 | Register Writes | |
| AFUR | Phase generator ring stage 1 | dff | The Clock Tree | Register Writes |
| AGET | Length counter load enable (upper bits) | not_x1 | Length Counters and Power Cycling | |
| AJEP | OAM DMA | |||
| AJER | CH1 prescaler /2 stage 1 | dffr | APU Clock Tree and Frame Sequencer | CH1/CH2: Pulse Channels, Sweep and Envelope |
| AJON | Mode 3 + non-DMA | and2 | Rendering Mode Control | OAM and VRAM Access, OAM DMA, CPU-Visible Mode Boundaries |
| AJUJ | OAM-access permit | nor3 | OAM and VRAM Access | OAM DMA, CPU-Visible Mode Boundaries |
| AKYD | CH2 length counter bit 5 (MSB) | tffnl | Length Counters and Power Cycling | |
| ALEF | Phase generator ring stage 2 | dff | The Clock Tree | Register Writes |
| ALES | Mode Transitions | |||
| ALET | 4 MHz PPU main clock | not_x2 | The Clock Tree | Rendering Mode Control, Register Writes, Register Reads |
| AMAB | OAM CPU-write address gate | and2 | OAM and VRAM Access | |
| AMUV | LCDC.3 → ~ma10 tri-state driver | not_if0 | Mode 3: The BG Pipeline | |
| AMYG | Mode 2: OAM Scan | |||
| ANEL | Scan counter reset chain stage 2 | dffr | OAM DMA | Mode 2: OAM Scan, Mode Transitions |
| ANOM | Scan counter reset | nor2 | Mode 2: OAM Scan | Mode Transitions |
| ANOS | Crystal feedback NAND | nand2 | The Clock Tree | |
| ANYP | SCY read enable (AND2 with ppu_rd) | and2 | Register Reads | |
| APAR | OAM DMA | Mode 2: OAM Scan | ||
| APOV | CUPA driver stage 4 | not | Register Writes | |
| APUG | The IF Register | |||
| APUK | Phase generator ring stage 3 | dff | The Clock Tree | Register Writes |
| APUV | CH1 prescaler clock buffer | not_x1 | APU Clock Tree and Frame Sequencer | |
| AREV | CUPA driver stage 5 | nand2 | Register Writes | |
| AROR | Sprite-trigger gate (LCDC.1-gated rendering enable) | and2 | LCDC Structure | Mode 2: OAM Scan, Mode 3: The BG Pipeline, Mode 3: The Sprite Pipeline |
| AROV | Register Writes | |||
| ARUR | SCX write enable | and2 | Register Writes | |
| ASAM | OAM CPU-address disable | or3 | Rendering Mode Control | OAM DMA |
| ASEN | BESU reset driver | or2 | Rendering Mode Control | Mode 2: OAM Scan, Mode Transitions |
| ATAG | APU 4 MHz polarity restore | not_x1 | APU Clock Tree and Frame Sequencer | |
| ATAL | 4 MHz buffered enable | not_x2 | The Clock Tree | Register Writes |
| ATAR | VID_RST active-high | not | Rendering Mode Control | Mode 2: OAM Scan, Mode Transitions |
| ATEJ | Rendering Mode Control | Mode 2: OAM Scan, Mode 3: The BG Pipeline, Mode 3: The Sprite Pipeline | ||
| ATEP | CH2 prescaler /2 stage 1 | dffr | APU Clock Tree and Frame Sequencer | CH1/CH2: Pulse Channels |
| ATYP | Register Writes | |||
| AVAP | Mode 2→3 trigger | not_x2 | Mode 2: OAM Scan | The Clock Tree, Rendering Mode Control, Register Writes, Register Reads |
| AVER | = NAND2(mode2, XYSO) — scan-phase term in BYCU | nand2 | OAM DMA | Mode 2: OAM Scan, OAM and VRAM Access |
| AVET | Crystal feedback NAND | nand2 | The Clock Tree | |
| AVOG | SCX read enable (AND2 with ppu_rd) | and2 | Register Reads | |
| AWOH | Mode Transitions | |||
| AXAD | Mode 3: The BG Pipeline | |||
| AZEG | CH2 prescaler clock buffer | not_x1 | APU Clock Tree and Frame Sequencer | |
| AZEM | AROR second input (rendering-active AND mid-scanline-control) | and2 | Rendering Mode Control | Mode 2: OAM Scan |
| AZET | Wave-RAM precharge-window term (with AZUS) | dffr | CH3: Wave Channel | |
| AZOF | PPU clock buffer | not_x6 | The Clock Tree | APU Clock Tree and Frame Sequencer |
| AZUS | wave_data_latch synchroniser stage 3 | dffr | CH3: Wave Channel | |
| AZYB | Slot counter reset driver | not_x1 | Mode 2: OAM Scan | Mode Transitions |
| BAFU | apu_wr buffer stage 1 | not_x1 | APU Clock Tree and Frame Sequencer | |
| BAFY | AMUV enable (BG tilemap stage) | not_x2 | Mode 3: The BG Pipeline | |
| BAGY | BYBA/DOBA reset driver | not_x1 | Mode 2: OAM Scan | |
| BALU | OAM-parse reset (active-high) | not_x1 | Mode 2: OAM Scan | Mode Transitions |
| BAMA | wave_data_latch synchroniser reset driver (= NOT(apu_reset)) | not_x1 | CH3: Wave Channel | |
| BANO | wave_data_latch synchroniser stage 2 | dffr | CH3: Wave Channel | |
| BAPY | Register Writes | |||
| BARY | CH3 DAC input bit 0 gate (AND with ch3_active) | and2 | CH3: Wave Channel | |
| BATU | The IF Register | |||
| BAVE | Sweep pace = 0 detector | and3 | Sweep and Envelope | |
| BAVU | ch4_1mhz — CH4 base clock (= NOT avok, ~1.048 MHz) | not_x1 | CH4: Noise Channel | |
| BAXO | Mode 3: The Sprite Pipeline | |||
| BEBA | = NOT(AVOG) — SCX read drivers' ena_n | not_x1 | Register Reads | |
| BEBU | Scan-complete aggregate | or3 | The Clock Tree | Rendering Mode Control, Mode 2: OAM Scan, Mode Transitions |
| BEDY | SCY write enable | and2 | Register Writes | |
| BEGO | Slot counter bit 2 | dffr | Mode 2: OAM Scan | |
| BEGU | Wave data latch bit 3 | drlatch_ee | CH3: Wave Channel | |
| BELU | data_phase gate (= NOR2(ATYP, clk_ena_n)) | nor2 | Register Reads | |
| BELY | CH3 DAC input bit 3 gate (AND with ch3_active) | and2 | CH3: Wave Channel | |
| BEPA | Wave data latch bit 7 | drlatch_ee | CH3: Wave Channel | |
| BERU | Register Writes | |||
| BESE | Slot counter bits 0–3 | dffr (toggle) | Mode 2: OAM Scan | |
| BESU | Scan-active latch (Q=1 during Mode 2) | nor_latch | Rendering Mode Control | Register Writes, Register Reads, OAM and VRAM Access |
| BETE | OAM DMA | |||
| BEVA | data_phase rebuffer | not_x6 | Register Reads | |
| BEXA | Sweep-fire latch | dffr | Sweep and Envelope | |
| BODE | OAM DMA | |||
| BOGA | PPU timing divider (held 0 under VID_RST) | Register Writes | Post-Boot State | |
| BOGE | OAM and VRAM Access | OAM DMA, Mode 2: OAM Scan | ||
| BONE | Sweep overflow channel-disable term (= NOT(ch1_sum_ovfl_n)) | not_x1 | Sweep and Envelope | |
| BOPA | CH3 DAC input bit 2 gate (AND with ch3_active) | and2 | CH3: Wave Channel | |
| BORA | Wave data latch bit 6 | drlatch_ee | CH3: Wave Channel | |
| BUDY | Wave data latch bit 2 | drlatch_ee | CH3: Wave Channel | |
| BUKO | Length counter bit-3→4 ripple inverter | not_x1 | Length Counters and Power Cycling | |
| BURE | Frame-sequencer 512 Hz clock | not_x2 | APU Clock Tree and Frame Sequencer | |
| BURY | BEXA async-reset driver (pace 0) | nor2 | Sweep and Envelope | |
| BUSA | wave_data_latch synchroniser stage 1 | dffr | CH3: Wave Channel | |
| BUVA | CH2 length counter bit 4 | tffnl | Length Counters and Power Cycling | |
| BUWY | = NOT(ANYP) — SCY read drivers' ena_n | not_x1 | Register Reads | |
| BUZA | Rendering Mode Control | |||
| BYBA | Scan-done flag | dffr | The Clock Tree | Mode 2: OAM Scan, Mode 3: The BG Pipeline, Mode Transitions |
| BYCU | = NAND3(CUFE, XUJY, AVER) — COTA driver | nand3 | OAM DMA | Mode 2: OAM Scan, OAM and VRAM Access |
| BYHA | Mode Transitions | |||
| BYJO | AZEM first input (mid-scanline control) | not_x1 | Mode 2: OAM Scan | |
| BYKA | CH3 DAC input bit 1 gate (AND with ch3_active) | and2 | CH3: Wave Channel | |
| BYLU | Frame sequencer /2 → 128 Hz (sweep tap) | dffr | APU Clock Tree and Frame Sequencer | Post-Boot State |
| BYMO | Length counter load enable (lower bits) | not_x1 | Length Counters and Power Cycling | |
| BYRA | Sweep shift-step counter bit 2 | tffnl | Sweep and Envelope | |
| BYRY | data_phase inverter (of BELU) | not_x2 | Register Reads | |
| CAGY | CH2 duty counter bit 1 | dffr_cc | CH1/CH2: Pulse Channels | |
| CAJA | Sweep shift-step counter bit 1 | tffnl | Sweep and Envelope | |
| CAKE | Mode 2: OAM Scan | |||
| CALA | CH1 overflow detector (NOT stage) | not_x1 | CH1/CH2: Pulse Channels | |
| CALO | CH1 prescaler /2 stage 2 | dffr | APU Clock Tree and Frame Sequencer | CH1/CH2: Pulse Channels |
| CALY | int_serial driver (toggle DFF on serial transfer clock) | dffr | The IF Register | |
| CAMA | CH2 divider toggle clock (NOR stage) | nor2 | CH1/CH2: Pulse Channels | |
| CAME | CH2 length counter bit 3 | tffnl | Length Counters and Power Cycling | |
| CANO | CH2 duty counter bit 0 (LSB) | dffr | CH1/CH2: Pulse Channels | |
| CAPE | Mode 2: OAM Scan | |||
| CAPY | CH1 length-clock gate | nor3 | Length Counters and Power Cycling | |
| CARE | Global sprite-save enable | and3 | Mode 2: OAM Scan | Mode Transitions, Scanline and Frame Timing |
| CARU | Frame sequencer /2 → 256 Hz (length tap) | dffr | APU Clock Tree and Frame Sequencer | Length Counters and Power Cycling, Post-Boot State |
| CARY | noise_counter_clk — CH4 shift-divider clock (ch4_1mhz & gary) | and2 | CH4: Noise Channel | |
| CASY | SCX bit-7 read driver | not_if0 | Register Reads | |
| CATA | SCX bit-5 read driver | not_if0 | Register Reads | |
| CATU | Scan-boundary trigger | dffr | OAM DMA | Mode 2: OAM Scan, Mode Transitions, CPU-Visible Mode Boundaries |
| CAVA | CH1 duty-pattern select | nor2 | CH1/CH2: Pulse Channels | |
| CAXO | CH1 duty-pattern select | nor2 | CH1/CH2: Pulse Channels | |
| CAXU | Mode 2: OAM Scan | |||
| CAXY | Sweep counter bit 2 | tffnl | Sweep and Envelope | |
| CEDE | inverted dma_addr_ext | not_x2 | OAM DMA | |
| CEDU | SCX bit-4 read driver | not_if0 | Register Reads | |
| CEDY | Mode 2: OAM Scan | Mode 3: The Sprite Pipeline | ||
| CEHA | Mode 2: OAM Scan | |||
| CEMO | CH2 prescaler /2 stage 2 | dffr | APU Clock Tree and Frame Sequencer | CH1/CH2: Pulse Channels |
| CENO | XUPY-phased scan-active copy | dffr | The Clock Tree | Rendering Mode Control, Mode 2: OAM Scan, Races |
| CERA | CH2 length counter bit 1 | tffnl | Length Counters and Power Cycling | |
| CERY | CH3 prescaler /2 stage | dffr | APU Clock Tree and Frame Sequencer | CH3: Wave Channel |
| CESY | Wave data latch bit 1 | drlatch_ee | CH3: Wave Channel | |
| CEVO | Wave data latch bit 5 | drlatch_ee | CH3: Wave Channel | |
| CEVU | CH1 duty-pattern select | nor2 | CH1/CH2: Pulse Channels | |
| CEXO | CH4 shift divider bit 0 | dffr | CH4: Noise Channel | |
| CODO | CH1 duty counter-state decode | not_x1 | CH1/CH2: Pulse Channels | |
| COMY | CH1 overflow capture (/2 toggle) | dffr | CH1/CH2: Pulse Channels | |
| CONU | CH2 length counter bit 2 | tffnl | Length Counters and Power Cycling | |
| COPA | Sweep shift-step counter bit 0 | tffnl | Sweep and Envelope | |
| COPU | CH1 divider bit 10 (MSB) | tffnl | CH1/CH2: Pulse Channels | |
| COSO | CH1 duty-pattern select | nor2 | CH1/CH2: Pulse Channels | |
| COTA | Stage-2 capture enable; complement WOVU drives the OAM bitline precharge | not_x1 | OAM DMA | Mode 2: OAM Scan, OAM and VRAM Access |
| COWE | CH1 PWM gated by channel-running | and2 | CH1/CH2: Pulse Channels | |
| COZE | Sweep counter-at-max detector | and3 | Sweep and Envelope | |
| CUFE | = OAI21(FEXX, dma_run, dma_phi_n) — $FExx/DMA bus-cycle term in BYCU | oai21 | OAM DMA | Mode 2: OAM Scan, OAM and VRAM Access |
| CUGA | SCX bit-2 read driver | not_if0 | Register Reads | |
| CULE | CH2 duty counter clock | not_x1 | CH1/CH2: Pulse Channels | |
| CUPA | Shared PPU register write strobe | combinational (derived from phase gen) | Register Writes | Register Reads, OAM and VRAM Access, Mode 2: OAM Scan |
| CUPO | Sweep counter bit 0 | tffnl | Sweep and Envelope | |
| CUSA | SCY bit-4 read driver | not_if0 | Register Reads | |
| CUVO | Wave data latch bit 4 | drlatch_ee | CH3: Wave Channel | |
| CUXY | Slot counter bit 1 | dffr | Mode 2: OAM Scan | |
| CYBO | CH3 prescaler clock buffer | not_x1 | APU Clock Tree and Frame Sequencer | |
| CYFO | Wave data latch bit 0 | drlatch_ee | CH3: Wave Channel | |
| CYKE | Mode 2: OAM Scan | |||
| CYMU | Sweep counter load enable (NOT stage) | not_x1 | Sweep and Envelope | |
| CYPU | Sweep counter bit 1 | tffnl | Sweep and Envelope | |
| CYRA | OAM DMA | Mode 2: OAM Scan | ||
| CYRE | CH2 length-clock gate (DEME) input | dffr | Length Counters and Power Cycling | |
| CYSE | CH2 PWM gated by channel-running | and2 | CH1/CH2: Pulse Channels | |
| CYVO | CH2 divider bit 3 | tffnl | CH1/CH2: Pulse Channels | |
| DAFA | Sweep counter load enable (NOR stage) | nor2 | Sweep and Envelope | |
| DAJO | CH1 duty counter clock | not_x1 | CH1/CH2: Pulse Channels | |
| DALE | CH4 shift divider bit 5 | dffr | CH4: Noise Channel | |
| DAPE | CH1 duty counter bit 2 (MSB) | dffr_cc | CH1/CH2: Pulse Channels | |
| DARE | CH2 duty counter-state decode | not_x1 | CH1/CH2: Pulse Channels | |
| DARY | CH4 divider low-tap mux (bits 0,1,2,3 group) | ao2222 | CH4: Noise Channel | |
| DASA | Mode 2: OAM Scan | |||
| DATO | CH4 LFSR output → envelope gate | or2 | CH4: Noise Channel | |
| DEGO | Per-slot X match, FEFY group (active-low) | nand3 | Mode 3: The Sprite Pipeline | |
| DEKO | CH4 shift divider bit 1 | dffr | CH4: Noise Channel | |
| DEME | CH2 length-clock gate | nor3 | Length Counters and Power Cycling | |
| DEMO | CH4 shift divider bit 7 | dffr | CH4: Noise Channel | |
| DEPO | OAM attribute bit 7 latch (BG-over-OBJ priority) | dlatch_ee | Mode 3: The Sprite Pipeline | |
| DERE | CH4 shift divider bit 12 | dffr | CH4: Noise Channel | |
| DERO | CH3 wave-position counter clock | not_x1 | CH3: Wave Channel | |
| DERU | CH1 divider ripple inverter (bits 8–10) | not_x1 | CH1/CH2: Pulse Channels | |
| DETE | CH4 shift divider bit 9 | dffr | CH4: Noise Channel | |
| DOBA | ALET-clocked scan-done-prev | dffr | The Clock Tree | Mode 2: OAM Scan, Mode Transitions, Scanline and Frame Timing |
| DOCA | CH2 divider toggle clock (NOT stage) | not_x1 | CH1/CH2: Pulse Channels | |
| DODA | CH3 length-clock gate | nor3 | Length Counters and Power Cycling | |
| DOJU | CH2 duty-pattern select | nor2 | CH1/CH2: Pulse Channels | |
| DOKA | Overflow-capture self-clear term (= AND2(COMY, ch1_1mhz)) | and2 | CH1/CH2: Pulse Channels | |
| DOKE | CH4 shift divider bit 6 | dffr | CH4: Noise Channel | |
| DOME | CH2 PWM latch | dffr | CH1/CH2: Pulse Channels | |
| DOMO | CH2 duty-pattern select | nor2 | CH1/CH2: Pulse Channels | |
| DONE | CH2 divider bit 0 | tffnl | CH1/CH2: Pulse Channels | |
| DOSE | CH4 shift divider bit 8 | dffr | CH4: Noise Channel | |
| DOTA | CH4 shift divider bit 11 | dffr | CH4: Noise Channel | |
| DOVE | CH2 duty-pattern select | nor2 | CH1/CH2: Pulse Channels | |
| DOXE | SCX bit-6 read driver | not_if0 | Register Reads | |
| DUGA | OAM DMA | |||
| DUJU | CH2 divider load enable | nor2 | CH1/CH2: Pulse Channels | |
| DURE | CH4 shift divider bit 4 | dffr | CH4: Noise Channel | |
| DUWO | CH1 PWM latch | dffr | CH1/CH2: Pulse Channels | |
| DYBA | = NOT(BYVA) — per-slot line-reset term | not_x1 | Mode 3: The Sprite Pipeline | |
| DYBE | Slot counter bit 3 (MSB) | dffr | Mode 2: OAM Scan | |
| DYDU | Per-slot X match, FEFY group (active-low) | nand3 | Mode 3: The Sprite Pipeline | |
| DYKA | Per-slot X match, FOVE group (active-low) | nand3 | Mode 3: The Sprite Pipeline | |
| DYKY | CUPA driver stage 1 | not | Register Writes | |
| DYMU | CH2 duty counter-state decode | and2 | CH1/CH2: Pulse Channels | |
| DYNU | CH2 divider bit 1 | tffnl | CH1/CH2: Pulse Channels | |
| DYRO | Gated 256 Hz length tick (= NOT(DEME)) | not_x1 | Length Counters and Power Cycling | |
| DYRU | COMY async-reset driver (= NOR(apu_reset, ch1_restart, DOKA)) | nor3 | CH1/CH2: Pulse Channels | |
| DYTA | CH2 duty-pattern select | nor2 | CH1/CH2: Pulse Channels | |
| DYVE | CH2 duty counter bit 2 (MSB) | dffr_cc | CH1/CH2: Pulse Channels | |
| DYWE | Slot-0 reset driver = OR2(DYBA, EBOJ) | or2 | Mode 3: The Sprite Pipeline | |
| EBEB | Slot-3 write-enable inverter | not_x1 | Mode 2: OAM Scan | Races |
| EBOJ | Slot-0 fetch-done flag (d = GUVA, clk = WUTY) | dffr | Mode 3: The Sprite Pipeline | Mode 2: OAM Scan |
| EBOS | Mode 2: OAM Scan | |||
| ECED | OAM DMA | Mode 2: OAM Scan, Mode 3: The Sprite Pipeline | ||
| EDOS | SCX bit-0 read driver | not_if0 | Register Reads | |
| EFAL | CH3 wave-position bit 4 (MSB) | dffr | CH3: Wave Channel | |
| EFAR | CH3 wave-position bit 0 (wave_nibble_sel) | dffr | CH3: Wave Channel | |
| EFUZ | CH3 wave-position bit 2 | dffr | CH3: Wave Channel | |
| EFYL | Per-slot X match, FOVE group (active-low) | nand3 | Mode 3: The Sprite Pipeline | |
| EGOG | CH2 duty counter-state decode | and2 | CH1/CH2: Pulse Channels | |
| EGOM | Per-slot X match, FOVE group (active-low) | nand3 | Mode 3: The Sprite Pipeline | |
| EKOB | SCX bit-1 read driver | not_if0 | Register Reads | |
| EKOV | CH1 divider bit 7 | tffnl | CH1/CH2: Pulse Channels | |
| ELYN | Scan counter bit 3 | dffr | Mode 2: OAM Scan | |
| ELYX | CH4 divider mid-tap mux (bits 4,5,6,7 group) | ao2222 | CH4: Noise Channel | |
| EMUS | CH1 divider bit 8 | tffnl | CH1/CH2: Pulse Channels | |
| EMYR | Volume saturation detector (vol 0 + down) | nor5 | Sweep and Envelope | |
| ENEF | Y-compare carry-chain bit 1 | full_add | Mode 2: OAM Scan | |
| ENEK | CH1 duty counter-state decode | and2 | CH1/CH2: Pulse Channels | |
| EPOR | CH4 shift divider bit 3 | dffr | CH4: Noise Channel | |
| EPYK | CH1 divider load enable | nor2 | CH1/CH2: Pulse Channels | |
| EROS | CH1 duty counter bit 1 | dffr_cc | CH1/CH2: Pulse Channels | |
| ERUC | Y-compare carry-chain bit 0 | full_add | Mode 2: OAM Scan | |
| ERUS | CH3 wave-position bit 1 | dffr | CH3: Wave Channel | |
| ERUT | CH4 shift divider bit 10 | dffr | CH4: Noise Channel | |
| ERYC | CH2 length counter bit 0 | tffnl | Length Counters and Power Cycling | |
| ERYF | CH4 divider low-tap aggregate | or2 | CH4: Noise Channel | |
| ESEP | CH4 shift divider bit 13 | dffr | CH4: Noise Channel | |
| ESUT | CH1 duty counter bit 0 (LSB) | dffr | CH1/CH2: Pulse Channels | |
| ETAN | Wave-position counter reset (active-low) | nor2 | CH3: Wave Channel | |
| ETYR | CH4 divider high-tap mux (bits 8–13) | ao222222 | CH4: Noise Channel | |
| EVAK | CH1 divider bit 9 | tffnl | CH1/CH2: Pulse Channels | |
| EXEL | CH3 wave-position bit 3 | dffr | CH3: Wave Channel | |
| EZEF | CH4 shift divider bit 2 | dffr | CH4: Noise Channel | |
| EZOF | CH2 divider bit 2 | tffnl | CH1/CH2: Pulse Channels | |
| EZOZ | CH1 duty counter-state decode | and2 | CH1/CH2: Pulse Channels | |
| EZUL | CH4 LFSR-output / bypass mux | mux | CH4: Noise Channel | |
| FABO | ch3_restart sample clock (= NOT(ch3_2mhz)) | not_x1 | CH3: Wave Channel | |
| FAHA | Scan counter bit 4 | dffr | Mode 2: OAM Scan | |
| FAJU | ch3_fdis enable pulse (= NOT(GYRA)) | not_x1 | CH3: Wave Channel | |
| FAMU | ~ma4 tri-state driver | not_if0 | Mode 3: The Sprite Pipeline | |
| FECO | Y-compare carry-chain bit 2 | full_add | Mode 2: OAM Scan | |
| FEFY | Sprites 0–4 X-match aggregate | nand5 | Mode 2: OAM Scan | Mode 3: The BG Pipeline, Mode 3: The Sprite Pipeline |
| FEME | CH4 LFSR shift-clock select (ff22_d7 ? etyr : eryf) | mux | CH4: Noise Channel | |
| FEMO | Mode 2: OAM Scan | |||
| FENA | Volume counter bit 3 | tffnl | Sweep and Envelope | |
| FENO | Volume counter bit 0 | tffnl | Sweep and Envelope | |
| FEPO | Sprite X priority aggregate | or2 | Rendering Mode Control | Mode 2: OAM Scan, Mode 3: The BG Pipeline, Mode 3: The Sprite Pipeline |
| FEPU | Mode 2: OAM Scan | |||
| FETE | Volume counter bit 1 | tffnl | Sweep and Envelope | |
| FETO | Scan-done decode | and4 | Mode 2: OAM Scan | Mode Transitions |
| FETY | CH3 counter-wrap interlock (/2 toggle) | dffr | CH3: Wave Channel | |
| FEVA | CH1 divider bit 6 | tffnl | CH1/CH2: Pulse Channels | |
| FEXX | $FExx address-range decode (= NOT(ROPE)) | not_x2 | OAM and VRAM Access | |
| FFFF | Interrupt Dispatch | |||
| FOBA | NR34 trigger M-cycle synchroniser | dffr | CH3: Wave Channel | |
| FOCO | Slot-3 selector | nand4 | Mode 2: OAM Scan | |
| FOFA | Mode 2: OAM Scan | |||
| FOMY | Volume counter bit 2 | tffnl | Sweep and Envelope | |
| FONE | Mode 2: OAM Scan | |||
| FONO | Mode 2: OAM Scan | Mode 3: The Sprite Pipeline | ||
| FONY | Scan counter bit 5 (MSB) | dffr | Mode 2: OAM Scan | |
| FOVE | Sprites 5–9 X-match aggregate | nand5 | Mode 2: OAM Scan | Mode 3: The BG Pipeline, Mode 3: The Sprite Pipeline |
| FOZU | CH3 active-bit nor_latch | nor_latch | CH3: Wave Channel | |
| FUFO | XYMO complement | not_x1 | Mode 3: The Sprite Pipeline | |
| FUKY | Mode 2: OAM Scan | |||
| FULO | CH1 divider toggle clock (NOR stage) | nor2 | CH1/CH2: Pulse Channels | |
| FUVE | Mode 2: OAM Scan | |||
| FUVO | ch3_fdis disable level (= NOR(ch3_amp_en_n, apu_reset)) | nor2 | CH3: Wave Channel | |
| FUWA | Y-compare carry-chain bit 5 | full_add | Mode 2: OAM Scan | |
| FUXO | CH2 divider bit 4 | tffnl | CH1/CH2: Pulse Channels | |
| FUXU | Slot-0 X save strobe (store-counter = 0 decode) | not_x1 | Mode 3: The Sprite Pipeline | |
| FYCU | Mode 2: OAM Scan | |||
| FYGO | FOZU clear term (= OR3(apu_reset, GEDO, ch3_amp_en_n)) | or3 | CH3: Wave Channel | |
| FYNE | horu_512hz buffer chain stage | not_x3 | APU Clock Tree and Frame Sequencer | |
| FYRE | Volume saturation detector (vol 15 + up) | not_x1 | Sweep and Envelope | |
| GACE | NOT(GOPU) — Y-compare sum bit 4, complemented | not_x1 | Mode 2: OAM Scan | |
| GALE | horu_512hz buffer chain stage | mux | APU Clock Tree and Frame Sequencer | |
| GAME | lfsr_out — CH4 LFSR output AND ch4_active | and2 | CH4: Noise Channel | |
| GANE | CH2 divider bit 7 | tffnl | CH1/CH2: Pulse Channels | |
| GANO | CH2 divider bit 5 | tffnl | CH1/CH2: Pulse Channels | |
| GARA | ch3_restart synchroniser DFF | dffr | CH3: Wave Channel | |
| GARY | CH4 shift-divider clock-enable latch (cleared by trigger) | dffr | CH4: Noise Channel | |
| GAVA | Scan counter clock | or2 | Mode 2: OAM Scan | |
| GAVU | NR34 trigger-bit capture | drlatch_ee | CH3: Wave Channel | |
| GAXE | CH1 divider bit 0 | tffnl | CH1/CH2: Pulse Channels | |
| GEDO | CH3 length-stop term (length expired AND NR34 length enable) | and2 | CH3: Wave Channel | |
| GEJY | OBJ_SIZE mux | ao22 | LCDC Structure | Mode 3: The Sprite Pipeline |
| GEKU | CH1 divider toggle clock (NOT stage) | not_x1 | CH1/CH2: Pulse Channels | |
| GENA | ch4_active nor_latch (set by ch4_restart) | nor_latch | CH4: Noise Channel | |
| GEPO | CH4 LFSR-reset OR (ch4_restart, apu_reset) | or2 | CH4: Noise Channel | |
| GEPY | CH4 length-clock gate | nor3 | Length Counters and Power Cycling | |
| GESE | sprite_y_match driver | not_x1 | Mode 2: OAM Scan | |
| GEWY | NOT(WUHU) — Y-compare sum bit 7, complemented | not_x1 | Mode 2: OAM Scan | |
| GEXY | horu_512hz buffer chain stage | not_x1 | APU Clock Tree and Frame Sequencer | |
| GOBA | SCY bit-1 read driver | not_if0 | Register Reads | |
| GOCA | CH2 divider bit 6 | tffnl | CH1/CH2: Pulse Channels | |
| GODO | SCY bit-3 read driver | not_if0 | Register Reads | |
| GOFU | CH4 prescaler load-enable NOR (ch4_restart, gary) | nor2 | CH4: Noise Channel | |
| GOFY | NR34 trigger nor_latch (q_n is held) | nor_latch | CH3: Wave Channel | |
| GOGE | CH4 LFSR reset (= NOT gepo) | not_x2 | CH4: Noise Channel | |
| GOJU | Y-compare carry-chain bit 6 | full_add | Mode 2: OAM Scan | |
| GOMO | Mode 3: The Sprite Pipeline | |||
| GONE | ch4_restart synchroniser DFF (clk hama_512khz) | dffr | CH4: Noise Channel | |
| GONU | SCY bit-2 read driver | not_if0 | Register Reads | |
| GOPU | Y-compare carry-chain bit 4 | full_add | Mode 2: OAM Scan | |
| GOSO | Scan counter bit 2 | dffr | Mode 2: OAM Scan | |
| GOVU | LCDC.2 mask | or2 | LCDC Structure | Mode 2: OAM Scan |
| GUFY | Volume saturation detector aggregate | or2 | Sweep and Envelope | |
| GUGU | Channel-disable latch (q_n = ch3_fdis) | nand_latch | CH3: Wave Channel | |
| GUNE | SCY bit-6 read driver | not_if0 | Register Reads | |
| GUNY | CH4 gary reset (NOR apu_reset, ch4_restart) | nor2 | CH4: Noise Channel | |
| GUSU | Mode 2: OAM Scan | |||
| GUVA | = NOT(YDUG) — slot-0 match select | nor2 | Mode 3: The Sprite Pipeline | |
| GUVU | NOT(FUWA) — Y-compare sum bit 5, complemented | not_x1 | Mode 2: OAM Scan | |
| GYBA | CH4 divider-enable-latch clock (= NOT ch4_1mhz) | not_x1 | CH4: Noise Channel | |
| GYDA | NOT(GOJU) — Y-compare sum bit 6, complemented | not_x1 | Mode 2: OAM Scan | |
| GYKY | Carry-chain bit 3 | full_add | Mode 2: OAM Scan | |
| GYRA | Trigger-pulse retime DFF (GYTA on ch3_2mhz; feeds FAJU) | dffr | CH3: Wave Channel | |
| GYSU | ch4_start capture DFF (clk apu_phi) | dffr | CH4: Noise Channel | |
| GYTA | CH3 trigger self-clear delay | dffr | CH3: Wave Channel | |
| GYZA | SCY bit-7 read driver | not_if0 | Register Reads | |
| GYZO | SCY bit-5 read driver | not_if0 | Register Reads | |
| HAFE | JOPA reset driver (pace 0) | nor4 | Sweep and Envelope | |
| HAMA | hama_512khz buffer (= NOT jeso_n) | not_x4 | CH4: Noise Channel | |
| HAPE | CH4 LFSR state bit | dffr | CH4: Noise Channel | |
| HAZO | CH4 trigger nor_latch (feeds ch4_restart.d) | nor_latch | CH4: Noise Channel | |
| HEFO | CH3 divider toggle clock (NOR stage) | nor2 | CH3: Wave Channel | |
| HENO | CH4 LFSR state bit | dffr | CH4: Noise Channel | |
| HEPA | CH4 LFSR state bit | dffr | CH4: Noise Channel | |
| HEPO | Saturation capture | dffr | Sweep and Envelope | |
| HEPU | CH2 divider bit 9 | tffnl | CH1/CH2: Pulse Channels | |
| HERA | CH3 divider load enable (active-low) | nor2 | CH3: Wave Channel | |
| HERO | CH2 divider bit 10 (MSB) | tffnl | CH1/CH2: Pulse Channels | |
| HEVY | CH2 divider bit 8 | tffnl | CH1/CH2: Pulse Channels | |
| HEZU | CH4 LFSR output bit | dffr | CH4: Noise Channel | |
| HOFO | Volume bit-0 toggle clock | or3 | Sweep and Envelope | |
| HOGA | NR44 trigger-bit (d7) write strobe → ch4_start | drlatch_ee | CH4: Noise Channel | |
| HORU | horu_512hz buffer chain stage | not_x3 | APU Clock Tree and Frame Sequencer | |
| HORY | CH4 LFSR state bit | dffr | CH4: Noise Channel | |
| HUCE | CH4 prescaler load enable (= NOT gofu) | not_x1 | CH4: Noise Channel | |
| HUNO | CH3 overflow capture (/2 toggle) | dffr | CH3: Wave Channel | |
| HURA | CH4 LFSR feedback XNOR (the two XNOR taps) | xnor | CH4: Noise Channel | |
| HYFE | CH1 divider bit 1 | tffnl | CH1/CH2: Pulse Channels | |
| HYFO | CH3 overflow detector (NOT stage) | not_x1 | CH3: Wave Channel | |
| HYKE | CH1 divider bit 5 | tffnl | CH1/CH2: Pulse Channels | |
| HYLY | Envelope counter load enable (NOR stage) | nor2 | Sweep and Envelope | |
| HYNO | CH4 prescaler terminal-count (jyfu&jyre&jyco) | and3 | CH4: Noise Channel | |
| HYRO | CH4 LFSR state bit | dffr | CH4: Noise Channel | |
| JAJU | CH4 LFSR state bit | dffr | CH4: Noise Channel | |
| JAKE | Envelope counter load enable (NOT stage) | not_x1 | Sweep and Envelope | |
| JAPU | CH3 divider bit 10 (MSB) | tffnl | CH3: Wave Channel | |
| JAVO | CH4 LFSR state bit | dffr | CH4: Noise Channel | |
| JEMA | CH1 divider bit 4 | tffnl | CH1/CH2: Pulse Channels | |
| JEME | Envelope-stop latch | nor_latch | Sweep and Envelope | |
| JEPE | CH4 LFSR state bit (7-bit inject point) | dffr | CH4: Noise Channel | |
| JERA | CH3 divider load-enable fan-out | not_x1 | CH3: Wave Channel | |
| JERY | ch4_fdis channel-disable latch | nand_latch | CH4: Noise Channel | |
| JESO | CH4 free-running 512 kHz prescaler bit | dffr | CH4: Noise Channel | |
| JEVY | Envelope counter bit 2 | tffnl | Sweep and Envelope | |
| JONA | Envelope counter bit 1 | tffnl | Sweep and Envelope | |
| JOPA | Envelope-fire latch | dffr | Sweep and Envelope | |
| JORE | Envelope counter bit 0 | tffnl | Sweep and Envelope | |
| JOTO | CH4 LFSR feedback/chain-entry DFF | dffr | CH4: Noise Channel | |
| JUPU | Envelope pace = 0 detector | nor3 | Sweep and Envelope | |
| JUTY | CH3 divider toggle clock (NOT stage) | not_x1 | CH3: Wave Channel | |
| JUXE | CH4 LFSR state bit | dffr | CH4: Noise Channel | |
| JYCO | CH4 divisor prescaler bit 0 (loadable toggle) | tffnl | CH4: Noise Channel | |
| JYFU | CH4 divisor prescaler bit 2 (loadable toggle) | tffnl | CH4: Noise Channel | |
| JYJA | CH4 LFSR feedback-DFF clock (= NOT ch4_lfsr_clk1) | not_x1 | CH4: Noise Channel | |
| JYNA | Frame sequencer /2 → 64 Hz (envelope tap) | dffr | APU Clock Tree and Frame Sequencer | Post-Boot State |
| JYRE | CH4 divisor prescaler bit 1 (loadable toggle) | tffnl | CH4: Noise Channel | |
| JYTY | CH1 divider bit 2 | tffnl | CH1/CH2: Pulse Channels | |
| KAFO | CH3 divider bit 6 | tffnl | CH3: Wave Channel | |
| KAHE | CPL source (AO22 LCDC.7 mux) | ao22 | LCDC Structure | LCD Output |
| KANU | CH4 divisor-prescaler toggle clock (= kyku) | not_x1 | CH4: Noise Channel | |
| KASA | CPL PPU-clock source (LCD-on arm) | not | LCD Output | |
| KASO | CH3 divider load enable (bits 4–7) | not_x1 | CH3: Wave Channel | |
| KAVU | CH4 LFSR 7-bit-mode feedback inject | ao22 | CH4: Noise Channel | |
| KEBO | FR PPU-clock source (LCD-on arm) | not | LCD Output | |
| KEDY | Mux selector complement (LCDC.7=0 arm) | not | LCD Output | |
| KEJU | CH3 divider bit 8 | tffnl | CH3: Wave Channel | |
| KEMU | CH3 divider bit 3 | tffnl | CH3: Wave Channel | |
| KENO | CH3 divider bit 7 | tffnl | CH3: Wave Channel | |
| KEPA | CH3 divider bit 5 | tffnl | CH3: Wave Channel | |
| KESE | CH3 divider ripple inverter (bits 8–10) | not_x1 | CH3: Wave Channel | |
| KETU | CH4 LFSR state bit | dffr | CH4: Noise Channel | |
| KEZA | CH3 divider bit 9 | tffnl | CH3: Wave Channel | |
| KOFO | FR pad driver | not | LCD Output | |
| KOMU | CH4 LFSR state bit | dffr | CH4: Noise Channel | |
| KONY | CH4 prescaler-clock gate inverter stage | not_x1 | CH4: Noise Channel | |
| KUNU | CH3 divider bit 2 | tffnl | CH3: Wave Channel | |
| KUPA | FR source (AO22 LCDC.7 mux) | ao22 | LCDC Structure | LCD Output |
| KUPE | CH3 divider bit 1 | tffnl | CH3: Wave Channel | |
| KUTA | CH4 LFSR state bit | dffr | CH4: Noise Channel | |
| KUTU | CH3 divider bit 0 | tffnl | CH3: Wave Channel | |
| KUZY | CH4 LFSR state bit | dffr | CH4: Noise Channel | |
| KYGU | CH3 divider bit 4 | tffnl | CH3: Wave Channel | |
| KYKO | CH3 divider load enable (bits 0–3) | not_x1 | CH3: Wave Channel | |
| KYKU | CH4 prescaler-clock gate (ch4_fdis OR jeso_512k) | or2 | CH4: Noise Channel | |
| KYMO | CPL pad driver | not | LCD Output | |
| KYNA | CH1 divider bit 3 | tffnl | CH1/CH2: Pulse Channels | |
| KYPE | CH1 divider ripple inverter (bits 4–7) | not_x1 | CH1/CH2: Pulse Channels | |
| KYRU | CH3 divider ripple inverter (bits 4–7) | not_x1 | CH3: Wave Channel | |
| KYVO | Envelope counter-at-max detector | and3 | Sweep and Envelope | |
| KYWY | CH4 LFSR state bit | dffr | CH4: Noise Channel | |
| LACE | BGP bit-4 read driver | not_if0 | Register Reads | |
| LAFO | LY bit 7 | dffr | Line Counters | Register Reads |
| LAJU | OBP1 bit-0 read driver | not_if0 | Register Reads | |
| LALU | STAT interrupt latch (IF bit 1) | dffsr | STAT Interrupts | Interrupt Dispatch, The IF Register |
| LAMA | LY reset | nor2 | Line Counters | Register Reads, Mode Transitions, CPU-Visible Mode Boundaries |
| LAPE | Mode 3: The Sprite Pipeline | |||
| LARA | OAM DMA | |||
| LARY | BGP bit-7 read driver | not_if0 | Register Reads | |
| LAVY | OAM DMA | |||
| LAXU | Fetch counter bit 0 | dffr | The Clock Tree | Mode 3: The BG Pipeline, Scanline and Frame Timing, Races |
| LEBA | OBP1 bit-6 read driver | not_if0 | Register Reads | |
| LEBO | Fetch-counter gated clock | nand2 | Mode 3: The BG Pipeline | Scanline and Frame Timing |
| LEFE | Mode 3: The Sprite Pipeline | |||
| LEGU | Mode 3: The BG Pipeline | |||
| LELU | OBP1 bit-7 read driver | not_if0 | Register Reads | |
| LEMA | LY bit 5 | dffr | Line Counters | |
| LEPA | OBP1 bit-1 read driver | not_if0 | Register Reads | |
| LESU | Mode 3: The Sprite Pipeline | |||
| LESY | Mode 3: The Sprite Pipeline | |||
| LEXA | LY bit 2 | dffr | Line Counters | |
| LOBE | BGP bit-3 read driver | not_if0 | Register Reads | |
| LOBY | POKY reset driver | not_x1 | Rendering Mode Control | Mode 3: The BG Pipeline |
| LODE | OBP1 bit-2 read driver | not_if0 | Register Reads | |
| LODY | BGP bit-6 read driver | not_if0 | Register Reads | |
| LOGO | DMA bus-read window | not_x1 | OAM DMA | |
| LOKA | WY bit-3 read driver | not_if0 | Register Reads | |
| LOKY | OAM DMA | |||
| LOLE | WX bit-3 read driver | not_if0 | Register Reads | |
| LOMA | Mode 3: The BG Pipeline | Races | ||
| LONY | Tile-fetch latch | nand_latch | Rendering Mode Control | Scanline and Frame Timing |
| LOPE | IF[0] capture (VBlank) | dffsr | Scanline and Frame Timing | The IF Register |
| LOTA | Mode 3: The Sprite Pipeline | |||
| LOTE | = NOT(MUMY) — OBP1 read drivers' ena_n | not_x1 | Register Reads | |
| LOVA | WX bit-0 read driver | not_if0 | Register Reads | |
| LOVU | LY bit 4 | dffr | Line Counters | |
| LOZE | Mode 3: The BG Pipeline | Mode 3: The Sprite Pipeline | ||
| LUGA | OBP1 bit-5 read driver | not_if0 | Register Reads | |
| LUKY | OBP1 bit-4 read driver | not_if0 | Register Reads | |
| LUMA | dma_addr_ext driver | not_x1 | OAM DMA | |
| LUNA | Temp-latch enable chain | not / nand3 | Mode 3: The BG Pipeline | Races |
| LURY | Rendering Mode Control | |||
| LUZO | Mode 3: The BG Pipeline | |||
| LYCO | LY bit-2 read driver | not_if0 | Register Reads | |
| LYDO | LY bit 3 | dffr | Line Counters | |
| LYFA | Register Writes | |||
| LYHA | Line Counters | Mode Transitions | ||
| LYKA | BGP bit-5 read driver | not_if0 | Register Reads | |
| LYKU | Mode 3: The Sprite Pipeline | |||
| LYME | Sprite palette pipe output stage (sprite_px_palette) | dffsr | Mode 3: The Sprite Pipeline | |
| LYRY | "Fetch complete" combinational | not_x1 | Mode 3: The BG Pipeline | Mode 3: The Sprite Pipeline |
| LYZA | OBP1 bit-3 read driver | not_if0 | Register Reads | |
| LYZU | Fetch counter bit 0 sample | dffr | The Clock Tree | Rendering Mode Control, Mode 3: The BG Pipeline, Scanline and Frame Timing |
| MACU | Mode 3: The BG Pipeline | |||
| MAKA | OAM DMA | |||
| MARA | WX bit-7 read driver | not_if0 | Register Reads | |
| MASO | Mode 3: The Sprite Pipeline | |||
| MATO | LY bit 6 | dffr | Line Counters | |
| MATU | OAM DMA | |||
| MECO | FR PPU-clock feeder | (DFF chain) | LCD Output | |
| MEDA | LCD vertical-sync source | dffr | The Clock Tree | Line Counters, LCD Output, Mode Transitions |
| MEFU | Mode 3: The Sprite Pipeline | |||
| MEGA | WY bit-4 read driver | not_if0 | Register Reads | |
| MEGU | Mode 3: The BG Pipeline | |||
| MEHE | NUNU clock buffer | not | Window Control | |
| MEKE | Reload enable chain | not / nand3 | The Timer | |
| MELE | WX bit-4 read driver | not_if0 | Register Reads | |
| MERA | WY bit-7 read driver | not_if0 | Register Reads | |
| MERY | Wrap detect | nor2 | The Timer | |
| MESU | Fetch counter bit 1 | dffr | Mode 3: The BG Pipeline | Scanline and Frame Timing |
| METE | Mode 3: The BG Pipeline | Races | ||
| MEVE | Mode 3: The Sprite Pipeline | |||
| MEXU | The Timer | |||
| MOBA | Wrap capture | dffr | The Timer | HALT and EI, The IF Register |
| MOCE | Self-stop condition | nand3 | The Clock Tree | Mode 3: The BG Pipeline, Mode 3: The Sprite Pipeline, Post-Boot State |
| MODA | Sprite attribute pipe stage | dffsr | Mode 3: The Sprite Pipeline | |
| MODU | Mode 3: The BG Pipeline | |||
| MOFO | Mode 3: The Sprite Pipeline | |||
| MOJU | Mode 3: The BG Pipeline | |||
| MOKA | OBP1 palette AO2222 combiner | ao2222 | LCD Output | |
| MOKO | WX bit-2 read driver | not_if0 | Register Reads | |
| MOLU | DMA read enable (AND2 with ppu_rd) | and2 | Register Reads | |
| MORY | DMA-vs-CPU address MUX select source | nand2 | OAM DMA | |
| MOSU | Window trigger | not_x2 | Mode 3: The BG Pipeline | Window Control, Mode Transitions |
| MOXE | ALET complement (fine-scroll path) | not_x1 | The Clock Tree | Mode 3: The BG Pipeline, Races |
| MUDE | LX reset | nor2 | Line Counters | |
| MUFE | WX bit-5 read driver | not_if0 | Register Reads | |
| MUKA | WX bit-1 read driver | not_if0 | Register Reads | |
| MUKU | Mode 3: The BG Pipeline | |||
| MULY | WX bit-6 read driver | not_if0 | Register Reads | |
| MUMY | OBP1 read enable (AND2 with ppu_rd) | and2 | Register Reads | |
| MURE | s_pad (VSYNC) driver | not_x1 | LCD Output | |
| MUWY | LY bit 0 | dffr | Line Counters | Register Reads, Mode 2: OAM Scan, Mode Transitions |
| MYDE | Mode 3: The BG Pipeline | |||
| MYJY | Mode 3: The BG Pipeline | |||
| MYRO | LY bit 1 | dffr | Line Counters | |
| MYTA | FRAME_END capture | dffr | The Clock Tree | Line Counters, Rendering Mode Control, Mode Transitions |
| MYTE | OAM DMA | |||
| MYTU | Mode 3: The Sprite Pipeline | |||
| MYVO | ALET complement | not_x1 | The Clock Tree | Mode 3: The BG Pipeline, Scanline and Frame Timing, Races |
| MYZO | Mode 3: The Sprite Pipeline | |||
| NAFY | Mode 3: The BG Pipeline | Races, Post-Boot State | ||
| NAPO | The Clock Tree | |||
| NASA | Mode 3: The BG Pipeline | |||
| NATY | Mode 3: The Sprite Pipeline | |||
| NAXY | OAM DMA | |||
| NAZE | Window Control | |||
| NEDA | Mode 3: The BG Pipeline | |||
| NEFO | Mode 3: The BG Pipeline | |||
| NELE | WY decode completion | not / nand5 / not | Window Control | |
| NEPO | Mode 3: The BG Pipeline | |||
| NERU | LY=0 decode | nor8 | Line Counters | Mode Transitions |
| NETA | VURY enable producer (tile-data stages) | and2 | Mode 3: The BG Pipeline | |
| NEVU | Window Control | |||
| NOCU | in_window driver | not | Mode 3: The BG Pipeline | |
| NOFU | Mode 3: The BG Pipeline | Races | ||
| NOGY | WX register match decoder | nand5 | Window Control | |
| NOJO | Window Control | |||
| NOKO | FRAME_END decode | and4 | Line Counters | Rendering Mode Control, Mode Transitions |
| NOLO | OAM DMA | |||
| NOPA | Window mode | dffr | The Clock Tree | Window Control, Scanline and Frame Timing, Races |
| NOZO | Mode 3: The BG Pipeline | |||
| NUDU | Mode 3: The BG Pipeline | |||
| NUFA | Window Control | |||
| NUGA | The Timer | |||
| NUKE | Sprite attribute pipe stage | dffsr | Mode 3: The Sprite Pipeline | |
| NUKO | WX match | not | Mode 3: The BG Pipeline | Window Control |
| NULY | Sprite-visibility mask | nor2 | Mode 3: The Sprite Pipeline | |
| NUNU | WX-match capture 2 | dffr | Window Control | |
| NUNY | Window-trigger pulse | and2 | Window Control | |
| NUPA | Window Control | |||
| NURA | BGP palette AO2222 combiner | ao2222 | LCD Output | |
| NURO | Mode 3: The Sprite Pipeline | |||
| NUVY | DMA bit-7 read driver | not_if1 | Register Reads | |
| NYBO | IF[2] capture | dffsr | The Timer | The IF Register |
| NYDU | NUGA pre-wrap capture | dffr | The Timer | |
| NYDY | Mode 3: The BG Pipeline | Races | ||
| NYFO | Trigger buffer | not / not | Window Control | |
| NYGO | = NOT(MOLU) — DMA read-enable inverter stage 1 | not_x1 | Register Reads | |
| NYKA | Fetch-complete capture | dffr | The Clock Tree | Mode 3: The BG Pipeline, Window Control, Scanline and Frame Timing |
| NYLU | Mode 3: The Sprite Pipeline | |||
| NYPE | The Clock Tree | Line Counters, Rendering Mode Control, LCD Output | ||
| NYVA | Fetch counter bit 2 | dffr | Mode 3: The BG Pipeline | Scanline and Frame Timing, Races |
| NYXU | BG fetch counter reset | nor3 | Rendering Mode Control | Mode 3: The BG Pipeline, Mode 3: The Sprite Pipeline, Window Control |
| NYZE | Match capture (even) | dffr | The Clock Tree | Rendering Mode Control, Mode 3: The BG Pipeline, Scanline and Frame Timing |
| PABA | BGP bit-1 read driver | not_if0 | Register Reads | |
| PAFU | WY register match decoder | nand5 | Window Control | |
| PAGA | Window Control | |||
| PAGO | STAT Interrupts | |||
| PAHA | ROXY set driver | not_x1 | Rendering Mode Control | Mode 3: The BG Pipeline |
| PAHO | PX-bit-3 capture | dffr | Rendering Mode Control | LCD Output, Scanline and Frame Timing, Post-Boot State |
| PALO | WY bits 4–7 + LCDC.5 decode | nand5 | Window Control | |
| PALU | Sprite attribute pipe stage | dffsr | Mode 3: The Sprite Pipeline | |
| PALY | LY==LYC comparator | not_x1 | STAT Interrupts | CPU-Visible Mode Boundaries, HALT and EI |
| PANE | DMA bit-3 read driver | not_if1 | Register Reads | |
| PANY | RYFA data driver (window/fine-scroll NOR) | nor2 | Mode 3: The BG Pipeline | Mode 3: The Sprite Pipeline, Window Control |
| PARE | DMA bit-4 read driver | not_if1 | Register Reads | |
| PARU | Mode 1 condition | not_x1 | STAT Interrupts | |
| PASO | Fine-counter reset | nor2 | Mode 3: The BG Pipeline | Post-Boot State |
| PATY | Pixel-mux output | or3 | Mode 3: The Sprite Pipeline | LCD Output |
| PEBA | Mode 3: The Sprite Pipeline | |||
| PEBO | Window Control | |||
| PECU | Fine-counter gated clock | nand2 | Mode 3: The BG Pipeline | |
| PEDA | The Timer | |||
| PEFO | Mode 3: The Sprite Pipeline | |||
| PEFU | Mode 3: The Sprite Pipeline | |||
| PELA | WY bit-5 read driver | not_if0 | Register Reads | |
| PERO | Palette-output multiplexer signal | combinational | Palette Latches | LCD Output |
| PERU | The Timer | |||
| PEZO | Window Control | |||
| PODA | WY bit-1 read driver | not_if0 | Register Reads | |
| POFO | STAT bit-4 read driver (mode-1 enable, from rufo_n) | not_if0 | Register Reads | |
| POFY | Loop inverter + feedback | not_x1 | LCD Output | Mode Transitions, Post-Boot State |
| POGU | cpg_pad (clock pulse gate) driver | not_x1 | LCD Output | |
| POHU | Match comparator | not_x1 | Mode 3: The BG Pipeline | |
| POKA | BG-pixel combine | nor3 | LCD Output | Mode 3: The Sprite Pipeline |
| POKY | Pixel-pipe data ready | nor_latch | Rendering Mode Control | Mode 3: The BG Pipeline, Window Control, Scanline and Frame Timing |
| POLO | WY bit-6 read driver | not_if0 | Register Reads | |
| POLY | DMA bit-0 read driver | not_if1 | Register Reads | |
| POME | Loop AVAP gate | nor2 | LCD Output | Mode Transitions |
| POMO | Window Control | |||
| POPU | VBlank capture | dffr | The Clock Tree | Line Counters, Rendering Mode Control, Mode Transitions |
| PORE | Mode 3: The BG Pipeline | |||
| PORY | NYKA pipeline stage | dffr | The Clock Tree | Mode 3: The BG Pipeline, Window Control, Scanline and Frame Timing |
| POTE | STAT bit-6 read driver (LYC enable, from rugu_n) | not_if0 | Register Reads | |
| POVA | Match pulse | and2 | Rendering Mode Control | Register Reads, Mode 3: The BG Pipeline, LCD Output |
| POVY | The Timer | |||
| POWU | OAM and VRAM Access | OAM DMA | ||
| PUDU | Mode 3: The Sprite Pipeline | |||
| PUFY | LYC bit-7 read driver | not_if0 | Register Reads | |
| PUKU | Window hit-and-latch | nor2 / nor3 | Window Control | |
| PUKY | WX-match decode | nand5 / not / nand5 / not | Window Control | |
| PUNU | WY bit-0 read driver | not_if0 | Register Reads | |
| PUPU | Register Writes | |||
| PUSY | = NOT(NYGO) — DMA read drivers' active-high ena | not_x1 | Register Reads | |
| PUXA | Match capture (odd) | dffr | The Clock Tree | Rendering Mode Control, Mode 3: The BG Pipeline, Scanline and Frame Timing |
| PUZO | STAT bit-3 read driver (mode-0 enable, from roxe_n) | not_if0 | Register Reads | |
| PYBO | Mode 3: The BG Pipeline | LCD Output | ||
| PYCO | WX-match capture 1 | dffr | Window Control | |
| PYGO | PORY pipeline stage | dffr | The Clock Tree | Rendering Mode Control, Mode 3: The BG Pipeline, Scanline and Frame Timing |
| PYGU | WY bit-2 read driver | not_if0 | Register Reads | |
| PYJO | Mode 3: The Sprite Pipeline | |||
| PYJU | Tile-index bit-7 capture DFF | dffr_cc_q | Mode 3: The BG Pipeline | |
| PYNU | Window-armed latch | nor_latch | Register Reads | Mode 3: The BG Pipeline, Window Control |
| RACE | LYC bit-4 read driver | not_if0 | Register Reads | |
| RACO | OAM and VRAM Access | |||
| RAGE | The Timer | |||
| RAHU | OAM and VRAM Access | |||
| RAJY | BG plane A output (LCDC.0 gated) | and2 | LCDC Structure | Mode 3: The Sprite Pipeline, LCD Output |
| RALU | Mode 3: The BG Pipeline | |||
| RALY | DMA bit-5 read driver | not_if1 | Register Reads | |
| RAMA | Mode 3: The Sprite Pipeline | |||
| RAMU | Mode 3: The Sprite Pipeline | |||
| RAPE | STAT Interrupts | |||
| RARO | BGP bit-0 read driver | not_if0 | Register Reads | |
| RATE | The Timer | |||
| RAVO | LD1 / LD0 drivers | not | Register Reads | LCD Output |
| RAZU | LYC bit-2 read driver | not_if0 | Register Reads | |
| REDO | BGP bit-2 read driver | not_if0 | Register Reads | |
| REDY | LYC bit-3 read driver | not_if0 | Register Reads | |
| REFE | STAT Interrupts | |||
| REGA | TIMA bits 0–7 | tffnl (toggle FF with load) | The Timer | |
| REJO | WY-match frame latch | nor_latch | Mode 3: The BG Pipeline | Window Control, Scanline and Frame Timing |
| REMA | DMA bit-2 read driver | not_if1 | Register Reads | |
| REMY | LD0 pad driver | not_x2 | Register Reads | LCD Output |
| RENE | Drain-detect stage 2 | dffr | The Clock Tree | Rendering Mode Control, Mode 3: The BG Pipeline, Mode 3: The Sprite Pipeline |
| REPU | REJO reset | or2 | Window Control | Scanline and Frame Timing |
| RESU | DMA bit-6 read driver | not_if1 | Register Reads | |
| RETU | LYC bit-0 read driver | not_if0 | Register Reads | |
| REVO | OAM and VRAM Access | |||
| REWO | Mode 3: The Sprite Pipeline | |||
| ROBY | Mode 3: The Sprite Pipeline | |||
| ROCO | Window Control | |||
| ROCY | Per-bit data-pad tri-state enable chain | combinational | OAM and VRAM Access | |
| ROFO | DMA bit-1 read driver | not_if1 | Register Reads | |
| ROGA | Fine count bit 1 | dffr | Mode 3: The BG Pipeline | |
| ROGE | WY match | not | Window Control | |
| ROKA | Mode 3: The Sprite Pipeline | |||
| ROMO | SUVU NAND4 input (POKY inverter) | not_x1 | Mode 3: The BG Pipeline | |
| RONE | Drain-gated match aggregate | nand4 | Mode 3: The BG Pipeline | |
| ROPE | = NAND2(SOHA, RYCU) — $FExx decode stage | nand2 | OAM and VRAM Access | |
| ROPO | LY==LYC synced match | dff17 | The Clock Tree | Line Counters, Register Reads, STAT Interrupts |
| ROPY | Mode 3 inverter for VRAM enables | not_x1 | Rendering Mode Control | OAM and VRAM Access |
| RORU | OAM DMA | |||
| ROSA | Sprite attribute pipe stage | dffsr | Mode 3: The Sprite Pipeline | |
| ROSY | Mode 3: The Sprite Pipeline | |||
| ROXE | STAT.3/.4/.5/.6 enables | drlatch_ee | STAT Interrupts | |
| ROXO | Inverted CLKPIPE buffer (fine-scroll clock source) | not_x1 | Mode 3: The BG Pipeline | LCD Output |
| ROXY | Fine-scroll gate | nor_latch | Rendering Mode Control | Mode 3: The BG Pipeline, Scanline and Frame Timing, Post-Boot State |
| ROZE | Fine-counter self-stop | nand3 | Mode 3: The BG Pipeline | |
| RUBU | Fine count bit 2 | dffr | Mode 3: The BG Pipeline | |
| RUBY | The Timer | |||
| RUDA | Mode 3: The Sprite Pipeline | |||
| RUFO | STAT Interrupts | |||
| RUGO | Sprite attribute pipe stage | dffsr | Mode 3: The Sprite Pipeline | |
| RUGU | STAT Interrupts | |||
| RUJU | Loop OR combiner | or3 | LCD Output | |
| RUPO | STAT bit 2 visible latch | nor_latch | STAT Interrupts | |
| RUTA | BG plane B gated by sprite priority | and2 | LCD Output | |
| RUTU | LINE_END capture | dffr | The Clock Tree | Line Counters, Rendering Mode Control, Register Writes |
| RUZE | ST pad buffer | not_x3 | LCD Output | |
| RYCE | One-shot fire | and2 | Mode 3: The BG Pipeline | Mode 3: The Sprite Pipeline |
| RYCU | ROPE input (= NOT(fexx_ffxx_n)) | not_x1 | OAM and VRAM Access | |
| RYDU | Mode 3: The Sprite Pipeline | |||
| RYDY | Window hit | nor3 | Mode 3: The BG Pipeline | Mode 3: The Sprite Pipeline, Window Control |
| RYFA | Drain-detect stage 1 | dffr | Rendering Mode Control | Mode 3: The BG Pipeline, Mode 3: The Sprite Pipeline, Window Control |
| RYFU | BG plane A gated by sprite priority | and2 | Mode 3: The Sprite Pipeline | LCD Output |
| RYKU | Fine count bit 0 | dffr | The Clock Tree | Mode 3: The BG Pipeline |
| RYNO | LCD Output | |||
| RYPO | CP pad driver | not_x3 | LCD Output | |
| RYSA | Mode 3: The BG Pipeline | |||
| RYVE | Register Writes | |||
| SABE | Gated fetch clock | nand2 | Mode 3: The Sprite Pipeline | Scanline and Frame Timing |
| SACU | Pixel pipe shift clock (CLKPIPE) | or2 | Register Writes | Register Reads, Mode 2: OAM Scan, Mode 3: The BG Pipeline |
| SADU | Rendering Mode Control | Register Reads, CPU-Visible Mode Boundaries | ||
| SADY | Mode 3: The BG Pipeline | |||
| SAJA | Mode 3: The Sprite Pipeline | |||
| SANU | LINE_END decode | and4 | The Clock Tree | Line Counters, Rendering Mode Control, Mode Transitions |
| SARY | WY-match sampler | dffr | Window Control | |
| SASY | STAT bit-5 read driver (mode-2 enable, from refe_n) | not_if0 | Register Reads | |
| SATA | Sprite attribute pipe stage | dffsr | Mode 3: The Sprite Pipeline | |
| SAVY | PX bit 1 | dffr | Mode 3: The BG Pipeline | |
| SAXO | LX bit 0 | dffr | The Clock Tree | Line Counters, Races, Post-Boot State |
| SAZO | OAM and VRAM Access | |||
| SEBA | Fetch-counter TULY capture stage 3 | dffr | Rendering Mode Control | Mode 3: The Sprite Pipeline, Scanline and Frame Timing, Races |
| SEBY | VRAM read-latch enable | not_x1 | OAM and VRAM Access | |
| SECA | TAKA set net | nor3 | Mode 3: The Sprite Pipeline | Mode Transitions |
| SEGA | Mode 3: The Sprite Pipeline | |||
| SEGO | STAT bit-2 read driver (LYC match, from rupo_n) | not_if1 | Register Reads | |
| SEGU | Buffered CLKPIPE | not_x4 | Mode 3: The BG Pipeline | Mode 3: The Sprite Pipeline, Scanline and Frame Timing, Post-Boot State |
| SEKO | Tile-boundary drain detector | nor2 | Mode 3: The BG Pipeline | Mode 3: The Sprite Pipeline, Window Control |
| SELA | Mode Transitions | STAT Interrupts | ||
| SELE | Mode 3: The Sprite Pipeline | |||
| SEMO | Mode 3: The Sprite Pipeline | |||
| SEMU | CP pixel-clock source (pre-buffer) | or2 | LCD Output | |
| SEPA | STAT write enable | and2 | Register Writes | |
| SERE | CPU data-bus enable stage 2 | and2 | OAM and VRAM Access | |
| SETU | Mode 3: The BG Pipeline | |||
| SOBO | Mode 3: The BG Pipeline | |||
| SOBU | Fetch request | dffr | Mode 3: The BG Pipeline | Mode 3: The Sprite Pipeline, Scanline and Frame Timing |
| SOCY | Window-halt gate | not_x1 | Mode 3: The BG Pipeline | Window Control, Post-Boot State |
| SOGU | TIMA clock gate | nor2 | The Timer | |
| SOHA | ROPE input (= NOT(ffxx)) | not_x1 | OAM and VRAM Access | |
| SOHU | Mode 3: The BG Pipeline | LCD Output | ||
| SOHY | Write-strobe driver | nand2 | OAM and VRAM Access | |
| SOKA | Mode 3: The Sprite Pipeline | |||
| SOMY | Sprite attribute pipe stage | dffsr | Mode 3: The Sprite Pipeline | |
| SONO | 1 MHz LINE_END capture clock (TALU complement) | not_x1 | The Clock Tree | Line Counters, Rendering Mode Control, Register Writes |
| SOVU | STAT Interrupts | |||
| SOWO | TEKY re-trigger block (TAKA inversion) | not_x1 | Mode 3: The Sprite Pipeline | |
| SOZU | Fine-count bit 2 match | xnor | Mode 3: The BG Pipeline | |
| STOP | OAM DMA | The Timer | ||
| SUBO | STAT Interrupts | |||
| SUDA | Trigger one-shot partner | dffr | Mode 3: The BG Pipeline | Mode 3: The Sprite Pipeline, Scanline and Frame Timing |
| SUDE | LX bit 4 | dffr | Line Counters | |
| SUHA | Per-bit SCX match | xnor | Mode 3: The BG Pipeline | |
| SUKO | Condition combining gate | ao2222 | STAT Interrupts | The IF Register |
| SUNY | Mode 3: The Sprite Pipeline | |||
| SUTO | Mode 3: The Sprite Pipeline | |||
| SUVU | Startup-/window-restart NAND4 (TAVE input) | nand4 | Rendering Mode Control | Mode 3: The BG Pipeline |
| SUZU | Window-activation trigger | not | Mode 3: The BG Pipeline | Window Control |
| SYBE | Mode 3: The BG Pipeline | |||
| SYBY | Fine-count bit 1 match | xnor | Mode 3: The BG Pipeline | |
| SYGU | The Clock Tree | LCD Output, Races | ||
| SYLO | RYDY first inverter (consumer-chain stage 1) | not | Mode 3: The BG Pipeline | Window Control |
| SYSY | OAM and VRAM Access | |||
| TACA | Mode 3: The BG Pipeline | |||
| TACE | Global DAC-off term — amp_en_n = AND4 of the four ch*_amp_en_n | and4 | CH3: Wave Channel | |
| TADE | BG plane B output (LCDC.0 gated) | and2 | LCDC Structure | LCD Output |
| TADY | VOGA + PX-counter reset | nor2 | Rendering Mode Control | Mode 3: The BG Pipeline, Window Control, Mode Transitions |
| TAHA | LX bit 5 | dffr | Line Counters | |
| TAKA | Fetch-running latch | nand_latch | Mode 3: The BG Pipeline | Mode 3: The Sprite Pipeline, Window Control, Mode Transitions |
| TAKO | Mode 3: The BG Pipeline | |||
| TALU | 1 MHz LX counter clock | not_x4 | The Clock Tree | Line Counters, Register Writes, Register Reads |
| TAME | Self-stop | nand2 | Mode 3: The Sprite Pipeline | |
| TAPA | Mode 2 condition | and2 | STAT Interrupts | |
| TAPE | The Timer | |||
| TAPU | CUPA driver stage 2 | not | Register Writes | |
| TARU | Mode 0 condition | and2 | STAT Interrupts | |
| TAVA | SOBU clock gate | not_x1 | Mode 3: The Sprite Pipeline | |
| TAVE | Startup / window-restart trigger | not | Mode 3: The BG Pipeline | Mode 3: The Sprite Pipeline |
| TAXY | CPU-bus-phase strobe assembly | combinational | OAM and VRAM Access | |
| TEBY | STAT bit-0 read driver (from SADU) | not_if1 | Register Reads | |
| TECY | The Timer | |||
| TEGO | = NOT(VAMA) — $FF49 active-high select | not_x2 | Register Reads | |
| TEKO | The Timer | |||
| TEKY | X-match trigger | and4 | Mode 2: OAM Scan | Mode 3: The BG Pipeline, Mode 3: The Sprite Pipeline, Scanline and Frame Timing |
| TELU | LX bit 3 | dffr | Line Counters | |
| TEPA | NOT(mode3) | not_x1 | Rendering Mode Control | |
| TEPO | BGP write enable sub-signal | combinational | Register Writes | Palette Latches |
| TEPY | = NOT(VUSO) — BGP read drivers' ena_n | not_x1 | Register Reads | |
| TESE | Sprite fetch counter bit 2 (SFETCH_S2) | dffr | Mode 3: The Sprite Pipeline | |
| TEVO | Tile-boundary / restart trigger | or3 | Mode 3: The BG Pipeline | Mode 3: The Sprite Pipeline, Window Control, Mode Transitions |
| TOBA | Mode 3: The BG Pipeline | LCD Output | ||
| TOBE | FF41 read enable | and2 | Register Reads | |
| TOBU | TULY capture chain | dffr | Rendering Mode Control | Mode 3: The Sprite Pipeline, Scanline and Frame Timing, Races |
| TOCA | Upper-nibble clock | not | Mode 3: The BG Pipeline | Races |
| TOFU | Video reset | not | Rendering Mode Control | Mode 3: The BG Pipeline, LCD Output, Mode Transitions |
| TOLE | CPU data-bus enable stage 1 | combinational | OAM and VRAM Access | |
| TOLU | mode1 inverter | not_x1 | STAT Interrupts | The IF Register |
| TOMU | RYDY second inverter (consumer-chain stage 2) | not | Mode 3: The BG Pipeline | Window Control |
| TOMY | Mode 3: The BG Pipeline | |||
| TORY | Mode 3: The Sprite Pipeline | |||
| TOXE | Fetch counter bits 0–2 | dffr | The Clock Tree | Mode 3: The Sprite Pipeline |
| TOZA | OAM DMA | |||
| TUHU | PX bits 4–7 | dffr | Mode 3: The BG Pipeline | |
| TUKU | Sprite-trigger window block (consumer-chain stage 3) | not | Mode 3: The Sprite Pipeline | Window Control |
| TUKY | Mode 3: The BG Pipeline | |||
| TULY | Sprite fetch counter bit 1 (SFETCH_S1) | dffr | Mode 3: The Sprite Pipeline | |
| TUTO | OAM and VRAM Access | |||
| TUVA | Inverter pair | not | STAT Interrupts | The IF Register |
| TUVO | OAM DMA | Mode 3: The Sprite Pipeline | ||
| TUXY | Window-trigger NAND (SUZU input) | nand2 | Mode 3: The BG Pipeline | Window Control |
| TYCO | Mode 3: The Sprite Pipeline | |||
| TYFA | CLKPIPE gate | and3 | Mode 3: The BG Pipeline | Scanline and Frame Timing, Post-Boot State |
| TYFO | Done decode B | dffr | Mode 3: The Sprite Pipeline | Scanline and Frame Timing |
| TYHO | OAM DMA | |||
| TYJY | OAM and VRAM Access | |||
| TYNO | Done decode A | nand3 | Mode 3: The Sprite Pipeline | |
| TYPO | LX bit 1 | dffr | Line Counters | |
| TYRY | LX bit 6 | dffr | Line Counters | Post-Boot State |
| TYTA | Mode 3: The Sprite Pipeline | |||
| UBAL | CUPA driver stage 3 | muxi | Register Writes | |
| UBUL | IF[3] capture (Serial) | dffsr | The IF Register | |
| UCOB | The Timer | |||
| UKAP | TAC mux chain | muxi | The Timer | |
| UKUP | reg_div16 bits 0–15 | dff | The Timer | |
| ULAK | IF[4] capture (Joypad) | dffsr | The IF Register | |
| UMOB | CPL audio-clock source (LCD-off arm) | (external) | LCD Output | |
| UPOF | The Timer | |||
| USEC | FR audio-clock source (LCD-off arm) | (external) | LCD Output | |
| UVYT | OAM DMA | |||
| VAFE | LYC bit-6 read driver | not_if0 | Register Reads | |
| VAFO | Mode 3: The Sprite Pipeline | |||
| VAHA | LCDC bit-6 read driver | not_if0 | Register Reads | |
| VAMA | $FF49 address decode | nand5 | Register Reads | |
| VANU | Mode 3: The Sprite Pipeline | |||
| VARE | Mode 3: The Sprite Pipeline | |||
| VARO | = NOT(WAFU) — LY read drivers' ena_n | not_x1 | Register Reads | |
| VARY | = NOT(WOFA) — $FF41 active-high select | not_x2 | Register Reads | Register Writes |
| VATO | LCDC bit-5 read driver | not_if0 | Register Reads | |
| VAVA | Priority mask pipe MSB / LSB | dffsr | Mode 3: The Sprite Pipeline | LCD Output |
| VAVE | NOT(TOBE) — STAT enable-bit read gate | not_x1 | Register Reads | |
| VAZU | LYC bit-5 read driver | not_if0 | Register Reads | |
| VCLK | LCD Output | |||
| VEGA | LY bit-0 read driver | not_if0 | Register Reads | |
| VEKU | Mode 3: The Sprite Pipeline | |||
| VELY | BGP write enable | and2 | Register Writes | Register Reads |
| VENA | 1 MHz toggle divider | dffr | The Clock Tree | Line Counters, Register Writes, Scanline and Frame Timing |
| VEVY | LCDC.6 → ~ma10 tri-state driver | not_if0 | Mode 3: The BG Pipeline | |
| VEZO | Mask pipe (LSB end) | dffsr | Mode 3: The Sprite Pipeline | |
| VOCA | = NOT(WORU) — $FF40 active-high select | not_x2 | Register Reads | Register Writes |
| VOGA | H-Blank capture DFF | dffr | The Clock Tree | Rendering Mode Control, Mode 3: The BG Pipeline, Mode Transitions |
| VOJO | LYC bit-1 read driver | not_if0 | Register Reads | |
| VOKE | LCDC bit-4 read driver | not_if0 | Register Reads | |
| VOMY | = NOT(WAXU) — WY read drivers' ena_n | not_x1 | Register Reads | |
| VONU | Fetch-counter TULY capture stage 2 | dffr | Rendering Mode Control | Mode 3: The Sprite Pipeline, Scanline and Frame Timing, Races |
| VOSA | Sprite attribute pipe stage | dffsr | Mode 3: The Sprite Pipeline | |
| VOTO | Mode 3: The Sprite Pipeline | |||
| VOTY | STAT Interrupts | The IF Register | ||
| VUJO | Window Control | |||
| VUMO | Sprite attribute pipe stage | dffsr | Mode 3: The Sprite Pipeline | |
| VUMY | = NOT(WAGE) — $FF4B active-high select | not_x2 | Register Reads | |
| VUPY | Mode 3: The Sprite Pipeline | |||
| VURY | VUZA → ~ma12 tri-state driver | not_if1 | Mode 3: The BG Pipeline | |
| VUSA | Fetch-done aggregate | or2 | Mode 3: The Sprite Pipeline | |
| VUSO | BGP read enable (AND2 with ppu_rd) | and2 | Register Reads | |
| VUZA | TILE_SEL XOR tile-index MSB | nor2 | Mode 3: The BG Pipeline | |
| VYBO | Operational per-dot gate | nor3 | Mode 3: The BG Pipeline | Mode 3: The Sprite Pipeline, Scanline and Frame Timing, Post-Boot State |
| VYCU | = NOT(WYZE) — WX read drivers' ena_n | not_x1 | Register Reads | |
| VYGA | = NOT(WYVO) — $FF4A active-high select | not_x2 | Register Reads | |
| VYNE | LY bit-4 read driver | not_if0 | Register Reads | |
| VYNO | Window line counter LSB | dffr | Window Control | |
| VYPU | int_vbl driver | not_x3 | The IF Register | |
| VYRE | LCDC read enable (AND2 with ppu_rd) | and2 | Register Reads | |
| VYSA | Mode 3: The Sprite Pipeline | |||
| VYXE | LCDC Structure | LCD Output | ||
| VYZO | LX bit 2 | dffr | Line Counters | |
| WAFU | LY read enable (AND2 with ppu_rd) | and2 | Register Reads | |
| WAGE | $FF4B address decode | nand5 | Register Reads | |
| WAGO | 8×16 half-select | xor | Mode 3: The Sprite Pipeline | |
| WAHE | OAM wordline-driver precharge (oam_wldrv_precharge_n = NOT(WUJY)) | not_x3 | OAM and VRAM Access | |
| WAMA | LY bit-5 read driver | not_if0 | Register Reads | |
| WAME | OAM output-enable inverter | not_x2 | OAM and VRAM Access | |
| WARE | SCY bit-0 read driver | not_if0 | Register Reads | |
| WARU | LCDC write enable | and2 | Register Writes | Mode 3: The BG Pipeline |
| WATE | $FF46 address decode | nand5 | Register Reads | |
| WAVO | LY bit-6 read driver | not_if0 | Register Reads | |
| WAVU | $FF43 address decode | nand5 | Register Reads | |
| WAXU | WY read enable (AND2 with ppu_rd) | and2 | Register Reads | |
| WAZO | WUJY input — wordline-precharge timing | or3 | OAM and VRAM Access | |
| WAZY | Window line counter clock | not_x1 | Window Control | |
| WEBA | Mode 3: The Sprite Pipeline | |||
| WEBU | $FF42 address decode | nand5 | Register Reads | |
| WEFY | Mode 3: The Sprite Pipeline | |||
| WEGO | XYMU set driver | or2 | Rendering Mode Control | Mode 3: The BG Pipeline, Mode Transitions |
| WEKU | = NOT(XYLY) — LYC read drivers' ena_n | not_x1 | Register Reads | |
| WELO | Slot-0 X store bit 4 (holds NOT(X)) | drlatch_ee | Mode 3: The Sprite Pipeline | |
| WERA | = NOT(WYBO) — $FF47 active-high select | not_x2 | Register Reads | Register Writes |
| WETA | $FF48 address decode | nand5 | Register Reads | |
| WETY | $FF45 address decode | nand5 | Register Reads | |
| WEWY | Scan counter bit 1 | dffr | Mode 2: OAM Scan | |
| WEXU | LCDC.4 (TILE_SEL) latch | drlatch_ee | LCDC Structure | Mode 3: The BG Pipeline |
| WEZE | LY bit-7 read driver | not_if0 | Register Reads | |
| WODA | Sprite attribute pipe stage | dffsr | Mode 3: The Sprite Pipeline | |
| WODU | Mode 0 condition | and2 | Rendering Mode Control | Register Writes, Register Reads, Mode 3: The BG Pipeline |
| WOFA | $FF41 address decode | nand5 | Register Reads | |
| WOJO | LCD control phase | nor2 | The Clock Tree | |
| WOJU | Slot-0 X comparator bit 4 (XOR vs NOT(PX)) | xor | Mode 3: The Sprite Pipeline | |
| WOJY | LY bit-3 read driver | not_if0 | Register Reads | |
| WOKY | LCDC.6 (WIN_MAP) latch | drlatch_ee | LCDC Structure | Mode 3: The BG Pipeline |
| WONE | OAM DMA | Mode 2: OAM Scan | ||
| WONY | SCX bit-3 read driver | not_if0 | Register Reads | |
| WOPE | Mode 3: The Sprite Pipeline | |||
| WORA | Mode 3: The Sprite Pipeline | |||
| WORU | $FF40 address decode | nand5 | Register Reads | |
| WOSU | WUVU sampler | dffr | The Clock Tree | Scanline and Frame Timing |
| WOTA | Y-compare decoder | nand6 | Mode 2: OAM Scan | |
| WOTE | Slot-0 X store bit 6 (holds NOT(X)) | drlatch_ee | Mode 3: The Sprite Pipeline | |
| WOVU | OAM bitline precharge driver (oam_bl_precharge_n = NOT(COTA)) | not_x2 | OAM and VRAM Access | |
| WOXA | Plane-A pixel-output gate | and2 | LCDC Structure | Mode 3: The Sprite Pipeline |
| WUDA | Mode 2: OAM Scan | |||
| WUFU | OBP0 palette AO2222 combiner | ao2222 | LCD Output | |
| WUFY | Mode 3: The Sprite Pipeline | |||
| WUGA | STAT bit-1 read driver (from XATY) | not_if1 | Register Reads | |
| WUHU | Y-compare carry-chain bit 7 | full_add | Mode 2: OAM Scan | |
| WUJY | = NAND2(WAZO, oam_bl_precharge_n) — wordline-precharge timing | nand2 | OAM and VRAM Access | |
| WUKA | LCDC bit-3 read driver | not_if0 | Register Reads | |
| WUKO | VEVY enable (window tilemap stage) | not_x2 | Mode 3: The BG Pipeline | |
| WUKY | Y-flip term | not_x1 | Mode 3: The Sprite Pipeline | |
| WURU | Sprite attribute pipe stage | dffsr | Mode 3: The Sprite Pipeline | |
| WUTY | Fetch done | not | Mode 2: OAM Scan | Mode 3: The BG Pipeline, Mode 3: The Sprite Pipeline |
| WUVA | LY bit-1 read driver | not_if0 | Register Reads | |
| WUVU | 2 MHz toggle divider | dffr | The Clock Tree | Register Writes, Mode 2: OAM Scan, Scanline and Frame Timing |
| WYBO | $FF47 address decode | nand5 | Register Reads | |
| WYCE | = NOT(VYRE) — LCDC read drivers' ena_n | not_x1 | Register Reads | |
| WYFU | Sprite attribute pipe stage | dffsr | Mode 3: The Sprite Pipeline | |
| WYHO | Mode 3: The Sprite Pipeline | |||
| WYJA | OAM write enable (combined) | ao21 | OAM and VRAM Access | OAM DMA, CPU-Visible Mode Boundaries |
| WYJU | LCDC bit-2 read driver | not_if0 | Register Reads | |
| WYLE | $FF44 address decode | nand5 | Register Reads | |
| WYMO | LCDC Structure | |||
| WYNO | OAM DMA | Mode 2: OAM Scan | ||
| WYPO | LCDC bit-0 read driver | not_if0 | Register Reads | |
| WYSO | Mode 3: The Sprite Pipeline | |||
| WYVO | $FF4A address decode | nand5 | Register Reads | |
| WYZA | Slot-0 X comparator bit 6 (XOR vs NOT(PX)) | xor | Mode 3: The Sprite Pipeline | |
| WYZE | WX read enable (AND2 with ppu_rd) | and2 | Register Reads | |
| XAFO | LCDC.3 (BG_MAP) latch | drlatch_ee | LCDC Structure | Mode 3: The BG Pipeline |
| XAFU | OAM DMA | Mode 2: OAM Scan | ||
| XAGE | Per-slot X match, FEFY group (active-low) | nand3 | Mode 3: The Sprite Pipeline | |
| XAJU | OBP0 bit-4 read driver | not_if0 | Register Reads | |
| XAKO | Slot-0 X store bit 7 (holds NOT(X)) | drlatch_ee | Mode 3: The Sprite Pipeline | |
| XANE | VRAM address tri-state-disable source | nor2 | Rendering Mode Control | OAM and VRAM Access |
| XANO | "PX at terminal count" (inverted XUGU) | not_x1 | Rendering Mode Control | Mode 3: The BG Pipeline, Mode Transitions, STAT Interrupts |
| XAPO | Rendering Mode Control | Mode 2: OAM Scan | ||
| XARO | = NOT(WEBU) — $FF42 active-high select | not_x2 | Register Reads | Register Writes |
| XARY | OBP0 bit-0 read driver | not_if0 | Register Reads | |
| XATY | Rendering Mode Control | Register Reads, CPU-Visible Mode Boundaries | ||
| XAVY | = NOT(WAVU) — $FF43 active-high select | not_x2 | Register Reads | Register Writes |
| XAWO | OBP0 bit-7 read driver | not_if0 | Register Reads | |
| XAXA | OBP0 bit-6 read driver | not_if0 | Register Reads | |
| XAYO | = NOT(WETA) — $FF48 active-high select | not_x2 | Register Reads | |
| XAYU | = NOT(WETY) — $FF45 active-high select | not_x2 | Register Reads | |
| XEBA | Slot-0 match, high-nibble collapse | nor4 | Mode 3: The Sprite Pipeline | |
| XEBU | LCDC bit-7 read driver | not_if0 | Register Reads | |
| XEDA | = NOT(WATE) — $FF46 active-high select | not_x2 | Register Reads | |
| XEDU | VRAM address tri-state enable | not_x2 | OAM and VRAM Access | |
| XEFY | Global sprite shifter parallel-load disable | not_x1 | Mode 3: The Sprite Pipeline | |
| XEGU | Mode 3: The Sprite Pipeline | |||
| XEHO | PX bit 0 | dffr | Mode 3: The BG Pipeline | |
| XENA | "No sprite match" (inverted FEPO) | not_x1 | Rendering Mode Control | Mode 3: The BG Pipeline, Mode Transitions, STAT Interrupts |
| XEPE | Slot-0 X store bit 0 (holds NOT(X)) | drlatch_ee | Mode 3: The Sprite Pipeline | |
| XERO | LCDC bit-1 read driver | not_if0 | Mode 3: The Sprite Pipeline | Register Reads |
| XETE | Sprite attribute pipe stage | dffsr | Mode 3: The Sprite Pipeline | |
| XEZE | Mode 3: The BG Pipeline | |||
| XOBO | OBP0 bit-5 read driver | not_if0 | Register Reads | |
| XOCE | OAM DMA | Mode 2: OAM Scan | ||
| XODO | VID_RST signal | combinational | Register Writes | LCDC Structure, Scanline and Frame Timing |
| XODU | PX bit 2 | dffr | Mode 3: The BG Pipeline | |
| XOFO | PYNU reset (WIN_EN gate) | nand3 | LCDC Structure | Window Control |
| XOGS | Interrupt Dispatch | |||
| XOGY | = NOT(WYLE) — $FF44 active-high select | not_x2 | Register Reads | |
| XOKE | OBP0 bit-1 read driver | not_if0 | Register Reads | |
| XONA | LCDC.7 storage latch | drlatch_ee | Register Writes | LCDC Structure |
| XOTA | WUVU clock | not_x1 | The Clock Tree | |
| XOTE | Mode 3: The Sprite Pipeline | |||
| XOVU | Mode 3: The Sprite Pipeline | |||
| XOZY | = NOT(XUFY) — OBP0 read drivers' ena_n | not_x1 | Register Reads | |
| XUBO | Register Writes | |||
| XUBY | OBP0 bit-3 read driver | not_if0 | Register Reads | |
| XUFY | OBP0 read enable (AND2 with ppu_rd) | and2 | Register Reads | |
| XUGU | Terminal count decode | nand5 | Rendering Mode Control | Mode 3: The BG Pipeline, Window Control, Mode Transitions |
| XUJA | OAM DMA | |||
| XUJY | Sprite-fetch counter term in BYCU | combinational | OAM DMA | Mode 2: OAM Scan, OAM and VRAM Access |
| XUKE | Mode 3: The BG Pipeline | |||
| XULA | Plane-B pixel-output gate | and2 | LCDC Structure | Mode 3: The Sprite Pipeline |
| XUNO | OBP0 bit-2 read driver | not_if0 | Register Reads | |
| XUNY | Slot-0 X store bit 5 (holds NOT(X)) | drlatch_ee | Mode 3: The Sprite Pipeline | |
| XUPY | 2 MHz scan-counter clock (NOT(wuvu_n) = WUVU.q) | not_x2 | The Clock Tree | Register Writes, Mode 2: OAM Scan, Mode 3: The BG Pipeline |
| XURE | Register Writes | |||
| XUSO | OAM Y-offset bit 0 latch (Mode 2) / tile-index bit 0 latch (Mode 3) | dlatch_ee | OAM DMA | Mode 2: OAM Scan, Mode 3: The Sprite Pipeline |
| XYDO | PX bit 3 | dffr | Mode 3: The BG Pipeline | LCD Output, Races |
| XYFY | XOTA complement | not_x1 | The Clock Tree | |
| XYJU | Mode 3: The Sprite Pipeline | |||
| XYKY | OAM DMA | Mode 2: OAM Scan, Mode 3: The Sprite Pipeline | ||
| XYLE | Mode 3: The BG Pipeline | |||
| XYLO | LCDC.1 (OBJ_EN) register bit | drlatch_ee | LCDC Structure | Mode 2: OAM Scan, Mode 3: The Sprite Pipeline |
| XYLY | LYC read enable (AND2 with ppu_rd) | and2 | Register Reads | |
| XYMO | LCDC.2 (OBJ_SIZE) register bit | drlatch_ee | LCDC Structure | Mode 2: OAM Scan, Mode 3: The Sprite Pipeline |
| XYMU | Rendering-mode latch (active-low Mode 3 indicator) | nor_latch | Rendering Mode Control | Register Writes, OAM and VRAM Access, OAM DMA |
| XYSO | Mode 2: OAM Scan | |||
| XYVA | Parallel divider branch | not_x1 | The Clock Tree | |
| XYVO | VBlank decode | and2 | Line Counters | Rendering Mode Control, Mode Transitions |
| YBEZ | Per-slot X match, FOVE group (active-low) | nand3 | Mode 3: The Sprite Pipeline | |
| YBOG | Mode 3: The Sprite Pipeline | |||
| YCEB | OAM DMA | Mode 2: OAM Scan | ||
| YDUG | Slot-0 X match (active-low) | nand3 | Mode 3: The Sprite Pipeline | |
| YDYV | OAM DMA | Mode 2: OAM Scan, Mode 3: The Sprite Pipeline | ||
| YFEL | Scan counter bit 0 (LSB) | dffr | Mode 2: OAM Scan | |
| YFUN | Slot-0 X comparator bit 5 (XOR vs NOT(PX)) | xor | Mode 3: The Sprite Pipeline | |
| YGEM | Per-slot X match, FOVE group (active-low) | nand3 | Mode 3: The Sprite Pipeline | |
| YJEX | Mode 3: The Sprite Pipeline | |||
| YKUA | HALT and EI | |||
| YLAH | Slot-0 X store bit 1 (holds NOT(X)) | drlatch_ee | Mode 3: The Sprite Pipeline | |
| YLOR | OAM DMA | Mode 2: OAM Scan, Mode 3: The Sprite Pipeline | ||
| YLOZ | Per-slot X match, FEFY group (active-low) | nand3 | Mode 3: The Sprite Pipeline | |
| YLYC | OAM A-side / B-side write strobe drivers | per-byte selector | OAM and VRAM Access | OAM DMA |
| YNKW | HALT and EI | |||
| YNYC | OAM and VRAM Access | OAM DMA | ||
| YOII | Interrupt Dispatch | HALT and EI, The IF Register | ||
| YPUK | Slot-0 X comparator bit 7 (XOR vs NOT(PX)) | xor | Mode 3: The Sprite Pipeline | |
| YRUM | OAM DMA | Mode 2: OAM Scan | ||
| YSES | OAM DMA | Mode 2: OAM Scan | ||
| YSEX | OAM DMA | Mode 2: OAM Scan | ||
| YVEL | OAM DMA | Mode 2: OAM Scan | ||
| YZAB | Mode 3: The Sprite Pipeline | |||
| YZOS | Mode 3: The Sprite Pipeline | |||
| ZACW | Interrupt Dispatch | |||
| ZAGO | Mode 2: OAM Scan | |||
| ZAHA | Slot-0 X comparator bit 2 (XOR vs NOT(PX)) | xor | Mode 3: The Sprite Pipeline | |
| ZAIJ | Interrupt Dispatch | HALT and EI | ||
| ZAKO | Slot-0 match, low-nibble collapse | nor4 | Mode 3: The Sprite Pipeline | |
| ZAOC | Interrupt Dispatch | |||
| ZAXE | OAM DMA | Mode 2: OAM Scan | ||
| ZAXY | Buffer stage | not_x2 | The Clock Tree | |
| ZEBA | Slot-0 X comparator bit 1 (XOR vs NOT(PX)) | xor | Mode 3: The Sprite Pipeline | |
| ZECA | OAM DMA | Mode 2: OAM Scan, Mode 3: The Sprite Pipeline | ||
| ZEME | Buffer stage | not_x4 | The Clock Tree | |
| ZEZY | Mode 3: The Sprite Pipeline | |||
| ZFEX | Interrupt Dispatch | |||
| ZIVV | HALT and EI | |||
| ZJJE | ime_pending — "IME pending" SR latch (q drives dmg-sim wire ime_state) | sm83_srlatch_r_n | HALT and EI | |
| ZKOG | Interrupt Dispatch | |||
| ZLOZ | Interrupt Dispatch | |||
| ZOGY | Slot-0 X comparator bit 0 (XOR vs NOT(PX)) | xor | Mode 3: The Sprite Pipeline | |
| ZOKY | Slot-0 X comparator bit 3 (XOR vs NOT(PX)) | xor | Mode 3: The Sprite Pipeline | |
| ZOLA | Slot-0 X store bit 2 (holds NOT(X)) | drlatch_ee | Mode 3: The Sprite Pipeline | |
| ZRSY | RETI / reset gating SR latch (q = zrsy) | sm83_srlatch_r_n | HALT and EI | |
| ZUCA | OAM DMA | Mode 2: OAM Scan | ||
| ZULU | Slot-0 X store bit 3 (holds NOT(X)) | drlatch_ee | Mode 3: The Sprite Pipeline | |
| ZUVE | OAM DMA | Mode 2: OAM Scan | ||
| ZYTY | Mode 3: The Sprite Pipeline | |||
| ZYVE | Mode 3: The Sprite Pipeline | |||
| ZZOM | Interrupt Dispatch |
Appendix B: Timing Constants
Quick reference for the numeric constants used throughout the book. Each value is developed in context in the linked chapter; this table is a lookup aid, not a substitute for the surrounding mechanism.
Clock and timing
| Unit | Value | Definition |
|---|---|---|
| Dot / T-cycle | ≈238.4 ns (dmg-sim: 244,000 ps) | One full master clock cycle (rise + fall) — see Methodology |
| M-cycle | 4 dots | CPU machine cycle |
| ATAL half-cycle | 0.5 dots | Interval between consecutive master-clock edges |
Mode 3 structure
Baseline: SCX=0, no sprites, no window. Developed in BG pipeline and Mode transitions.
| Interval | Dots |
|---|---|
| AVAP → first SACU (startup pipeline) | 7.026 |
| AVAP → WODU (Mode 3 core) | 173.045 |
| WODU → XYMU (VOGA → WEGO → XYMU) | 0.436 |
| AVAP → XYMU (Mode 3 total) | 173.481 |
| SACU rising edges per Mode 3 | 167 (invariant with SCX) |
Fine scroll
Developed in BG pipeline.
| Constant | Value |
|---|---|
Cost per SCX & 7 increment | Exactly 1 dot (ROXY suppression at startup) |
| First SACU at SCX=N | 7 + (N & 7) + 0.026 dots after AVAP |
| Mode 3 core duration at SCX=N | 173 + (N & 7) + 0.045 dots |
| SACU count | 167, invariant with SCX |
NYXU and the BG fetch counter
NYXU = NOR3(AVAP, MOSU, TEVO). The BG fetch counter counts 0→5 per 8-dot tile; TEVO resets it on every tile boundary. Developed in BG pipeline and Window control.
| Event | Value |
|---|---|
| AVAP → NYXU pulse width | 0.492 dots |
| TEVO → NYXU pulse width | 0.495 dots |
| MOSU → NYXU pulse width | 0.503 dots |
| TEVO firings per scanline (normal BG) | 21 at SCX=0; 22 at SCX&7 ∈ {1..7} (the tail TEVO closing tile 21) |
| NYXU assertions per scanline | 22 at SCX=0, 23 at SCX&7 ∈ {1..7} (1 AVAP + TEVO), +1 MOSU if window |
Sprite fetch
Trigger chain: TEKY → SOBU (1 DFF) → RYCE → TAKA (combinational, ~800 ps from SOBU). SUDA is a separate DFF on the next ALET falling edge. The sprite fetch counter (TOXE/TULY/TESE) counts 0→5 over 5 dots. Developed in Sprite pipeline.
| Event | Value |
|---|---|
| TEKY↑ → TAKA↓ (fetch freeze) | 6.010 dots, zero variance (gate-resolved) |
| Net sprite penalty | exactly 6 dots (6 suppressed SACU edges) |
| SACU resume after TAKA↓ | +1.024 dots (next ALET falling edge) |
Clock-tree phase relationships
Developed in Mode transitions and Register writes.
| Relationship | Value |
|---|---|
| VOGA capture after WODU↑ | 0.435 dots (same-dot ALET rising edge) |
| WODU → VOGA → WEGO → XYMU chain | 0.436 dots |
| CUPA pulse width | 1.493 dots (T3–T4) |
| VID_RST (XODO) deassertion after CUPA↑ on LCDC.7=1 write | 3,173 ps |
All PPU dividers (WUVU, VENA, TALU, XUPY, BOGA) are held at 0 while VID_RST is asserted; counting starts from zero on deassertion. WOSU is a sampler, not a counted divider; it tracks WUVU with a half-dot delay.
STAT interrupt
Developed in STAT interrupts.
| Relationship | Value |
|---|---|
| WODU → LALU propagation | 2,637 ps (0.011 dots, combinational) |
| STAT Mode 0 interrupt fires before the Mode 3→0 transition (XYMU clear) | 0.425 dots |
Palette writes
Developed in Palette latches.
| Event | Value |
|---|---|
| Palette dlatch update after the write-dot SACU falling | ~15.6 ns (data-bus settling — structural) |
| LCD output pins update after SACU falling | 1,019–1,711 ps |
On the write dot, the shifted pixel uses the OLD palette value; the new value first affects the next SACU rising edge.
XYMU polarity
- XYMU = 0 during Mode 3 (active rendering)
- XYMU = 1 during Modes 0, 1, and 2 ("not rendering" — active-high meaning)
Post-boot state
Full table in Post-boot state. Headline: LY=0, LX=98 (mid-Mode-0 of the first scanline, 15 M-cycles before LINE_END), XYMU=1, BESU=0, scan counter=39. Scan counter bit order: YFEL (LSB) → WEWY → GOSO → ELYN → FAHA → FONY (MSB).
Appendix C: Open Questions
Everything in this book that is not settled by primary-source evidence is tagged inline as an Open question and collected here. Each entry names the residual unknown and what kind of measurement would close it. The presence of this list is deliberate: a hardware reference that hides its edges invites silent over-trust.
Below-netlist LCD-interface overlays (a family)
Three measured behaviours live below the die model's scope — the netlist reproduces none of them at any timing calibration, yet hardware reference photographs pin all three:
- The BGP OR-overlap (palette latches) — a
second-or-later mid-Mode-3 BGP write's first emitted column reads
new | old, with a BESU-reset recovery state and a visible-emission-engagement clause. - The LCDC.1 OFF single-pixel transient (sprite pipeline) — OFF-direction only, one shade-3 column, with an empirically-scoped per-scanline firing set that netlist state cannot distinguish.
- The LCDC.0 one-column overlay (LCD output) — bidirectional, every write, first column emits with the old enable state.
The candidate mechanisms (LCD-glass column-driver sample-and-hold vs pad-driver residue) predict identical column-level behaviour; the on-die pad drivers are measured stateless, pushing the state off-die. Closing this needs hardware-level capture (Slowpeek-class probing of LD0/LD1/CP at the flex cable) across writes spanning H-Blank. The behavioural rules are sufficient for emulation regardless.
A hardware-annotation discrepancy
The gambatte m2enable/late_m1disable_ly0_3 ROM expects IF[1]=0 at a read the simulated
Case-4 SUKO glitch places at 1; an equivalent-cascade ROM with the clear
on the other side is netlist-consistent. Margin-sensitive: the
disputed dip is 1,524 ps wide and the narrowest dip hardware demonstrably
fires is 1,802 ps — a real unit could sit between the two, making the
annotation and the simulation both correct on different silicon
(STAT interrupts). Needs a hardware measurement on
multiple units.
Smaller residuals
- The community first-word formulas: the suppressed-precharge mechanism
and the clean row-copy (single-access) and multi-row (two-read) cases are
measured (OAM corruption); the community's
alignment-dependent first-word AND/OR mixes are analog bitline resolution
below the digital model's reach — a digital SRAM cell cannot partially
retain its old value, so the model copies cleanly. Like the LCD-interface
overlays above, the behaviour is hardware-established (the blargg
oam_bugsuite); the community formulas remain its reference. - VRAM-side write straddle: a CPU VRAM write whose strobe spans a Mode 3 entry/exit boundary tri-states the address pads and write strobe mid-cycle, so the off-chip VRAM SRAM sees a truncated write cycle. What that partial cycle does to the SRAM is off-die, outside the netlist's scope (lock boundaries).
- HSYNC at the glass: the ST pulse shape is measured (LCD output); only the interpretation at the glass — pulse-width expectations, row-driver response — still rests on community pin-role references. Closing it needs a hardware-level capture of the ST pin and the LCD's row-driver response (Slowpeek-class flex-cable probing), as for the other glass-side behaviours.
- HALT-vs-running handler write ordering: the one-M-cycle M2/m7 ordering difference is measured, uniform, and modelled correctly. Two things stay open: its cause inside the SM83 M-cycle sequencer (a cell-level reconstruction not yet attempted), and one reference image whose column shift runs the opposite way, which needs real-hardware confirmation (HALT and EI).
Appendix D: Glossary
ALET / MYVO — the PPU's main 4 MHz clock and its complement; the canonical per-dot edge vocabulary. "MYVO rising" = "ALET falling".
ATAL half-cycle — half a dot: the interval between consecutive master-clock edges.
AVAP — the scan-complete pulse that starts Mode 3; fires at dot 80 of each rendering scanline.
CLK9 — the CPU-side M-cycle clock: one rising edge per M-cycle,
physically the dot-0 ck1_ck2↑ master-clock edge (+8,440 ps of buffer;
+2,983 ps after the derived ALET↑, which it is sometimes quoted against).
CLKPIPE / SACU — the pixel pump: the gated, deeply-buffered clock that shifts the pixel pipeline once per dot during active rendering.
CUPA — the shared PPU register write strobe; 1.493 dots wide, spanning
T3–T4 of the write M-cycle. The APU's apu_wr is its sibling.
data phase — the second half of a CPU M-cycle (dots ≈ 2–4), during
which the bus carries data; data_phase_n defines the IF-latch
transparency window and the CPU's read-latch point (tail of T4, +3.995 dots).
dot — one full master-clock cycle (rise + fall); synonymous with T-cycle. ≈ 238.4 ns on hardware; 244,000 ps in the simulation timebase.
drain (tile boundary) — the two-dot interval after a tile fetch completes in which the NYKA→PORY→RENE/RYFA cascade empties, ending in the SEKO pulse that resets the fetcher for the next tile.
fetch overlap — the SM83's co-issue of the next opcode fetch in an instruction's final M-cycle when the bus is free; instructions whose terminal cycle is bus-busy get a dedicated post-body fetch cycle (m6/m7).
frame sequencer — the APU's 512/256/128/64 Hz tick source: a 512 Hz
tap of the timer divider plus a 3-bit ripple counter with its own
(apu_reset) reset domain.
ge (gate equivalent) — the unit of combinational depth in the static netlist analysis; the master clock is the conventional origin.
LINE_END — the LX=113 condition (SANU) and its captured pulse (RUTU) that terminate each 456-dot scanline.
M-cycle — one CPU machine cycle = 4 dots.
Mode 3 baseline — 173.481 dots = a 173.045-dot AVAP→WODU span (a 7.026-dot startup cascade, then 167 pixel-clock edges) + the 0.436-dot WODU→XYMU VOGA tail, before fine-scroll/sprite/window penalties.
prelude (Mode 3) — the AVAP→first-SACU startup window (7.026 dots at SCX=0) during which the first tile is fetched and fine scroll is paid.
T-cycle — see dot.
VID_RST — the LCDC.7-driven reset domain (XODO/XAPO and branches) that holds most of the PPU cleared while the LCD is off.
WODU — the Mode 0 condition (the H-Blank condition): pixel counter at terminal count with no sprite match; drives both the Mode 3→0 transition (via VOGA) and the STAT interrupt (combinationally, 0.425 dots earlier).
XYMU — the rendering-mode latch; 0 during Mode 3 ("not rendering" polarity), 23 consumers.
x-window (bus settling) — the post-transition interval during which a tri-state-driven CPU bus bit has not resolved; reads in the trigger M-cycle of a mode transition latch the pre-transition value through it.
Appendix E: Sources
Primary sources (ground truth)
- msinger/dmg-schematics (msinger and rgalland) — transistor-level reverse engineering of the DMG-CPU B die. The origin of every gate name in this book.
- msinger/dmg-sim — SystemVerilog simulation generated from the schematics, run under Icarus Verilog. The source of every "dmg-sim measurement" citation.
- Static netlist analysis (gb-propagation-delay-analysis) — graph analysis over the netlist: combinational depths (ge), race pairs, clock domains, critical paths.
Hardware test ROMs (hardware-verified expectations)
Test ROMs whose expected values are validated on real hardware corroborate behavioural conclusions throughout:
- gbmicrotest (and GateBoy, its die-photo-derived sibling simulator)
- Mooneye and the wilbertpol fork
- the gambatte hardware-test suite
- SameSuite
- AGE test ROMs
- blargg's test ROMs
- Hacktix's test ROMs (
strikethrough.gb) - Mealybug Tearoom's mid-Mode-3 write tests, whose DMG reference photographs anchor the below-netlist LCD overlay rules
Behavioural documentation
- gb-ctr (Gekkio's Game Boy: Complete Technical Reference) — the behavioural reference this book deliberately does not duplicate; cited wherever a chapter condenses well-covered ground.
- Pan Docs — community reference; several of its narrative rules are given gate-level form (and in two cases corrected) in this book.
- TCAGBD and the Mealybug Tearoom PPU notes — cross-references for high-level framing.
dmgcpuwiki pad documentation — pin-role descriptions for the LCD interface (explicitly marked as not netlist-derivable where used).
Behavioural emulators
No emulator was used as a source of hardware truth, and none is cited in the text. The gambatte references throughout are to its hardware-test ROMs (above), not to the emulator's behaviour.