Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

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:

  1. The DMG-CPU B netlistmsinger/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.

  2. Gate-level simulationmsinger/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.

  3. Static propagation-delay analysisgb-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:

  1. 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.
  2. dmg-sim runs the ROM and dumps an FST waveform of the full cell-level state (1 ps timescale).
  3. Named-signal transitions are read from the waveform with picosecond timestamps, and converted to dots using the simulation's measured dot period.
  4. 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

UnitDefinition
dotOne 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-cycleOne CPU machine cycle = 4 dots.
ATAL half-cycleHalf 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.
geGate equivalent — the unit of combinational depth used by the static netlist analysis (see races).
psSimulation 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 expressConventionExample
A duration or offsetdots, fractional where needed"0.436 dots after WODU↑"
A segment of an M-cycleT-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 cyclesM1, M2, … (M1 = fetch)"dispatch M1–M4"
A counter positionthe 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:

GateRoleTypeClock / TriggerNotes
BYBAScan-done flagdffrXUPY (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:

CalloutCarries
At a glanceA chapter's headline facts — the numbers and the one rule to remember
In this partThe first chapter of each part sketches its siblings
RuleA behavioural rule an emulator must honour, stated in its sharpest form
MeasuredA self-contained measured result worth pulling out of the prose
For implementorsModelling advice — how to structure an implementation so the rule falls out
PitfallA known wrong model, naming trap, or test-harness trap
Open questionA 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

In this part

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.

At a glance

  • One dot = one full ck1_ck2 cycle. 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.

GateRoleTypeClock / TriggerNotes
ck1_ck2Master 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.
ANOSCrystal feedback NANDnand2Input: ck1_ck2Pairs with AVET
AVETCrystal feedback NANDnand2Input: ck1_ck2Pairs with ANOS
ATAL4 MHz buffered enablenot_x2Input: ANOS/AVET outputMaster 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.
ADEHATAL complementnot_x1Input: ATALPairs with ATAL as the complementary 4 MHz enables driving the CPU phase-generator ring.
AZOFPPU clock buffernot_x6Input: ATALFan-out buffer
ZAXYBuffer stagenot_x2Input: AZOF
ZEMEBuffer stagenot_x4Input: ZAXYFan-out point: drives ALET (main PPU clock) and XYVA (parallel divider branch).
ALET4 MHz PPU main clocknot_x2Input: ZEMEDepth 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.
MYVOALET complementnot_x1Input: ALETDepth 22.2 ge (5.9 ge more than ALET)
MOXEALET complement (fine-scroll path)not_x1Input: ALETSame 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.
XYVAParallel divider branchnot_x1Input: ZEMESingle-stage inverter feeding XOTA. Same phase as ALET (both compute NOT(ZEME)) on a physically separate routing path; no downstream consumers beyond XOTA.
XOTAWUVU clocknot_x1Input: XYVADrives the WUVU DFF. "XOTA rising = ALET falling" is the canonical derived-edge framing used throughout the book's WUVU/XUPY divider timing descriptions.
WUVU2 MHz toggle dividerdffrXOTA~Q → D feedback (toggle). Halves XOTA frequency; drives VENA (via ~Q) and feeds the XUPY inverter, WOSU, and the LCD-control-phase NORs.
VENA1 MHz toggle dividerdffrWUVU.~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.
WOSUWUVU samplerdffrXYFYCaptures 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.
TALU1 MHz LX counter clocknot_x4Input: VENADrives the LX counter (SAXO chain), NYPE (LINE_END redistribution), ROPO (LY==LYC latch), and related scanline-level DFFs.
SONO1 MHz LINE_END capture clock (TALU complement)not_x1Input: TALUClocks RUTU (line counters) — captures SANU (LINE_END decode) on the SONO edge, half an M-cycle after the LX increment.
XYFYXOTA complementnot_x1Input: XOTAClocks 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_ck2 rising (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 signalSourceFrequencyWhat it drives
sacu (CLKPIPE)combinational, via alet chainper-dot, variable arrivalPixel shift registers, X match, PX counter — 52 cells
aletcrystal via buffer chain4 MHzBG fetch cycle DFFs (NYKA, LYZU, PYGO, RENE), sprite fetch control (DOBA), window mode (NOPA), H-Blank capture (VOGA)
myvo = NOT(alet)crystal via buffer chain4 MHz (opposite edge)PORY (fetch cycle counter), VRAM timing
lebo = NAND2(moce, alet)gated aletconditional per-dotLAXU (fetch counter bit 0) — stops when MOCE goes low
laxumesunyvaripple from LAXUper-dot, delayedFetch counter bits 1, 2 (tile fetch cycle counting)
talu = NOT(vena_n) (= VENA.q)1 MHz from VENA1 MHzLX counter (SAXO chain), NYPE, ROPO
sono = NOT(talu) (= vena_n)complementary to TALU1 MHzRUTU (LINE_END), SYGU
xotabuffered 4 MHz4 MHzWUVU (video clock divider)
xyfy = NOT(xota)opposite to XOTA4 MHz (opposite edge)WOSU
wuvutoggle at 2 MHz2 MHzVENA (via ~Q)
xupy = NOT(wuvu_n) (= WUVU.q)2 MHz2 MHzBYBA (OAM scan complete capture), CENO (OAM phase control)
nypefrom RUTU (LINE_END)per-linePOPU (VBlank), MEDA (LY==0), MYTA (FRAME_END)
popuVBlank flagper-frameNAPO
muwy (LY bit 0) → rippleper-line from RUTUper-lineLY counter chain, sprite store clocks — 181 registers via gated paths
sabe = NAND2(lape, tame)gated NOT(alet)conditional per-dotTOXE (sprite fetch counter bit 0) — advances when ALET rises, stops at count 5
toxetulyteseripple from TOXEper-dot, delayedSprite fetch counter bits 1, 2
seba / sprite fetchgated per-lineper-line/per-spriteSprite Y comparator, store writes, priority
roxo = NOT(segu)from CLKPIPE bufferper-dotPUXA (fine scroll match capture)
moxe = NOT(alet)same phase as MYVO4 MHz (opposite edge)NYZE (fine scroll match second stage)
pecu = NAND2(roze, roxo)gated CLKPIPE inverseconditionalRYKU (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:

PairUpstream captures onDownstream captures onPipeline
ALET / MYVOALET↑ — NYKAMYVO↑ (= ALET↓) — PORYhalf a dot; the book's prototypical example (Races)
TALU / SONOTALU↑ — the LX counter (SAXO)SONO↑ — RUTU captures SANUhalf an M-cycle, LX → LINE_END
XOTA / XYFYXOTA↑ — WUVUXYFY↑ — WOSU samples WUVUhalf 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.

Measured: the BYBA/DOBA phase split

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).

At a glance

  • 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.

GateRoleTypeClock / TriggerNotes
SAXOLX bit 0dffrTALUReset by MUDE
TYPOLX bit 1dffrSAXO (~Q ripple)Reset by MUDE
VYZOLX bit 2dffrTYPO (~Q ripple)Reset by MUDE
TELULX bit 3dffrVYZO (~Q ripple)Reset by MUDE
SUDELX bit 4dffrTELU (~Q ripple)Reset by MUDE
TAHALX bit 5dffrSUDE (~Q ripple)Reset by MUDE
TYRYLX bit 6dffrTAHA (~Q ripple)Reset by MUDE
MUDELX resetnor2Inputs: LYHA, RUTULINE_END or video reset
SANULINE_END decodeand4Inputs: SAXO, SUDE, TAHA, TYRYFires at LX=113 (0b1110001)
RUTULINE_END capturedffrSONOData: 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).

GateRoleTypeClock / TriggerNotes
MUWYLY bit 0dffrRUTUReset by LAMA
MYROLY bit 1dffrMUWY (~Q ripple)Reset by LAMA
LEXALY bit 2dffrMYRO (~Q ripple)Reset by LAMA
LYDOLY bit 3dffrLEXA (~Q ripple)Reset by LAMA
LOVULY bit 4dffrLYDO (~Q ripple)Reset by LAMA
LEMALY bit 5dffrLOVU (~Q ripple)Reset by LAMA
MATOLY bit 6dffrLEMA (~Q ripple)Reset by LAMA
LAFOLY bit 7dffrMATO (~Q ripple)Reset by LAMA
LAMALY resetnor2Inputs: LYHA, MYTAFRAME_END or video reset
XYVOVBlank decodeand2Inputs: LOVU, LAFOFires at LY ≥ 144
POPUVBlank capturedffrnype (NYPE rising)Data: XYVO
NOKOFRAME_END decodeand4Inputs: LYDO, LOVU, LAFO, MUWYFires at LY = 153
MYTAFRAME_END capturedffrnype_n (NYPE falling; one TALU period after POPU)Data: NOKO; drives LY reset via LAMA
NERULY=0 decodenor8Inputs: all LY bitsFires when LY = 0
MEDALCD vertical-sync sourcedffrnype_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:

DecodeFunctionFires atCaptured byOnConsequence
XYVOAND(LY[4], LY[7])LY ≥ 144POPUnypeMode 1 (VBlank) begins
NOKOAND(LY[0], LY[3], LY[4], LY[7])LY = 153MYTAnype_n↑ — one TALU period after POPULAMA goes low; LY resets to 0
NERUNOR8(all LY bits)LY = 0MEDAnype_ndrives 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.

Pitfall: collapsing NYPE to one edge

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.

At a glance

  • 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:

ModeConditionDuration
Mode 2 (OAM Scan)XYMU=1, CENO active, OAM scan in progress80 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 VBlankremainder 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).

GateRoleTypeClock / TriggerNotes
XYMURendering-mode latch (active-low Mode 3 indicator)nor_latchSet: WEGO; Reset: AVAPPrimary here; 23 downstream consumers
AVAPMode 2→3 triggernot_x2Input: BEBUSee OAM scan for the detection chain
WODUMode 0 conditionand2Inputs: XENA, XANOSee STAT interrupts for the full breakdown
VOGAH-Blank capture DFFdffrALETData: WODU; reset: TADY
WEGOXYMU set driveror2Inputs: TOFU, VOGADrives XYMU's set input
TOFUVideo resetnotInput: XAPOReset from LCDC disable / system reset
TADYVOGA + PX-counter resetnor2Inputs: TOFU, ATEJOne 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:

SignalTypeAsync?Notes
XYMUNOR latchYesReset by AVAP (Mode 3 starts, XYMU↓); set by WEGO (Mode 3 ends, XYMU↑).
BESU (scan-active)NOR latchYesSet 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 latchYesSet (suppress CLKPIPE) by PAHA; reset (enable CLKPIPE) by POVA
POKY (pixel pipe data ready)NOR latchYesSet (data ready) by PYGO; reset by LOBY
LONY (tile fetching)NAND latchYesSet by LURY, reset by NYXU
VOGA (H-Blank)DFF (clk=ALET)NoSynchronous capture of WODU on the same-dot ALET rising edge
RUTU (LINE_END)DFF (clk=SONO)NoSynchronous capture of SANU
POPU (VBlank)DFF (clk=NYPE)NoSynchronous capture of XYVO
MYTA (FRAME_END)DFF (clk=NYPE falling)NoSynchronous 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

In this part

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_rd enables 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

PageCovers
ReadsThe combinational read path, the full per-register driver map, and the STAT driver-family split
WritesThe CUPA strobe, the phase generator behind it, and what a mid-rendering write affects
Palette latchesBGP/OBP0/OBP1's latch cells, the mid-Mode-3 write race, and the BGP OR-overlap
LCDC structureLCDC'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.

At a glance

  • 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

GateRoleTypeClock / TriggerNotes
ppu_rdCPU read assertnot_x4 (buffered)combinationalBuffer-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 decodenot_x1combinationalActive-high when address[15:4] matches $FF4. Shared input to every $FF4x register address decoder.
wyle$FF44 address decodenand5combinationalInputs: ff4x + low-nibble inversions (xola, xeno, walo, xera). Active-low when address = $FF44.
wofa$FF41 address decodenand5combinationalInputs: 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-highnot_x2combinational= NOT(wyle). Drives wafu.
ff41$FF41 active-highnot_x1combinational= NOT(wofa). Drives tobe.
wafu$FF44 read enableand2combinationalInputs: 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 enableand2combinationalInputs: 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:

  1. The CPU asserts cpu_rd with address $FF44 on its port.
  2. ppu_rd follows cpu_rd through two inverter stages; no phase gating.
  3. The address decoders fire: ff4x (high-byte match) combines with the low-nibble decode wyle to drive ff44 active-high.
  4. wafu = AND2(ppu_rd, ff44) enables the 8 tri-state drivers from the LY DFFs (through VARO, the drivers' active-low ena_n).
  5. 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.
  6. The CPU samples cpu_port_d (connected to d[] via transmission gates) at the SM83-internal data_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's clk_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)):

BitDriverSourceFamily
0TEBYSADUNOT_IF1 (TOBE)
1WUGAXATYNOT_IF1 (TOBE)
2SEGOrupo_nNOT_IF1 (TOBE)
3PUZOroxe_nNOT_IF0 (VAVE)
4POFOrufo_nNOT_IF0 (VAVE)
5SASYrefe_nNOT_IF0 (VAVE)
6POTErugu_nNOT_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).

RegisterDecode → selectEnableena_nDrivers d0 → d7
LCDC $FF40WORU → VOCAVYREWYCEWYPO XERO WYJU WUKA VOKE VATO VAHA XEBU
STAT $FF41WOFA → VARYTOBEVAVE (bits 3–6)TEBY WUGA SEGO PUZO POFO SASY POTE (split, no bit 7)
SCY $FF42WEBU → XAROANYPBUWYWARE GOBA GONU GODO CUSA GYZO GUNE GYZA
SCX $FF43WAVU → XAVYAVOGBEBAEDOS EKOB CUGA WONY CEDU CATA DOXE CASY
LY $FF44WYLE → XOGYWAFUVAROVEGA WUVA LYCO WOJY VYNE WAMA WAVO WEZE
LYC $FF45WETY → XAYUXYLYWEKURETU VOJO RAZU REDY RACE VAZU VAFE PUFY
DMA $FF46WATE → XEDAMOLUNYGO → PUSY (ena)POLY ROFO REMA PANE PARE RALY RESU NUVY
BGP $FF47WYBO → WERAVUSOTEPYRARO PABA REDO LOBE LACE LYKA LODY LARY
OBP0 $FF48WETA → XAYOXUFYXOZYXARY XOKE XUNO XUBY XAJU XOBO XAXA XAWO
OBP1 $FF49VAMA → TEGOMUMYLOTELAJU LEPA LODE LYZA LUKY LUGA LEBA LELU
WY $FF4AWYVO → VYGAWAXUVOMYPUNU PODA PYGU LOKA MEGA PELA POLO MERA
WX $FF4BWAGE → VUMYWYZEVYCULOVA 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_rd is 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_phase fires. 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_phase gates the CPU's internal capture of cpu_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.

At a glance

  • 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.
GateRoleTypeClock / TriggerNotes
CUPAShared PPU register write strobecombinational (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
DYKYCUPA driver stage 1notInput: TAPU
TAPUCUPA driver stage 2notInput: UBAL
UBALCUPA driver stage 3muxiTest gate
APOVCUPA driver stage 4notInput: AREV
AREVCUPA driver stage 5nand2Inputs: cpu_write, AFAS
AFASRing decode driving CUPAnor2Inputs: ADAR, ATYPHigh three ATAL half-cycles per M-cycle (1.504 dots measured)
AFURPhase generator ring stage 1dffDriven by ATAL/ADEH4-stage ring (AFUR/ALEF/APUK/ADYK)
ALEFPhase generator ring stage 2dffDriven by ATAL/ADEH
APUKPhase generator ring stage 3dffDriven by ATAL/ADEH
ADYKPhase generator ring stage 4dffDriven by ATAL/ADEH
WARULCDC write enableand2Inputs: VOCA, CUPAEnables XUBO/XURE
SEPASTAT write enableand2Inputs: VARY, CUPAEnables RYVE/PUPU
VELYBGP write enableand2Inputs: WERA, CUPAEnables TEPO/LYFA (palette latches)
ARURSCX write enableand2Inputs: XAVY, CUPA
BEDYSCY write enableand2Inputs: XARO, CUPA
XODOVID_RST signalcombinationalInput: XONADeasserts 3,173 ps after CUPA↑ on an LCDC.7=1 write
XONALCDC.7 storage latchdrlatch_eed: d7; ena: XURE/XUBOq (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):

EventTime 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.

At a glance

  • 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 | old OR-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.

GateRoleTypeClock / TriggerNotes
BGP (8 × dlatch_ee)BG palettedlatch_eeEnable: VELY = AND2(WERA, CUPA)Per-bit cells not individually named here
OBP0 (8 × dlatch_ee)Sprite palette 0dlatch_eeEnable: per-register AND with CUPA
OBP1 (8 × dlatch_ee)Sprite palette 1dlatch_eeEnable: per-register AND with CUPA
TEPOBGP write enable sub-signalcombinationalInputs: VELY chainFires during a CUPA write to BGP
PEROPalette-output multiplexer signalcombinationalInputs: palette outputsFeeds 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
+0CUPA↑ (bus write strobe begins)
+1,306TEPO↓ (BGP write enable opens the latches)
+1,320SACU↓ (the write dot's pixel clock falls)
+16,870 / +17,060BGP dlatch output changes (per write direction) — the CPU data bus settles here, past the pixel edge
+18,510PERO changes
+19,464REMY (LD0) changes (RAVO/LD1 likewise when the transition flips it)
+129,254Next 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 first cp_pad↑ after the new CUPA (the same-dot late-half emission edge at +0.534 dots after CUPA↑) samples a palette value equivalent to BGP_new | BGP_old rather than BGP_new alone. All subsequent cp_pad↑ events sample BGP_new directly. When either clause fails — the CUPA is the first BGP write since BESU↑, or no visible cp_pad↑ has occurred since the prior write — the first cp_pad↑ after the new CUPA samples BGP_new cleanly.

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

TestWrite structureOutcome under the rule
Mealybug m3_bgp_change, LY ≥ 1W1 in Mode 2; W2…W7 in Mode 3W1 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 scanlineLY=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 dotW1 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_padW1 clean; W2 fires OR post-column-0. Reference: column 0 = shade 3 (bare W1), then the one-column transient
Mealybug m3_obp0_changeSingle OBP0 write, OR ≡ NEW ($04 → $0C)Cannot discriminate — OR ≡ NEW either way; the OBP scope question stays open (below)

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_pad chain (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_change LY ≥ 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/_5 setup 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 (AGE m3-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 visible cp_pad↑ between W1 and W2 — the hardware reference shows clean NEW (so the condition is the intervening cp_pad↑, not merely "second-or-later since BESU↑"). Edge timing: W2 CUPA↑ at AVAP+15.4962 dots, ST_pad↓ at +15.4933, first visible cp_pad↑ at +16.0444, ld_pad[1:0] = 0b00 (shade 0) at the column-0 sample edge.

Mechanism

Open question: the OR-overlap 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

For implementors

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.

At a glance

  • 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 bitGate / wire nameConsumerChapter
0 (BG_AND_WIN_EN)VYXE / LCDC_BGENpBG/window pixel plane gate (RAJY/TADE); mid-Mode-3 toggle behaviourLCD output
1 (OBJ_EN)XYLO / LCDC_SPENpSprite trigger (AROR → per-sprite NAND3 decode)Sprite pipeline
1 (OBJ_EN)XYLO / LCDC_SPENpPixel MUX gates (XULA, WOXA)Sprite pipeline
2 (OBJ_SIZE)XYMO / LCDC_SPSIZEpY-compare decoder (GOVU) — Mode 2OAM scan
2 (OBJ_SIZE)XYMO / LCDC_SPSIZEpSprite tile VRAM ma4 (GEJY) — Mode 3Sprite pipeline
3 (BG_MAP)XAFO / LCDC_BGMAPpBG tilemap address during fetch counter=0BG pipeline
4 (TILE_SEL)WEXU / LCDC_BGTILEpBG tile-data address at counter=2/4BG pipeline
5 (WIN_EN)WYMO / LCDC_WINENpWindow enable (XOFO → PYNU.r)Window control
6 (WIN_MAP)WOKY / LCDC_WINMAPpWindow tilemap address at counter=0BG pipeline
7 (LCDEN)XONA / LCDC_LCDENpVID_RST gate (LCDC.7 → XONA → XODO)Register writes
7 (LCDEN)XONA / LCDC_LCDENpCPL/FR audio/PPU-clock mux (KAHE, KUPA AO22) — all modes including LCD-offLCD 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.

At a glance

  • 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_wr net 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

PageCovers
OAM writesThe on-chip write-strobe chain (AJUJ → AMAB → WYJA), the CUPA-pulse window, and the per-byte strobes
VRAM writesThe off-chip pad path, the Mode-3 tri-state lock, and the SM83 bus-phase strobe
Lock boundariesM-cycle-quantised lock transitions, and the straddle cases at a mode edge
OAM corruptionThe 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.

GateRoleTypeInputsNotes
FEXX$FExx address-range decodenot_x2= NOT(ROPE)Active-high while cpu_port_a is in $FE00–$FEFF
AJONMode 3 + non-DMAand2mode3, BOGEBOGE = NOT(dma_run). AJON=1 only during Mode 3 with DMA inactive
AJUJOAM-access permitnor3dma_run, mode2, AJONAJUJ=1 only when DMA is inactive AND neither Mode 2 nor Mode 3 is active
AMABOAM CPU-write address gateand2fexx, AJUJAMAB=1 only when the CPU addresses $FExx during a permitted window
WYJAOAM write enable (combined)ao21AMAB, ppu_wr, POWUWYJA = (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 / YNYCOAM A-side / B-side write strobe driversper-byte selectorWYJA + per-byte address decodeDrive the active-low oam_a_wr_n / oam_b_wr_n pad outputs

Lock-window behaviour. The permit AJUJ decides the outcome:

StatePermitOutcome
Outside Mode 2/3, no DMAAJUJ=1, AMAB=1WYJA passes ppu_wr — write lands
Mode 2mode2=1 → AJUJ=0blocked (BESU gates OAM — OAM scan)
Mode 3XYMU.q=0 → AJON=1 → AJUJ=0blocked
DMAdma_run=1 → AJUJ=0CPU 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.

GateRoleTypeInputsNotes
ROPYMode 3 inverter for VRAM enablesnot_x1mode3ROPY = NOT(mode3); high outside Mode 3
XANEVRAM address tri-state-disable sourcenor2vram_to_oam, mode3XANE=1 when neither VRAM-sourced DMA nor Mode 3 holds — the CPU may drive the address pads
XEDUVRAM address tri-state enablenot_x2XANEXEDU = NOT(XANE); drives the not_if0 ena_n of the ma_pad[*] address pads
TOLECPU data-bus enable stage 1combinationalCPU bus enablesCarries CPU-side write-window timing
SERECPU data-bus enable stage 2and2TOLE, ROPYActive only when CPU drive is enabled AND outside Mode 3
ROCY / RAHU / SAZO / REVOPer-bit data-pad tri-state enable chaincombinationalSERE + per-bit fan-outDrive not_if0 ena_n on md_pad[*]
SOHYWrite-strobe drivernand2TYJY (CPU bus-phase write enable), SEREDrives mwr_n_pad low when both arms hold
TAXY / SYSY / RACO / TUTOCPU-bus-phase strobe assemblycombinationalT1T2_n / SOTO_n / TYJY chainGenerate the CPU-side write window feeding SOHY
mwr_n_padOff-chip write strobetri-state pad outputSOHYActive-low; visible to the off-chip VRAM SRAM
ma_pad[12:0]Off-chip address padstri-state pad outputsXEDU enable; CPU bus addressDriven when XANE=1
md_pad[7:0]Off-chip data padstri-state pad outputsSERE chain enable; CPU bus dataDriven 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_pad low. 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.

At a glance

  • 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 strobeasam (OAM) and ropy = 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

ReadMode 0 / VBlankMode 2Mode 3
OAM ($FE00$FE9F)stored byte0xFF0xFF
VRAM ($8000$9FFF)stored bytestored byte0xFF

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:

GateRoleTypeLogic
ASAMOAM CPU-address disableor3oam_addr_cpu_n = OR(mode2, mode3, dma_run) — high disables the eight OAM-address tri-state drivers
WAMEOAM output-enable inverternot_x2oam_oe_n = NOT(oam_oe); OAM's output enable is steered to the PPU parser during Mode 2/3
ROPYMode 3 inverter for VRAM enablesnot_x1= NOT(mode3); high outside Mode 3, low in Mode 3 — closes the VRAM read latch via SERE → SEBY
SEBYVRAM read-latch enablenot_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_n

The $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.

GateRoleTypeLogic
FEXX$FExx address-range decodenot_x2= NOT(ROPE); ROPE = NAND2(SOHA, RYCU)
CUFE$FExx/DMA bus-cycle termoai21= NOT((FEXX OR dma_run) AND dma_phi_n) — low for the bus-phase half-M-cycle of any $FExx access
BYCUCOTA drivernand3Inputs: CUFE, XUJY, AVER
COTAStage-2 capture enable / bitline-precharge sourcenot_x1= NOT(BYCU); gates the Stage-2 latches (OAM scan)
WOVUOAM bitline precharge drivernot_x2oam_bl_precharge_n = NOT(COTA)
WAHE / WUJYOAM wordline-driver prechargenot_x3 / nand2WUJY = 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
~5FEXX↑ — $FE00 decodes
~106the in-flight precharge beat completes normally; row 11's second wordline epoch opens
~347CUFE↓ (the dma_phi_n phase gate arrives)
~470 / ~590AVER pulses for the next scan step — BYCU and COTA do not respond; no precharge beat fires
~593row 12's wordline opens onto bitlines still carrying row 11's read charge — the cells are overwritten
~835CUFE↑ (bus phase ends, exactly 2 dots low)
~961precharge beats resume; row 12's second epoch is clean — the damage is already committed

Three structural facts fall out:

  • No write strobe is involvedoam_a_wr2/oam_b_wr2 never 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.

For implementors

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.

At a glance

  • 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_run keeps 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

PageCovers
The OAM bus under DMAFour-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 HALTThe 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 conflictsAddress-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.

At a glance

  • Arbitration is symmetric and clean: across every PPU-mode × dma_run combination, 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.

SourceDrive-enable signalDriver gateAsserted when
CPU portoam_addr_cpu_nASAM (or3)OR3(mode2, mode3, dma_run) = 0 (= AJUJ = 1, lock open)
DMAoam_addr_dma_nDUGA (not_x2)dma_run = 1
OAM scan (Mode 2)oam_addr_parse_nAPAR (not_x2)mode2 = 1
Sprite fetch (Mode 3)oam_addr_render_nBETE (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 modedma_runCPUDMAParseRenderBus owner
Mode 0 / VBlank0driveCPU
Mode 0 / VBlank1driveDMA
Mode 20driveParse
Mode 21driveDMA
Mode 30driveRender
Mode 31driveDMA

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; with dma_phi it 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:

  1. Read-phase sub-window — DMA address asserted, no write strobe; the SRAM presents the prior contents of DMA's destination cell.
  2. 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:

BankStoresWrite strobe
Aodd addresses (X position, attribute)oam_a_wr2, selected when a0=1
Beven 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:

  1. 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.
  2. Mode-3 sprite-fetch capture — XUJA = 0 requires mode3=1 plus sprite-fetch counter state (TUVO decode); one pulse per sprite fetch.
  3. CPU OAM readcpu_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:

ModelVerdict
(i) Aligned-pair: latch (OAM[addr & ~1], OAM[addr | 1]) regardless of a0Structurally correct — the two-bank SRAM forces this on every read
(ii) Bus-following per-dot: each latch samples whatever single byte dma_a addressesIncorrect — the 16 dlatches share one enable; they never alternate per dot
(iii) Same-byte-twice on both operandsIncorrect — the banks store different physical bytes

Pitfall: the aligned-pair model is not exercised during the overlap

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.

Measured: capture resumes at the DMA-end, mid-scan

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.

At a glance

  • 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 structurallydma_phi is pinned, the byte counter freezes, but dma_run keeps 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 on dma_phi_n; the LARA/LOKY latch pair arms; MATU (clocked by dma_phi) raises dma_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.qSource
BOGE↓+2,741 psNOT(dma_run)
mode2↓+3,688 ps totalAND2(BOGE, oam_parsing) → 0
oam_addr_parse_n+4,507 psAPAR — parse driver tri-states; DMA takes the bus
AJEPalready high — no edgeNAND2(mode2, XOCE); the quantised engage always lands at XOCE=0
ODL held lowwhile AJEP = 1BODE

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 past clk_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.qSource
BOGE↑+1,941 psNOT(dma_run)
mode2↑+3,723 ps totalAND2(BOGE, oam_parsing) — requires BESU.q=1
next XOCE↑ edge0–2 dotsfree-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.

For implementors

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.

Measured: a 17 ms HALT contributes zero bytes

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.

At a glance

  • 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.

GateRoleTypeLogic
LOGODMA bus-read windownot_x1NOT(RORU) — per-T-cycle DMA address window
MORYDMA-vs-CPU address MUX select sourcenand2NAND2(dma_run, LOGO)
LUMAdma_addr_ext drivernot_x1dma_addr_ext = AND2(dma_run, LOGO)
per-bit address MUXesaddress pad sourcemuxsel = dma_addr_ext: DMA address vs CPU address
CEDEinverted dma_addr_extnot_x2gates 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 targetdma_addr_ext during the CPU strobeAddress pads carry
external bus, DMA source on external bus1DMA's address — the CPU's intended address never reaches the pads
VRAM bus, DMA source on VRAM1DMA's address
HRAM ($FF80–$FFFE)irrelevantHRAM is on-chip, on neither bus; CPU access proceeds normally

Pitfall: there is no address collision to detect

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:

CandidateResponds?Reason
On-die OAM SRAMNoOAM uses its own internal address bus, not the external pads; its output enable stays 0 throughout (measured)
On-die HRAMNoThe HRAM enable chain rides the CPU-internal address bus, not the external pads
On-package WRAM dieYesSelects on cs_n_pad + a14=1 alone (see below)
Cart devicesMootWRAM 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.

For implementors

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

In this part

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.

At a glance

  • 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

PageCovers
The counter and BESUThe 6-bit ripple counter, its clocking and reset, the scan-active latch and its XUPY-phased copy
The Y comparatorThe 8-stage carry chain, the WOTA decoder, LCDC.2 masking, and capture durability
The sprite store10-of-40 priority, the slot counter and write enables, reset domains, and the LCDC.1 trigger branch
AVAP and the 80 dotsThe 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).

BitSignalTypeClocked byNotes
0 (LSB)YFELdffrGAVAReset: ANOM
1WEWYdffrYFEL (~Q ripple)Reset: ANOM
2GOSOdffrWEWY (~Q ripple)Reset: ANOM
3ELYNdffrGOSO (~Q ripple)Reset: ANOM
4FAHAdffrELYN (~Q ripple)Reset: ANOM
5 (MSB)FONYdffrFAHA (~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

GateRoleTypeClock / TriggerNotes
GAVAScan counter clockor2Inputs: XUPY, FETODrives the YFEL→FONY ripple chain
ANOMScan counter resetnor2Inputs: ATEJ, ATAROutput is oam_parse_reset_n; see the CATU/ANEL chain (Mode transitions)
FETOScan-done decodeand4Inputs: oam_parse_a2, a3, a4, a7Wire name last_sprite. Fires at counter = 39. Also feeds BYBA's D input
ASENBESU reset driveror2Inputs: ATAR, AVAPRising edge clears the scan-active latch — at scan completion or video reset
ATARVID_RST active-highnotInput: XAPODrives 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_a2a7 (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.

GateRoleTypeClock / TriggerNotes
BESUScan-active latch (Q=1 during Mode 2)nor_latchSet: CATU.Q at Mode 2 start; Reset: ASENWire name oam_parsing. Drives the OAM-bus lock directly: mode2 = AND2(BOGE, BESU.Q)
CENOXUPY-phased scan-active copydffrXUPY risingData: 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:

  1. The Mode 3 rendering gateoam_rendering = AND2(ceno_n, mode3).
  2. 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.

GateRoleTypeClock / TriggerNotes
GESEsprite_y_match drivernot_x1combinational (input: WOTA)sprite_y_match = NOT(WOTA)
WOTAY-compare decodernand6combinationalInputs: GACE, GUVU, GYDA, GEWY, wuhu_c, GOVU. Low when the Y-in-range condition holds
GYKYCarry-chain bit 3full_addcombinationalSee the chain table
GOVULCDC.2 maskor2combinationalGOVU = OR2(ff40_d2, GYKY) — masks the bit-3 comparison for 8×16 sprites
XUSOOAM Y-offset bit 0 latch (Mode 2) / tile-index bit 0 latch (Mode 3)dlatch_eeEnable: COTAData: 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 register8 × dlatch on the bank-B busdlatchEnable: 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_x1 on a V-counter bit (EBOS…GUSU = NOT(v0v7)).
  • 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.
StageGatea input (NOT LY bit)b input (OAM-Y latch q̄)cincoutsum
0ERUCEBOSxuso_nconst0eruc_cERUC
1ENEFDASAxegu_neruc_cenef_cENEF
2FECOFUKYyjex_nenef_cfeco_cFECO
3GYKYFUVExyju_nfeco_cgyky_cGYKY
4GOPUFEPUybog_ngyky_cgopu_cGOPU
5FUWAFOFAwyso_ngopu_cfuwa_cFUWA
6GOJUFEMOxote_nfuwa_cgoju_cGOJU
7WUHUGUSUyzab_ngoju_cwuhu_cWUHU

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_x1 cells, 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 familyRoleTypeClock / TriggerCountNotes
Sprite store slotsPer-slot X-position latchesdrlatch_eeEnable: save_sprite_numN80 (10 slots × 8)Data: OAM bus (depth 0)
EBEBSlot-3 write-enable inverternot_x1combinational1drives 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
CAREGlobal sprite-save enableand3combinational1Inputs: XOCE, CEHA, sprite_y_match; sprite_save_en_n = NOT(CARE)
FOCOSlot-3 selectornand4combinational1Decodes slot counter = 3; one of 10 per-slot selectors
BESE / CUXY / BEGO / DYBESlot counter bits 0–3dffr (toggle)CAKE, then ~Q ripple4Reset: AZYB
AZYBSlot counter reset drivernot_x1combinational1AZYB = NOT(ATEJ) — same start-of-line reset as the scan counter

The priority mechanism:

  1. sprite_y_match is combinationally established per OAM entry.
  2. When it holds AND XOCE=1 AND CEHA=1, CARE=1 releases the write enables.
  3. 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).
  4. save_sprite_numN = NOR2(sprite_save_en_n, selector_N) fires only when enabled AND targeted.
  5. CAKE pulses once per stored sprite, advancing the counter.
  6. 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.

GateRoleTypeClock / TriggerNotes
AVAPMode 2→3 triggernot_x2Input: BEBUWire name stop_oam_parsing. Primary here; consumed in Mode transitions
BEBUScan-complete aggregateor3Inputs: DOBA, BALU, byba_nThe third input is BYBA's Q_n pin, not Q
BYBAScan-done flagdffrXUPY risingD: FETO. Reset: BAGY (active-low)
DOBAALET-clocked scan-done-prevdffrALET risingD: BYBA.Q. Reset: BAGY
BALUOAM-parse reset (active-high)not_x1Input: ANOM
BAGYBYBA/DOBA reset drivernot_x1Input: BALUANOM 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)DotsTypeMechanism
Counter 38→39 (XUPY rising N)00.000ClockedOnly YFEL toggles; upper bits hold
YFEL.Q settled+2960.001CombinationalDFF Q delay
FETO↑+2,6020.011CombinationalAND4 of settled ripple Qs
XUPY rising N+1+488,0002.000ClockedNext XUPY edge
BYBA.Q↑+488,9022.004ClockedCaptures FETO=1, 902 ps after the edge
BEBU↓+489,5582.006Combinationalbyba_n↓ collapses the OR3
AVAP↑+490,6792.011CombinationalMode 2→3 trigger
BESU↓+491,3852.014Latch resetASEN clears the scan-active latch
ALET rising+606,9062.487ClockedFirst ALET↑ after BYBA↑
DOBA.Q↑+607,7902.491ClockedCaptures BYBA=1
BEBU↑+608,0362.492CombinationalDOBA restores BEBU
AVAP↓+608,6132.494CombinationalPulse 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.

At a glance

  • 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 & 7 step), 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

PageCovers
The tile fetcherThe 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 clockThe PX counter, terminal count 167, the CLKPIPE/SACU gating tree, and the four halt conditions
Startup and fine scrollThe 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 registersThe 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.

At a glance

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

The fetch counter

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

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

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

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

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

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

The 8-dot tile cycle

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

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

The 2-dot hold, stage by stage:

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

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

Pitfall: the PANY/NUKO slip

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

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

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

The fetch cycle and VRAM addressing

Each tile fetch takes 6 dots:

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

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

LCDC.3/4/6 sampling during the fetch

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

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

The enables derive combinationally from the fetch counter:

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

The sampling cadence:

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

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

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

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

Measured: the LCDC.4 write path

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

LCDC.5 transients and the fetcher arbitration

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

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

For implementors

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

SCX/SCY sampling during the fetch

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

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

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

Measured: the SCY fetch path

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

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.

At a glance

  • 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.

GateRoleTypeClockNotes
XEHOPX bit 0dffrSACUToggle
SAVYPX bit 1dffrSACUD = XOR(SAVY, XEHO)
XODUPX bit 2dffrSACUD = XOR(XUKE, XODU); XUKE = AND2(SAVY, XEHO)
XYDOPX bit 3dffrSACUD = XOR(XYDO, XYLE); XYLE = AND2(XUKE, XODU)
TOCAUpper-nibble clocknotXYDO
TUHU / TUKY / TAKO / SYBEPX bits 4–7dffrTOCAToggle / XOR carry, mirroring the lower nibble
XUGUTerminal count decodenand5SYBE, SAVY, TUKY, XEHO, XODUFires at PX = 167
TADYVOGA + PX-counter resetnor2TOFU, ATEJShared 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).

Measured: 167 SACU edges per Mode 3, invariant with SCX

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.

GateRoleTypeInputsNotes
SACUPixel pipe shift clock (CLKPIPE)or2ROXY, SEGUDepth 63.8 ge; fan-out 53
ROXYFine-scroll gatenor_latchSet: PAHA; Reset: POVASee Startup and fine scroll
SEGUBuffered CLKPIPEnot_x4TYFADepth 49.4 ge
TYFACLKPIPE gateand3POKY, SOCY, VYBODepth 48.5 ge
POKYPixel-pipe data readynor_latchSet: PYGO; Reset: LOBYLOBY = NOT(mode3): clears POKY outside Mode 3
SOCYWindow-halt gatenot_x1TOMUSOCY = NOT(RYDY) via triple inversion (TOMU, SYLO) — drops during window activation
VYBOOperational per-dot gatenor3MYVO, FEPO, WODUDepth 46.9 ge
FEPOSprite X priority aggregateor2FOVE, FEFYDepth 41.9 ge

Four halt conditions — any one stalls CLKPIPE (SACU stuck high):

HaltCauseMechanism
ROXY=1Fine scroll activeForces SACU=1 directly
POKY=0Data not ready (outside Mode 3, or first tile not yet fetched)TYFA=0 → SEGU=1
SOCY=0Window activation (RYDY=1)TYFA=0 → SEGU=1
VYBO=0Sprite 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.

Pitfall: TAKA is not on the SACU halt path

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.

At a glance

  • 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 ff43 holds 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.

GateRoleTypeClock / TriggerNotes
PECUFine-counter gated clocknand2Inputs: ROZE, ROXOFrozen when ROZE=0 (counter at 7)
ROZEFine-counter self-stopnand3Inputs: RUBU, ROGA, RYKU0 only at count 7 — MOCE's analogue
RYKUFine count bit 0dffrPECUAsync reset by PASO
ROGAFine count bit 1dffrRYKU (~Q ripple)Async reset by PASO
RUBUFine count bit 2dffrROGA (~Q ripple)Async reset by PASO
PASOFine-counter resetnor2Inputs: PAHA, TEVOAsserted outside Mode 3 or at tile boundaries
SUHAPer-bit SCX matchxnorff43_d0 vs RYKU1 when bit 0 matches
SYBYFine-count bit 1 matchxnorff43_d1 vs ROGA1 when bit 1 matches
SOZUFine-count bit 2 matchxnorff43_d2 vs RUBU1 when bit 2 matches
RONEDrain-gated match aggregatenand4Inputs: ROXY, SUHA, SYBY, SOZU
POHUMatch comparatornot_x1Input: RONEPOHU = AND4(ROXY, SUHA, SYBY, SOZU) — self-terminating via the ROXY term
PUXAMatch capture (odd)dffrROXOData: POHU
NYZEMatch capture (even)dffrMOXEData: PUXA
POVAMatch pulseand2Inputs: NYZE.Q_n, PUXASingle-dot pulse; drives ROXY's reset
ROXYFine-scroll gatenor_latchSet: PAHA; Reset: POVAThe SACU suppressor

Rule: PAHA polarity, stated once for the whole book

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).

Rule: the ff43 value at the count=N ROXO↑ wins

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)TypeMechanism
AVAP↑0.000CombinationalOAM scan complete
XYMU↓0.000LatchAVAP drives XYMU.r directly
NYXU↓0.002CombinationalNOR3 delay (439 ps)
LAXU/MESU/NYVA reset0.002Async resetFetch counter cleared
LAXU↑0.986ClockedFirst counter toggle after NYXU releases
NYKA↑5.481DFF (ALET↑)Fetch-complete captures after state 5
PORY↑5.991DFF (MYVO↑)Half-dot later
SUVU↓, TAVE↑5.996CombinationalFirst TEVO of the scanline fires
PYGO↑6.480DFF (ALET↑)
POKY↑6.481NOR latchData-ready latches (+342 ps)
ROXY↓6.490NOR latchCleared through the fine-scroll match chain (SCX=0: immediate match)
SACU↓ (glitch)6.511CombinationalROXY opens the OR before the first proper pulse
SACU↑7.026First pixel clock
XEHO↑7.026ClockedPX 0→1, within the rounding of the same edge
RYFA↑13.991DFFDrain-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

Rule: fine scroll is clock gating, not push-and-discard

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):

SCXMode 3 core, AVAP → WODU (dots)SACU countFirst SACU (dots from AVAP)
0173.0451677.026
3176.04516710.026
7180.04516714.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):

Δ dotsSignalMechanism
+0.0349PAHA↓NOT(mode3) — XYMU.Q_n rose on the AVAP reset just before
+0.0390PASO↑Fine-counter reset released
+5.4805NYKA↑Fetch state 5 complete
+5.9910PORY↑
+5.9963TEVO↑Startup tile pulse
+5.9973PASO↓Briefly re-asserted by TEVO
+6.4800PYGO↑
+6.4814POKY↑Data-ready
+6.4827SEGU↓First CLKPIPE enable
+6.4838TEVO↓
+6.4846ROXO↑Clocks PUXA and the fine counter

Per-SCX match pipeline:

StageSCX=0SCX=3SCX=7
Counter ticks (one per ROXO↑)03 (+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):

ROMSCX=7 CUPA↑count=3 ROXO↑SCX seen at captureDiscard endsFirst WODU↑
_1 (hardware: mode 3)+5.496+9.4837 (write in time)+13.488, count=7+180.045 (= 173+7)
_2 (hardware: mode 0)+9.496+9.4833 (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.

At a glance

  • 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 familyRoleTypeClock / TriggerCountNotes
BG shifter plane ATile data plane 0dffsrSACU8MYDE–PYBO range
BG shifter plane BTile data plane 1dffsrSACU8TOMY–SOHU range
Temp data latchesVRAM bus capturedlatch_ee_qEnable: LUNA/LOMA8LEGU, LUZO, MEGU, MUKU, MYJY, NASA, NEFO, NUDU
LUNA / LOMA / METE / NYDY / NOFUTemp-latch enable chainnot / nand3from NYVA5Depth 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:

  1. 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.
  2. 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.

At a glance

  • 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 ← 0 write 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

PageCovers
Sprite storage and X matchingThe sprite shift registers, the transparency-gated load, the per-slot store and X comparators, FEPO, and the attribute pipes
The fetch state machineThe 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 modelMeasured 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 pathsOBJ_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.

At a glance

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

Sprite pixel shift registers

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

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

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

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

The transparency-gated parallel load

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

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

Rule: priority lives in the load path

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

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

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

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

Sprite X match

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

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

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

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

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

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

Why a reset slot cannot fire

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

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

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

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.

At a glance

  • 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

GateRoleTypeClock / TriggerNotes
TOXE / TULY / TESEFetch counter bits 0–2dffrSABE, then ~Q rippleReset by TAKA↑
SABEGated fetch clocknand2LAPE, TAMEAdvances on ALET rising — opposite edge from the BG fetcher
TAMESelf-stopnand2TESE, TOXEFreezes SABE at count 5
TEKYX-match triggerand4FEPO, TUKU, LYRY, SOWOTUKU = NOT(RYDY) blocks during window activation; LYRY requires BG fetch complete; SOWO = NOT(TAKA) blocks re-trigger
SOBUFetch requestdffrTAVA (clk5 = ALET rising)Captures TEKY — the one DFF in the trigger chain. No reset domain (r_n tied high)
SUDATrigger one-shot partnerdffrLAPE (clk4 = ALET falling)Captures SOBU ~1 dot later; suda_n closes RYCE. No reset domain
RYCEOne-shot fireand2SOBU, suda_nHigh from SOBU's capture until SUDA mirrors it
SECATAKA set netnor3RYCE, ROSY, ATEJThree set arms: Mode-3 trigger, LCD-off, and the line-end pulse
TAKAFetch-running latchnand_latchSet: SECA; Reset: VEKU = NOR2(WUTY, TAVE)Freezes SACU via FEPO's persistence; WUTY = normal end-of-fetch, TAVE = startup carry-over clear
WUTYFetch donenotVUSAClears TAKA; clocks the per-slot fetched-flag DFFs
VUSAFetch-done aggregateor2TYNO, tyfo_nTwo parallel decode branches
TYNODone decode Anand3SEBA, TOXE, VONU
TYFODone decode BdffrLAPECaptures TOXE; no reset domain
TOBU / VONU / SEBATULY capture chaindffrclk5 / clk5 / clk4Mode-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:

StageCellsMode 2 (scan)Mode 3 (fetch)
1, bank BYDYV…ZECAY byte (OAM byte 0)tile index (byte 2)
1, bank AXYKY…ECEDX byte (byte 1)attribute (byte 3)
2, Y-sideXUSO, XEGU, YJEX, XYJU, YBOG, WYSO, XOTE, YZABY-compare operand Bsprite tile VRAM row bits
2, X-sideYLOR, ZYTY, ZYVE, ZEZY, GOMO, BAXO, YZOS, DEPOX-store fan-outattribute 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.

Pitfall: the capture is not gated off under DMA

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.

At a glance

  • 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):

SPRITEXSingle 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).

Measured: the plateau is a function of (K + SCX) mod 8

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

Rule: stacked-sprite composition

mode3_ext(N, K, S) = single_sprite_penalty(K, S) + (N − 1) × 6.000 dots

The rule's measured anchors:

ConfigurationPredictedMeasured
N=1, K=0 (prelude-absorbed)11.00011.000
N=2, K=017.00017.000
N=3, K=023.00023.000
5 × K=7 + 5 × K=167, SCX=170.00070.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 NMode 3 extension (dots)Integer part
1+5.9786 = 1 × 6
2+11.975 (two ROM variants)12 = 2 × 6
3+17.97818 = 3 × 6
9+53.97754 = 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.

At a glance

  • 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).

GateRoleTypeInputsNotes
XULAPlane-B pixel-output gateand2XYLO, sprite_px_b7 (WUFY)
WOXAPlane-A pixel-output gateand2XYLO, sprite_px_a7 (VUPY)
NULYSprite-visibility masknor2WOXA, XULA1 ⇔ 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 windowOutcome
Entirely before TEKY's ALET edgeAROR=0 kills the trigger — no fetch, no penalty
Entirely after SOBU's captureFetch completes; XULA/WOXA gate the output (sprite suppressed at the mux)
Straddling the edgeSub-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.

For implementors

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.

Open question: the transient's mechanism

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:

GateRoleTypeInputsNotes
FUFOXYMO complementnot_x1XYMO
GEJYOBJ_SIZE muxao22xuso_n, FUFO, ff40_d2, WAGO8×8: passes xuso_n (Y-offset bit 0); 8×16: passes WAGO
WAGO8×16 half-selectxorsprite_y_store3, WUKYWhich half of the 16-row sprite
WUKYY-flip termnot_x1YZOSYZOS latches OAM attribute bit 6
FAMU~ma4 tri-state drivernot_if0in = GEJY, ena_n = ABONOne of 6 ~ma4 drivers; the sprite-fetch arm
ABONFAMU enablenot_x2 chainfrom TULY/VONU/mode3Active (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.

At a glance

  • 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.
GateRoleTypeClock / TriggerNotes
PALOWY bits 4–7 + LCDC.5 decodenand5ff40_d5, NOJO, PAGA, PEZO, NUPAThe XNOR inputs compare WY bits 4–7 to LY bits 4–7. LCDC.5 is part of the WY-match decode itself
NELE / PAFU / ROGEWY decode completionnot / nand5 / not+ NAZE, PEBO, POMO, NEVU (WY bits 0–3)wy_match = LCDC.5 ∧ (LY == WY)
SARYWY-match samplerdffrclk = hclk (TALU)Latches wy_match; reset by ppu_reset_n
REPUREJO resetor2mode1, ppu_reset2Holds REJO cleared throughout every VBlank
REJOWY-match frame latchnor_latchs = SARY; r = REPUwy_latch gates the WX decode — no PX==WX cascade can start without it
PUKY / NUFA / NOGY / NUKOWX-match decodenand5 / not / nand5 / notPX bits vs WX bits + wy_latchNUKO = wy_latch ∧ (PX == WX)
PYCOWX-match capture 1dffrclk = ROCO (pixel-clock-derived; requires POKY=1)The POKY gate blocks WX=0 from firing before the first BG fetch completes
NUNUWX-match capture 2dffrclk = MEHE (= NOT(ALET))Feeds PYNU.s
XOFOPYNU reset (WIN_EN gate)nand3ff40_d5, NOT(ATEJ), ppu_reset_nReleases PYNU only when LCDC.5=1 outside the line-end pulse
PYNUWindow-armed latchnor_latchs = NUNU; r = XOFO
NUNYWindow-trigger pulseand2PYNU, nopa_nWire name win_start; high while armed but not yet captured
PUKU / RYDYWindow hit-and-latchnor2 / nor3NUNY+RYDY feedback / PUKU, PORY, ppu_reset2RYDY self-latches via PUKU until PORY clears it
NOPAWindow modedffrclk = ALET; d = PYNUOnce captured, the fetcher targets the window tilemap
NYFO / MOSUTrigger buffernot / notwin_startMOSU drives NYXU
WAZY / VYNO / VUJOWindow line counternot_x1 / dffr ripplewy_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:

  1. LCDC.5 lives inside the decode. PALO's NAND5 includes ff40_d5, so wy_match is force-low whenever the window is disabled — SARY can only capture 1 during scanlines where LCDC.5 is held high.
  2. 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_match capture. 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).
  3. Once set, REJO cannot be disarmed mid-frame. A WY rewrite that drops wy_match cannot 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:

SCXPX==7 window (dots from AVAP)REJO↑ atSame-line outcome
0[13.03, 14.03]+16.997window closed 2.97 dots earlier — no fire
2[15.03, 16.03]+16.997closed 0.97 dots earlier — no fire
3[16.03, 17.03]+16.997REJO 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):

  1. PX reaches WX → NOGY drops → NUKO rises.
  2. PYCO captures NUKO on the next ROCO edge.
  3. NUNU captures PYCO on the next MEHE edge.
  4. PYNU sets (window armed).
  5. NUNY = AND2(PYNU, nopa_n) rises → win_start → MOSU rises.
  6. PUKU drops; RYDY = NOR3(0,0,0) rises (window hit).
  7. NOPA captures PYNU on the next ALET edge (window mode active).
  8. NUNY falls → MOSU falls.
  9. PUKU stays low — RYDY's own feedback holds it.
  10. 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):

EventValue
MOSU↑+6.989 dots from AVAP
NYXU↓441 ps after MOSU↑
MOSU pulse width0.494 dots
NYXU pulse width0.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:

Rule: the joint (WX, SCX) trigger model

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:

  1. 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).
  2. 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_wx exercises 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_change family): 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_clk branch stays silent). Measured end to end: the boundary's PANY/RYFA/SEKO/TEVO/NYXU all slip exactly +1.000 dot (dmg-sim measurement).

Pitfall: the slip needs no rendered window

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.

At a glance

  • 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

GateRoleTypeInputsNotes
TADEBG plane B output (LCDC.0 gated)and2SOHU (bg_px_b), VYXE (ff40_d0)LCDC.0=0 forces plane B to 0
RAJYBG plane A output (LCDC.0 gated)and2PYBO (bg_px_a), VYXESame gating, plane A
RYFUBG plane A gated by sprite priorityand2RAJY, VAVAVAVA from the priority pipe (sprite pipeline)
RUTABG plane B gated by sprite priorityand2TADE, sprite_px_priorityPlane-B analogue of RYFU
POKABG-pixel combinenor3NULY, RUTA, RYFUbgpx = NOT(POKA); feeds the MOKA/NURA/WUFU combiners (sprite pipeline drives NULY here)
MOKAOBP1 palette AO2222 combinerao2222bgpx vs sprite selection
NURABGP palette AO2222 combinerao2222bgpx vs sprite selection
WUFUOBP0 palette AO2222 combinerao2222bgpx vs sprite selection
PATYPixel-mux outputor3MOKA, NURA, WUFU
RAVO / REMYLD1 / LD0 driversnotPATY / 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 dotsPrior cp_pad↑ — the LCD captures dot N−1's pixel. LD0/LD1 still hold the OLD palette result
0CUPA↑
+0.005 / +0.076 / +0.080 dotsSACU↓ / PERO / LD-driver transition to the NEW result
+0.534 dotsNext cp_pad↑ — the LCD captures the NEW-palette pixel

No latching exists between the pixel-mux output and the pad.

Pitfall: no extra pipeline stage

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 second cp_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:

MemberScope
LCDC.0bidirectional, every write
LCDC.1OFF direction only
BGPsecond-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:

GateRoleTypeInputsNotes
POMELoop AVAP gatenor2AVAP, POFY
RUJULoop OR combineror3PAHO, POME, TOFUTOFU is the video-reset arm
POFYLoop inverter + feedbacknot_x1RUJUFeeds POME and the pad buffer
PAHOPX-bit-3 capturedffrROXOData: XYDO (PX bit 3); reset by XYMU outside Mode 3
RUZEST pad buffernot_x3POFY

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.

Measured: the ST pulse shape

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).

Open question: HSYNC at the glass

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):

PadDriver cellSourcePrimary description
s (VSYNC)MURE (not_x1)MEDALine counters (the LY=0 capture)
st (HSYNC)RUZE (not_x3)POFYthis 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.Qper-scanline gate
cplKYMO (not_x1)KAHE (AO22 on LCDC.7)this chapter
frKOFO (not_x1)KUPA (AO22 on LCDC.7)this chapter
ld0REMYPEROpixel plane 0
ld1RAVOPATYpixel 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.

At a glance

  • 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:

CellTypeRole
ASENor2OR2(ATAR, AVAP) — clears the scan-active latch BESU
NYXUnor3NOR3(AVAP, MOSU, TEVO) — BG fetch counter reset (BG pipeline)
POMEnor2NOR2(AVAP, POFY) — per-scanline synchroniser of the ST sync loop (LCD output)
XYMUnor_latchreset pin — Mode 3 starts

Pitfall: AVAP is not ALET-edge-aligned

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

GateRoleTypeClock / Trigger
WODUMode 0 conditionand2XENA, XANO (STAT interrupts)
VOGAH-Blank capture DFFdffrALET rising — primary here
WEGOXYMU set driveror2TOFU, VOGA
XYMURendering-mode latch (active-low Mode 3 indicator)nor_latchSet: 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.0000
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):

  1. LX reaches 113; SANU fires.
  2. RUTU captures on the SONO edge and holds for one full TALU cycle.
  3. LY increments (MUWY toggles on RUTU's rise).
  4. 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.
  5. POPU captures XYVO on NYPE's rise: LY < 144 → Mode 2 begins; LY ≥ 144 → Mode 1 (VBlank).
  6. 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"]
StageCellTypeClockInputsDrives
1CATUdffrXUPYD = AND2(SELA, ALES); reset = ABEZABAF, ANEL, BESU
2ANELdffrAWOH = NOT(XUPY)D = CATU; reset = ABEZBYHA
3BYHAoa21ABAF (= NOT(CATU)), ANEL, ABEZATEJ
4ATEJnot_x2BYHAANOM + 5 more consumers
5ANOMnor2ATEJ, ATARscan 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)

  1. LY reaches 153: NOKO=1; MYTA captures it on NYPE's falling-edge distribution.
  2. LAMA = NOR2(LYHA, MYTA) goes low — LY resets to 0.
  3. NERU (the LY=0 NOR8) rises.
  4. 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.
  5. 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.

At a glance

  • 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).
GateRoleTypeClock / TriggerNotes
LALUSTAT interrupt latch (IF bit 1)dffsrClock: VOTY; D tied highEdge-triggered by the SUKO pulse
SUKOCondition combining gateao2222(ROXE·TARU) + (RUFO·PARU) + (REFE·TAPA) + (RUGU·ROPO)
TUVA / VOTYInverter pairnotSUKO → LALU clock
TARUMode 0 conditionand2WODU, TOLUTOLU = NOT(mode1) — non-VBlank gate
PARUMode 1 conditionnot_x1popu_n= POPU.Q; also feeds TOLU
TAPAMode 2 conditionand2TOLU, SELASELA ≡ RUTU buffered — the line-end pulse
ROPOLY==LYC synced matchdff17TALU risingCaptures PALY
ROXE / RUFO / REFE / RUGUSTAT.3/.4/.5/.6 enablesdrlatch_eeCUPA strobe
WODUMode 0 conditionand2XENA, XANOFull breakdown below; mode-control is the primary chapter
PALYLY==LYC comparatornot_x1combinationalPer-bit XNORs → SUBO/SOVU → RAPE → PALY
RUPOSTAT bit 2 visible latchnor_latchs = ROPO.Q; r = PAGOTransparent 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.

Pitfall: the two-pass STAT-write model

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↑0PX=167 decode asserts
WODU↑ (early pulse)+483XENA still high
FEPO↑+3,141terminal sprite match
XENA↓+3,205
WODU↓+3,494pulse 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.

Two Mode 0 falling arrivals

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

Rule: the through-zero test

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.

Open question: Case 4 vs hardware annotation

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 riseEventROPO captures
1POPU's edge; LY already 153(153 == LYC)
2MYTA fires; LY resets after the capture(153 == LYC) — pre-reset, via the race
3LY 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.

At a glance

  • 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):

PhaseDotsDurationWhat happens
Mode 2 (OAM scan)0–7980 dots40 OAM entries, 2 dots each; sprite store builds
Mode 3 (pixel transfer)80 – ~253.5+173.481 + (SCX & 7) + penaltiesFetch and shift; CLKPIPE drives output
Mode 0 (H-Blank)remainder456 − the restCPU 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 & 7 step, 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:

PageCovers
LCD-on power-upThe 454-dot first scanline, the pin-level startup transient, and the reset-domain taxonomy of every PPU DFF
LCD-on → first WODUThe 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):

ComponentFirst scanlineSteady-stateDiff
Line-start reference → first LX-incrementing TALU↑2.021 (XODO↓ phase) + 1.483 (divider startup) = 3.5046.000 (first TALU↑ lands inside the RUTU pulse and doesn't count)−2.496
112 further LX increments448.000448.0000
LX=113 → SANU → RUTU≈2.0≈2.0≈0
Total453.500456.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":

  1. 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.
  2. 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.
  3. 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.

For implementors

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

PinPre-LCD-onFirst edge after LCD-onCadence
s (VSYNC)static 0+17.1 ms — the LY 153→0 boundary ending the first frameonce per frame
st (HSYNC)static 0+19.2 µs (first-scanline Mode 3)per scanline
cpstatic 0+20.7 µshalf-dot period during Mode 3
cpgstatic 0+6.7 µsper scanline
cpl / fralternating at ~8/~4 kHz (audio mux)seamlessper scanline
ld0 / ld1static 0first Mode 3 pixelpixel 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:

ClassReset conditionMembersBehaviour
Direct LCD-offppu_reset_n / ppu_reset2_ndividers WUVU/VENA/WOSU; RUTU, NYPE, POPU, MYTA, MEDA; NOPAHeld at 0 while LCDC.7=0; released at the VID_RST edge
Indirect LCD-offcombinational nets containing VID_RST termsLX, LY, PX, VOGA, scan counter, fine-scroll counter, BG fetch counter, AVAP generator, sprite fetch counter, sprite storeHeld at 0 during LCD-off; reset at their per-scanline/per-tile boundaries during rendering
Mode-3-onlythe mode3 netLYZU, PUXA, NYZE, PYGO, RENE, RYFA, PAHO, SEBA, TOBU, VONUZero at every Mode 3 entry, on every scanline, every frame — identical by construction
No-reset persistencetied-high r_n or data-driven set/reset onlyBG + sprite shift registers (32 cells), tile temp latches, SOBU/SUDA/TYFO, XYMU, ROXYHold state indefinitely across LCD-off
CPU-registerCPU-write/system-reset onlyLCDC, SCX/SCY, WX/WY, palettes, LYC, STAT enables, ROPOSurvive 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:

  1. The sprite store is empty on the first frame — the LCD-on bypass never sets BESU, CARE never arms, and oam_data_latch never pulses (mode transitions). Consequences by OAM content:

    CaseOAM at LY=0Divergence on LD0/LD1?
    1no Y-visible spritenone — 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
    2Y-visible sprite, on-screen Xyes — the first frame misses the sprite; steady-state frames render it
    3Y-visible sprite at OAM X=0timing 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)
  2. 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.

Pitfall: the fetch-overlap cell

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:

SCXWODU M-cyclephasein-flightsliphandler A
0M651.556inc#61no61
1M652.556inc#61+162
2M653.556inc#61+162
3M660.556inc#62no62
4M661.556inc#62no62
5M662.556inc#62+163
6M663.556inc#62+163
7M670.556inc#63no63

Skip-boot note

For implementors

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.

At a glance

  • 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_if1 bus drivers stay unresolved through the rest of the M-cycle (the x-window). LY reads have no such window (not_if0 drivers).
  • 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]
before10 (Mode 2)
~2.2 ns … ~8.5 ns00 (transient)
after ~8.5 ns11 (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):

AnchorWODU↑ positionXYMU↑ vs CPU latchLatchesMechanism
lcdon_to_stat0_cread M-cycle, T4 (+3.556)+0.006 dots afterPRE (0x83)cascade misses the latch
sprite_0_anext M-cycle, T3 (+2.556)+3.00 dots afterPRE (0x83)cascade entirely after the read
sprite_0_bread M-cycle, T3 (+2.556)−1.00 dots beforePOST (0x80)cascade flips the already-settled bus
win0_bread M-cycle, T2 (+1.556)−2.00 dots beforePOST (0x80)cascade beats the read driver; bus settles directly to POST
lcdon_to_stat0_dprior M-cycle, T4 (+3.556)−4.00 dots beforePOST (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.

Rule: the driver-cell distinction

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).

At a glance

  • 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:

ClockDFFs
aletNYKA, LYZU, PYGO, RENE, DOBA, NOPA, VOGA
myvoPORY
sacuPX bits 0–3 + 16 BG shifter + 16 sprite shifter + 16 attribute pipe (52 total)
taluSAXO, NYPE
sonoRUTU, 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)ClockClock depthData inputs
PORYMYVO22.2 ge (tightest)NYKA, NAFY (6.2 ge)
LYZU, PYGO, RENEALET16.3 geLAXU, PORY, RYFA
NYZEMOXE16.7 gePUXA

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.

AreaDomainNotes
Palette registersPer-frameStable absent writes; the mid-Mode-3 write ordering is the structural case above
SCXPer-frameMid-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)
SCYPer-frameSame 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, WYPer-frameWY compared once per line; WX per-dot but the compare is shallow
LCDCPer-frameAll consumer sampling is combinational from the latches; the interesting cases are the per-bit mid-write behaviours covered in their consumer chapters
STAT enablesPer-frameCUPA-strobed; the FF41 bus-settle transient is in STAT interrupts
LY == LYCPer-frameFull line to settle (boundary races excepted — STAT interrupts)
LCD output pathStaticPixel data is combinational from shifter MSBs
External cartridge address padsPer-lineCPU-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 busPer-lineFour 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

In this part

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.

At a glance

  • 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.
GateRoleTypeClock / TriggerNotes
UKUP … UPOFreg_div16 bits 0–15dffM-cycle boundary, then ~Q rippleThe free-running divider
reset_div_nDivider reset netnorUCOB (DIV write), reset, TAPEResets all 16 DFFs
REGA … NUGATIMA bits 0–7tffnl (toggle FF with load)LSB by SOGU; ripple by lower bit's qmexu↑ switches all 8 to level-sensitive load
UKAP / TEKO / TECYTAC mux chainmuxiSelect the reg_div16 bit driving TIMA
SOGUTIMA clock gatenor2TECY, sabo_nTAC.2 low ties the TIMA clock high
NYDUNUGA pre-wrap capturedffrCLK9↑Holds NUGA's previous M-cycle value
MERYWrap detectnor2NUGA, nydu_nRises when the MSB just fell
MOBAWrap capturedffrCLK9↑Q = int_timer
MEKE / MEXUReload enable chainnot / nand3MEXU drives the tffnl load pins
NYBOIF[2] capturedffsrclocked by int_timerIF 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 selectFrequencyreg_div16 bit feeding TIMA
004,096 Hzbit 7
01262,144 Hzbit 1
1065,536 Hzbit 3
1116,384 Hzbit 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-cycleTIMA = 0xFF entering
SOGU↓ (increment edge)+2,998 psselected bit falls
TIMA ripples 0xFF → 0x00+4,600 → +15,500 ps~1,550 ps per stage, NUGA last
MERY↑ (wrap detect)+15,700 ps173 ps after NUGA
Reload M-cycleopens at the next CLK9↑
MOBA.q↑ (= int_timer)+0 psDFF capture on the boundary edge
NYBO.q↑ (IF[2] set)+931 ps
MEKE↓+951 ps
MERY↓+1,032 psNYDU has caught up
MEXU↑ (TMA load begins)+2,005 psall 8 tffnl cells go transparent
TIMA visibly = TMA≈ +2,000 pslevel-sensitive load
MEXU↓+978,463 ps2,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.

At a glance

  • 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 nameCell / wireRoleTypeNotes
irq_latchedYOII (g42)Pending-interrupt captureDFF on CLK9Samples IF & IE once per M-cycle; forks into the running-CPU and halt-release chains
irq_pendingwire int_pendingDispatch eligibilitycombinationalPer-bit IF & IE through the priority chain
dispatch_triggerwire int_takeDispatch triggercombinationalPulses in the eval phase (T4) of fetch M-cycles
dispatch_activeZACW, wire int_entryDispatch captureDFF on CLK9Q↑ starts the 5-M-cycle sequence
CLK9CPU M-cycle clockOne 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:

  1. Per-bit IF & IE latch (irq_latch<0..4>) — an enabled D-latch, not a DFF: transparent while data_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.
  2. Priority chain (irq_prio_bit<0..4>) — a precharged wired-NAND bus: precharged high through T1–T3, evaluated when write_phase rises in T4. The highest-priority pending bit wins.
  3. 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-cycle irq_pending↑ to dispatch_trigger↑ is the wait for the eval phase, not gate propagation.
  4. 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_n and data_phase — the set condition.
  5. 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-cycleBusState change
1internalIME cleared; the IDU −1 step on PC (below)
2internal
3write PC-high to SP−1SP decremented
4write PC-low to SP−2SP decremented; vector resolved
5opcode fetch from vectorPC = $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:

  1. M3 data phase: reg_ie captures the pushed byte; the per-bit NAND re-evaluates combinationally — but the IF&IE latch is opaque (data phase).
  2. 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.
  3. 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.

For implementors

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.

At a glance

  • data_phase is 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:

  1. Every per-bit IF latch is transparent for the whole HALT (the enable is data_phase_n) — a source rise propagates combinationally to irq_pending regardless of its dot phase.
  2. 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):

Sourceirq_pending↑ phase in M-cycleSetup 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-cycle5,854,711 ps ≈ 6 M-cycles
VBlank / LYC~88% into the M-cyclemeets (~117 ns margin)5 M-cycles
HBlank STATWODU is ALET↓-aligned; ≥122 ns from any CLK9↑ at every SCXmeets5 M-cycles
OAM STATRUTU at +1.5 dots; chain 4,847 psmeets (~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 commit dispatch_active on 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.

Open question: the M2/m7 ordering

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_active cannot 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:

IMEIF & IE at HALT decodeWhat happens
1≠ 0Dispatch 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≠ 0No 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= 0Halt 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.

At a glance

  • 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.
BitCellClock (source)Source driver
0 (VBlank)LOPEint_vblPOPU.q (mode1) through TOLU + VYPU
1 (STAT)LALUint_statSUKO through TUVA + VOTY (STAT interrupts)
2 (Timer)NYBOint_timerMOBA.q — a CLK9-aligned DFF (timer)
3 (Serial)UBULint_serialCALY toggle at the transfer clock
4 (Joypad)ULAKint_jpAND2(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 pathPulse positionWidthvs +3.5-dot source rise
FF0F write+2.0–3.5 dots (T3–T4)1.493 dotslands after — rise wins
Dispatch ack+3.0–4.0 dots (T4 into next M-cycle)1.000 dotlands inside — clear wins

For implementors

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

In this part

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.

At a glance

  • APU register writes commit on the same dot-2–3.5 window as PPU register writes — apu_wr and 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) & 7 is 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 with atal_4mhz (≈ +1 ns buffer delay);
  • apu_4mhz — the opposite phase;
  • apu_wr — in phase with cpu_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):

SignalEdgePhaseΔ from atal_4mhz
AJER / ATEP / CERY (prescaler stage 1, all channels)togglestart of every T-cycle+1,900 to +3,200 ps
apu_wrrisestart of T3+7,500 ps
apu_wrfallmid 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

For implementors

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_1mhzafter 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 atre-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.

Pitfall: deriving the frame sequencer from the divider

  1. (reg_div16 >> 11) & 7 is 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.
  2. 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".

At a glance

  • 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_1mhz cycle late; a retrigger of a running channel and every natural overflow run at the steady period (Rule below).
  • The duty counter clocks on chN_frst falling and resets only on apu_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)RoleTypeClock / TriggerNotes
AJER / ATEPPrescaler /2 stage 1dffrapu_4mhz (per-channel buffer)Toggle; free-running 2 MHz; only reset is apu_resetnever reloaded by triggers
CALO / CEMOPrescaler /2 stage 2dffrstage-1 rippleToggle; free-running 1 MHz; output (buffered / directly) is chN_1mhz
ch1_fdis / ch2_fdisChannel-disable latchnand_latchSet: DAC-off / apu_reset; Reset: trigger (one cycle delayed)While set, gates the divider toggle clock low
FULO→GEKU / CAMA→DOCADivider toggle clocknor2 + notchN_1mhz gated by chN_fdisBit 0 toggles on the active-high rise
GAXE HYFE JYTY KYNA / DONE DYNU EZOF CYVODivider bits 0–3tffnlbit-to-bit rippleLoad enable fume / cogu
JEMA HYKE FEVA EKOV / FUXO GANO GOCA GANEDivider bits 4–7tffnlripple via inverter (KYPE / sibling)Load enable dega / erog
EMUS EVAK COPU / HEVY HEPU HERODivider bits 8–10tffnlripple via inverter (DERU / sibling)MSB q = chN_ftick — the overflow edge
CALA + COMY / sibling pairOverflow detectornot + dffr (/2 toggle)clk: NOT(chN_ftick)Produces the one-cycle self-clearing pulse chN_frst; also async-reset by triggers
ch1_restart / ch2_restartTrigger synchroniserdffrchN_1mhzCaptures the NRx4-bit-7 write; aligns it to the next chN_1mhz
EPYK / DUJUDivider load enablenor2chN_frst, chN_restartActive-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.

Rule: the 2-bit prescaler is untouchable

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 pulse chN_frst that opens the reload.
  • Trigger and overflow share the same load net. chN_restart and chN_frst are 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).

Measured: the natural-overflow reload

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

EdgeWhat happens
Divider MSB falls (overflow)chN_ftick↓ → CALA↑ → COMY toggles → chN_frst
chN_frstLoad enable falls → all 11 stages enter level-sensitive load (q ← period source)
Next chN_1mhzAND(COMY, chN_1mhz) asserts COMY's async reset → chN_frst
chN_frstLoad enable rises → stages return to toggle mode holding the loaded value
NRx4 trigger writeAsync-loads the period source (CH1: acc_d8..10; CH2: NR24 latches); the strobe arms the synchroniser
Next chN_1mhz↑ after the writechN_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.

Rule: only the channel-enabling trigger is one cycle late

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).

For implementors

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)RoleTypeClock / TriggerNotes
DAJO / CULECounter clocknot_x1NOT(chN_frst)Rising edge = chN_frst falling
ESUT / CANODuty counter bit 0dffrDAJO / CULEToggle; reset only by apu_reset
EROS / CAGYDuty counter bit 1dffr_ccbit-0 ripple
DAPE / DYVEDuty counter bit 2 (MSB)dffr_ccbit-1 ripple
COSO CAVA CEVU CAXO / DOMO DYTA DOJU DOVEDuty-pattern selectsnor2 ×4NRx1[7:6] decode, one per duty value
ENEK EZOZ CODO / EGOG DYMU DARECounter-state decodesand2 / notcounter=6 / counter ∈ {6,7} / counter ∈ {0..5}
ch1_pwm / ch2_pwmDuty waveform outputao22224:1 mux: OR of four (decode ∧ select) pairs
DUWO / DOMEPWM latchdffrchN_frstCaptures the pre-advance step; reset to 0 by apu_reset
COWE / CYSEPWM gated by channel-runningand2ANDed with the envelope to form the 4-bit DAC input

The duty decode:

NRx1[7:6]Pattern (high at step k)Decode
0000000010 (12.5%)counter = 6
0100000011 (25%)counter ∈ {6,7}
1000001111 (50%)counter bit 2
1111111100 (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).

Measured: the pinned-step staircase

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).

Rule: the trigger phase quantum is one tick

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.

Measured: a sweep period cut defers the next duty capture

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 0x7FF0x7F0 and constant-0x7FF ROMs).

Rule: after a period change the emitted level lags the counter by one new-period overflow

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.

At a glance

  • 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:

GateRoleTypeClock / TriggerNotes
CERYCH3 prescaler /2 stagedffrapu_4mhz (via CYBO buffer)Toggle; free-running; q = ch3_2mhz directly; only reset is apu_resetnever reloaded by triggers
GUGUChannel-disable latch (q_n = ch3_fdis)nand_latchEnable: FAJU = NOT(GYRA), the trigger pulse retimed one ch3_2mhz edge behind GYTA; Disable: FUVO = NOR(ch3_amp_en_n, apu_reset) — a held levelWhile ch3_fdis is high, gates the divider toggle clock low
HEFO → JUTYDivider toggle clocknor2 + notch3_2mhz gated by ch3_fdisOne toggle per T-cycle
KUTU KUPE KUNU KEMUDivider bits 0–3tffnlbit-to-bit rippleLoad enable kyko
KYGU KEPA KAFO KENODivider bits 4–7tffnlripple via KYRU inverterLoad enable kaso
KEJU KEZA JAPUDivider bits 8–10tffnlripple via KESE inverterMSB q = ch3_ftick — the overflow edge
HYFO + HUNOOverflow detectornot + dffr (/2 toggle)clk: NOT(ch3_ftick)One-ch3_2mhz-cycle self-clearing pulse ch3_frst; also async-reset by triggers
ch3_restartTrigger synchroniser qdffrFABO (= NOT(ch3_2mhz))Captures the NR34-bit-7 write on ch3_2mhz↓ — opposite-phase sample edge to CH1/CH2
HERACH3 divider load enable (active-low)nor2ch3_frst, ch3_restartFanned 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.

GateRoleTypeClock / TriggerNotes
DEROCH3 wave-position counter clocknot_x1NOT(ch3_frst)Rising edge = ch3_frst falling
EFARCH3 wave-position bit 0 (wave_nibble_sel)dffrDEROToggle; reset by ETAN
ERUS / EFUZ / EXEL / EFALBits 1–4 (wave-RAM byte address)dffrbit-to-bit rippleReset by ETAN; EFAL's q_n clocks FETY
ETANWave-position counter reset (active-low)nor2ch3_restart, FETYClears the counter on trigger or wrap
FETYCH3 counter-wrap interlock (/2 toggle)dffrEFAL↓ (counter wrap 31→0)Holds the counter at 0 for one extra overflow; cleared by the next ch3_frst
  • Triggers reset it to 0ch3_restart is 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:

StageCellTypeClock / EnableRole
1GAVUdrlatch_eeenable: apu_wr ∧ ff1eWrite-strobe latch — captures the trigger bit at T3/T4 of the write M-cycle
2FOBAdffrapu_phiM-cycle-boundary DFF
3GOFYnor_latchTag latch
4GARAdffrch3_2mhzq is ch3_restart
5GYTAdffrone edge behind GARAAsync-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_latchch3_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.

Measured: DAC-off never resets the data latch

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.

Rule: locked access goes wherever CH3 points

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).

Measured: the nine-subtest locked-write sweep

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 from wave_data_latch↑ to one T-cycle past its fall (≈ 2 T-cycles per overflow) — then with row = 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).

For implementors

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.

At a glance

  • 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_512khz period late: the prescaler count is frozen while ch4_fdis is 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
GateRoleTypeClock / TriggerNotes
ch4_1mhz (BAVU)Noise base clocknotapu 1 MHz~1.048 MHz (4 T); divider and prescaler source
JYCO JYRE JYFUDivisor prescaler bits 0–2tffnltoggle clk kanu (bit 0), rippleLoaded from NR43[2:0] (ff22_d0..2_n) via l(huce); reloaded on trigger
HYNOPrescaler terminal-countand3AND(jyfu,jyre,jyco) — all-ones = terminal
GARYDivider-clock enable latchdffrgyba (= NOT ch4_1mhz)d = hyno; reset guny = NOR(apu_reset, ch4_restart)trigger clears it; gates the shift divider and reloads the prescaler
HUCEPrescaler load enablenothuce = NOT gofu = ch4_restart OR gary — reloads JYCO/JYRE/JYFU on a trigger and on every terminal count
CARYShift-divider clockand2noise_counter_clk = ch4_1mhz AND gary
CEXO … ESEP14-bit shift dividerdffr (ripple ÷2)noise_counter_clk (bit 0), rippleReset only apu_reset4_n — never reloaded or reset by a trigger
ETYR / ERYF (+ DARY, ELYX)Divider tap muxesao / orNR43 shift code (ff22_d4..d7) one-hot-selects one divider bit
FEMELFSR shift-clock selectmuxsel = ff22_d7ch4_lfsr_clk1 = ff22_d7 ? etyr : eryf (high vs low tap bank)
ch4_fdis (JERY)Channel-disable latchnand_latchSet: DAC-off / apu_reset; Reset: trigger (delayed)While set, holds the prescaler toggle clock (kanu) — see load-settle
JESOFree-running ÷2 bitdffrch4_1mhzReset apu_reset5_n — never trigger-reset; its buffered output is hama_512khz (the 512 kHz timebase)
KYKU→KANUPrescaler toggle clockor + notkanu = ch4_fdis OR jeso_512kkanu is the tclk_n of JYCO; ch4_fdis = 1 freezes the prescaler
GONEch4_restart synchroniserdffrhama_512khzCaptures the NR44-bit-7 write, aligned to the next 512 kHz rise
GYSUch4_startdffrapu_phiFirst 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 = 1q ← 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.

Rule: the divisor prescaler reloads, the shift divider does not

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.

Measured: how the period emerges (dmg-sim, divisor code 0)

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 hogach4_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.

Measured: cold first-clock latency (dmg-sim, divisor code 0)

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 (hogach4_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.

Measured: re-trigger phase dependence (dmg-sim)

First LFSR clock from ch4_restart↑, by the divider's frozen count. At period 32 (shift 2, the tap spans eight counts):

frozen countfirst LFSR clock
0 (= power-on cold)24 T
216 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:

Measured: the synchroniser adds write-phase jitter (dmg-sim)

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)

Measured: cold-only +1 on non-zero divisor codes (dmg-sim, 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.

For implementors

Three independent gate-level effects shape the noise first-clock timing; a faithful model needs all three:

  1. 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.
  2. 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.
  3. Gate the prescaler count on ch4_fdis for divisor codes ≥ 1 — a channel-enabling trigger's first clock is one hama_512khz period 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.

Measured: a mid-run code change is captured at the next terminal (dmg-sim)

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 windowprescaler at the writenew code in effect
inside the windowreloads to the new code at once≈ at the write
outside (counting)finishes its period at the old codeat 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.

At a glance

  • 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:

GateRoleTypeClock / TriggerNotes
cate_128hzSweep toggle clocknot chain= byfe_128hz, the frame-sequencer 128 Hz tap (APU clocks)Held low while apu_reset=1
CUPO / CYPU / CAXYSweep counter bits 0–2tffnlcate_128hz, then bit-to-bit rippleLoad: q ← NOT(NR10[4..6])no reset path at all
COZESweep counter-at-max detectorand3CUPO, CYPU, CAXYHigh at counter = 7; drives BEXA's data
BAVESweep pace = 0 detectorand3NR10[6:4] complementsHolds BEXA in async reset via BURY
BEXASweep-fire latchdffrAJER (the 2 MHz CH1 prescaler)Captures COZE; while high, reloads the counter; falls on the next AJER↑. Held at 0 when pace = 0
DAFA → CYMULoad enablenor2 + notBEXA, ch1_restartCYMU = OR(BEXA, ch1_restart) drives all three l pins

The two reload paths:

  • Trigger reload — the same ch1_restart synchroniser 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.

Rule: the +1 quirk is the load window

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:

  1. The counter reloads (above).
  2. 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).
  3. The overflow check runs regardless of shift, but it does not resolve at the fire — it samples a registered snapshot of the shadow (below), shift M-cycles late. When it resolves, with direction = add and adder carry-out, ch1_sum_ovfl_n falls, 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:

GateRoleTypeClock / TriggerNotes
BYRA / CAJA / COPASweep shift-step counter bits 2–0tffnlch1_shift_clk, then bit-to-bit rippleLoad ~shift; saturate shift steps after a fresh shadow
ch1_ld_sumSweep-adder operand-load strobenot_x4Pulses on BYRA·CAJA·COPA saturationClocks the adder's operand registers — the adder never reads the live shadow

Rule: a non-overflowing fire rechecks ~shift M-cycles later

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.

Measured: the overflow-check latency is shift-dependent (dmg-sim)

From the fire to the channel-disable:

shiftfire → disable (M-cycles)event
01.0single calc (no double-check)
11.5recheck
77.5recheck

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).

Measured: the sweep timeline end to end

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.

GateRoleTypeClock / TriggerNotes
kene64 Hz envelope toggle clocknot chain from JYNAJYNA = /2 of byfe_128hzThe frame-sequencer ripple's last stage (APU clocks)
JORE / JONA / JEVYEnvelope counter bits 0–2tffnlkene, then bit-to-bit rippleLoad: q ← NOT(NRx2[0..2]) via JAKE
KYVOEnvelope counter-at-max detectorand3JORE, JONA, JEVYHigh at counter = 7; drives JOPA's data
JUPUEnvelope pace = 0 detectornor3NRx2[2:0]Feeds both JOPA's reset (via HAFE) and HOFO
JOPAEnvelope-fire latchdffrhoru_512hzCaptures KYVO; while high, reloads the counter; falls on the following 512 Hz tick. Held at 0 when pace = 0
HYLY → JAKELoad enablenor2 + notJOPA, ch2_restartJAKE = OR(JOPA, ch2_restart) drives all three l pins
HOFOVolume bit-0 toggle clockor3JOPA, JUPU, ch2_eg_stopHigh while firing, paced-off, or stopped — the toggle commits on its fall
FENO / FETE / FOMY / FENAVolume counter bits 0–3tffnlHOFO, then direction-muxed rippleLoad: q ← NRx2[7:4] on trigger
(per-bit AO22 muxes)Direction ripple selectsao22NRx2[3]Up: ripple on carry; down: ripple on borrow
EMYR / FYRE → GUFYSaturation detectorsnor5 / nand5+not / or2vol 0 + down / vol 15 + up
HEPOSaturation capturedffrJOPA↑Reset on trigger or apu_reset
JEME / ch2_eg_stopEnvelope-stop latchnor_latchSet: HEPO; Reset: trigger / apu_resetPins 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.)

Rule: the volume step is a two-edge event

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$18 on 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.

Measured: zombie volume stepping — every pace-0 write ticks the counter +1

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):

  1. 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.
  2. The frame-sequencer-phase race (_1, env1 — a ROM that never touches NR52 or DIV, so its kene phase 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.
  3. 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.

For implementors

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.

At a glance

  • 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:

BitCellLoad valueLoad enableCount clockNotes
0ERYCNR21[0]bymoDYRO (= NOT(DEME), the gated 256 Hz tick)First stage
1CERANR21[1]bymoERYC ripple
2CONUNR21[2]bymoCERA ripple
3CAMENR21[3]bymoCONU ripple
4BUVANR21[4]agetCAME ripple (via BUKO)Upper bits load via aget — a fan-out split of the same NRx1 strobe
5AKYDNR21[5]agetBUVA rippleq_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.

Measured: polarity

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).

Rule: the extra clock is a level test

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:

  1. No clear — the cells have no reset path; the value holds.
  2. 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.

At a glance

  • 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

SignalValueNotes
WUVU / VENA / TALU / XUPY0 / 1 / 1 / 0Divider phases; VENA=1 with WUVU=0 follows from the toggle timing since LCD-on
BOGA / ALET1 / 1T1 of the M-cycle; ALET rising phase
XYMU1Not in Mode 3
BESU / AVAP0 / 0Scan complete; no transition in flight
LY0
LX98FST-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 counter39Terminal (0b100111)
MEDA1Mid-frame-pulse (fires at each LY 153→0 wrap, holds through LY=0)
POFY / PAHO0 / 0ST loop settled; PX-bit-3 capture in its XYMU-held reset
BG fetch counter5 (frozen)Terminal value held by the MOCE self-stop since the last Mode 3
Fetch cascade DFFs (NYKA, PORY, LYZU, PYGO, RENE, RYFA)all 0Mode-3-only / NAFY reset domains
Fine-scroll counter + match pipelineall 0; ROXY = 1PASO-held; SACU gated high awaiting the next Mode 3
CLKPIPE treeSACU=1, SEGU=1, TYFA=0, VYBO=0, SOCY=1All 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 latchesboot-ROM residueNever observable: the two-parallel-loads mechanism overwrites BG content before any pixel samples, and sprite content is mux-gated (BG pipeline)

CPU-visible registers

RegisterValueSource
LCDC0x91boot ROM
STAT0x85Mode 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 / LYC0x00power-on defaults
BGP0xFCboot ROM
OBP0 / OBP1undefined; use 0xFFthe palette latches have no reset path — true silicon power-on state is undefined; real DMGs consistently read 0xFF
DIV (FF04)0xABsee the divider block below
TIMA / TMA0x00 / 0x00
TAC0xF8
IF0xE1bit 0 (VBlank) set — captured at the boot ROM's last line-144 entry; benign (IE=0) but visible to cartridge code
IE0x00

Rule: the LYC-match consistency check

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:

  1. 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_div16 diverges on the timer-phase test family. Pre-load the full 0xEAF3.
  2. The simulation's bit 15 is a harness artifact. The boot-ROM execution measurement shows reg_div16 = (0x8000 + M-cycles) mod 2^16 because 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:

ItemValue
Duty step counter2
Period divider0x7F9 (counting toward 0x7FF — six ticks to the next overflow)
Prescaler1
Reload value (acc_d = {NR14[2:0], NR13})0x7C1
Volume0 — but the DAC is ON (NR12 = 0xF3 retained) and the running latch is SET
Envelopesaturated-stopped (ch1_eg_stop = 1); counter holds ~pace = 0b100
Sweepinactive (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.

Pitfall: the frame-sequencer seed

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.

GateRoleTypePrimary chapterAlso appears in
ABAFMode Transitions
ABAKPer-slot-reset shared gating termor2Mode 2: OAM ScanMode Transitions
ABEZMode Transitions
ABONFAMU enablenot_x2 chainMode 3: The Sprite Pipeline
ACORWave data latch reset driver (= NOT(apu_reset))not_x1CH3: Wave Channel
ACYLOAM DMA
ADARRegister Writes
ADEHATAL complementnot_x1The Clock TreeRegister Writes
ADYKPhase generator ring stage 4dffThe Clock TreeRegister Writes
AFASRing decode driving CUPA (inputs ADAR, ATYP)nor2Register Writes
AFURPhase generator ring stage 1dffThe Clock TreeRegister Writes
AGETLength counter load enable (upper bits)not_x1Length Counters and Power Cycling
AJEPOAM DMA
AJERCH1 prescaler /2 stage 1dffrAPU Clock Tree and Frame SequencerCH1/CH2: Pulse Channels, Sweep and Envelope
AJONMode 3 + non-DMAand2Rendering Mode ControlOAM and VRAM Access, OAM DMA, CPU-Visible Mode Boundaries
AJUJOAM-access permitnor3OAM and VRAM AccessOAM DMA, CPU-Visible Mode Boundaries
AKYDCH2 length counter bit 5 (MSB)tffnlLength Counters and Power Cycling
ALEFPhase generator ring stage 2dffThe Clock TreeRegister Writes
ALESMode Transitions
ALET4 MHz PPU main clocknot_x2The Clock TreeRendering Mode Control, Register Writes, Register Reads
AMABOAM CPU-write address gateand2OAM and VRAM Access
AMUVLCDC.3 → ~ma10 tri-state drivernot_if0Mode 3: The BG Pipeline
AMYGMode 2: OAM Scan
ANELScan counter reset chain stage 2dffrOAM DMAMode 2: OAM Scan, Mode Transitions
ANOMScan counter resetnor2Mode 2: OAM ScanMode Transitions
ANOSCrystal feedback NANDnand2The Clock Tree
ANYPSCY read enable (AND2 with ppu_rd)and2Register Reads
APAROAM DMAMode 2: OAM Scan
APOVCUPA driver stage 4notRegister Writes
APUGThe IF Register
APUKPhase generator ring stage 3dffThe Clock TreeRegister Writes
APUVCH1 prescaler clock buffernot_x1APU Clock Tree and Frame Sequencer
AREVCUPA driver stage 5nand2Register Writes
ARORSprite-trigger gate (LCDC.1-gated rendering enable)and2LCDC StructureMode 2: OAM Scan, Mode 3: The BG Pipeline, Mode 3: The Sprite Pipeline
AROVRegister Writes
ARURSCX write enableand2Register Writes
ASAMOAM CPU-address disableor3Rendering Mode ControlOAM DMA
ASENBESU reset driveror2Rendering Mode ControlMode 2: OAM Scan, Mode Transitions
ATAGAPU 4 MHz polarity restorenot_x1APU Clock Tree and Frame Sequencer
ATAL4 MHz buffered enablenot_x2The Clock TreeRegister Writes
ATARVID_RST active-highnotRendering Mode ControlMode 2: OAM Scan, Mode Transitions
ATEJRendering Mode ControlMode 2: OAM Scan, Mode 3: The BG Pipeline, Mode 3: The Sprite Pipeline
ATEPCH2 prescaler /2 stage 1dffrAPU Clock Tree and Frame SequencerCH1/CH2: Pulse Channels
ATYPRegister Writes
AVAPMode 2→3 triggernot_x2Mode 2: OAM ScanThe Clock Tree, Rendering Mode Control, Register Writes, Register Reads
AVER= NAND2(mode2, XYSO) — scan-phase term in BYCUnand2OAM DMAMode 2: OAM Scan, OAM and VRAM Access
AVETCrystal feedback NANDnand2The Clock Tree
AVOGSCX read enable (AND2 with ppu_rd)and2Register Reads
AWOHMode Transitions
AXADMode 3: The BG Pipeline
AZEGCH2 prescaler clock buffernot_x1APU Clock Tree and Frame Sequencer
AZEMAROR second input (rendering-active AND mid-scanline-control)and2Rendering Mode ControlMode 2: OAM Scan
AZETWave-RAM precharge-window term (with AZUS)dffrCH3: Wave Channel
AZOFPPU clock buffernot_x6The Clock TreeAPU Clock Tree and Frame Sequencer
AZUSwave_data_latch synchroniser stage 3dffrCH3: Wave Channel
AZYBSlot counter reset drivernot_x1Mode 2: OAM ScanMode Transitions
BAFUapu_wr buffer stage 1not_x1APU Clock Tree and Frame Sequencer
BAFYAMUV enable (BG tilemap stage)not_x2Mode 3: The BG Pipeline
BAGYBYBA/DOBA reset drivernot_x1Mode 2: OAM Scan
BALUOAM-parse reset (active-high)not_x1Mode 2: OAM ScanMode Transitions
BAMAwave_data_latch synchroniser reset driver (= NOT(apu_reset))not_x1CH3: Wave Channel
BANOwave_data_latch synchroniser stage 2dffrCH3: Wave Channel
BAPYRegister Writes
BARYCH3 DAC input bit 0 gate (AND with ch3_active)and2CH3: Wave Channel
BATUThe IF Register
BAVESweep pace = 0 detectorand3Sweep and Envelope
BAVUch4_1mhz — CH4 base clock (= NOT avok, ~1.048 MHz)not_x1CH4: Noise Channel
BAXOMode 3: The Sprite Pipeline
BEBA= NOT(AVOG) — SCX read drivers' ena_nnot_x1Register Reads
BEBUScan-complete aggregateor3The Clock TreeRendering Mode Control, Mode 2: OAM Scan, Mode Transitions
BEDYSCY write enableand2Register Writes
BEGOSlot counter bit 2dffrMode 2: OAM Scan
BEGUWave data latch bit 3drlatch_eeCH3: Wave Channel
BELUdata_phase gate (= NOR2(ATYP, clk_ena_n))nor2Register Reads
BELYCH3 DAC input bit 3 gate (AND with ch3_active)and2CH3: Wave Channel
BEPAWave data latch bit 7drlatch_eeCH3: Wave Channel
BERURegister Writes
BESESlot counter bits 0–3dffr (toggle)Mode 2: OAM Scan
BESUScan-active latch (Q=1 during Mode 2)nor_latchRendering Mode ControlRegister Writes, Register Reads, OAM and VRAM Access
BETEOAM DMA
BEVAdata_phase rebuffernot_x6Register Reads
BEXASweep-fire latchdffrSweep and Envelope
BODEOAM DMA
BOGAPPU timing divider (held 0 under VID_RST)Register WritesPost-Boot State
BOGEOAM and VRAM AccessOAM DMA, Mode 2: OAM Scan
BONESweep overflow channel-disable term (= NOT(ch1_sum_ovfl_n))not_x1Sweep and Envelope
BOPACH3 DAC input bit 2 gate (AND with ch3_active)and2CH3: Wave Channel
BORAWave data latch bit 6drlatch_eeCH3: Wave Channel
BUDYWave data latch bit 2drlatch_eeCH3: Wave Channel
BUKOLength counter bit-3→4 ripple inverternot_x1Length Counters and Power Cycling
BUREFrame-sequencer 512 Hz clocknot_x2APU Clock Tree and Frame Sequencer
BURYBEXA async-reset driver (pace 0)nor2Sweep and Envelope
BUSAwave_data_latch synchroniser stage 1dffrCH3: Wave Channel
BUVACH2 length counter bit 4tffnlLength Counters and Power Cycling
BUWY= NOT(ANYP) — SCY read drivers' ena_nnot_x1Register Reads
BUZARendering Mode Control
BYBAScan-done flagdffrThe Clock TreeMode 2: OAM Scan, Mode 3: The BG Pipeline, Mode Transitions
BYCU= NAND3(CUFE, XUJY, AVER) — COTA drivernand3OAM DMAMode 2: OAM Scan, OAM and VRAM Access
BYHAMode Transitions
BYJOAZEM first input (mid-scanline control)not_x1Mode 2: OAM Scan
BYKACH3 DAC input bit 1 gate (AND with ch3_active)and2CH3: Wave Channel
BYLUFrame sequencer /2 → 128 Hz (sweep tap)dffrAPU Clock Tree and Frame SequencerPost-Boot State
BYMOLength counter load enable (lower bits)not_x1Length Counters and Power Cycling
BYRASweep shift-step counter bit 2tffnlSweep and Envelope
BYRYdata_phase inverter (of BELU)not_x2Register Reads
CAGYCH2 duty counter bit 1dffr_ccCH1/CH2: Pulse Channels
CAJASweep shift-step counter bit 1tffnlSweep and Envelope
CAKEMode 2: OAM Scan
CALACH1 overflow detector (NOT stage)not_x1CH1/CH2: Pulse Channels
CALOCH1 prescaler /2 stage 2dffrAPU Clock Tree and Frame SequencerCH1/CH2: Pulse Channels
CALYint_serial driver (toggle DFF on serial transfer clock)dffrThe IF Register
CAMACH2 divider toggle clock (NOR stage)nor2CH1/CH2: Pulse Channels
CAMECH2 length counter bit 3tffnlLength Counters and Power Cycling
CANOCH2 duty counter bit 0 (LSB)dffrCH1/CH2: Pulse Channels
CAPEMode 2: OAM Scan
CAPYCH1 length-clock gatenor3Length Counters and Power Cycling
CAREGlobal sprite-save enableand3Mode 2: OAM ScanMode Transitions, Scanline and Frame Timing
CARUFrame sequencer /2 → 256 Hz (length tap)dffrAPU Clock Tree and Frame SequencerLength Counters and Power Cycling, Post-Boot State
CARYnoise_counter_clk — CH4 shift-divider clock (ch4_1mhz & gary)and2CH4: Noise Channel
CASYSCX bit-7 read drivernot_if0Register Reads
CATASCX bit-5 read drivernot_if0Register Reads
CATUScan-boundary triggerdffrOAM DMAMode 2: OAM Scan, Mode Transitions, CPU-Visible Mode Boundaries
CAVACH1 duty-pattern selectnor2CH1/CH2: Pulse Channels
CAXOCH1 duty-pattern selectnor2CH1/CH2: Pulse Channels
CAXUMode 2: OAM Scan
CAXYSweep counter bit 2tffnlSweep and Envelope
CEDEinverted dma_addr_extnot_x2OAM DMA
CEDUSCX bit-4 read drivernot_if0Register Reads
CEDYMode 2: OAM ScanMode 3: The Sprite Pipeline
CEHAMode 2: OAM Scan
CEMOCH2 prescaler /2 stage 2dffrAPU Clock Tree and Frame SequencerCH1/CH2: Pulse Channels
CENOXUPY-phased scan-active copydffrThe Clock TreeRendering Mode Control, Mode 2: OAM Scan, Races
CERACH2 length counter bit 1tffnlLength Counters and Power Cycling
CERYCH3 prescaler /2 stagedffrAPU Clock Tree and Frame SequencerCH3: Wave Channel
CESYWave data latch bit 1drlatch_eeCH3: Wave Channel
CEVOWave data latch bit 5drlatch_eeCH3: Wave Channel
CEVUCH1 duty-pattern selectnor2CH1/CH2: Pulse Channels
CEXOCH4 shift divider bit 0dffrCH4: Noise Channel
CODOCH1 duty counter-state decodenot_x1CH1/CH2: Pulse Channels
COMYCH1 overflow capture (/2 toggle)dffrCH1/CH2: Pulse Channels
CONUCH2 length counter bit 2tffnlLength Counters and Power Cycling
COPASweep shift-step counter bit 0tffnlSweep and Envelope
COPUCH1 divider bit 10 (MSB)tffnlCH1/CH2: Pulse Channels
COSOCH1 duty-pattern selectnor2CH1/CH2: Pulse Channels
COTAStage-2 capture enable; complement WOVU drives the OAM bitline prechargenot_x1OAM DMAMode 2: OAM Scan, OAM and VRAM Access
COWECH1 PWM gated by channel-runningand2CH1/CH2: Pulse Channels
COZESweep counter-at-max detectorand3Sweep and Envelope
CUFE= OAI21(FEXX, dma_run, dma_phi_n) — $FExx/DMA bus-cycle term in BYCUoai21OAM DMAMode 2: OAM Scan, OAM and VRAM Access
CUGASCX bit-2 read drivernot_if0Register Reads
CULECH2 duty counter clocknot_x1CH1/CH2: Pulse Channels
CUPAShared PPU register write strobecombinational (derived from phase gen)Register WritesRegister Reads, OAM and VRAM Access, Mode 2: OAM Scan
CUPOSweep counter bit 0tffnlSweep and Envelope
CUSASCY bit-4 read drivernot_if0Register Reads
CUVOWave data latch bit 4drlatch_eeCH3: Wave Channel
CUXYSlot counter bit 1dffrMode 2: OAM Scan
CYBOCH3 prescaler clock buffernot_x1APU Clock Tree and Frame Sequencer
CYFOWave data latch bit 0drlatch_eeCH3: Wave Channel
CYKEMode 2: OAM Scan
CYMUSweep counter load enable (NOT stage)not_x1Sweep and Envelope
CYPUSweep counter bit 1tffnlSweep and Envelope
CYRAOAM DMAMode 2: OAM Scan
CYRECH2 length-clock gate (DEME) inputdffrLength Counters and Power Cycling
CYSECH2 PWM gated by channel-runningand2CH1/CH2: Pulse Channels
CYVOCH2 divider bit 3tffnlCH1/CH2: Pulse Channels
DAFASweep counter load enable (NOR stage)nor2Sweep and Envelope
DAJOCH1 duty counter clocknot_x1CH1/CH2: Pulse Channels
DALECH4 shift divider bit 5dffrCH4: Noise Channel
DAPECH1 duty counter bit 2 (MSB)dffr_ccCH1/CH2: Pulse Channels
DARECH2 duty counter-state decodenot_x1CH1/CH2: Pulse Channels
DARYCH4 divider low-tap mux (bits 0,1,2,3 group)ao2222CH4: Noise Channel
DASAMode 2: OAM Scan
DATOCH4 LFSR output → envelope gateor2CH4: Noise Channel
DEGOPer-slot X match, FEFY group (active-low)nand3Mode 3: The Sprite Pipeline
DEKOCH4 shift divider bit 1dffrCH4: Noise Channel
DEMECH2 length-clock gatenor3Length Counters and Power Cycling
DEMOCH4 shift divider bit 7dffrCH4: Noise Channel
DEPOOAM attribute bit 7 latch (BG-over-OBJ priority)dlatch_eeMode 3: The Sprite Pipeline
DERECH4 shift divider bit 12dffrCH4: Noise Channel
DEROCH3 wave-position counter clocknot_x1CH3: Wave Channel
DERUCH1 divider ripple inverter (bits 8–10)not_x1CH1/CH2: Pulse Channels
DETECH4 shift divider bit 9dffrCH4: Noise Channel
DOBAALET-clocked scan-done-prevdffrThe Clock TreeMode 2: OAM Scan, Mode Transitions, Scanline and Frame Timing
DOCACH2 divider toggle clock (NOT stage)not_x1CH1/CH2: Pulse Channels
DODACH3 length-clock gatenor3Length Counters and Power Cycling
DOJUCH2 duty-pattern selectnor2CH1/CH2: Pulse Channels
DOKAOverflow-capture self-clear term (= AND2(COMY, ch1_1mhz))and2CH1/CH2: Pulse Channels
DOKECH4 shift divider bit 6dffrCH4: Noise Channel
DOMECH2 PWM latchdffrCH1/CH2: Pulse Channels
DOMOCH2 duty-pattern selectnor2CH1/CH2: Pulse Channels
DONECH2 divider bit 0tffnlCH1/CH2: Pulse Channels
DOSECH4 shift divider bit 8dffrCH4: Noise Channel
DOTACH4 shift divider bit 11dffrCH4: Noise Channel
DOVECH2 duty-pattern selectnor2CH1/CH2: Pulse Channels
DOXESCX bit-6 read drivernot_if0Register Reads
DUGAOAM DMA
DUJUCH2 divider load enablenor2CH1/CH2: Pulse Channels
DURECH4 shift divider bit 4dffrCH4: Noise Channel
DUWOCH1 PWM latchdffrCH1/CH2: Pulse Channels
DYBA= NOT(BYVA) — per-slot line-reset termnot_x1Mode 3: The Sprite Pipeline
DYBESlot counter bit 3 (MSB)dffrMode 2: OAM Scan
DYDUPer-slot X match, FEFY group (active-low)nand3Mode 3: The Sprite Pipeline
DYKAPer-slot X match, FOVE group (active-low)nand3Mode 3: The Sprite Pipeline
DYKYCUPA driver stage 1notRegister Writes
DYMUCH2 duty counter-state decodeand2CH1/CH2: Pulse Channels
DYNUCH2 divider bit 1tffnlCH1/CH2: Pulse Channels
DYROGated 256 Hz length tick (= NOT(DEME))not_x1Length Counters and Power Cycling
DYRUCOMY async-reset driver (= NOR(apu_reset, ch1_restart, DOKA))nor3CH1/CH2: Pulse Channels
DYTACH2 duty-pattern selectnor2CH1/CH2: Pulse Channels
DYVECH2 duty counter bit 2 (MSB)dffr_ccCH1/CH2: Pulse Channels
DYWESlot-0 reset driver = OR2(DYBA, EBOJ)or2Mode 3: The Sprite Pipeline
EBEBSlot-3 write-enable inverternot_x1Mode 2: OAM ScanRaces
EBOJSlot-0 fetch-done flag (d = GUVA, clk = WUTY)dffrMode 3: The Sprite PipelineMode 2: OAM Scan
EBOSMode 2: OAM Scan
ECEDOAM DMAMode 2: OAM Scan, Mode 3: The Sprite Pipeline
EDOSSCX bit-0 read drivernot_if0Register Reads
EFALCH3 wave-position bit 4 (MSB)dffrCH3: Wave Channel
EFARCH3 wave-position bit 0 (wave_nibble_sel)dffrCH3: Wave Channel
EFUZCH3 wave-position bit 2dffrCH3: Wave Channel
EFYLPer-slot X match, FOVE group (active-low)nand3Mode 3: The Sprite Pipeline
EGOGCH2 duty counter-state decodeand2CH1/CH2: Pulse Channels
EGOMPer-slot X match, FOVE group (active-low)nand3Mode 3: The Sprite Pipeline
EKOBSCX bit-1 read drivernot_if0Register Reads
EKOVCH1 divider bit 7tffnlCH1/CH2: Pulse Channels
ELYNScan counter bit 3dffrMode 2: OAM Scan
ELYXCH4 divider mid-tap mux (bits 4,5,6,7 group)ao2222CH4: Noise Channel
EMUSCH1 divider bit 8tffnlCH1/CH2: Pulse Channels
EMYRVolume saturation detector (vol 0 + down)nor5Sweep and Envelope
ENEFY-compare carry-chain bit 1full_addMode 2: OAM Scan
ENEKCH1 duty counter-state decodeand2CH1/CH2: Pulse Channels
EPORCH4 shift divider bit 3dffrCH4: Noise Channel
EPYKCH1 divider load enablenor2CH1/CH2: Pulse Channels
EROSCH1 duty counter bit 1dffr_ccCH1/CH2: Pulse Channels
ERUCY-compare carry-chain bit 0full_addMode 2: OAM Scan
ERUSCH3 wave-position bit 1dffrCH3: Wave Channel
ERUTCH4 shift divider bit 10dffrCH4: Noise Channel
ERYCCH2 length counter bit 0tffnlLength Counters and Power Cycling
ERYFCH4 divider low-tap aggregateor2CH4: Noise Channel
ESEPCH4 shift divider bit 13dffrCH4: Noise Channel
ESUTCH1 duty counter bit 0 (LSB)dffrCH1/CH2: Pulse Channels
ETANWave-position counter reset (active-low)nor2CH3: Wave Channel
ETYRCH4 divider high-tap mux (bits 8–13)ao222222CH4: Noise Channel
EVAKCH1 divider bit 9tffnlCH1/CH2: Pulse Channels
EXELCH3 wave-position bit 3dffrCH3: Wave Channel
EZEFCH4 shift divider bit 2dffrCH4: Noise Channel
EZOFCH2 divider bit 2tffnlCH1/CH2: Pulse Channels
EZOZCH1 duty counter-state decodeand2CH1/CH2: Pulse Channels
EZULCH4 LFSR-output / bypass muxmuxCH4: Noise Channel
FABOch3_restart sample clock (= NOT(ch3_2mhz))not_x1CH3: Wave Channel
FAHAScan counter bit 4dffrMode 2: OAM Scan
FAJUch3_fdis enable pulse (= NOT(GYRA))not_x1CH3: Wave Channel
FAMU~ma4 tri-state drivernot_if0Mode 3: The Sprite Pipeline
FECOY-compare carry-chain bit 2full_addMode 2: OAM Scan
FEFYSprites 0–4 X-match aggregatenand5Mode 2: OAM ScanMode 3: The BG Pipeline, Mode 3: The Sprite Pipeline
FEMECH4 LFSR shift-clock select (ff22_d7 ? etyr : eryf)muxCH4: Noise Channel
FEMOMode 2: OAM Scan
FENAVolume counter bit 3tffnlSweep and Envelope
FENOVolume counter bit 0tffnlSweep and Envelope
FEPOSprite X priority aggregateor2Rendering Mode ControlMode 2: OAM Scan, Mode 3: The BG Pipeline, Mode 3: The Sprite Pipeline
FEPUMode 2: OAM Scan
FETEVolume counter bit 1tffnlSweep and Envelope
FETOScan-done decodeand4Mode 2: OAM ScanMode Transitions
FETYCH3 counter-wrap interlock (/2 toggle)dffrCH3: Wave Channel
FEVACH1 divider bit 6tffnlCH1/CH2: Pulse Channels
FEXX$FExx address-range decode (= NOT(ROPE))not_x2OAM and VRAM Access
FFFFInterrupt Dispatch
FOBANR34 trigger M-cycle synchroniserdffrCH3: Wave Channel
FOCOSlot-3 selectornand4Mode 2: OAM Scan
FOFAMode 2: OAM Scan
FOMYVolume counter bit 2tffnlSweep and Envelope
FONEMode 2: OAM Scan
FONOMode 2: OAM ScanMode 3: The Sprite Pipeline
FONYScan counter bit 5 (MSB)dffrMode 2: OAM Scan
FOVESprites 5–9 X-match aggregatenand5Mode 2: OAM ScanMode 3: The BG Pipeline, Mode 3: The Sprite Pipeline
FOZUCH3 active-bit nor_latchnor_latchCH3: Wave Channel
FUFOXYMO complementnot_x1Mode 3: The Sprite Pipeline
FUKYMode 2: OAM Scan
FULOCH1 divider toggle clock (NOR stage)nor2CH1/CH2: Pulse Channels
FUVEMode 2: OAM Scan
FUVOch3_fdis disable level (= NOR(ch3_amp_en_n, apu_reset))nor2CH3: Wave Channel
FUWAY-compare carry-chain bit 5full_addMode 2: OAM Scan
FUXOCH2 divider bit 4tffnlCH1/CH2: Pulse Channels
FUXUSlot-0 X save strobe (store-counter = 0 decode)not_x1Mode 3: The Sprite Pipeline
FYCUMode 2: OAM Scan
FYGOFOZU clear term (= OR3(apu_reset, GEDO, ch3_amp_en_n))or3CH3: Wave Channel
FYNEhoru_512hz buffer chain stagenot_x3APU Clock Tree and Frame Sequencer
FYREVolume saturation detector (vol 15 + up)not_x1Sweep and Envelope
GACENOT(GOPU) — Y-compare sum bit 4, complementednot_x1Mode 2: OAM Scan
GALEhoru_512hz buffer chain stagemuxAPU Clock Tree and Frame Sequencer
GAMElfsr_out — CH4 LFSR output AND ch4_activeand2CH4: Noise Channel
GANECH2 divider bit 7tffnlCH1/CH2: Pulse Channels
GANOCH2 divider bit 5tffnlCH1/CH2: Pulse Channels
GARAch3_restart synchroniser DFFdffrCH3: Wave Channel
GARYCH4 shift-divider clock-enable latch (cleared by trigger)dffrCH4: Noise Channel
GAVAScan counter clockor2Mode 2: OAM Scan
GAVUNR34 trigger-bit capturedrlatch_eeCH3: Wave Channel
GAXECH1 divider bit 0tffnlCH1/CH2: Pulse Channels
GEDOCH3 length-stop term (length expired AND NR34 length enable)and2CH3: Wave Channel
GEJYOBJ_SIZE muxao22LCDC StructureMode 3: The Sprite Pipeline
GEKUCH1 divider toggle clock (NOT stage)not_x1CH1/CH2: Pulse Channels
GENAch4_active nor_latch (set by ch4_restart)nor_latchCH4: Noise Channel
GEPOCH4 LFSR-reset OR (ch4_restart, apu_reset)or2CH4: Noise Channel
GEPYCH4 length-clock gatenor3Length Counters and Power Cycling
GESEsprite_y_match drivernot_x1Mode 2: OAM Scan
GEWYNOT(WUHU) — Y-compare sum bit 7, complementednot_x1Mode 2: OAM Scan
GEXYhoru_512hz buffer chain stagenot_x1APU Clock Tree and Frame Sequencer
GOBASCY bit-1 read drivernot_if0Register Reads
GOCACH2 divider bit 6tffnlCH1/CH2: Pulse Channels
GODOSCY bit-3 read drivernot_if0Register Reads
GOFUCH4 prescaler load-enable NOR (ch4_restart, gary)nor2CH4: Noise Channel
GOFYNR34 trigger nor_latch (q_n is held)nor_latchCH3: Wave Channel
GOGECH4 LFSR reset (= NOT gepo)not_x2CH4: Noise Channel
GOJUY-compare carry-chain bit 6full_addMode 2: OAM Scan
GOMOMode 3: The Sprite Pipeline
GONEch4_restart synchroniser DFF (clk hama_512khz)dffrCH4: Noise Channel
GONUSCY bit-2 read drivernot_if0Register Reads
GOPUY-compare carry-chain bit 4full_addMode 2: OAM Scan
GOSOScan counter bit 2dffrMode 2: OAM Scan
GOVULCDC.2 maskor2LCDC StructureMode 2: OAM Scan
GUFYVolume saturation detector aggregateor2Sweep and Envelope
GUGUChannel-disable latch (q_n = ch3_fdis)nand_latchCH3: Wave Channel
GUNESCY bit-6 read drivernot_if0Register Reads
GUNYCH4 gary reset (NOR apu_reset, ch4_restart)nor2CH4: Noise Channel
GUSUMode 2: OAM Scan
GUVA= NOT(YDUG) — slot-0 match selectnor2Mode 3: The Sprite Pipeline
GUVUNOT(FUWA) — Y-compare sum bit 5, complementednot_x1Mode 2: OAM Scan
GYBACH4 divider-enable-latch clock (= NOT ch4_1mhz)not_x1CH4: Noise Channel
GYDANOT(GOJU) — Y-compare sum bit 6, complementednot_x1Mode 2: OAM Scan
GYKYCarry-chain bit 3full_addMode 2: OAM Scan
GYRATrigger-pulse retime DFF (GYTA on ch3_2mhz; feeds FAJU)dffrCH3: Wave Channel
GYSUch4_start capture DFF (clk apu_phi)dffrCH4: Noise Channel
GYTACH3 trigger self-clear delaydffrCH3: Wave Channel
GYZASCY bit-7 read drivernot_if0Register Reads
GYZOSCY bit-5 read drivernot_if0Register Reads
HAFEJOPA reset driver (pace 0)nor4Sweep and Envelope
HAMAhama_512khz buffer (= NOT jeso_n)not_x4CH4: Noise Channel
HAPECH4 LFSR state bitdffrCH4: Noise Channel
HAZOCH4 trigger nor_latch (feeds ch4_restart.d)nor_latchCH4: Noise Channel
HEFOCH3 divider toggle clock (NOR stage)nor2CH3: Wave Channel
HENOCH4 LFSR state bitdffrCH4: Noise Channel
HEPACH4 LFSR state bitdffrCH4: Noise Channel
HEPOSaturation capturedffrSweep and Envelope
HEPUCH2 divider bit 9tffnlCH1/CH2: Pulse Channels
HERACH3 divider load enable (active-low)nor2CH3: Wave Channel
HEROCH2 divider bit 10 (MSB)tffnlCH1/CH2: Pulse Channels
HEVYCH2 divider bit 8tffnlCH1/CH2: Pulse Channels
HEZUCH4 LFSR output bitdffrCH4: Noise Channel
HOFOVolume bit-0 toggle clockor3Sweep and Envelope
HOGANR44 trigger-bit (d7) write strobe → ch4_startdrlatch_eeCH4: Noise Channel
HORUhoru_512hz buffer chain stagenot_x3APU Clock Tree and Frame Sequencer
HORYCH4 LFSR state bitdffrCH4: Noise Channel
HUCECH4 prescaler load enable (= NOT gofu)not_x1CH4: Noise Channel
HUNOCH3 overflow capture (/2 toggle)dffrCH3: Wave Channel
HURACH4 LFSR feedback XNOR (the two XNOR taps)xnorCH4: Noise Channel
HYFECH1 divider bit 1tffnlCH1/CH2: Pulse Channels
HYFOCH3 overflow detector (NOT stage)not_x1CH3: Wave Channel
HYKECH1 divider bit 5tffnlCH1/CH2: Pulse Channels
HYLYEnvelope counter load enable (NOR stage)nor2Sweep and Envelope
HYNOCH4 prescaler terminal-count (jyfu&jyre&jyco)and3CH4: Noise Channel
HYROCH4 LFSR state bitdffrCH4: Noise Channel
JAJUCH4 LFSR state bitdffrCH4: Noise Channel
JAKEEnvelope counter load enable (NOT stage)not_x1Sweep and Envelope
JAPUCH3 divider bit 10 (MSB)tffnlCH3: Wave Channel
JAVOCH4 LFSR state bitdffrCH4: Noise Channel
JEMACH1 divider bit 4tffnlCH1/CH2: Pulse Channels
JEMEEnvelope-stop latchnor_latchSweep and Envelope
JEPECH4 LFSR state bit (7-bit inject point)dffrCH4: Noise Channel
JERACH3 divider load-enable fan-outnot_x1CH3: Wave Channel
JERYch4_fdis channel-disable latchnand_latchCH4: Noise Channel
JESOCH4 free-running 512 kHz prescaler bitdffrCH4: Noise Channel
JEVYEnvelope counter bit 2tffnlSweep and Envelope
JONAEnvelope counter bit 1tffnlSweep and Envelope
JOPAEnvelope-fire latchdffrSweep and Envelope
JOREEnvelope counter bit 0tffnlSweep and Envelope
JOTOCH4 LFSR feedback/chain-entry DFFdffrCH4: Noise Channel
JUPUEnvelope pace = 0 detectornor3Sweep and Envelope
JUTYCH3 divider toggle clock (NOT stage)not_x1CH3: Wave Channel
JUXECH4 LFSR state bitdffrCH4: Noise Channel
JYCOCH4 divisor prescaler bit 0 (loadable toggle)tffnlCH4: Noise Channel
JYFUCH4 divisor prescaler bit 2 (loadable toggle)tffnlCH4: Noise Channel
JYJACH4 LFSR feedback-DFF clock (= NOT ch4_lfsr_clk1)not_x1CH4: Noise Channel
JYNAFrame sequencer /2 → 64 Hz (envelope tap)dffrAPU Clock Tree and Frame SequencerPost-Boot State
JYRECH4 divisor prescaler bit 1 (loadable toggle)tffnlCH4: Noise Channel
JYTYCH1 divider bit 2tffnlCH1/CH2: Pulse Channels
KAFOCH3 divider bit 6tffnlCH3: Wave Channel
KAHECPL source (AO22 LCDC.7 mux)ao22LCDC StructureLCD Output
KANUCH4 divisor-prescaler toggle clock (= kyku)not_x1CH4: Noise Channel
KASACPL PPU-clock source (LCD-on arm)notLCD Output
KASOCH3 divider load enable (bits 4–7)not_x1CH3: Wave Channel
KAVUCH4 LFSR 7-bit-mode feedback injectao22CH4: Noise Channel
KEBOFR PPU-clock source (LCD-on arm)notLCD Output
KEDYMux selector complement (LCDC.7=0 arm)notLCD Output
KEJUCH3 divider bit 8tffnlCH3: Wave Channel
KEMUCH3 divider bit 3tffnlCH3: Wave Channel
KENOCH3 divider bit 7tffnlCH3: Wave Channel
KEPACH3 divider bit 5tffnlCH3: Wave Channel
KESECH3 divider ripple inverter (bits 8–10)not_x1CH3: Wave Channel
KETUCH4 LFSR state bitdffrCH4: Noise Channel
KEZACH3 divider bit 9tffnlCH3: Wave Channel
KOFOFR pad drivernotLCD Output
KOMUCH4 LFSR state bitdffrCH4: Noise Channel
KONYCH4 prescaler-clock gate inverter stagenot_x1CH4: Noise Channel
KUNUCH3 divider bit 2tffnlCH3: Wave Channel
KUPAFR source (AO22 LCDC.7 mux)ao22LCDC StructureLCD Output
KUPECH3 divider bit 1tffnlCH3: Wave Channel
KUTACH4 LFSR state bitdffrCH4: Noise Channel
KUTUCH3 divider bit 0tffnlCH3: Wave Channel
KUZYCH4 LFSR state bitdffrCH4: Noise Channel
KYGUCH3 divider bit 4tffnlCH3: Wave Channel
KYKOCH3 divider load enable (bits 0–3)not_x1CH3: Wave Channel
KYKUCH4 prescaler-clock gate (ch4_fdis OR jeso_512k)or2CH4: Noise Channel
KYMOCPL pad drivernotLCD Output
KYNACH1 divider bit 3tffnlCH1/CH2: Pulse Channels
KYPECH1 divider ripple inverter (bits 4–7)not_x1CH1/CH2: Pulse Channels
KYRUCH3 divider ripple inverter (bits 4–7)not_x1CH3: Wave Channel
KYVOEnvelope counter-at-max detectorand3Sweep and Envelope
KYWYCH4 LFSR state bitdffrCH4: Noise Channel
LACEBGP bit-4 read drivernot_if0Register Reads
LAFOLY bit 7dffrLine CountersRegister Reads
LAJUOBP1 bit-0 read drivernot_if0Register Reads
LALUSTAT interrupt latch (IF bit 1)dffsrSTAT InterruptsInterrupt Dispatch, The IF Register
LAMALY resetnor2Line CountersRegister Reads, Mode Transitions, CPU-Visible Mode Boundaries
LAPEMode 3: The Sprite Pipeline
LARAOAM DMA
LARYBGP bit-7 read drivernot_if0Register Reads
LAVYOAM DMA
LAXUFetch counter bit 0dffrThe Clock TreeMode 3: The BG Pipeline, Scanline and Frame Timing, Races
LEBAOBP1 bit-6 read drivernot_if0Register Reads
LEBOFetch-counter gated clocknand2Mode 3: The BG PipelineScanline and Frame Timing
LEFEMode 3: The Sprite Pipeline
LEGUMode 3: The BG Pipeline
LELUOBP1 bit-7 read drivernot_if0Register Reads
LEMALY bit 5dffrLine Counters
LEPAOBP1 bit-1 read drivernot_if0Register Reads
LESUMode 3: The Sprite Pipeline
LESYMode 3: The Sprite Pipeline
LEXALY bit 2dffrLine Counters
LOBEBGP bit-3 read drivernot_if0Register Reads
LOBYPOKY reset drivernot_x1Rendering Mode ControlMode 3: The BG Pipeline
LODEOBP1 bit-2 read drivernot_if0Register Reads
LODYBGP bit-6 read drivernot_if0Register Reads
LOGODMA bus-read windownot_x1OAM DMA
LOKAWY bit-3 read drivernot_if0Register Reads
LOKYOAM DMA
LOLEWX bit-3 read drivernot_if0Register Reads
LOMAMode 3: The BG PipelineRaces
LONYTile-fetch latchnand_latchRendering Mode ControlScanline and Frame Timing
LOPEIF[0] capture (VBlank)dffsrScanline and Frame TimingThe IF Register
LOTAMode 3: The Sprite Pipeline
LOTE= NOT(MUMY) — OBP1 read drivers' ena_nnot_x1Register Reads
LOVAWX bit-0 read drivernot_if0Register Reads
LOVULY bit 4dffrLine Counters
LOZEMode 3: The BG PipelineMode 3: The Sprite Pipeline
LUGAOBP1 bit-5 read drivernot_if0Register Reads
LUKYOBP1 bit-4 read drivernot_if0Register Reads
LUMAdma_addr_ext drivernot_x1OAM DMA
LUNATemp-latch enable chainnot / nand3Mode 3: The BG PipelineRaces
LURYRendering Mode Control
LUZOMode 3: The BG Pipeline
LYCOLY bit-2 read drivernot_if0Register Reads
LYDOLY bit 3dffrLine Counters
LYFARegister Writes
LYHALine CountersMode Transitions
LYKABGP bit-5 read drivernot_if0Register Reads
LYKUMode 3: The Sprite Pipeline
LYMESprite palette pipe output stage (sprite_px_palette)dffsrMode 3: The Sprite Pipeline
LYRY"Fetch complete" combinationalnot_x1Mode 3: The BG PipelineMode 3: The Sprite Pipeline
LYZAOBP1 bit-3 read drivernot_if0Register Reads
LYZUFetch counter bit 0 sampledffrThe Clock TreeRendering Mode Control, Mode 3: The BG Pipeline, Scanline and Frame Timing
MACUMode 3: The BG Pipeline
MAKAOAM DMA
MARAWX bit-7 read drivernot_if0Register Reads
MASOMode 3: The Sprite Pipeline
MATOLY bit 6dffrLine Counters
MATUOAM DMA
MECOFR PPU-clock feeder(DFF chain)LCD Output
MEDALCD vertical-sync sourcedffrThe Clock TreeLine Counters, LCD Output, Mode Transitions
MEFUMode 3: The Sprite Pipeline
MEGAWY bit-4 read drivernot_if0Register Reads
MEGUMode 3: The BG Pipeline
MEHENUNU clock buffernotWindow Control
MEKEReload enable chainnot / nand3The Timer
MELEWX bit-4 read drivernot_if0Register Reads
MERAWY bit-7 read drivernot_if0Register Reads
MERYWrap detectnor2The Timer
MESUFetch counter bit 1dffrMode 3: The BG PipelineScanline and Frame Timing
METEMode 3: The BG PipelineRaces
MEVEMode 3: The Sprite Pipeline
MEXUThe Timer
MOBAWrap capturedffrThe TimerHALT and EI, The IF Register
MOCESelf-stop conditionnand3The Clock TreeMode 3: The BG Pipeline, Mode 3: The Sprite Pipeline, Post-Boot State
MODASprite attribute pipe stagedffsrMode 3: The Sprite Pipeline
MODUMode 3: The BG Pipeline
MOFOMode 3: The Sprite Pipeline
MOJUMode 3: The BG Pipeline
MOKAOBP1 palette AO2222 combinerao2222LCD Output
MOKOWX bit-2 read drivernot_if0Register Reads
MOLUDMA read enable (AND2 with ppu_rd)and2Register Reads
MORYDMA-vs-CPU address MUX select sourcenand2OAM DMA
MOSUWindow triggernot_x2Mode 3: The BG PipelineWindow Control, Mode Transitions
MOXEALET complement (fine-scroll path)not_x1The Clock TreeMode 3: The BG Pipeline, Races
MUDELX resetnor2Line Counters
MUFEWX bit-5 read drivernot_if0Register Reads
MUKAWX bit-1 read drivernot_if0Register Reads
MUKUMode 3: The BG Pipeline
MULYWX bit-6 read drivernot_if0Register Reads
MUMYOBP1 read enable (AND2 with ppu_rd)and2Register Reads
MUREs_pad (VSYNC) drivernot_x1LCD Output
MUWYLY bit 0dffrLine CountersRegister Reads, Mode 2: OAM Scan, Mode Transitions
MYDEMode 3: The BG Pipeline
MYJYMode 3: The BG Pipeline
MYROLY bit 1dffrLine Counters
MYTAFRAME_END capturedffrThe Clock TreeLine Counters, Rendering Mode Control, Mode Transitions
MYTEOAM DMA
MYTUMode 3: The Sprite Pipeline
MYVOALET complementnot_x1The Clock TreeMode 3: The BG Pipeline, Scanline and Frame Timing, Races
MYZOMode 3: The Sprite Pipeline
NAFYMode 3: The BG PipelineRaces, Post-Boot State
NAPOThe Clock Tree
NASAMode 3: The BG Pipeline
NATYMode 3: The Sprite Pipeline
NAXYOAM DMA
NAZEWindow Control
NEDAMode 3: The BG Pipeline
NEFOMode 3: The BG Pipeline
NELEWY decode completionnot / nand5 / notWindow Control
NEPOMode 3: The BG Pipeline
NERULY=0 decodenor8Line CountersMode Transitions
NETAVURY enable producer (tile-data stages)and2Mode 3: The BG Pipeline
NEVUWindow Control
NOCUin_window drivernotMode 3: The BG Pipeline
NOFUMode 3: The BG PipelineRaces
NOGYWX register match decodernand5Window Control
NOJOWindow Control
NOKOFRAME_END decodeand4Line CountersRendering Mode Control, Mode Transitions
NOLOOAM DMA
NOPAWindow modedffrThe Clock TreeWindow Control, Scanline and Frame Timing, Races
NOZOMode 3: The BG Pipeline
NUDUMode 3: The BG Pipeline
NUFAWindow Control
NUGAThe Timer
NUKESprite attribute pipe stagedffsrMode 3: The Sprite Pipeline
NUKOWX matchnotMode 3: The BG PipelineWindow Control
NULYSprite-visibility masknor2Mode 3: The Sprite Pipeline
NUNUWX-match capture 2dffrWindow Control
NUNYWindow-trigger pulseand2Window Control
NUPAWindow Control
NURABGP palette AO2222 combinerao2222LCD Output
NUROMode 3: The Sprite Pipeline
NUVYDMA bit-7 read drivernot_if1Register Reads
NYBOIF[2] capturedffsrThe TimerThe IF Register
NYDUNUGA pre-wrap capturedffrThe Timer
NYDYMode 3: The BG PipelineRaces
NYFOTrigger buffernot / notWindow Control
NYGO= NOT(MOLU) — DMA read-enable inverter stage 1not_x1Register Reads
NYKAFetch-complete capturedffrThe Clock TreeMode 3: The BG Pipeline, Window Control, Scanline and Frame Timing
NYLUMode 3: The Sprite Pipeline
NYPEThe Clock TreeLine Counters, Rendering Mode Control, LCD Output
NYVAFetch counter bit 2dffrMode 3: The BG PipelineScanline and Frame Timing, Races
NYXUBG fetch counter resetnor3Rendering Mode ControlMode 3: The BG Pipeline, Mode 3: The Sprite Pipeline, Window Control
NYZEMatch capture (even)dffrThe Clock TreeRendering Mode Control, Mode 3: The BG Pipeline, Scanline and Frame Timing
PABABGP bit-1 read drivernot_if0Register Reads
PAFUWY register match decodernand5Window Control
PAGAWindow Control
PAGOSTAT Interrupts
PAHAROXY set drivernot_x1Rendering Mode ControlMode 3: The BG Pipeline
PAHOPX-bit-3 capturedffrRendering Mode ControlLCD Output, Scanline and Frame Timing, Post-Boot State
PALOWY bits 4–7 + LCDC.5 decodenand5Window Control
PALUSprite attribute pipe stagedffsrMode 3: The Sprite Pipeline
PALYLY==LYC comparatornot_x1STAT InterruptsCPU-Visible Mode Boundaries, HALT and EI
PANEDMA bit-3 read drivernot_if1Register Reads
PANYRYFA data driver (window/fine-scroll NOR)nor2Mode 3: The BG PipelineMode 3: The Sprite Pipeline, Window Control
PAREDMA bit-4 read drivernot_if1Register Reads
PARUMode 1 conditionnot_x1STAT Interrupts
PASOFine-counter resetnor2Mode 3: The BG PipelinePost-Boot State
PATYPixel-mux outputor3Mode 3: The Sprite PipelineLCD Output
PEBAMode 3: The Sprite Pipeline
PEBOWindow Control
PECUFine-counter gated clocknand2Mode 3: The BG Pipeline
PEDAThe Timer
PEFOMode 3: The Sprite Pipeline
PEFUMode 3: The Sprite Pipeline
PELAWY bit-5 read drivernot_if0Register Reads
PEROPalette-output multiplexer signalcombinationalPalette LatchesLCD Output
PERUThe Timer
PEZOWindow Control
PODAWY bit-1 read drivernot_if0Register Reads
POFOSTAT bit-4 read driver (mode-1 enable, from rufo_n)not_if0Register Reads
POFYLoop inverter + feedbacknot_x1LCD OutputMode Transitions, Post-Boot State
POGUcpg_pad (clock pulse gate) drivernot_x1LCD Output
POHUMatch comparatornot_x1Mode 3: The BG Pipeline
POKABG-pixel combinenor3LCD OutputMode 3: The Sprite Pipeline
POKYPixel-pipe data readynor_latchRendering Mode ControlMode 3: The BG Pipeline, Window Control, Scanline and Frame Timing
POLOWY bit-6 read drivernot_if0Register Reads
POLYDMA bit-0 read drivernot_if1Register Reads
POMELoop AVAP gatenor2LCD OutputMode Transitions
POMOWindow Control
POPUVBlank capturedffrThe Clock TreeLine Counters, Rendering Mode Control, Mode Transitions
POREMode 3: The BG Pipeline
PORYNYKA pipeline stagedffrThe Clock TreeMode 3: The BG Pipeline, Window Control, Scanline and Frame Timing
POTESTAT bit-6 read driver (LYC enable, from rugu_n)not_if0Register Reads
POVAMatch pulseand2Rendering Mode ControlRegister Reads, Mode 3: The BG Pipeline, LCD Output
POVYThe Timer
POWUOAM and VRAM AccessOAM DMA
PUDUMode 3: The Sprite Pipeline
PUFYLYC bit-7 read drivernot_if0Register Reads
PUKUWindow hit-and-latchnor2 / nor3Window Control
PUKYWX-match decodenand5 / not / nand5 / notWindow Control
PUNUWY bit-0 read drivernot_if0Register Reads
PUPURegister Writes
PUSY= NOT(NYGO) — DMA read drivers' active-high enanot_x1Register Reads
PUXAMatch capture (odd)dffrThe Clock TreeRendering Mode Control, Mode 3: The BG Pipeline, Scanline and Frame Timing
PUZOSTAT bit-3 read driver (mode-0 enable, from roxe_n)not_if0Register Reads
PYBOMode 3: The BG PipelineLCD Output
PYCOWX-match capture 1dffrWindow Control
PYGOPORY pipeline stagedffrThe Clock TreeRendering Mode Control, Mode 3: The BG Pipeline, Scanline and Frame Timing
PYGUWY bit-2 read drivernot_if0Register Reads
PYJOMode 3: The Sprite Pipeline
PYJUTile-index bit-7 capture DFFdffr_cc_qMode 3: The BG Pipeline
PYNUWindow-armed latchnor_latchRegister ReadsMode 3: The BG Pipeline, Window Control
RACELYC bit-4 read drivernot_if0Register Reads
RACOOAM and VRAM Access
RAGEThe Timer
RAHUOAM and VRAM Access
RAJYBG plane A output (LCDC.0 gated)and2LCDC StructureMode 3: The Sprite Pipeline, LCD Output
RALUMode 3: The BG Pipeline
RALYDMA bit-5 read drivernot_if1Register Reads
RAMAMode 3: The Sprite Pipeline
RAMUMode 3: The Sprite Pipeline
RAPESTAT Interrupts
RAROBGP bit-0 read drivernot_if0Register Reads
RATEThe Timer
RAVOLD1 / LD0 driversnotRegister ReadsLCD Output
RAZULYC bit-2 read drivernot_if0Register Reads
REDOBGP bit-2 read drivernot_if0Register Reads
REDYLYC bit-3 read drivernot_if0Register Reads
REFESTAT Interrupts
REGATIMA bits 0–7tffnl (toggle FF with load)The Timer
REJOWY-match frame latchnor_latchMode 3: The BG PipelineWindow Control, Scanline and Frame Timing
REMADMA bit-2 read drivernot_if1Register Reads
REMYLD0 pad drivernot_x2Register ReadsLCD Output
RENEDrain-detect stage 2dffrThe Clock TreeRendering Mode Control, Mode 3: The BG Pipeline, Mode 3: The Sprite Pipeline
REPUREJO resetor2Window ControlScanline and Frame Timing
RESUDMA bit-6 read drivernot_if1Register Reads
RETULYC bit-0 read drivernot_if0Register Reads
REVOOAM and VRAM Access
REWOMode 3: The Sprite Pipeline
ROBYMode 3: The Sprite Pipeline
ROCOWindow Control
ROCYPer-bit data-pad tri-state enable chaincombinationalOAM and VRAM Access
ROFODMA bit-1 read drivernot_if1Register Reads
ROGAFine count bit 1dffrMode 3: The BG Pipeline
ROGEWY matchnotWindow Control
ROKAMode 3: The Sprite Pipeline
ROMOSUVU NAND4 input (POKY inverter)not_x1Mode 3: The BG Pipeline
RONEDrain-gated match aggregatenand4Mode 3: The BG Pipeline
ROPE= NAND2(SOHA, RYCU) — $FExx decode stagenand2OAM and VRAM Access
ROPOLY==LYC synced matchdff17The Clock TreeLine Counters, Register Reads, STAT Interrupts
ROPYMode 3 inverter for VRAM enablesnot_x1Rendering Mode ControlOAM and VRAM Access
RORUOAM DMA
ROSASprite attribute pipe stagedffsrMode 3: The Sprite Pipeline
ROSYMode 3: The Sprite Pipeline
ROXESTAT.3/.4/.5/.6 enablesdrlatch_eeSTAT Interrupts
ROXOInverted CLKPIPE buffer (fine-scroll clock source)not_x1Mode 3: The BG PipelineLCD Output
ROXYFine-scroll gatenor_latchRendering Mode ControlMode 3: The BG Pipeline, Scanline and Frame Timing, Post-Boot State
ROZEFine-counter self-stopnand3Mode 3: The BG Pipeline
RUBUFine count bit 2dffrMode 3: The BG Pipeline
RUBYThe Timer
RUDAMode 3: The Sprite Pipeline
RUFOSTAT Interrupts
RUGOSprite attribute pipe stagedffsrMode 3: The Sprite Pipeline
RUGUSTAT Interrupts
RUJULoop OR combineror3LCD Output
RUPOSTAT bit 2 visible latchnor_latchSTAT Interrupts
RUTABG plane B gated by sprite priorityand2LCD Output
RUTULINE_END capturedffrThe Clock TreeLine Counters, Rendering Mode Control, Register Writes
RUZEST pad buffernot_x3LCD Output
RYCEOne-shot fireand2Mode 3: The BG PipelineMode 3: The Sprite Pipeline
RYCUROPE input (= NOT(fexx_ffxx_n))not_x1OAM and VRAM Access
RYDUMode 3: The Sprite Pipeline
RYDYWindow hitnor3Mode 3: The BG PipelineMode 3: The Sprite Pipeline, Window Control
RYFADrain-detect stage 1dffrRendering Mode ControlMode 3: The BG Pipeline, Mode 3: The Sprite Pipeline, Window Control
RYFUBG plane A gated by sprite priorityand2Mode 3: The Sprite PipelineLCD Output
RYKUFine count bit 0dffrThe Clock TreeMode 3: The BG Pipeline
RYNOLCD Output
RYPOCP pad drivernot_x3LCD Output
RYSAMode 3: The BG Pipeline
RYVERegister Writes
SABEGated fetch clocknand2Mode 3: The Sprite PipelineScanline and Frame Timing
SACUPixel pipe shift clock (CLKPIPE)or2Register WritesRegister Reads, Mode 2: OAM Scan, Mode 3: The BG Pipeline
SADURendering Mode ControlRegister Reads, CPU-Visible Mode Boundaries
SADYMode 3: The BG Pipeline
SAJAMode 3: The Sprite Pipeline
SANULINE_END decodeand4The Clock TreeLine Counters, Rendering Mode Control, Mode Transitions
SARYWY-match samplerdffrWindow Control
SASYSTAT bit-5 read driver (mode-2 enable, from refe_n)not_if0Register Reads
SATASprite attribute pipe stagedffsrMode 3: The Sprite Pipeline
SAVYPX bit 1dffrMode 3: The BG Pipeline
SAXOLX bit 0dffrThe Clock TreeLine Counters, Races, Post-Boot State
SAZOOAM and VRAM Access
SEBAFetch-counter TULY capture stage 3dffrRendering Mode ControlMode 3: The Sprite Pipeline, Scanline and Frame Timing, Races
SEBYVRAM read-latch enablenot_x1OAM and VRAM Access
SECATAKA set netnor3Mode 3: The Sprite PipelineMode Transitions
SEGAMode 3: The Sprite Pipeline
SEGOSTAT bit-2 read driver (LYC match, from rupo_n)not_if1Register Reads
SEGUBuffered CLKPIPEnot_x4Mode 3: The BG PipelineMode 3: The Sprite Pipeline, Scanline and Frame Timing, Post-Boot State
SEKOTile-boundary drain detectornor2Mode 3: The BG PipelineMode 3: The Sprite Pipeline, Window Control
SELAMode TransitionsSTAT Interrupts
SELEMode 3: The Sprite Pipeline
SEMOMode 3: The Sprite Pipeline
SEMUCP pixel-clock source (pre-buffer)or2LCD Output
SEPASTAT write enableand2Register Writes
SERECPU data-bus enable stage 2and2OAM and VRAM Access
SETUMode 3: The BG Pipeline
SOBOMode 3: The BG Pipeline
SOBUFetch requestdffrMode 3: The BG PipelineMode 3: The Sprite Pipeline, Scanline and Frame Timing
SOCYWindow-halt gatenot_x1Mode 3: The BG PipelineWindow Control, Post-Boot State
SOGUTIMA clock gatenor2The Timer
SOHAROPE input (= NOT(ffxx))not_x1OAM and VRAM Access
SOHUMode 3: The BG PipelineLCD Output
SOHYWrite-strobe drivernand2OAM and VRAM Access
SOKAMode 3: The Sprite Pipeline
SOMYSprite attribute pipe stagedffsrMode 3: The Sprite Pipeline
SONO1 MHz LINE_END capture clock (TALU complement)not_x1The Clock TreeLine Counters, Rendering Mode Control, Register Writes
SOVUSTAT Interrupts
SOWOTEKY re-trigger block (TAKA inversion)not_x1Mode 3: The Sprite Pipeline
SOZUFine-count bit 2 matchxnorMode 3: The BG Pipeline
STOPOAM DMAThe Timer
SUBOSTAT Interrupts
SUDATrigger one-shot partnerdffrMode 3: The BG PipelineMode 3: The Sprite Pipeline, Scanline and Frame Timing
SUDELX bit 4dffrLine Counters
SUHAPer-bit SCX matchxnorMode 3: The BG Pipeline
SUKOCondition combining gateao2222STAT InterruptsThe IF Register
SUNYMode 3: The Sprite Pipeline
SUTOMode 3: The Sprite Pipeline
SUVUStartup-/window-restart NAND4 (TAVE input)nand4Rendering Mode ControlMode 3: The BG Pipeline
SUZUWindow-activation triggernotMode 3: The BG PipelineWindow Control
SYBEMode 3: The BG Pipeline
SYBYFine-count bit 1 matchxnorMode 3: The BG Pipeline
SYGUThe Clock TreeLCD Output, Races
SYLORYDY first inverter (consumer-chain stage 1)notMode 3: The BG PipelineWindow Control
SYSYOAM and VRAM Access
TACAMode 3: The BG Pipeline
TACEGlobal DAC-off term — amp_en_n = AND4 of the four ch*_amp_en_nand4CH3: Wave Channel
TADEBG plane B output (LCDC.0 gated)and2LCDC StructureLCD Output
TADYVOGA + PX-counter resetnor2Rendering Mode ControlMode 3: The BG Pipeline, Window Control, Mode Transitions
TAHALX bit 5dffrLine Counters
TAKAFetch-running latchnand_latchMode 3: The BG PipelineMode 3: The Sprite Pipeline, Window Control, Mode Transitions
TAKOMode 3: The BG Pipeline
TALU1 MHz LX counter clocknot_x4The Clock TreeLine Counters, Register Writes, Register Reads
TAMESelf-stopnand2Mode 3: The Sprite Pipeline
TAPAMode 2 conditionand2STAT Interrupts
TAPEThe Timer
TAPUCUPA driver stage 2notRegister Writes
TARUMode 0 conditionand2STAT Interrupts
TAVASOBU clock gatenot_x1Mode 3: The Sprite Pipeline
TAVEStartup / window-restart triggernotMode 3: The BG PipelineMode 3: The Sprite Pipeline
TAXYCPU-bus-phase strobe assemblycombinationalOAM and VRAM Access
TEBYSTAT bit-0 read driver (from SADU)not_if1Register Reads
TECYThe Timer
TEGO= NOT(VAMA) — $FF49 active-high selectnot_x2Register Reads
TEKOThe Timer
TEKYX-match triggerand4Mode 2: OAM ScanMode 3: The BG Pipeline, Mode 3: The Sprite Pipeline, Scanline and Frame Timing
TELULX bit 3dffrLine Counters
TEPANOT(mode3)not_x1Rendering Mode Control
TEPOBGP write enable sub-signalcombinationalRegister WritesPalette Latches
TEPY= NOT(VUSO) — BGP read drivers' ena_nnot_x1Register Reads
TESESprite fetch counter bit 2 (SFETCH_S2)dffrMode 3: The Sprite Pipeline
TEVOTile-boundary / restart triggeror3Mode 3: The BG PipelineMode 3: The Sprite Pipeline, Window Control, Mode Transitions
TOBAMode 3: The BG PipelineLCD Output
TOBEFF41 read enableand2Register Reads
TOBUTULY capture chaindffrRendering Mode ControlMode 3: The Sprite Pipeline, Scanline and Frame Timing, Races
TOCAUpper-nibble clocknotMode 3: The BG PipelineRaces
TOFUVideo resetnotRendering Mode ControlMode 3: The BG Pipeline, LCD Output, Mode Transitions
TOLECPU data-bus enable stage 1combinationalOAM and VRAM Access
TOLUmode1 inverternot_x1STAT InterruptsThe IF Register
TOMURYDY second inverter (consumer-chain stage 2)notMode 3: The BG PipelineWindow Control
TOMYMode 3: The BG Pipeline
TORYMode 3: The Sprite Pipeline
TOXEFetch counter bits 0–2dffrThe Clock TreeMode 3: The Sprite Pipeline
TOZAOAM DMA
TUHUPX bits 4–7dffrMode 3: The BG Pipeline
TUKUSprite-trigger window block (consumer-chain stage 3)notMode 3: The Sprite PipelineWindow Control
TUKYMode 3: The BG Pipeline
TULYSprite fetch counter bit 1 (SFETCH_S1)dffrMode 3: The Sprite Pipeline
TUTOOAM and VRAM Access
TUVAInverter pairnotSTAT InterruptsThe IF Register
TUVOOAM DMAMode 3: The Sprite Pipeline
TUXYWindow-trigger NAND (SUZU input)nand2Mode 3: The BG PipelineWindow Control
TYCOMode 3: The Sprite Pipeline
TYFACLKPIPE gateand3Mode 3: The BG PipelineScanline and Frame Timing, Post-Boot State
TYFODone decode BdffrMode 3: The Sprite PipelineScanline and Frame Timing
TYHOOAM DMA
TYJYOAM and VRAM Access
TYNODone decode Anand3Mode 3: The Sprite Pipeline
TYPOLX bit 1dffrLine Counters
TYRYLX bit 6dffrLine CountersPost-Boot State
TYTAMode 3: The Sprite Pipeline
UBALCUPA driver stage 3muxiRegister Writes
UBULIF[3] capture (Serial)dffsrThe IF Register
UCOBThe Timer
UKAPTAC mux chainmuxiThe Timer
UKUPreg_div16 bits 0–15dffThe Timer
ULAKIF[4] capture (Joypad)dffsrThe IF Register
UMOBCPL audio-clock source (LCD-off arm)(external)LCD Output
UPOFThe Timer
USECFR audio-clock source (LCD-off arm)(external)LCD Output
UVYTOAM DMA
VAFELYC bit-6 read drivernot_if0Register Reads
VAFOMode 3: The Sprite Pipeline
VAHALCDC bit-6 read drivernot_if0Register Reads
VAMA$FF49 address decodenand5Register Reads
VANUMode 3: The Sprite Pipeline
VAREMode 3: The Sprite Pipeline
VARO= NOT(WAFU) — LY read drivers' ena_nnot_x1Register Reads
VARY= NOT(WOFA) — $FF41 active-high selectnot_x2Register ReadsRegister Writes
VATOLCDC bit-5 read drivernot_if0Register Reads
VAVAPriority mask pipe MSB / LSBdffsrMode 3: The Sprite PipelineLCD Output
VAVENOT(TOBE) — STAT enable-bit read gatenot_x1Register Reads
VAZULYC bit-5 read drivernot_if0Register Reads
VCLKLCD Output
VEGALY bit-0 read drivernot_if0Register Reads
VEKUMode 3: The Sprite Pipeline
VELYBGP write enableand2Register WritesRegister Reads
VENA1 MHz toggle dividerdffrThe Clock TreeLine Counters, Register Writes, Scanline and Frame Timing
VEVYLCDC.6 → ~ma10 tri-state drivernot_if0Mode 3: The BG Pipeline
VEZOMask pipe (LSB end)dffsrMode 3: The Sprite Pipeline
VOCA= NOT(WORU) — $FF40 active-high selectnot_x2Register ReadsRegister Writes
VOGAH-Blank capture DFFdffrThe Clock TreeRendering Mode Control, Mode 3: The BG Pipeline, Mode Transitions
VOJOLYC bit-1 read drivernot_if0Register Reads
VOKELCDC bit-4 read drivernot_if0Register Reads
VOMY= NOT(WAXU) — WY read drivers' ena_nnot_x1Register Reads
VONUFetch-counter TULY capture stage 2dffrRendering Mode ControlMode 3: The Sprite Pipeline, Scanline and Frame Timing, Races
VOSASprite attribute pipe stagedffsrMode 3: The Sprite Pipeline
VOTOMode 3: The Sprite Pipeline
VOTYSTAT InterruptsThe IF Register
VUJOWindow Control
VUMOSprite attribute pipe stagedffsrMode 3: The Sprite Pipeline
VUMY= NOT(WAGE) — $FF4B active-high selectnot_x2Register Reads
VUPYMode 3: The Sprite Pipeline
VURYVUZA → ~ma12 tri-state drivernot_if1Mode 3: The BG Pipeline
VUSAFetch-done aggregateor2Mode 3: The Sprite Pipeline
VUSOBGP read enable (AND2 with ppu_rd)and2Register Reads
VUZATILE_SEL XOR tile-index MSBnor2Mode 3: The BG Pipeline
VYBOOperational per-dot gatenor3Mode 3: The BG PipelineMode 3: The Sprite Pipeline, Scanline and Frame Timing, Post-Boot State
VYCU= NOT(WYZE) — WX read drivers' ena_nnot_x1Register Reads
VYGA= NOT(WYVO) — $FF4A active-high selectnot_x2Register Reads
VYNELY bit-4 read drivernot_if0Register Reads
VYNOWindow line counter LSBdffrWindow Control
VYPUint_vbl drivernot_x3The IF Register
VYRELCDC read enable (AND2 with ppu_rd)and2Register Reads
VYSAMode 3: The Sprite Pipeline
VYXELCDC StructureLCD Output
VYZOLX bit 2dffrLine Counters
WAFULY read enable (AND2 with ppu_rd)and2Register Reads
WAGE$FF4B address decodenand5Register Reads
WAGO8×16 half-selectxorMode 3: The Sprite Pipeline
WAHEOAM wordline-driver precharge (oam_wldrv_precharge_n = NOT(WUJY))not_x3OAM and VRAM Access
WAMALY bit-5 read drivernot_if0Register Reads
WAMEOAM output-enable inverternot_x2OAM and VRAM Access
WARESCY bit-0 read drivernot_if0Register Reads
WARULCDC write enableand2Register WritesMode 3: The BG Pipeline
WATE$FF46 address decodenand5Register Reads
WAVOLY bit-6 read drivernot_if0Register Reads
WAVU$FF43 address decodenand5Register Reads
WAXUWY read enable (AND2 with ppu_rd)and2Register Reads
WAZOWUJY input — wordline-precharge timingor3OAM and VRAM Access
WAZYWindow line counter clocknot_x1Window Control
WEBAMode 3: The Sprite Pipeline
WEBU$FF42 address decodenand5Register Reads
WEFYMode 3: The Sprite Pipeline
WEGOXYMU set driveror2Rendering Mode ControlMode 3: The BG Pipeline, Mode Transitions
WEKU= NOT(XYLY) — LYC read drivers' ena_nnot_x1Register Reads
WELOSlot-0 X store bit 4 (holds NOT(X))drlatch_eeMode 3: The Sprite Pipeline
WERA= NOT(WYBO) — $FF47 active-high selectnot_x2Register ReadsRegister Writes
WETA$FF48 address decodenand5Register Reads
WETY$FF45 address decodenand5Register Reads
WEWYScan counter bit 1dffrMode 2: OAM Scan
WEXULCDC.4 (TILE_SEL) latchdrlatch_eeLCDC StructureMode 3: The BG Pipeline
WEZELY bit-7 read drivernot_if0Register Reads
WODASprite attribute pipe stagedffsrMode 3: The Sprite Pipeline
WODUMode 0 conditionand2Rendering Mode ControlRegister Writes, Register Reads, Mode 3: The BG Pipeline
WOFA$FF41 address decodenand5Register Reads
WOJOLCD control phasenor2The Clock Tree
WOJUSlot-0 X comparator bit 4 (XOR vs NOT(PX))xorMode 3: The Sprite Pipeline
WOJYLY bit-3 read drivernot_if0Register Reads
WOKYLCDC.6 (WIN_MAP) latchdrlatch_eeLCDC StructureMode 3: The BG Pipeline
WONEOAM DMAMode 2: OAM Scan
WONYSCX bit-3 read drivernot_if0Register Reads
WOPEMode 3: The Sprite Pipeline
WORAMode 3: The Sprite Pipeline
WORU$FF40 address decodenand5Register Reads
WOSUWUVU samplerdffrThe Clock TreeScanline and Frame Timing
WOTAY-compare decodernand6Mode 2: OAM Scan
WOTESlot-0 X store bit 6 (holds NOT(X))drlatch_eeMode 3: The Sprite Pipeline
WOVUOAM bitline precharge driver (oam_bl_precharge_n = NOT(COTA))not_x2OAM and VRAM Access
WOXAPlane-A pixel-output gateand2LCDC StructureMode 3: The Sprite Pipeline
WUDAMode 2: OAM Scan
WUFUOBP0 palette AO2222 combinerao2222LCD Output
WUFYMode 3: The Sprite Pipeline
WUGASTAT bit-1 read driver (from XATY)not_if1Register Reads
WUHUY-compare carry-chain bit 7full_addMode 2: OAM Scan
WUJY= NAND2(WAZO, oam_bl_precharge_n) — wordline-precharge timingnand2OAM and VRAM Access
WUKALCDC bit-3 read drivernot_if0Register Reads
WUKOVEVY enable (window tilemap stage)not_x2Mode 3: The BG Pipeline
WUKYY-flip termnot_x1Mode 3: The Sprite Pipeline
WURUSprite attribute pipe stagedffsrMode 3: The Sprite Pipeline
WUTYFetch donenotMode 2: OAM ScanMode 3: The BG Pipeline, Mode 3: The Sprite Pipeline
WUVALY bit-1 read drivernot_if0Register Reads
WUVU2 MHz toggle dividerdffrThe Clock TreeRegister Writes, Mode 2: OAM Scan, Scanline and Frame Timing
WYBO$FF47 address decodenand5Register Reads
WYCE= NOT(VYRE) — LCDC read drivers' ena_nnot_x1Register Reads
WYFUSprite attribute pipe stagedffsrMode 3: The Sprite Pipeline
WYHOMode 3: The Sprite Pipeline
WYJAOAM write enable (combined)ao21OAM and VRAM AccessOAM DMA, CPU-Visible Mode Boundaries
WYJULCDC bit-2 read drivernot_if0Register Reads
WYLE$FF44 address decodenand5Register Reads
WYMOLCDC Structure
WYNOOAM DMAMode 2: OAM Scan
WYPOLCDC bit-0 read drivernot_if0Register Reads
WYSOMode 3: The Sprite Pipeline
WYVO$FF4A address decodenand5Register Reads
WYZASlot-0 X comparator bit 6 (XOR vs NOT(PX))xorMode 3: The Sprite Pipeline
WYZEWX read enable (AND2 with ppu_rd)and2Register Reads
XAFOLCDC.3 (BG_MAP) latchdrlatch_eeLCDC StructureMode 3: The BG Pipeline
XAFUOAM DMAMode 2: OAM Scan
XAGEPer-slot X match, FEFY group (active-low)nand3Mode 3: The Sprite Pipeline
XAJUOBP0 bit-4 read drivernot_if0Register Reads
XAKOSlot-0 X store bit 7 (holds NOT(X))drlatch_eeMode 3: The Sprite Pipeline
XANEVRAM address tri-state-disable sourcenor2Rendering Mode ControlOAM and VRAM Access
XANO"PX at terminal count" (inverted XUGU)not_x1Rendering Mode ControlMode 3: The BG Pipeline, Mode Transitions, STAT Interrupts
XAPORendering Mode ControlMode 2: OAM Scan
XARO= NOT(WEBU) — $FF42 active-high selectnot_x2Register ReadsRegister Writes
XARYOBP0 bit-0 read drivernot_if0Register Reads
XATYRendering Mode ControlRegister Reads, CPU-Visible Mode Boundaries
XAVY= NOT(WAVU) — $FF43 active-high selectnot_x2Register ReadsRegister Writes
XAWOOBP0 bit-7 read drivernot_if0Register Reads
XAXAOBP0 bit-6 read drivernot_if0Register Reads
XAYO= NOT(WETA) — $FF48 active-high selectnot_x2Register Reads
XAYU= NOT(WETY) — $FF45 active-high selectnot_x2Register Reads
XEBASlot-0 match, high-nibble collapsenor4Mode 3: The Sprite Pipeline
XEBULCDC bit-7 read drivernot_if0Register Reads
XEDA= NOT(WATE) — $FF46 active-high selectnot_x2Register Reads
XEDUVRAM address tri-state enablenot_x2OAM and VRAM Access
XEFYGlobal sprite shifter parallel-load disablenot_x1Mode 3: The Sprite Pipeline
XEGUMode 3: The Sprite Pipeline
XEHOPX bit 0dffrMode 3: The BG Pipeline
XENA"No sprite match" (inverted FEPO)not_x1Rendering Mode ControlMode 3: The BG Pipeline, Mode Transitions, STAT Interrupts
XEPESlot-0 X store bit 0 (holds NOT(X))drlatch_eeMode 3: The Sprite Pipeline
XEROLCDC bit-1 read drivernot_if0Mode 3: The Sprite PipelineRegister Reads
XETESprite attribute pipe stagedffsrMode 3: The Sprite Pipeline
XEZEMode 3: The BG Pipeline
XOBOOBP0 bit-5 read drivernot_if0Register Reads
XOCEOAM DMAMode 2: OAM Scan
XODOVID_RST signalcombinationalRegister WritesLCDC Structure, Scanline and Frame Timing
XODUPX bit 2dffrMode 3: The BG Pipeline
XOFOPYNU reset (WIN_EN gate)nand3LCDC StructureWindow Control
XOGSInterrupt Dispatch
XOGY= NOT(WYLE) — $FF44 active-high selectnot_x2Register Reads
XOKEOBP0 bit-1 read drivernot_if0Register Reads
XONALCDC.7 storage latchdrlatch_eeRegister WritesLCDC Structure
XOTAWUVU clocknot_x1The Clock Tree
XOTEMode 3: The Sprite Pipeline
XOVUMode 3: The Sprite Pipeline
XOZY= NOT(XUFY) — OBP0 read drivers' ena_nnot_x1Register Reads
XUBORegister Writes
XUBYOBP0 bit-3 read drivernot_if0Register Reads
XUFYOBP0 read enable (AND2 with ppu_rd)and2Register Reads
XUGUTerminal count decodenand5Rendering Mode ControlMode 3: The BG Pipeline, Window Control, Mode Transitions
XUJAOAM DMA
XUJYSprite-fetch counter term in BYCUcombinationalOAM DMAMode 2: OAM Scan, OAM and VRAM Access
XUKEMode 3: The BG Pipeline
XULAPlane-B pixel-output gateand2LCDC StructureMode 3: The Sprite Pipeline
XUNOOBP0 bit-2 read drivernot_if0Register Reads
XUNYSlot-0 X store bit 5 (holds NOT(X))drlatch_eeMode 3: The Sprite Pipeline
XUPY2 MHz scan-counter clock (NOT(wuvu_n) = WUVU.q)not_x2The Clock TreeRegister Writes, Mode 2: OAM Scan, Mode 3: The BG Pipeline
XURERegister Writes
XUSOOAM Y-offset bit 0 latch (Mode 2) / tile-index bit 0 latch (Mode 3)dlatch_eeOAM DMAMode 2: OAM Scan, Mode 3: The Sprite Pipeline
XYDOPX bit 3dffrMode 3: The BG PipelineLCD Output, Races
XYFYXOTA complementnot_x1The Clock Tree
XYJUMode 3: The Sprite Pipeline
XYKYOAM DMAMode 2: OAM Scan, Mode 3: The Sprite Pipeline
XYLEMode 3: The BG Pipeline
XYLOLCDC.1 (OBJ_EN) register bitdrlatch_eeLCDC StructureMode 2: OAM Scan, Mode 3: The Sprite Pipeline
XYLYLYC read enable (AND2 with ppu_rd)and2Register Reads
XYMOLCDC.2 (OBJ_SIZE) register bitdrlatch_eeLCDC StructureMode 2: OAM Scan, Mode 3: The Sprite Pipeline
XYMURendering-mode latch (active-low Mode 3 indicator)nor_latchRendering Mode ControlRegister Writes, OAM and VRAM Access, OAM DMA
XYSOMode 2: OAM Scan
XYVAParallel divider branchnot_x1The Clock Tree
XYVOVBlank decodeand2Line CountersRendering Mode Control, Mode Transitions
YBEZPer-slot X match, FOVE group (active-low)nand3Mode 3: The Sprite Pipeline
YBOGMode 3: The Sprite Pipeline
YCEBOAM DMAMode 2: OAM Scan
YDUGSlot-0 X match (active-low)nand3Mode 3: The Sprite Pipeline
YDYVOAM DMAMode 2: OAM Scan, Mode 3: The Sprite Pipeline
YFELScan counter bit 0 (LSB)dffrMode 2: OAM Scan
YFUNSlot-0 X comparator bit 5 (XOR vs NOT(PX))xorMode 3: The Sprite Pipeline
YGEMPer-slot X match, FOVE group (active-low)nand3Mode 3: The Sprite Pipeline
YJEXMode 3: The Sprite Pipeline
YKUAHALT and EI
YLAHSlot-0 X store bit 1 (holds NOT(X))drlatch_eeMode 3: The Sprite Pipeline
YLOROAM DMAMode 2: OAM Scan, Mode 3: The Sprite Pipeline
YLOZPer-slot X match, FEFY group (active-low)nand3Mode 3: The Sprite Pipeline
YLYCOAM A-side / B-side write strobe driversper-byte selectorOAM and VRAM AccessOAM DMA
YNKWHALT and EI
YNYCOAM and VRAM AccessOAM DMA
YOIIInterrupt DispatchHALT and EI, The IF Register
YPUKSlot-0 X comparator bit 7 (XOR vs NOT(PX))xorMode 3: The Sprite Pipeline
YRUMOAM DMAMode 2: OAM Scan
YSESOAM DMAMode 2: OAM Scan
YSEXOAM DMAMode 2: OAM Scan
YVELOAM DMAMode 2: OAM Scan
YZABMode 3: The Sprite Pipeline
YZOSMode 3: The Sprite Pipeline
ZACWInterrupt Dispatch
ZAGOMode 2: OAM Scan
ZAHASlot-0 X comparator bit 2 (XOR vs NOT(PX))xorMode 3: The Sprite Pipeline
ZAIJInterrupt DispatchHALT and EI
ZAKOSlot-0 match, low-nibble collapsenor4Mode 3: The Sprite Pipeline
ZAOCInterrupt Dispatch
ZAXEOAM DMAMode 2: OAM Scan
ZAXYBuffer stagenot_x2The Clock Tree
ZEBASlot-0 X comparator bit 1 (XOR vs NOT(PX))xorMode 3: The Sprite Pipeline
ZECAOAM DMAMode 2: OAM Scan, Mode 3: The Sprite Pipeline
ZEMEBuffer stagenot_x4The Clock Tree
ZEZYMode 3: The Sprite Pipeline
ZFEXInterrupt Dispatch
ZIVVHALT and EI
ZJJEime_pending — "IME pending" SR latch (q drives dmg-sim wire ime_state)sm83_srlatch_r_nHALT and EI
ZKOGInterrupt Dispatch
ZLOZInterrupt Dispatch
ZOGYSlot-0 X comparator bit 0 (XOR vs NOT(PX))xorMode 3: The Sprite Pipeline
ZOKYSlot-0 X comparator bit 3 (XOR vs NOT(PX))xorMode 3: The Sprite Pipeline
ZOLASlot-0 X store bit 2 (holds NOT(X))drlatch_eeMode 3: The Sprite Pipeline
ZRSYRETI / reset gating SR latch (q = zrsy)sm83_srlatch_r_nHALT and EI
ZUCAOAM DMAMode 2: OAM Scan
ZULUSlot-0 X store bit 3 (holds NOT(X))drlatch_eeMode 3: The Sprite Pipeline
ZUVEOAM DMAMode 2: OAM Scan
ZYTYMode 3: The Sprite Pipeline
ZYVEMode 3: The Sprite Pipeline
ZZOMInterrupt 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

UnitValueDefinition
Dot / T-cycle≈238.4 ns (dmg-sim: 244,000 ps)One full master clock cycle (rise + fall) — see Methodology
M-cycle4 dotsCPU machine cycle
ATAL half-cycle0.5 dotsInterval between consecutive master-clock edges

Mode 3 structure

Baseline: SCX=0, no sprites, no window. Developed in BG pipeline and Mode transitions.

IntervalDots
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 3167 (invariant with SCX)

Fine scroll

Developed in BG pipeline.

ConstantValue
Cost per SCX & 7 incrementExactly 1 dot (ROXY suppression at startup)
First SACU at SCX=N7 + (N & 7) + 0.026 dots after AVAP
Mode 3 core duration at SCX=N173 + (N & 7) + 0.045 dots
SACU count167, 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.

EventValue
AVAP → NYXU pulse width0.492 dots
TEVO → NYXU pulse width0.495 dots
MOSU → NYXU pulse width0.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 scanline22 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.

EventValue
TEKY↑ → TAKA↓ (fetch freeze)6.010 dots, zero variance (gate-resolved)
Net sprite penaltyexactly 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.

RelationshipValue
VOGA capture after WODU↑0.435 dots (same-dot ALET rising edge)
WODU → VOGA → WEGO → XYMU chain0.436 dots
CUPA pulse width1.493 dots (T3–T4)
VID_RST (XODO) deassertion after CUPA↑ on LCDC.7=1 write3,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.

RelationshipValue
WODU → LALU propagation2,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.

EventValue
Palette dlatch update after the write-dot SACU falling~15.6 ns (data-bus settling — structural)
LCD output pins update after SACU falling1,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:

  1. 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.
  2. 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.
  3. 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_bug suite); 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.
  • dmgcpu wiki 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.