Skip to content
RetrogamingDeep Dive Published Updated 8 min readViews unavailable

Nintendo DS VRAM Banks: Route A-I Memory to the Right Consumer

Map Nintendo DS VRAM banks A-I to 2D, 3D, LCD, and ARM7 consumers without confusing physical capacity, virtual windows, and graphics offsets.

Nintendo DS video memory is not one flat 656 KiB heap. It is nine physical banks with different capacities and different routing options. The ARM9 chooses a destination for each bank through the VRAM control registers: main or sub 2D graphics, sprite memory, 3D texture or palette storage, an LCD-visible mapping, or ARM7 work RAM in the special cases of banks C and D. A pointer is useful only when the bank is currently routed to the address window and engine that the program expects.

That separation explains a common homebrew failure: code writes valid tile or pixel bytes, yet the display stays blank. The CPU store can complete even when no bank is mapped to the background window being read by the video engine. Fixing the register assignment is different from changing a tile-map offset, a sprite address, or a display mode.

Start with nine physical banks

The capacities are deliberately uneven:

Bank Capacity Typical mapping choices
A–B 128 KiB each Main-engine backgrounds or sprites, or 3D textures
C 128 KiB Main or sub-engine backgrounds, 3D textures, or ARM7 work RAM
D 128 KiB Main-engine backgrounds, sub-engine sprites, 3D textures, or ARM7 work RAM
E 64 KiB Main backgrounds or sprites, texture palettes, main background extended palettes
F–G 16 KiB each Main backgrounds or sprites, texture palettes, extended palettes
H 32 KiB Sub-engine backgrounds or sub background extended palette
I 16 KiB Sub background, sub sprites, or sub sprite extended palette

Together those banks provide 656 KiB, but they are not interchangeable chunks. The libnds API uses a separate enum for each bank because the hardware does not offer the same destination choices for every bank. For example, its definitions include VRAM_C_ARM7 but do not define a corresponding ARM7 mode for bank A; bank H has sub-engine background choices, while bank I can instead serve sub sprites or their extended palette.

Think of a bank as a capacity-bearing chip with a selected consumer, not as an allocation that automatically belongs to the CPU. The bank label identifies physical storage. The selected mapping decides which hardware block sees it and at which logical address. Two different banks can be placed in consecutive windows of one engine, but the engine’s total region and the bank’s own size still bound what is available.

Separate bank routing from graphics offsets

The VRAM control registers select whether a bank is enabled, the mapping class or destination, and an offset within that destination. In libnds, a mode such as VRAM_A_MAIN_BG_0x06000000 encodes the main-background destination and slot offset. A helper such as vramSetBankA() writes the enable bit together with the selected mode; it does not copy bytes or configure the rest of the display engine.

For example, mapping banks A and B into adjacent main-background slots gives software a larger main 2D graphics region:

videoSetMode(MODE_0_2D);
vramSetBankA(VRAM_A_MAIN_BG_0x06000000);
vramSetBankB(VRAM_B_MAIN_BG_0x06020000);

This only establishes where those banks are visible to the main background engine. The background layer still needs a supported tile format, a tile-data base, a map base, and matching character and screen blocks. In text mode, tile and map offsets are expressed relative to the start of the background graphics region; the background registers do not remap the physical banks. Bitmap backgrounds use a different data layout, so do not apply a text-background offset recipe to a bitmap mode without checking that mode’s addressing rules.

The same distinction applies to 3D. Mapping a bank to a texture slot makes its bytes available to the 3D texture hardware, but it does not create a texture object, upload a palette, or configure the texture-image address that the geometry engine uses. Set the VRAM route and the texture-image/palette parameters as separate steps. A correct VRAMCNT write alone is not evidence that a texture or background is valid.

Respect the per-bank routing matrix

Banks A and B can serve main-engine backgrounds, main sprites, or 3D textures. C and D do not expose those same sprite routes: C can back main or sub backgrounds, 3D textures, or ARM7 work RAM, while D can back main backgrounds, sub-engine sprites, 3D textures, or ARM7 work RAM. Reassigning C or D away from ARM7 therefore changes a cross-CPU memory contract; notify the ARM7-side code before the ARM9 changes that mapping.

Smaller banks often solve palette or capacity problems rather than providing a miniature version of A-D. Bank E can be a main background or sprite bank, a 3D texture-palette bank, or a main extended-background-palette bank. F and G support multiple offset slots and additional main extended palette mappings. H and I target the sub engine: H supports sub backgrounds and their extended palette, while I supports sub backgrounds at its later slot, sub sprites, and sub sprite extended palettes. Consult the enum for the exact legal modes of the bank being configured; never copy a mode constant from another bank because the symbolic values look similar.

Extended palettes matter when a background or sprite mode needs more color choices than its ordinary palette path exposes. They occupy separate bank mappings and require the matching display-engine enable/configuration. Merely loading RGB values into an extended-palette bank will not change a layer that still selects an ordinary palette. Test the full chain: bank-to-palette route, graphics mode, palette-selection bits, and the tile or sprite attributes that use it.

Make remapping an ownership transition

Changing a bank’s route changes which hardware consumer can see that storage. It does not migrate data to a new bank, synchronize two CPUs, or update pointers held by a graphics library. A pointer into a window is meaningful only while the expected bank is mapped there. If the program moves a bank from main BG memory to texture memory, existing background code may still write to the old numerical address; those writes no longer update the bank it used before.

Use one layer of software as the mapping authority. If libnds background or 3D helpers allocate VRAM, coordinate with those helpers instead of independently writing VRAMCNT behind their backs. The single-bank setters replace the mapping value as well as setting the enable bit; avoid an ad hoc read-modify-write that accidentally carries an old destination or offset into the new mode. For grouped changes, vramSetPrimaryBanks() and vramSetBanks_EFG() report previous settings that can be restored with their matching restore functions. Save the return value before switching modes, and restore it only after the old consumer has stopped using the banks.

Perform a mode transition at a known point in the frame, commonly during VBlank, and keep the consuming layer disabled or pointed at valid data until the new mapping and contents are ready. This avoids a frame in which the video engine reads a half-reconfigured resource. For a C/D handoff to ARM7, use IPC to establish who owns the bank and when the receiver may access it; a shared physical bank is not a mutual-exclusion primitive.

Validate the mapping, then the rendered result

Build a small test screen before loading a large asset pack. Map one known bank to a known engine slot, write a checkerboard or distinctive tile pattern through the corresponding address, configure one layer to read it, and confirm the expected pixels. Read back a small test region while the same mapping is active. Then change one variable at a time: the bank, destination slot, tile base, map base, or palette mode. This isolates routing mistakes from image-format and layer-priority mistakes.

When debugging, inspect the register value and the bank enum used to produce it. Compare the selected offset with the actual virtual window and check that the asset fits within the mapped bank or group of banks. A successful CPU write is not proof that the video engine is consuming those bytes. Likewise, an emulator displaying the result is useful evidence but should be compared with the target hardware when timing-sensitive changes or less common bank combinations matter.

If the screen becomes corrupt after a mode change, stop the consumer before trying another mapping. Restore the previous VRAM settings, reinitialize any library-owned background or texture allocations, and verify that ARM7 code is not still using C or D. Keeping a small register snapshot and a bank-to-owner table in the project makes these transitions much easier to audit than scattering numeric control values through rendering code.

The reliable model is simple: physical banks provide the bytes, control registers route them, and graphics configuration tells an engine how to interpret them. Keep those three responsibilities separate, respect the bank-specific routing matrix, and validate both memory visibility and the final rendered output.

Related:

Sources:

Comments