OAM stores sprite attributes for the PPU. Understanding how OAM works is critical for sprite programming.
OAM is 544 bytes total:
| Section | Bytes | Content |
|---|---|---|
| Main Table | 0-511 | 128 sprites × 4 bytes each |
| Extended Table | 512-543 | 128 sprites × 2 bits each |
| Byte | Content |
|---|---|
| 0 | X position (lower 8 bits) |
| 1 | Y position |
| 2 | Tile number (lower 8 bits) |
| 3 | Attributes (see below) |
This is the most important thing to understand about OAM writes.
For the main table (bytes 0-511), the SNES uses a 16-bit internal write buffer:
The extended table (bytes 512-543) is the exception: writes there apply immediately, byte by byte, with no pair-latching. (Confirmed by the register references — the latch logic only applies below internal address $200.)
For the main table, if you only write one byte (e.g., just the X position), nothing happens. The byte sits in the internal buffer but is never committed to actual OAM.
Always write bytes in pairs:
PVSnesLib uses a shadow buffer approach:
For direct OAM writes without a shadow buffer, you must respect the pair-write requirement.
| Register | Address | Description |
|---|---|---|
| OAMADDL | $2102 | OAM address (low byte) |
| OAMADDH | $2103 | OAM address (high byte) + priority rotation |
| OAMDATA | $2104 | OAM data write |
For the extended table:
$2101 (OBJSEL) configures sprite sizes:
| Value | Small | Large |
|---|---|---|
| 0x00 | 8x8 | 16x16 |
| 0x20 | 8x8 | 32x32 |
| 0x40 | 8x8 | 64x64 |
| 0x60 | 16x16 | 32x32 |
| 0x80 | 16x16 | 64x64 |
| 0xA0 | 32x32 | 64x64 |
To hide a sprite, set its Y position to 240 and set its X-position high bit (placing it at X=256, off-screen to the right):
Y=240 alone only hides sprites up to 16 px tall: sprite Y wraps vertically, so a 32x32 sprite at Y=240 shows its last ~16 rows at the top of the screen, and no Y value can fully hide a 64x64. This is exactly what the lib's oamHide() / oamClear() do — prefer them over manual pokes.
One refinement worth knowing: a sprite at X=256 is still treated as X=0 for the per-scanline range/time evaluation. A hidden large sprite whose Y wraps onto visible lines therefore still consumes the 32-sprites / 34-slivers budget on those lines — if the top of your screen drops sprites mysteriously, audit your hidden large sprites.
A sprite's OAM Y is the picture line of its top row, 0 being the first visible line. The PPU does draw a sprite one scanline below its Y, and it never outputs scanline 0: the two cancel. The arbiter, word for word (SNESdev wiki, Sprites, https://snes.nesdev.org/wiki/Sprites):
Like the NES, sprites appear 1 line lower than their Y value, however because the first line of rendering is always hidden on SNES, a sprite with Y=0 will appear to begin on the first visible line. However, a background with Y scroll of 0 will appear to have its top pixel cut off by the hidden line.
So only the background needs a correction, and the library applies it: bgSetScroll() writes y - 1 to BGnVOFS. Every sprite API stores the y it is given, and a direct write to oamMemory[id * 4 + 1] stores y too. A sprite and the background it stands on take the same camera value.
Measured on luna (2026-10-09): an 8x8 sprite with OAM Y = 0 covers picture lines 0 to 7; with Y = 255 it covers lines 0 to 6, its top row lost on the scanline that is never output.
History, because code written against it exists. From 2026-04-27 to 2026-10-09 oamSet(), oamSetY(), oamSetXY() and the oamSetFast macros stored y - 1, on a reading of the first half of the sentence above without the second; the dynamic engine followed on 2026-09-26. Every sprite was drawn one line too high, and since bgSetScroll() got its - 1 (2026-09-12) one line above a background scrolled to the same y. Code that subtracted 1 by hand before a direct oamMemory[] write must stop; code that added 1 to a y passed to oamSet() to line a sprite up with the background must stop too.
Sentinel values that hide a sprite (OBJ_HIDE_Y = 240) are positions like any other: 240 is below the last visible line (223).
The X axis has no such subtlety.
OAM writes should be done during VBlank or forced blank (screen off). Writing during active display can cause visual glitches.