NES PPU Palette RAM: Mirroring, Backdrop Color, and Output Masks
Trace NES PPU palette indexing, universal-background aliases, grayscale masking, and emphasis output without hard-coding one console's RGB palette.
The NES Picture Processing Unit does not store a modern RGB framebuffer for each pixel. It combines pattern bits, palette selection, priority, and PPU output controls to select a small color code, which then enters the console’s video-generation path. An emulator that treats palette RAM as a fixed 32-entry RGB table can render common scenes while still getting address aliases, transparent pixels, grayscale, or console-specific output behavior wrong.
Keep three layers separate: the addressable palette-RAM entry, the 6-bit color code stored there, and the final analog or RGB output produced for a particular PPU model and video region. That separation makes palette bugs testable and prevents a display shader from hiding an incorrect PPU state model.
Decode the 32-byte palette space
The PPU exposes 32 palette bytes at $3F00-$3F1F, mirrored throughout $3F00-$3FFF. Background and sprite rendering each select among four palettes of four entries. Pattern data contributes a two-bit pixel value; background attribute data or sprite attributes select one of the four palettes. The resulting palette lookup is an index, not an RGB triplet.
Entry zero has special composition behavior. A sprite pixel whose pattern value is zero is transparent, allowing the background to show through. Background pattern value zero selects the backdrop path, and the backdrop color is shared with the universal background entry. Each sprite-palette color-zero address mirrors its corresponding background palette color-zero address: $3F10 maps to $3F00, $3F14 to $3F04, $3F18 to $3F08, and $3F1C to $3F0C. These are four distinct address aliases, rather than aliases of one stored byte. During normal rendering, the universal backdrop color comes from $3F00.
Normalize palette addresses only after confirming that the PPU address is in the palette range. The following helper demonstrates the 32-byte mirror and the four internal aliases; it does not model the PPU address latch, read buffer, rendering-time access restrictions, or mapper-specific extensions.
#include <assert.h>
#include <stdint.h>
static uint8_t
palette_index(uint16_t ppu_address)
{
assert(ppu_address >= 0x3F00 && ppu_address <= 0x3FFF);
uint8_t index = (uint8_t)(ppu_address & 0x1F);
if ((index & 0x13) == 0x10)
index &= 0x0F;
return index;
}
static void
test_palette_aliases(void)
{
assert(palette_index(0x3F00) == palette_index(0x3F10));
assert(palette_index(0x3F04) == palette_index(0x3F14));
assert(palette_index(0x3F08) == palette_index(0x3F18));
assert(palette_index(0x3F0C) == palette_index(0x3F1C));
assert(palette_index(0x3F00) == palette_index(0x3F20));
assert(palette_index(0x3F14) != palette_index(0x3F00));
assert(palette_index(0x3F18) != palette_index(0x3F04));
assert(palette_index(0x3F1C) != palette_index(0x3F08));
}
The 32-byte mask handles the $3F20-$3FFF mirrors. The second mask folds the four sprite entry-zero addresses onto their background counterparts. Keep this canonicalization in the PPU memory path so CPU reads, CPU writes, rendering, and debugger views agree about the same storage.
Separate pixel selection from palette output
For a background pixel, the pattern planes supply a value from zero to three and the attribute table selects a background palette. For a sprite pixel, OAM attributes select a sprite palette and also participate in priority. A zero sprite pattern value is transparent; a nonzero value may still be behind a nonzero background pixel according to the sprite’s priority bit. Palette RAM should not decide which layer wins. Resolve layer transparency and priority first, then use the selected palette address to obtain the PPU color code.
The four background palettes and four sprite palettes do not mean every displayed pixel can independently choose any of 32 colors. Tile pattern values and attribute selection constrain the choice, and mapper hardware can add special behavior beyond the base PPU rules. When debugging a mismatch, capture the raw pattern value, selected attribute palette, sprite/background candidate, priority result, final palette address, and six-bit color code. A screenshot alone shows the last stage and cannot identify which selection step diverged.
Treat grayscale and emphasis as later output controls
Palette RAM holds a six-bit color code that selects among the PPU’s output colors. PPUMASK also has a grayscale control and emphasis bits. The grayscale operation ANDs the selected color code with $30, selecting from the gray columns of the code table; it is not an RGB luminance conversion. The emphasis bits modify the output signal independently of grayscale. These controls are not alternate palette RAM entries and should not be baked into every stored byte.
Do not assume one universal RGB table for every NES-family machine. NTSC and PAL video behavior differs, and RGB PPU variants used in arcade or specialty systems can have different palette characteristics. Apply a selected palette model at the video-output stage, keep the raw PPU code available for debugging, and document whether a chosen table is measured, modeled, or a frontend approximation. A shader that merely looks familiar is not evidence that the PPU memory mapping is correct.
Validate aliases and rendered behavior independently
Start with unit tests for all $3F00-$3FFF mirrors and the four universal-background aliases. Add tests that background and sprite entry zero follow their distinct transparency rules, that attribute bits select the intended palette, and that grayscale and emphasis changes do not overwrite palette RAM. Keep tests for palette storage separate from tests for a frontend’s RGB conversion.
Use a trusted NES PPU test ROM or a controlled emulator fixture to exercise address reads and writes, then compare raw palette RAM and selected color codes in a debugger. A useful regression captures the PPU model, region, mapper, address, raw entry, grayscale/emphasis bits, layer selection, and final output index. If only the RGB image is recorded, a later palette-table update can make it impossible to tell whether the PPU behavior changed or only the presentation model did.
This article covers the base palette-RAM and output-mask path. It does not define mapper-controlled extended attributes, exact analog decoding for every board, or a single “correct” RGB palette for all display pipelines. Keeping those boundaries explicit lets the emulator reproduce the PPU contract while allowing well-labeled output profiles for different hardware and displays.
Related:
- NES PPU VBlank and NMI Timing: Model the Event, Not Just the Frame
- NES PPU Sprite Evaluation: Secondary OAM, Overflow Bug, and Fetch Timing
Sources: