This tutorial covers the FAR qualifier: how to place bulk C data in the 56 KB of WRAM above the 8 KB bank-0 band, what it costs, and when not to use it. It assumes you have read the Graphics tutorial; the DMA tutorial is useful background because most FAR buffers end up as DMA sources.
The SNES has 128 KB of WRAM, but the compiler's default addressing is bank-0-implicit: a plain C global must live in $00:0000-$1FFF, the 8 KB that also holds the stack and the direct-page registers. Every link prints how much of that band is left:
A tilemap built in RAM (2 KB), a palette buffer (512 bytes) and a brick grid were enough to leave breakout with 1.4 KB for everything else. The second band is $7E:2000-$FFFF, 56 KB, and FAR is how a C object gets there:
The same link then prints both bands:
A FAR object behaves like any other global. It is zero at boot (crt0 clears the far band with one DMA, 22 ms once), it can carry an initialiser, and every access to it is compiled with bank-honouring addressing — the qualifier rides on the type, like const, so a u8 FAR *p pointer keeps the property through function calls.
Casting a FAR pointer to a plain pointer explicitly compiles and is the one way to get a silent wrong-bank access. Do it only when the object really is in bank 0.
Measured with the b2_deref bench (cycles per access, loop overhead removed):
| Access | bank 0 | FAR |
|---|---|---|
| byte load through a pointer walk p[k] | 116 | 121 |
| word load through a pointer walk | 132 | 137 |
| byte load arr[k] | 107 | 88 |
| byte store through a pointer walk | 88 | 74 |
| byte store arr[k] | 79 | 41 |
Indexing a FAR array by name is cheaper than the bank-0 form: the backend folds arr + k into one absolute-long-indexed instruction (lda.l arr,x) instead of materialising the address first. A pointer walk stages the 24-bit pointer once in a direct-page pair and then reads with [tcc__r9],y, within 4 % of the bank-0 path. Struct fields through a FAR pointer stage the pointer once per basic block:
Use FAR for bulk data that is read or written in loops and handed to DMA: tilemaps built in RAM, palette buffers for fades, HDMA tables, entity pools, level state. That is exactly what fills the 8 KB band and exactly what the far forms handle at no visible cost.
Keep in bank 0 the small hot state that lib helpers walk through a plain pointer: Rect, AnimPlayer, AudioSample, AudioVoiceState. Those helpers (rectContains, collideRect, animTick, audioGetSampleInfo, …) take plain pointers on purpose; the compiler refuses a FAR argument there, so nothing goes wrong silently. Two helpers that legitimately fill a bulk buffer accept either band: sramLoad / sramLoadOffset and dsp1Raster.
examples/games/breakout builds its two tilemaps and its palette in RAM and re-uploads them after every brick hit. The buffers are declared in assembly and reached from C:
The whole migration was the FAR on four extern lines, the three local helpers taking FAR pointers, and (const u8 *) on the DMA calls. The ROM renders pixel for pixel the same, and bank 0 went from 1436 to 6880 free bytes.
A loop in C over a plain pointer reads and writes bank $00, so it cannot fill a FAR buffer from a const table: the table is in ROM, the buffer in bank $7E. The string module does it. Its seven functions — memcpy, memmove, memset, strlen, strcmp, strcpy, strncpy — follow the full 24-bit pointer on both sides, so every pairing of ROM, plain RAM and FAR RAM works, and a ROM links only the functions it calls.
memcpy and memset move a word at a time: 23 CPU cycles a word in the copy loop, so about 12 a byte. For VRAM, CGRAM and OAM the data goes through PPU ports: that is DMA's job (dmaCopyVram() and friends), not memcpy's.
Three things differ from a desktop C library, all following from the target: a size is an unsigned int, 16 bits, so 65535 bytes at most; a destination is typed FAR (the type every pointer converts to), and the functions that return their destination return it as a FAR pointer; strcmp returns the difference of the first two differing bytes, taken as unsigned, of which only the sign matters.
symmap.py --check-ram-budget game.sym prints both bands. In luna, a FAR symbol resolves to its $7E:xxxx address from the .sym file, so --peek far_buf:16 reads it directly. If a value reads as zero or as garbage where you expect data, check first that no explicit cast dropped the qualifier on the way — that is the only path the compiler does not guard.