OpenSNES
Modern Open-Source SNES Development SDK
Loading...
Searching...
No Matches
Graphics & Backgrounds Tutorial

This tutorial covers SNES graphics fundamentals including video modes, backgrounds, and tilemaps.

SNES Video Modes

The SNES has 8 video modes (0-7). OpenSNES primarily uses:

Mode BG1 BG2 BG3 BG4 Best For
0 4 colors 4 colors 4 colors 4 colors Text, simple UI
1 16 colors 16 colors 4 colors - Most games
7 256 colors (rotatable) - - - Racing, rotation effects

Setting Up Graphics

Initialize Display

#include <snes.h>
int main(void) {
// Initialize console (sets up display, enables NMI)
// Set video mode 1 (most common for games)
// Enable BG1 on main screen
// Turn on display
while (1) {
}
return 0;
}
int main(void)
Definition main.c:66
void consoleInit(void)
Initialize SNES hardware.
void WaitForVBlank(void)
Wait for next VBlank period.
void setScreenOn(void)
Enable screen display.
#define BG_MODE1
Definition video.h:35
#define REG_TM
Main screen designation (W).
Definition registers.h:187
#define TM_BG1
Definition registers.h:443
OpenSNES common-case master header.
void setMode(u8 mode, u8 flags)
Set background mode.

Loading Tiles to VRAM

Tiles are 8x8 pixel graphics stored in VRAM. For 4bpp (16 color) tiles:

// Tile data (32 bytes per 8x8 tile for 4bpp)
const u8 my_tiles[] = {
// Plane 0-1 (16 bytes)
0xFF, 0x00, 0xFF, 0x00, 0xFF, 0x00, 0xFF, 0x00,
0xFF, 0x00, 0xFF, 0x00, 0xFF, 0x00, 0xFF, 0x00,
// Plane 2-3 (16 bytes)
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00
};
void load_tiles(void) {
u16 i;
// Set VRAM address (tiles at $1000)
REG_VMAIN = 0x80; // Increment on high byte
REG_VMADDL = 0x00;
REG_VMADDH = 0x10; // $1000
// Copy tile data
for (i = 0; i < sizeof(my_tiles); i += 2) {
REG_VMDATAL = my_tiles[i];
REG_VMDATAH = my_tiles[i + 1];
}
}
static u8 i
Definition main.c:155
#define REG_VMADDH
VRAM address high (W).
Definition registers.h:124
#define REG_VMAIN
VRAM address increment mode (W).
Definition registers.h:112
#define REG_VMADDL
VRAM address low (W).
Definition registers.h:121
#define REG_VMDATAL
VRAM data write low (W).
Definition registers.h:127
#define REG_VMDATAH
VRAM data write high (W).
Definition registers.h:130
unsigned short u16
16-bit unsigned integer (0 to 65535)
Definition types.h:53
unsigned char u8
8-bit unsigned integer (0 to 255)
Definition types.h:47

The background module does the same with DMA, and points the layer at the tiles in one call. bgInitTileSetData() loads tiles only (no palette), for a second tile set or a layer that shares a palette:

extern u8 hud_tiles[], hud_tiles_end[];
extern u8 extra_tiles[], extra_tiles_end[];
/* DMA the tiles to VRAM $3000 and set BG3's tile base there */
bgInitTileSetData(2, hud_tiles, (u16)(hud_tiles_end - hud_tiles), 0x3000);
/* 0xFF as the layer: load the tiles, leave every tile base alone */
bgInitTileSetData(0xFF, extra_tiles, (u16)(extra_tiles_end - extra_tiles), 0x5000);
void bgInitTileSetData(u8 bgNumber, const u8 *tileSource, u16 tileSize, u16 vramAddr)
Load tile graphics only (no palette).

Like any DMA to VRAM, call it during force blank or VBlank. bgInit(bg) puts a layer's scroll back to (0, 0).

Setting Up Tilemaps

Tilemaps define which tiles appear where on screen. Each entry is 2 bytes:

Bits: vhopppcc cccccccc
v = vertical flip
h = horizontal flip
o = priority
ppp = palette (0-7)
cccccccccc = tile number (0-1023)
void setup_tilemap(void) {
u16 x, y;
// Set VRAM address for tilemap ($0400)
REG_VMADDL = 0x00;
REG_VMADDH = 0x04;
// Fill 32x32 tilemap
for (y = 0; y < 32; y++) {
for (x = 0; x < 32; x++) {
REG_VMDATAL = 0x01; // Tile number 1
REG_VMDATAH = 0x00; // No flip, palette 0
}
}
}

Setting Palettes

SNES uses 15-bit color (RGB555):

void setup_palette(void) {
// Set CGRAM address (palette 0)
REG_CGADD = 0;
// Color 0: Black (transparent for BG)
REG_CGDATA = 0x00;
REG_CGDATA = 0x00;
// Color 1: White
REG_CGDATA = 0xFF;
REG_CGDATA = 0x7F;
// Color 2: Red
REG_CGDATA = 0x1F;
REG_CGDATA = 0x00;
// Color 3: Green
REG_CGDATA = 0xE0;
REG_CGDATA = 0x03;
// Color 4: Blue
REG_CGDATA = 0x00;
REG_CGDATA = 0x7C;
}
#define REG_CGADD
CGRAM address (W).
Definition registers.h:154
#define REG_CGDATA
CGRAM data write (W).
Definition registers.h:157

Background Scrolling

s16 scroll_x = 0;
s16 scroll_y = 0;
void update_scroll(void) {
// Write scroll registers (write twice for 16-bit value)
REG_BG1HOFS = (u8)scroll_x;
REG_BG1HOFS = (u8)(scroll_x >> 8);
REG_BG1VOFS = (u8)scroll_y;
REG_BG1VOFS = (u8)(scroll_y >> 8);
}
#define REG_BG1VOFS
BG1 vertical scroll (W, 2x write).
Definition registers.h:91
#define REG_BG1HOFS
BG1 horizontal scroll (W, 2x write).
Definition registers.h:88
signed short s16
16-bit signed integer (-32768 to 32767)
Definition types.h:50

Parallax Scrolling

For depth effect, scroll backgrounds at different speeds:

// In main loop:
scroll_x++;
// BG1 (foreground) - full speed
REG_BG1HOFS = (u8)scroll_x;
REG_BG1HOFS = (u8)(scroll_x >> 8);
// BG2 (background) - half speed
REG_BG2HOFS = (u8)(scroll_x >> 1);
REG_BG2HOFS = (u8)((scroll_x >> 1) >> 8);
#define REG_BG2HOFS
BG2 horizontal scroll (W, 2x write).
Definition registers.h:94

Using gfx4snes

Convert PNG images to SNES format using gfx4snes (based on PVSnesLib):

# Convert 8x8 tiles with palette output
gfx4snes -s 8 -p -i tiles.png
# Output: tiles.pic (tile data), tiles.pal (palette)
# Convert 16x16 sprites with palette
gfx4snes -s 16 -p -i sprites.png
# Convert with metasprite definition (for animation)
gfx4snes -s 16 -p -T -X 32 -Y 48 -i character.png
# Output: character.pic, character.pal, character_meta.inc
# Convert against a committed palette (do not re-quantise)
gfx4snes -s 8 -c master.pal -i tiles.png

-c <file.pal> imposes a committed palette instead of quantising the image's own colours. The build fails — naming the offending colour — if the image uses any colour not in that palette. This is the fix for the "adding one tile re-quantises the whole palette and shifts every existing tile's hue" problem: author the palette once, commit it, and impose it on every conversion so a new tile can only reuse existing entries.

Include the generated data in your assembly file:

sprite_tiles:
.incbin "res/sprites.pic"
sprite_tiles_end:
sprite_pal:
.incbin "res/sprites.pal"
sprite_pal_end:

Tiles made at run time

Art generated by the game — a dot, a bar, a glyph built from a bitmask — starts as colour indices, one byte per pixel. tileEncode4bpp() (module tile, <snes/tile.h>; 2bpp and 8bpp variants too) turns 64 of them, row by row, into the planar tile the PPU reads:

#include <snes/tile.h> /* LIB_MODULES += tile */
static u8 pixels[64];
static u8 tile[32];
u8 i;
for (i = 0; i < 64; i++)
pixels[i] = (i & 9) ? 3 : 0; /* any pattern of colours 0-15 */
tileEncode4bpp(pixels, tile);
dmaCopyVram(tile, 0x1000, 32); /* during VBlank or force blank */
void dmaCopyVram(const u8 *source, u16 vramAddr, u16 size)
Copy data to VRAM (PVSnesLib compatible).
Build SNES tiles from pixels at run time.
void tileEncode4bpp(const u8 *pixels, u8 *tile)
64 colour indices (0-15) to one 4bpp tile

A tile costs about 47 000 master cycles, an eighth of a frame: build tiles while loading, or a few per frame. backgrounds/mode2, basics/game_skeleton, basics/panel_hud and sprites/sprite_swarm build their art this way.

Compressed tiles (LZ77)

Tile data compresses well — it is repetitive by construction — and the SDK can decompress it straight into VRAM, so the ROM carries the small form and the PPU gets the big one. Add -z to the gfx4snes invocation and link the module:

LIB_MODULES := console lzss background sprite
...
$(GFX4SNES) -z -s 8 -o 16 -u 16 -e 0 -p -m -i $<
extern u8 patterns[]; /* the compressed .pic, as produced by -z */
setScreenOff(); /* VRAM writes need forced blank */
#define VRAM_BG_TILES
Definition vram_map.h:8
void setScreenOff(void)
Disable screen display (blank).
void lzssDecodeVram(const u8 *source, u16 address)
Decompress LZ77 data directly to VRAM.

The saving is worth measuring rather than assuming. For the artwork in examples/backgrounds/mode1_lz77, the compressed .pic is 652 bytes and its LZ77 header declares 2592 bytes of tile data — the same 2592 bytes that examples/backgrounds/mode1 stores uncompressed for the same image. That is a quarter of the ROM footprint for one call at load time.

The format is the familiar GBA-style LZ77: a 0x10 tag byte, a three-byte little-endian decompressed length, then flag bytes whose bits select a literal or a back-reference. Any tool that emits that layout will do.

Warning
lzssDecodeVram writes VRAM directly and disables interrupts while it runs. Call it during forced blank, with the rest of your setup — not from a running frame, where the PPU would drop the writes silently.

Example: Complete Background Setup

See examples/scrolling/parallax_scroll/ for a complete parallax scrolling example.

Hi-res and interlace (Modes 5/6, SETINI)

BG Mode 5 renders 512 horizontal pixels (16x8 tiles, stored as 8x8 character pairs N/N+1); adding screen interlace (videoSetInterlace(1), SETINI bit 0 through the lib's write-only shadow) doubles vertical to 448 lines. Three traps, all demonstrated by examples/backgrounds/mode5_hires:

  1. Hi-res content displays through BOTH screens: setMainScreen(LAYER_BG1) AND setSubScreen(LAYER_BG1). The sub screen draws the even pixel columns and the main screen the odd ones (snesdev-wiki, Uncommon graphics mode games; its Backgrounds page has it inverted, a documented error in the corpus), so main-only leaves the even columns blank.
  2. In interlace, tile texel rows map 1:1 to hi-res lines — a full-height page needs 56 tile rows (a 32x64 tilemap), not 28.
  3. On a modern LCD the alternating columns show as fringing; a period CRT blended them. The example's README has the full explainer.

videoSetObjInterlace, videoSetOverscan and videoSetPseudoHires cover the other SETINI bits — overscan shrinks the VBlank DMA budget by ~15 lines, see the header warning.

Next Steps