Skip to main content

Documentation Index

Fetch the complete documentation index at: https://mintlify.com/pocket-stack/pocketjs/llms.txt

Use this file to discover all available pages before exploring further.

The AOT runtime is a fixed native C program that consumes PJGB cartridge data. It targets GBA hardware directly: bare-metal startup, hardware registers, no operating system, and no dynamic allocation during gameplay. All state lives in global structs in EWRAM and IWRAM. The runtime does not run Solid, Vue Vapor, QuickJS, or any part of the PocketJS framework — that boundary is intentional and keeps AOT independent of the PSP UI product line.

Responsibilities

  • Initialize Mode 0 backgrounds, OBJ sprites, palettes, and VRAM tile transfers.
  • Step the active map, actor state, flags, and script VM each frame.
  • Render textbox windows using the baked glyph tile data from the cartridge.
  • Decode player input into grid movement, collision checks, and NPC interaction.
  • Run the suspendable script VM to completion each frame, or resume it when a suspension condition clears.
  • Expose a fixed EWRAM debug block so the mGBA test harness can read game state without symbols.

Memory map

The runtime uses GBA hardware resources as follows:
RegionUsage
Charblock 0 (BG VRAM 0x06000000)4bpp BG tiles: blank + tileset + font glyphs + textbox fill tile. Up to 512 tiles (v1 limit).
Screenblock 8 (BG VRAM + 0x10000)BG0 map screenblock: u16[32×32] tile indices for the current map.
Screenblock 9BG1 screenblock for the textbox / choice menu overlay.
OBJ VRAM (0x06010000)4bpp OBJ tiles for sprites. Up to 1024 tiles. Loaded once from TILES_OBJ.
BG palette (0x05000000)Bank 0 = tileset (up to 16 colors); bank 15 = textbox font palette.
OBJ palette (0x05000200)One bank of 16 colors per sprite (up to 16 banks).
OAM (0x07000000)Sprite hardware shadow, committed at VBlank via DMA.
EWRAM (0x02000000)Game struct (global state) + debug block at 0x02000000.
IWRAMStack and small hot data.

Source modules

crt0.s — startup

ARM assembly startup: sets the stack pointer, zeroes .bss, copies .data from ROM to EWRAM, and calls main. The GBA requires an AGB-format entry point at address 0 in ROM.

gba.ld — linker script

Places the GBA ROM header at offset 0, the .text section in ROM (0x08000000), .data/.bss in EWRAM (0x02000000), and the stack in IWRAM.

cart.c — cartridge access

Reads the PJGB container blob linked into the ROM as pjgb_cart[]. The single exported function cart_chunk(kind, id, &size) scans the chunk directory and returns a pointer to the requested chunk’s payload.
void cart_load(const void *blob);
const u8 *cart_chunk(u32 kind, u32 id, u32 *out_size);

video.c — display initialization

Sets DISPCNT for Mode 0 (tiled BG + OBJ), configures BG0 (map layer, charblock 0, screenblock 8) and BG1 (textbox overlay, charblock 0, screenblock 9), and transfers OBJ tiles and both palette banks from the cartridge into VRAM once at startup.
void video_init(void);
void video_load_palettes(void);
void video_load_obj_tiles(void);

bg.c — background map rendering

Loads the current map’s tile data into charblock 0 (BG palette bank 0 tiles) and writes the u16[w×h] screen entries into screenblock 8. When the camera scrolls, bg_set_scroll writes the pixel offsets to REG_BG0HOFS and REG_BG0VOFS.
void bg_load_map(void);
void bg_set_scroll(void);

obj.c — sprite OAM management

Maintains a shadow OAM array. Each frame, obj_reset hides all shadow OAM entries, then obj_draw_scene places the player and any visible actors using obj_put. At VBlank, obj_commit DMA-transfers the shadow OAM to hardware OAM.
void obj_reset(void);
void obj_commit(void);
void obj_draw_scene(void);
void obj_put(int slot, int sx, int sy, int tile, int palbank);

input.c — button polling

Reads REG_KEYINPUT each frame into g.keys and tracks the previous frame’s state in g.keys_prev. key_pressed returns true only for keys newly held this frame.
void input_poll(void);
int key_held(int mask);
int key_pressed(int mask);

map.c — map loading and collision

map_enter switches the active map: reads the MAP chunk for the given map index, sets up the tile/collision/actor/warp pointers, loads BG tiles and the screenblock, and places the player at the given entrance. map_solid checks tile-level collision and actor solidity. map_actor_at returns the actor slot at a tile position for interaction.
void map_enter(int map_id, int tx, int ty, int dir);
int map_solid(int tx, int ty);
int map_actor_at(int tx, int ty);

player.c — movement and interaction

Each frame (when the script VM is not active), player_update reads directional input, moves the player one tile at a time with grid-aligned animation (pixel-smooth walking across the 8-pixel tile), checks collision via map_solid, and on A press calls vm_start for the actor at the adjacent tile’s on_talk script.

actor.c — NPC animation

actors_update advances the walk animation timers and frame indices for all actors on the current map, and handles movement patterns (wander, patrol).

camera.c — scroll clamping

camera_follow centers the camera on the player and clamps to the map bounds so the viewport never shows outside the map area.

script_vm.c — bytecode interpreter

The script VM is a small stack machine with a 16-entry s16 stack. It runs until it hits an END opcode or a suspension:
  • VM_SUSP_TEXT — waiting for the player to dismiss a text box (A press).
  • VM_SUSP_CHOICE — waiting for the player to make a menu selection.
  • VM_SUSP_WAIT — waiting for N frames to elapse.
On each frame, vm_tick first checks whether the current suspension has cleared. If so, it resumes execution and runs opcodes until the next suspension or END.
void vm_start(int script_id, int actor_slot);
void vm_tick(void);
int vm_active(void);
The complete opcode set mirrors the compiler’s OP constants (defined in pjgb_gen.h):
OpcodeOperandsEffect
ENDTerminate; clear vm.active.
NOPNo operation.
TEXTu16 textIdShow text box; suspend VM_SUSP_TEXT.
SET_FLAGu16 flagIdflags[flagId] = 1.
CLEAR_FLAGu16 flagIdflags[flagId] = 0.
PUSH_FLAGu16 flagIdPush flags[flagId] (0 or 1).
PUSH_CONSTi16 valuePush a compile-time constant.
POPDiscard top of stack.
DUPDuplicate top of stack.
EQ / NE / NOTStack comparisons / negation.
JUMPi16 relUnconditional relative branch.
JUMP_IF_FALSEi16 relPop; branch if zero.
CHOICEu8 n, u16 t0..t(n-1)Show choice menu (up to 7 options); suspend VM_SUSP_CHOICE; push chosen index.
LOCK_PLAYERplayer.locked = 1.
RELEASE_PLAYERplayer.locked = 0.
FACE_PLAYERu8 slotTurn actor slot to face the player. 0xFF = the actor that started the script.
WARPu8 map, u16 x, u16 y, u8 dirCall map_enter; teleport player.
SET_VARu16 varId, i16 valvars[varId] = val.
ADD_VARu16 varId, i16 deltavars[varId] += delta.
PUSH_VARu16 varIdPush vars[varId].
GIVE_ITEMu16 itemId, u8 countv1 stub; increments an inventory variable.
BATTLEu16 battleIdv1 stub; push 1 (win).
WAITu16 framesSuspend VM_SUSP_WAIT for N frames.
PLAY_SFXu16 sfxIdv1 stub; no-op.

textbox.c — dialogue rendering

The textbox uses BG1 at higher priority than BG0. Text characters are rendered as glyph tiles from the font region of charblock 0 using the textbox palette (bank 15). The choice menu is rendered in the same BG1 area with an arrow cursor.
void textbox_init(void);
void textbox_show(int text_id);
void textbox_hide(void);
int textbox_active(void);
void textbox_tick(void);
void choice_show(int n, const u16 *text_ids);
int choice_active(void);
void choice_tick(void);
int choice_result(void);
const char *text_get(int text_id);

debug.c — EWRAM debug block

The debug block is written to the top of EWRAM at 0x02000000 every frame. Its layout is defined by DBG.* constants in pjgb_gen.h and mirrors the spec/pjgb.ts DBG object exactly. The mGBA test harness reads this block by absolute bus address — no symbols required.
void debug_init(void);   // write magic 'PJDB', zero fields
void debug_update(void); // mirror player pos, map id, text/script active, flags, vars, frame counter

The frame loop

main.c implements the game loop:
int main(void) {
  cart_load(pjgb_cart);
  g.game = (const GameHeader *)cart_chunk(CHUNK_GAME, 0, 0);

  video_init();
  textbox_init();
  debug_init();

  map_enter(g.game->start_map, g.game->start_x, g.game->start_y, g.game->start_dir);

  for (;;) {
    input_poll();
    vm_tick();
    if (!vm_active()) player_update();
    textbox_tick();
    choice_tick();
    actors_update();
    camera_follow();
    bg_set_scroll();
    obj_reset();
    obj_draw_scene();
    debug_update();
    vblank_wait();
    obj_commit();
    g.frame++;
  }
}
The loop runs at 60 fps locked to VBlank. The script VM runs first each frame; if a script is active, player_update is skipped so the player cannot move while a dialogue or choice is in progress.

Flags and variables

Flags and variables are stored in g.flags[16] (128 packed bits) and g.vars[16] (16 signed 16-bit integers) in the Game struct. They are persistent for the lifetime of a play session. The inline helpers flag_get and flag_set in runtime.h handle the bit packing:
static inline int  flag_get(int id)        { return (g.flags[id>>3] >> (id&7)) & 1; }
static inline void flag_set(int id, int v) {
  if (v) g.flags[id>>3] |=  (1 << (id&7));
  else   g.flags[id>>3] &= ~(1 << (id&7));
}

Build

To build the ROM without the compiler (linking against an existing gen_cart.c):
bash aot/runtime/build.sh
The script invokes arm-none-eabi-gcc with -mcpu=arm7tdmi -marm -ffreestanding -nostdlib -O2 and the gba.ld linker script, compiling all runtime C files and the startup assembly in one pass, then objcopy to strip ELF metadata. Normally you do not need to invoke build.sh directly — bun run build in the aot/ directory runs the full pipeline including the compiler, which writes gen_cart.c and invokes arm-none-eabi-gcc via compiler/rom.ts.

The test harness (test/harness/mgba_runner.c)

The headless E2E test harness is a C program that links against libmgba. It loads a .gba ROM, executes a JSON scenario (sequences of advance, press, read, and screenshot steps), and prints a JSON result with named memory reads.
{
  "steps": [
    { "op": "advance", "frames": 30 },
    { "op": "press",   "buttons": ["UP"], "frames": 16, "release": 4 },
    { "op": "read",    "name": "player_y", "addr": 33554438, "size": 2 },
    { "op": "screenshot", "path": "/tmp/out.ppm" }
  ]
}
The addr field is the absolute GBA bus address of a debug block field — for example, DEBUG_ADDR + DBG.PLAYER_Y = 0x02000006. The .debug.json emitted by the compiler contains the resolved addresses for every flag, map id, text id, and script id, so the test scenarios can be written symbolically and translated to addresses by aot/test/e2e.ts. Build the harness once:
bash aot/test/harness/build.sh
The runtime is structured for real GBA hardware but currently requires a Nintendo logo header and logo bitmap (design §26.6) before it will boot on an unmodified cartridge slot. It boots correctly in mGBA and passes all 19 E2E assertions in the headless harness.

Build docs developers (and LLMs) love