PlayStation GPU Texture Pages and CLUTs: Decode the VRAM Addressing Model
Trace PS1 textured primitives through texture-page attributes, packed texels, CLUT coordinates, and texture windows to debug faithful rendering.
The original PlayStation’s GPU does not consume a modern linear texture object with an independent palette attached. Texture data, lookup tables, and the displayed image all occupy GPU VRAM, and textured drawing commands identify a texture page and, for indexed color modes, a color lookup table (CLUT). That shared-memory model explains why a texture can look wrong even when its source pixels and polygon UVs appear correct: the page, pixel depth, palette origin, and texture-window transform are part of the draw state.
This guide describes the model from the perspective of debugging software and emulator implementations. The public PSX-SPX specification is a reverse-engineered technical reference, not an official Sony SDK manual. It documents many GPU details and known version distinctions, but statements marked uncertain in that reference should remain qualified until corroborated with hardware or well-established test suites.
Start with the textured primitive packet
Textured polygon and rectangle commands carry vertex positions and UV coordinates, along with draw attributes. In the primitive packet, the upper bits associated with texture-coordinate words encode palette/page attributes: the first UV word carries CLUT selection, while the next carries texture-page information. The GPU uses those fields together with the 8-bit U and V coordinates to fetch texels. A renderer that extracts UV values but discards the upper attribute bits will appear to support textured geometry while selecting the wrong palette or page.
Keep packet decoding distinct from rasterization. First parse the command type, vertex count, shading/texturing flags, coordinates, UVs, CLUT, and texture-page state. Then apply texture-window coordinate manipulation if enabled. Finally resolve the texel according to page color depth and CLUT selection, before blending and writing the pixel subject to draw-area and mask rules.
Texture-page coordinates are measured in VRAM words
The draw-mode texture-page attribute selects horizontal and vertical origins plus texture color depth and semi-transparency mode. The horizontal page base advances in units of 64 16-bit VRAM words; the vertical page base advances in 256-line units. Because indexed formats pack several pixels into each word, the number of texels represented by a horizontal span depends on the selected depth. A 4-bit texel occupies one nibble, an 8-bit texel one byte, and a 15-bit texel one 16-bit word.
Do not confuse the page-origin unit with a universal pixel width. The same base field is interpreted with a different texel packing ratio for each mode. If an emulator converts every page to a fixed 256-pixel-wide conventional texture without carefully reproducing address wrapping and packing, page-edge cases can diverge from hardware. The VRAM address calculation must be consistent with the selected mode and GPU revision’s documented address space.
The texture-page attribute also includes the two-bit semi-transparency mode and the texture color mode. Those settings affect later pixel processing; they do not alter the meaning of the CLUT as an independent host-side palette object. A 15-bit texture uses colors stored directly in VRAM and does not need a CLUT, while 4-bit and 8-bit textures interpret packed texels as indexes into palette data.
Resolve indexed texels through a CLUT
A CLUT is stored in VRAM. In 4-bit mode it contains 16 16-bit entries; in 8-bit mode it contains 256 entries. The CLUT attribute selects its location, with the horizontal coordinate encoded in 16-word units and a vertical coordinate selecting the row. The texel value indexes that table. This means CLUT placement is constrained by how the attribute encodes coordinates, not by an arbitrary host pointer.
An illustrative address-resolution pipeline is:
packed = vram[texture_word_address(page, u, v, depth)]
index = unpack_texel(packed, u, depth)
if depth == 4 or depth == 8:
color15 = vram[clut_word_address(clut, index)]
else:
color15 = packed_texture_color(packed)
pixel = apply_texture_modulation_and_blending(color15, draw_attributes)
The pseudocode intentionally leaves address wrapping, transparency, mask bits, dithering, and exact modulation rules to the GPU specification. It is a decomposition aid, not executable code. In particular, do not treat palette index zero as transparent based on generic indexed-image conventions alone; model the PS1 GPU’s documented pixel and transparency semantics, including the relevant high bit and draw mode.
When a scene displays the right shapes with the wrong colors, inspect the command stream before rewriting the texture decoder. Verify the CLUT coordinates extracted from the first UV attribute word, the texture-page color depth, and the order in which software uploads the palette and texture data. A stale CLUT is a different defect from a wrong index calculation.
Apply texture-window masks and offsets in UV space
The GPU’s texture-window command specifies mask and offset fields that transform texture coordinates so a smaller region repeats. It is not simply a host-side crop rectangle. The documented operation clears selected U/V bits according to the masks, then inserts selected offset bits. This can create a tiled or repeated view of texture data without duplicating the underlying pixels across the page.
An implementation should preserve the order of operations: obtain the primitive’s UV coordinate, apply the active texture-window transformation, then resolve the resulting coordinate under the texture-page and depth rules. A common bug is to apply the transform after converting U into a byte or word offset. That moves bits in the wrong coordinate domain, particularly in packed 4-bit and 8-bit modes.
Test masks and offsets independently on a synthetic texture whose rows and columns have distinct labels. Use all-zero masks as a control, then enable one U or V mask bit and vary the corresponding offset. Include wraparound at the 8-bit UV boundary and verify that the pattern repeats at the expected texel period. Only after the unit test passes should you use a commercial title as a regression case.
Keep VRAM ownership visible in debugging tools
Because texture pages and CLUTs are regions of the same GPU memory, a debugger should display their coordinates in a shared VRAM view. Overlay the current page origin and selected CLUT, but do not imply that these regions are permanently reserved or non-overlapping. Software may intentionally reuse or update VRAM for textures, framebuffers, and display buffers at different times. The active draw state determines how a given rectangle is interpreted.
Useful per-draw diagnostics include command opcode, primitive type, UV coordinates, CLUT coordinates, page origin, color depth, texture-window mask/offset, semi-transparency mode, dither setting, and the raw VRAM words fetched for a failing pixel. A texture viewer that shows only decoded RGBA output hides whether the error began in command parsing, address calculation, nibble selection, palette lookup, or final pixel processing.
Validate with small invariants and real traces
Build tests for 4-bit and 8-bit packed texels, each valid index range, CLUT coordinate decoding, direct 15-bit color, texture-window masks, UV boundaries, and VRAM region transitions. Include primitives that switch page or CLUT attributes between adjacent commands without an intervening global draw-mode change; textured primitive attributes and global state do not have identical update paths. Compare raw VRAM fetches and final framebuffer values, not only screenshots.
Then capture a known-good command stream from a game or hardware trace and replay it against the emulator. Save the raw packet words, VRAM snapshot, GPU register state, and expected image region as one reproducible fixture. When testing on hardware, record console/GPU model and video mode. PSX-SPX documents differences between GPU versions, including extended VRAM behavior on some revisions, so do not silently apply a later device’s capability to every retail unit.
The central debugging principle is to follow the data path in order: packet attributes, texture-page addressing, packed texel extraction, CLUT resolution when applicable, texture-window transformation, then color and mask processing. This makes “wrong texture” a sequence of testable hypotheses instead of a single vague rendering symptom.
Related:
- Tile-Based Rendering: How 2D Consoles Built Scenes Without a Modern Framebuffer
- Cartridge Mappers and Bank Switching: How Consoles Addressed Games Larger Than Memory
Sources: