Nametable file format:
The ultimate goal is to create an external file that stores all the nametable data.
.export _nametable_0, _nametable_1, _nametable_2, _nametable_3
.export _nametable_4, _nametable_5, _nametable_6, _nametable_7
.segment "RODATA"
_nametable_0: .repeat 30
.byte $00,$00,$00,$00,$00,$00,$00,$00,$00,$00,$00,$00,$00,$00,$00,$00,$00,$00,$00,$00,$00,$00,$00,$00,$00,$00,$40,$41,$41,$41,$41,$41
.endrepeat
_nametable_1: .repeat 30
.byte $00,$00,$00,$00,$00,$00,$00,$00,$00,$00,$00,$00,$00,$00,$00,$00,$00,$00,$00,$00,$00,$00,$00,$00,$00,$00,$40,$41,$41,$41,$41,$41
.endrepeat
_nametable_2: .repeat 30
.byte $00,$00,$00,$00,$00,$00,$00,$00,$00,$00,$00,$00,$00,$00,$00,$00,$00,$00,$00,$00,$00,$00,$00,$00,$00,$00,$40,$41,$41,$41,$41,$41
.endrepeat
_nametable_3: .repeat 30
.byte $00,$00,$00,$00,$00,$00,$00,$00,$00,$00,$00,$00,$00,$00,$00,$00,$00,$00,$00,$00,$00,$00,$00,$00,$00,$00,$40,$41,$41,$41,$41,$41
.endrepeat
_nametable_4: .repeat 30
.byte $00,$00,$00,$00,$00,$00,$00,$00,$00,$00,$00,$00,$00,$00,$00,$00,$00,$00,$00,$00,$00,$00,$00,$00,$00,$00,$40,$41,$41,$41,$41,$41
.endrepeat
_nametable_5: .repeat 30
.byte $00,$00,$00,$00,$00,$00,$00,$00,$00,$00,$00,$00,$00,$00,$00,$00,$00,$00,$00,$00,$00,$00,$00,$00,$00,$00,$40,$41,$41,$41,$41,$41
.endrepeat
_nametable_6: .repeat 30
.byte $00,$00,$00,$00,$00,$00,$00,$00,$00,$00,$00,$00,$00,$00,$00,$00,$00,$00,$00,$00,$00,$00,$00,$00,$00,$00,$40,$41,$41,$41,$41,$41
.endrepeat
_nametable_7: .repeat 30
.byte $00,$00,$00,$00,$00,$00,$00,$00,$00,$00,$00,$00,$00,$00,$00,$00,$00,$00,$00,$00,$00,$00,$00,$00,$00,$00,$40,$41,$41,$41,$41,$41
.endrepeat
Nametables are stored as raw byte arrays in PRG ROM and copied or decompressed into PPU VRAM when loaded.
- Uncompressed Nametables ($1,024$ bytes each): I can store roughly 15 to 20 uncompressed Nametables before filling up your 32 KB PRG ROM (leaving ~12 KB for game engine code, physics, music, and logic).
- RLE-Compressed Nametables (~150 to 300 bytes each): I can easily link 60 to 100+ compressed Nametables in 32 KB PRG ROM using
vram_unrle().
Suggested Memory Budget Breakdown in 32 KB PRG ROM:
- C Engine & Game Logic ~10 KB
- neslib Library & Runtime ~2 KB
- Audio Engine & SFX (FamiTone2) ~3 KB
- Available Space for Backgrounds ~17 KB
- Max Nametable Capacity:
- Uncompressed ~17 Full Screens
- *RLE Compressed Allocation ~60 to 100+ Screens
*An RLE-Compressed Nametable is a background screen layout that has been processed using Run-Length Encoding (RLE) to significantly reduce its size in PRG ROM.Instead of storing all 1,024 individual tile bytes for a full $32 \times 30$ grid, RLE compresses repeated identical tiles (like blank sky, floor blocks, or solid walls) into compact sequence commands.
Uncompressed (16 bytes):
$00, $00, $00, $00, $00, $00, $00, $00, $00, $00, $4A, $4B, $00, $00, $00, $00
RLE Compressed (6 bytes):
[10x $00], [1x $4A], [1x $4B], [4x $00]
Display 1 nametable as the background:
//#link "chr_generic.s"
//#link "nametable.s"
#include <nes.h>
#include "neslib.h"
// Declare external assembly symbol exported in nametable.s
extern const unsigned char nametable_0[];
// Sample palette (Background palette 0)
const unsigned char palette[16] = {
0x0f, 0x00, 0x10, 0x30, // Black background, Dark Gray, Light Gray, White
0x0f, 0x00, 0x10, 0x30,
0x0f, 0x00, 0x10, 0x30,
0x0f, 0x00, 0x10, 0x30
};
void load_attributes_separately(const unsigned char *tiles, const unsigned char *attributes) {
// 1. Write tile bytes
vram_adr(0x2000);
vram_write(tiles, 960);
// 2. Write attribute bytes to $23C0 (Nametable 0 attribute offset)
vram_adr(0x23C0);
vram_write(attributes, 64);
}
void main(void) {
bank_bg(0); // Backgrounds from $0000 (Font/Tiles)
bank_spr(1); // Sprites from $1000 (Player)
// Disable rendering while writing to PPU memory
ppu_off();
// Load background color palette
pal_bg(palette);
// Set PPU address to Nametable 0 start ($2000)
vram_adr(0x2000);
// Write the full 960-byte nametable array from nametable.s
// (32 rows x 30 columns = 960 bytes)
vram_write(nametable_0, 960);
// Reset scroll position
scroll(0, 0);
// Enable rendering
ppu_on_all();
// Infinite loop to keep rendering active
while (1) {
ppu_wait_frame();
}
}