-
Notifications
You must be signed in to change notification settings - Fork 0
PPU 5C77 5C78
References: docs/ppu.md, docs/scheduler.md. Sources cited inline in the underlying spec: SNESdev PPU registers / Backgrounds / Sprites / Mode 7, Fullsnes.
The SNES's video subsystem is two physical chips sharing the CPU's B-bus ($2100–$21FF) with a common 16-bit data bus but separate address buses:
-
PPU1 (5C77): OAM/sprites and the background/sprite rendering pipeline, including the Mode 7 hardware-multiply result (
MPYL/M/H,$2134–$2136).STAT77($213E) reports version plus the sprite over/time flags. -
PPU2 (5C78): CGRAM (palette) access, color math/output, and the timing/status flags — the H/V counters, VBlank/HBlank, and the counter-latch.
STAT78($213F) reports version, NTSC/PAL, and the counter-latch flag.
crates/rustysnes-ppu models both chips together as one crate; the PPU only sees a narrow PpuBus trait for VRAM/CGRAM/OAM access, keeping it decoupled from the rest of the machine.
| Mode | BG1 | BG2 | BG3 | BG4 | Layers | Notes |
|---|---|---|---|---|---|---|
| 0 | 2bpp | 2bpp | 2bpp | 2bpp | 4 | separate palette region per BG |
| 1 | 4bpp | 4bpp | 2bpp | — | 3 | BG3 priority-bit selectable |
| 2 | 4bpp | 4bpp | (OPT) | — | 2 | offset-per-tile |
| 3 | 8bpp | 4bpp | — | — | 2 | high-color |
| 4 | 8bpp | 2bpp | (OPT) | — | 2 | offset-per-tile |
| 5 | 4bpp | 2bpp | — | — | 2 | forced 512-px hi-res |
| 6 | 4bpp | — | (OPT) | — | 1 | hi-res + offset-per-tile |
| 7 | 8bpp | — | — | — | 1 | affine; +EXTBG adds BG2 |
BGMODE ($2105) selects the mode plus per-BG tile size (8×8 vs 16×16). Offset-per-tile is Modes 2/4/6 only; hi-res Modes 5/6 force a 512-pixel-wide horizontal output.
Mode 7 is a single 128×128-tile map (256 distinct 8×8 tiles), 8bpp, transformed by a 2×2 affine matrix. The matrix coefficients (M7A–M7D, $211B–$211E) are each 16-bit signed 8.8 fixed-point, written low-byte-then-high-byte; the center point (M7X/M7Y, $211F/$2120) is 13-bit signed, also written twice. The PPU reuses its general-purpose hardware multiplier for the Mode 7 matrix multiply, exposing the signed 24-bit product at MPYL/M/H. EXTBG (a SETINI bit) turns Mode 7's high color bit into a per-pixel priority bit instead, letting BG2 layer under BG1 in Mode 7.
128 sprites, backed by 544 bytes of OAM (a 512-byte low table plus a 32-byte high table). Each low-table entry is 4 bytes: X (low), Y, tile index (low), and a flags byte (name high bit, 3-bit palette, 2-bit priority, V/H flip); the high table adds the X high bit and a per-sprite size toggle. OBSEL ($2101) selects the sprite name-table bases and one of eight size pairs.
Hardware enforces per-scanline limits: 32 sprites per line (Range Over, STAT77 bit 6) and 34 sprite-tiles per line (Time Over, STAT77 bit 7), both reset at the end of VBlank. Because tile fetch happens in reverse OAM-index order, lower-indexed sprites survive an overflow and higher-indexed ones drop — a detail that affects which sprites visibly disappear when a scene exceeds the limit.
Per docs/scheduler.md's binding numbering convention: 341 dots per line, each dot nominally 4 master clocks. A normal line is 1364 master clocks; a short line (NTSC non-interlace alternating frames) is 1360; a long line (PAL interlace) is 1368. NTSC runs 262 lines per frame, PAL 312 (plus one extra when interlaced). Active picture output spans dots 22–277 on the visible lines; VBlank begins at line 225 (or 240 in overscan mode); HBlank spans dot 274 through dot 1 of the next line.
Reading SLHV ($2137) latches the current H/V position; OPHCT/OPVCT ($213C/$213D) expose the latched 9-bit values via a read-twice protocol; reading STAT78 clears the latch and resets the read-toggle.
-
CGRAM holds 256 15-bit BGR colors (5 bits each for R/G/B).
CGADD($2121) selects the entry;CGDATA($2122) is written twice (low byte, then high byte) to set one color. -
VRAM is 64 KiB (32K words), word-addressed.
VMAIN($2115) configures the address auto-increment step (1/32/128 words), address remapping, and which half-write (low/high) triggers the increment. -
Color math blends the main and sub screens per
CGWSEL/CGADSUB(add/subtract, half, per-layer enable, a fixed addend color) inside window regions defined byCGWSEL,W1/W2, and their boolean combination mode.TM/TS($212C/$212D) select which layers appear on the main and sub screens respectively.
docs/ppu.md documents two specific hardware-accurate details the implementation follows closely, since getting them wrong visibly breaks real games: the subscreen color-math addend must fall back to the fixed COLDATA color (not CGRAM entry 0) when the subscreen pixel at that column is backdrop, and a background tile's CGRAM index must fold in its 3-bit palette-group field — both confirmed against ares pixel-diffs on Super Mario World.
Standard output is 256×224; overscan mode extends this to 256×239. Forced hi-res (Modes 5/6) doubles the horizontal resolution to 512; interlace mode can double the vertical line count. SETINI ($2133) controls interlace, OBJ interlace, overscan, pseudo-hires, the EXTBG bit, and external sync.
rustysnes-ppu composites each visible scanline per-line, at the end of that line, into a 256×239 15-bit framebuffer — rather than rendering pixel-by-pixel at true dot resolution. This is bit-identical to a per-dot renderer in the finished frame (the project's determinism contract only requires the completed frame be reproducible) and considerably simpler to implement. Per docs/ppu.md's current implementation status, BG modes 0–7 tile fetch, per-mode priority tables, 16×16 tiles, mosaic, the full Mode 7 affine pipeline, the 128-sprite OAM pipeline with its range/time limits, color math, windows, and master brightness are all implemented this way.
Not yet wired to true dot resolution (and explicitly flagged as deferred, not silently approximated): hi-res Modes 5/6 currently render at 256 wide rather than the doubled 512, offset-per-tile (Modes 2/4/6), pseudo-hires, and interlace field doubling. Multi-split-per-line raster effects (more than one HDMA-driven register change within a single scanline) also need true dot-resolution rendering — a single split per line already works correctly with the per-line compositor, since it samples end-of-line register state.
The committable PPU/DMA/HDMA layer is undisbeliever's snes-test-roms suite, covering HDMA timing, force-blank mid-frame behavior, mid-scanline VRAM access, and OAM dropout. Visual validation uses deterministic framebuffer hashes against a canonical set of commercial titles. See Testing-Strategy for the full accuracy battery this fits into, and Lockstep-Scheduler for how the PPU's dot timeline integrates with the CPU and DMA/HDMA on the shared master clock.
RustySNES is a cycle-accurate Super Nintendo / Super Famicom emulator written in pure Rust. Licensed under MIT OR Apache-2.0. | GitHub Repository | Web Demo | API Docs