Skip to main content

Overview

Una Aventura Inesperada uses the Nintendo DS dual-screen architecture with two distinct rendering modes: framebuffer mode (MODE_FB0) for the main screen displaying menus/HUD, and tiled background mode (MODE_0_2D) for the sub-screen displaying gameplay.

Display Modes

The game switches between display modes depending on the current screen:

Framebuffer Mode (Main Screen)

Used for menus, HUD, and full-screen images:
REG_DISPCNT = MODE_FB0;
MODE_FB0
Display Mode
16-bit framebuffer mode that provides direct pixel access. The main screen uses VRAM_A as the framebuffer.Resolution: 256x192 pixels (49,152 pixels total)Color depth: 16-bit (RGB555 format via RGB15 macro)
Framebuffer rendering example from GenerarNivel() (main.c:801):
// Load HUD image via DMA
dmaCopy(imagen, VRAM_A, 256*192*2);
REG_DISPCNT = MODE_FB0;

// Direct pixel manipulation for stamina bar
for(int lin = COMIENZO_LINEA_BARRA_ESTAMINA; lin < ALTO_BARRA_ESTAMINA; lin++) {
    for(int col = COMIENZO_COLUMNA_BARRA_ESTAMINA; col < ANCHO_BARRA_ESTAMINA; col++) {
        fb[lin*256 + col] = RGB15(0, 30, 0); // Bright green
    }
}
The framebuffer pointer fb is initialized in main() (main.c:169): fb = VRAM_A;Each pixel is addressed as fb[y*256 + x] where y ∈ [0,192) and x ∈ [0,256).

Tiled Background Mode (Sub Screen)

Used for gameplay with tile-based graphics:
REG_DISPCNT_SUB = MODE_0_2D | DISPLAY_BG0_ACTIVE;
MODE_0_2D
Display Mode
Tiled background mode supporting up to 4 background layers. The game uses only BG0.
DISPLAY_BG0_ACTIVE
Layer Flag
Enables background layer 0 for rendering. Other layers (BG1-BG3) remain disabled.
Background layer configuration from CrearMenu() (main.c:884):
BGCTRL_SUB[0] = BG_32x32 | BG_COLOR_256 | BG_MAP_BASE(0) | BG_TILE_BASE(1);
BG_32x32
Map Size
32x32 tile map (1024 tiles total, each 8x8 pixels)
BG_COLOR_256
Color Mode
256-color palette mode (each tile references palette indices)
BG_MAP_BASE(0)
Map Memory
Map data starts at base 0 of BG map RAM
BG_TILE_BASE(1)
Tile Memory
Tile graphics start at base 1 of BG tile RAM

Dual Screen Setup

The Nintendo DS has two screens, configured independently:

Main Screen (Upper Display)

Purpose: Displays menus, HUD, dialog images, and cinematics VRAM Banks: VRAM_A (primary) and VRAM_B (unused but enabled)
// Main screen VRAM configuration (main.c:167-169)
VRAM_A_CR = VRAM_ENABLE | VRAM_A_LCD;
VRAM_B_CR = VRAM_ENABLE | VRAM_B_LCD;
fb = VRAM_A;
VRAM_A_CR
VRAM Control
Configures VRAM bank A for LCD display output.Flags:
  • VRAM_ENABLE - Enable the bank
  • VRAM_A_LCD - Map to main screen LCD

Sub Screen (Lower Display)

Purpose: Displays tile-based gameplay graphics VRAM Bank: VRAM_C (mapped to sub-screen backgrounds)
// Sub screen VRAM configuration (main.c:163-164)
REG_DISPCNT_SUB = MODE_0_2D | DISPLAY_BG0_ACTIVE;
VRAM_C_CR = VRAM_ENABLE | VRAM_C_SUB_BG;
VRAM_C_CR
VRAM Control
Configures VRAM bank C for sub-screen background tiles.Flags:
  • VRAM_ENABLE - Enable the bank
  • VRAM_C_SUB_BG - Map to sub-screen background memory
VRAM banks must be properly configured before use. Accessing unmapped VRAM will read as zero or cause graphical glitches.

VRAM Configuration

The game uses three VRAM banks:

Memory Layout

BankSizePurposeMapping
VRAM_A128KBMain screen framebufferVRAM_A_LCD
VRAM_B128KBUnused (enabled but not accessed)VRAM_B_LCD
VRAM_C128KBSub screen tiles/mapsVRAM_C_SUB_BG

Tile and Map Memory

Configured in main() (main.c:171-172):
tileMemory = (u8*) BG_TILE_RAM_SUB(1);
mapMemory = (u16*) BG_MAP_RAM_SUB(0);
BG_TILE_RAM_SUB(1)
Tile Memory Pointer
Points to tile base 1 in sub-screen VRAM. Each tile is 64 bytes (8x8 pixels, 8 bits per pixel).Address calculation: Base address + (tile_index × 64)
BG_MAP_RAM_SUB(0)
Map Memory Pointer
Points to map base 0 in sub-screen VRAM. Each entry is a 16-bit tile index.Map size: 32×24 tiles (768 entries, though 32×32 is allocated)

Framebuffer Drawing

Direct pixel manipulation is used for the stamina bar (main.c:809-813):
#define ANCHO_BARRA_ESTAMINA 32
#define ALTO_BARRA_ESTAMINA 155
#define COMIENZO_LINEA_BARRA_ESTAMINA 38
#define COMIENZO_COLUMNA_BARRA_ESTAMINA 20

// Draw full stamina bar
for(int lin = COMIENZO_LINEA_BARRA_ESTAMINA; lin < ALTO_BARRA_ESTAMINA; lin++) {
    for(int col = COMIENZO_COLUMNA_BARRA_ESTAMINA; col < ANCHO_BARRA_ESTAMINA; col++) {
        fb[lin*256 + col] = RGB15(0, 30, 0);
    }
}

Stamina Bar Updates

The ActualizarBarraMovimientos() function (main.c:853) erases segments:
void ActualizarBarraMovimientos() {
    int pixelesSegmento = ALTO_BARRA_ESTAMINA / maximoMovimientosJugador;
    
    // Calculate how many pixels to erase
    int pixelesABorrar = pixelesSegmento * (maximoMovimientosJugador - movimientosJugador) + 1;
    int altoActual = ALTO_BARRA_ESTAMINA - pixelesABorrar;
    
    // Erase top portion of bar
    for(int lin = COMIENZO_LINEA_BARRA_ESTAMINA; lin < altoActual; lin++) {
        for(int col = COMIENZO_COLUMNA_BARRA_ESTAMINA; col < ANCHO_BARRA_ESTAMINA; col++) {
            fb[lin*256 + col] = RGB15(0, 0, 0); // Black
        }
    }
}

RGB15 Macro

Colors use the RGB555 format (5 bits per channel):
RGB15(r, g, b)  // r, g, b ∈ [0, 31]
Examples from the palette initialization (main.c:175-201):
BG_PALETTE_SUB[0] = RGB15(0, 0, 0);      // Black
BG_PALETTE_SUB[1] = RGB15(15, 4, 6);     // Dark red
BG_PALETTE_SUB[3] = RGB15(9, 20, 9);     // Green (grass)
BG_PALETTE_SUB[26] = RGB15(31, 31, 31);  // White
RGB555 precision: Each channel has 32 levels (0-31). To convert 8-bit RGB:RGB15_value = RGB8_value >> 3For example, RGB(128, 255, 64) becomes RGB15(16, 31, 8).

Background Layers

The game uses only BG0 on the sub-screen:
REG_DISPCNT_SUB = MODE_0_2D | DISPLAY_BG0_ACTIVE;
BG0_ACTIVE
Layer Enable
Enables background layer 0. Layers 1-3 remain disabled to save processing power.
The background is configured as a 32×32 tile map (main.c:884), but only a 32×24 region is visible (matching the 256×192 screen resolution).

Map Memory Layout

Tiles are stored in row-major order:
pos_mapMemory = fila * 32 + columna;  // Tile at (columna, fila)
mapMemory[pos_mapMemory] = tile_index;
Example: Player at position (16, 22) occupies tiles:
  • (16, 22): mapMemory[22*32 + 16]
  • (17, 22): mapMemory[22*32 + 17]
  • (16, 23): mapMemory[23*32 + 16]
  • (17, 23): mapMemory[23*32 + 17]

DMA Transfers

The game uses Direct Memory Access (DMA) for high-speed memory copies:

Image Loading

dmaCopy(source, destination, size);
Examples:
// Load 256×192 image to main screen (main.c:801)
dmaCopy(HUDBitmap, VRAM_A, 256*192*2);

// Load menu image (main.c:917)
dmaCopy(menuCreditosBitmap, VRAM_A, 256*192*2);

// Load dialog image (main.c:943)
dmaCopy(Pregunta1_1Bitmap, VRAM_A, 256*192*2);
source
unsigned int*
Pointer to source data (typically a bitmap array from an include file)
destination
void*
VRAM destination (VRAM_A for main screen)
size
size_t
Number of bytes to copy. For 256×192 16-bit images: 256 × 192 × 2 = 98,304 bytes

Tile Loading

The InicializarTeselas() function (main.c:1065) loads all tile graphics:
// Player frame 1, part 1 (tile index 0)
dmaCopy(t_jugadorF1Parte1, tileMemory, sizeof(t_jugadorF1Parte1));

// Player frame 1, part 2 (tile index 8)
dmaCopy(t_jugadorF1Parte2, tileMemory + (64*8), sizeof(t_jugadorF1Parte2));

// Wall left part 1 (tile index 7)
dmaCopy(t_muroIzqParte1, tileMemory + (64*7), sizeof(t_muroIzqParte1));

// Enemy frame 1, part 1 (tile index 5)
dmaCopy(t_enemigoF1Parte1, tileMemory + (64*5), sizeof(t_enemigoF1Parte1));
Tile memory offset calculation:Each 8×8 tile occupies 64 bytes (8 pixels × 8 pixels × 1 byte per pixel).To load tile N: tileMemory + (64 * N)

DMA Performance

DMA is significantly faster than CPU memcpy:
OperationCPU CopyDMA Copy
98KB image~50,000 cycles~1,000 cycles
64-byte tile~320 cycles~10 cycles
DMA limitations:
  • Source and destination must be 16-bit aligned
  • Size must be a multiple of 2 (for 16-bit transfers)
  • DMA blocks CPU execution until complete

Palette System

The sub-screen uses a 256-color palette initialized in main() (main.c:175-201):
BG_PALETTE_SUB[0] = RGB15(0, 0, 0);      // Black (void tiles)
BG_PALETTE_SUB[1] = RGB15(15, 4, 6);     // Dark red
BG_PALETTE_SUB[2] = RGB15(18, 8, 5);     // Brown
BG_PALETTE_SUB[3] = RGB15(9, 20, 9);     // Green (grass)
BG_PALETTE_SUB[4] = RGB15(31, 25, 20);   // Beige (floor)
BG_PALETTE_SUB[5] = RGB15(28, 23, 18);   // Tan
BG_PALETTE_SUB[6] = RGB15(12, 6, 11);    // Purple
BG_PALETTE_SUB[7] = RGB15(20, 5, 10);    // Maroon
BG_PALETTE_SUB[8] = RGB15(6, 8, 10);     // Dark blue-gray
BG_PALETTE_SUB[9] = RGB15(31, 25, 8);    // Gold
BG_PALETTE_SUB[10] = RGB15(11, 17, 21);  // Blue-gray
// ... 26 colors total

Palette Indices in Tile Data

Each pixel in a tile stores an 8-bit palette index (0-255). The tile data format:
Tile (8×8 pixels, 64 bytes):
[row0_col0][row0_col1]...[row0_col7]  // 8 bytes
[row1_col0][row1_col1]...[row1_col7]  // 8 bytes
...
[row7_col0][row7_col1]...[row7_col7]  // 8 bytes
```c

Each byte is a palette index that maps to `BG_PALETTE_SUB[index]`.

<Note>
The game uses only **26 of 256 available palette slots** to minimize data size and simplify color management.
</Note>

## Rendering Pipeline

### Frame-by-Frame Process

1. **VBlank wait** - `swiWaitForVBlank()` blocks until vertical blank period
2. **Input processing** - `TeclasJugador()` interrupt updates game state
3. **Map updates** - Tile indices in `mapMemory` are modified
4. **Animation** - `ActualizarAnimacion()` timer updates sprite tiles
5. **Hardware rendering** - DS graphics hardware reads VRAM and displays

### Tile Map Updates

Example from player movement (main.c:296-314):

```c
// Restore floor tiles at old position
mapMemory[(posJugFila)*32 + posJugColumna] = 
    ComprobarSuelo((posJugFila)*32 + posJugColumna, mapaAcutal);
mapMemory[(posJugFila)*32 + (posJugColumna+1)] = 
    ComprobarSuelo((posJugFila)*32 + (posJugColumna+1), mapaAcutal);
mapMemory[(posJugFila+1)*32 + posJugColumna] = 
    ComprobarSuelo((posJugFila+1)*32 + posJugColumna, mapaAcutal);
mapMemory[(posJugFila+1)*32 + (posJugColumna+1)] = 
    ComprobarSuelo((posJugFila+1)*32 + (posJugColumna+1), mapaAcutal);

posJugFila -= 2; // Move 2 tiles up

// Draw player sprite at new position
pos_mapMemory = posJugFila*32 + posJugColumna;
mapMemory[pos_mapMemory] = ElegirFondoJugador(0, pos_mapMemory);

pos_mapMemory = posJugFila*32 + (posJugColumna+1);
mapMemory[pos_mapMemory] = ElegirFondoJugador(1, pos_mapMemory);

pos_mapMemory = (posJugFila+1)*32 + posJugColumna;
mapMemory[pos_mapMemory] = ElegirFondoJugador(2, pos_mapMemory);

pos_mapMemory = (posJugFila+1)*32 + (posJugColumna+1);
mapMemory[pos_mapMemory] = ElegirFondoJugador(3, pos_mapMemory);
No double buffering: The game writes directly to active VRAM. Updates must complete within VBlank (~1.6ms) to avoid tearing. Fortunately, tile map updates are fast (4-8 writes per movement).

Performance Optimization

The rendering system is optimized for the DS hardware:
  • DMA for large transfers - 50× faster than CPU memcpy
  • Minimal per-frame updates - Only changed tiles are rewritten
  • Single background layer - Reduces hardware workload
  • Paused animations during dialogs - Saves CPU cycles
  • VBlank synchronization - Prevents tearing without double buffering
Rendering budget per frame (60 FPS):
  • VBlank period: approximately 1.6ms (for VRAM writes)
  • Active display: approximately 15ms (for game logic)
The game easily stays within budget with typical updates:
  • Player movement: approximately 8 tile writes (less than 100μs)
  • Animation update: approximately 12 tile writes (less than 150μs)
  • Stamina bar: approximately 144 pixel writes (less than 500μs)

Build docs developers (and LLMs) love