Variables & Memory

GBVM variable system, heap layout, stack memory, constants, math utilities, and the interrupt system. The variable system uses C for engine code and the GBVM virtual machine at runtime.

Variable System Overview

All game variables in GB Studio are INT16 (signed 16-bit integers), with a range of -32768 to 32767. They are stored in the GBVM heap and referenced by index (0–767). Variables are the primary mechanism for tracking game state, scores, flags, positions, and any other mutable data.

  • Type: INT16 (signed 16-bit)
  • Range: -32768 to 32767
  • Storage: GBVM heap memory
  • Addressing: by index (0–767)
  • Used for: game state, scores, flags, positions, counters
Note
Even boolean flags consume a full INT16 slot. There is no bit-packing at the variable level — use bitwise operations in RPN expressions if you need to pack multiple flags into a single variable.

GBVM Memory Layout

The GBVM virtual machine manages three key memory regions: the shared heap, per-context stacks, and the instruction budget.

ConstantValueDescription
VM_HEAP_SIZE768Shared variable slots (INT16 each)
VM_MAX_CONTEXTS16Concurrent script threads
VM_CONTEXT_STACK_SIZE64Words per thread stack
INSTRUCTIONS_PER_QUANT0x10 (16)Instructions executed per time slice

Heap Organization

The heap is a flat array of 768 INT16 slots. Global game variables occupy the lower indices; system and temporary storage use higher indices.

Index 0-N:       Global game variables (defined in project)
Index N+1...:    System variables, temporary storage
Total:           768 slots × 2 bytes = 1536 bytes
Capacity Limit
With 768 slots total and system overhead, large projects can approach the variable limit. Monitor your variable count in the GB Studio editor.

Stack Memory

Each of the 16 script contexts has its own 64-word stack, used for local variables, function arguments, and RPN calculations.

  • Stack grows downward (negative offsets from stack pointer)
  • Local variables are allocated via VM_PUSH_CONST or _declareLocal
  • Must be cleaned up with VM_POP before script exit

Function Argument Offsets

ConstantOffsetDescription
FN_ARG0-1First argument (top of caller's stack)
FN_ARG1-2Second argument
FN_ARG2-3Third argument
FN_ARG3-4Fourth argument
FN_ARG4-5Fifth argument
FN_ARG5-6Sixth argument
FN_ARG6-7Seventh argument
FN_ARG7-8Eighth argument

Variable References

In Event Scripts (.gbsres JSON)

Variables are referenced by their ID (UUID or index) in event JSON files.

// Direct variable reference
{"type": "variable", "value": "12"}

// In expressions — $ID$ syntax references variable by index
{"type": "expression", "value": "$12$ + 1"}

// Multiple variables in one expression
{"type": "expression", "value": "$07$ >= 6"}

In GBVM Assembly

GBVM instructions use VAR_* defines to reference variables by name.

VM_SET_CONST  VAR_MY_VARIABLE, 42    ; Set variable to constant
VM_GET_INT8   VAR_RESULT, _my_c_var  ; Read C variable into GBVM variable
VM_SET_INT8   _my_c_var, VAR_SOURCE  ; Write GBVM variable to C variable

In Event Plugin JavaScript

Event plugins use the helpers API to work with variables.

// Reading a variable field from the event input
const varAlias = helpers.getVariableAlias(input.myVariable);

// Setting a variable from a script value
helpers.variableSetToScriptValue(varAlias, input.myValue);

// Using in RPN calculations
helpers._rpn()
  .ref(varAlias)        // Push variable value
  .int16(10)            // Push constant
  .operator(".ADD")     // Add top two values
  .refSet(varAlias)     // Pop and store result
  .stop();

GBVM Variable Commands

Core GBVM instructions for reading, writing, and manipulating variables.

CommandDescription
VM_SET_CONSTSet variable to a constant value
VM_SETCopy one variable to another
VM_GET_INT8Read a byte from C memory into a variable
VM_SET_INT8Write a variable value to a C memory byte
VM_GET_INT16Read a 16-bit value from C memory into a variable
VM_SET_INT16Write a variable value to a C memory 16-bit location
VM_PUSH_CONSTPush a constant onto the stack
VM_PUSH_VALUEPush a variable's value onto the stack
VM_POPPop N values from the stack

Usage Examples

; Set a variable to 100
VM_SET_CONST    VAR_SCORE, 100

; Copy one variable to another
VM_SET          VAR_BACKUP, VAR_SCORE

; Read a C byte variable into GBVM
VM_GET_INT8     .LOCAL_RESULT, _game_over

; Write GBVM variable to C memory
VM_SET_INT8     _elevator_done, VAR_STATUS

; Stack operations for RPN
VM_PUSH_CONST   0           ; Push initial value
VM_PUSH_VALUE   VAR_SCORE   ; Push variable value
VM_POP          2           ; Clean up stack

Constants and Defines

Common GBVM Constants

ConstantValue
.TRUE1
.FALSE0

Direction Constants

ConstantValueDirection
.DIR_DOWN0Down
.DIR_RIGHT1Right
.DIR_UP2Up
.DIR_LEFT3Left

Actor State Constants

ConstantValueDescription
STATE_DEFAULT0Default animation state
STATE_WALK1Walking animation state

Math Utilities

Angle System (math.h)

GB Studio uses a 256-unit angle system where the full circle spans 0–255. Trigonometric functions are provided as BANKED lookup tables returning INT8 values.

  • Full circle: 256 units (0–255)
  • SIN(angle) / COS(angle) — BANKED functions
  • Return type: INT8 (range -128 to 127)

Angle Constants

ConstantValueDegrees
ANGLE_0DEG0
ANGLE_45DEG3245°
ANGLE_90DEG6490°
ANGLE_180DEG128180°

SIN/COS Reference Table

AngleDegreesDirectionSINCOS
0Up0127
3245°Up-Right9090
6490°Right1270
96135°Down-Right90-90
128180°Down0-128
192270°Left-1280
Easing Curves
SIN(0..64) traces a quarter-sine for asymmetric easing (gentle ease-in, strong ease-out). COS(0..128) gives a symmetric S-curve. Both are useful for animation and scroll easing.

Fixed-Point Math

GB Studio uses fixed-point arithmetic to simulate fractional values on integer hardware. The primary format is 12.5 (12 integer bits, 5 fractional bits).

OperationMacro / ShiftDescription
Pixel → SubpixelPX_TO_SUBPX(a) = a << 5Multiply by 32
Subpixel → PixelSUBPX_TO_PX(a) = a >> 5Divide by 32
Tile → SubpixelTILE_TO_SUBPX(a) = a << 8Multiply by 256
Subpixel → TileSUBPX_TO_TILE(a) = a >> 8Divide by 256
Pixel → TilePX_TO_TILE(a) = a >> 3Divide by 8
Tile → PixelTILE_TO_PX(a) = a << 3Multiply by 8
Common Mistake
Always use << 5 / >> 5 for pixel-subpixel conversion, never << 4. The 12.5 format means 32 subpixels per pixel, not 16.

Some plugins use 8.8 fixed-point (8 integer bits, 8 fractional bits) for smoother interpolation, particularly in camera shake and easing calculations.

Random Numbers

UINT8 r = rand();  // 0-255 pseudo-random

// For range [min, max]:
UINT8 val = min + (rand() % (max - min + 1));

Interrupt System

The Game Boy provides several hardware interrupts. GB Studio primarily uses VBlank and LCD (STAT) interrupts.

Available Interrupts

InterruptFrequencyUse
VBlankEvery frame (~60 Hz)OAM DMA, palette writes, scroll registers
LCD (STAT)Per-scanline (configurable)Parallax, split-screen effects
TimerConfigurableNot typically used by GB Studio

VBlank ISR

The VBlank interrupt fires once per frame during the vertical blanking period. This is the only safe time to write to VRAM, OAM, and palette registers.

void VBL_isr(void) NONBANKED {
    if ((WY_REG = win_pos_y) < MENU_CLOSED_Y)
        SHOW_WIN;
    else
        HIDE_WIN;

    if (hide_sprites) HIDE_SPRITES;
    else SHOW_SPRITES;

    scroll_shadow_update();
}
Critical: hide_sprites Variable
VBL_isr checks the hide_sprites variable every VBlank and overrides the LCDC register accordingly. Calling the HIDE_SPRITES macro alone (which writes LCDC directly) will be immediately overridden on the next VBlank. You must set hide_sprites = TRUE to persist sprite hiding across frames.

LCD ISR

LCD interrupts are triggered at configurable scanlines via the LYC (Line Y Compare) register. GB Studio provides three built-in LCD ISRs:

ISR FunctionEnum ValueUse
simple_LCD_isrLCD_simpleBasic scanline interrupt
parallax_LCD_isrLCD_parallaxMulti-layer parallax scrolling
fullscreen_LCD_isrLCD_fullscreenFull-screen effects

ISR Management

// Remove all standard LCD ISRs
remove_LCD_ISRs();

// Install a custom ISR
CRITICAL {
    add_LCD(my_custom_LCD_isr);
}

// Remove a specific ISR
remove_LCD(my_custom_LCD_isr);
ISR Installation Timing
The scene loading process (core.c) installs a standard LCD ISR based on scene_LCD_type after load_scene(). If your plugin uses a custom ISR, call remove_LCD_ISRs() followed by add_LCD() in state_init() to replace it. Also clear parallax_rows and set scene_LCD_type = LCD_simple to prevent conflicts.

The LYC sync value used by GB Studio is 150, which falls within the VBlank period and is safe for register changes.

Common Patterns

Using Variables as Flags

; Set a flag
VM_SET_CONST  VAR_MY_FLAG, 1

; Check flag (branch if not equal to 0)
VM_IF_CONST   .NE, VAR_MY_FLAG, 0, label_true, 0

Reading Engine Fields into Variables

Engine fields are global C variables exposed to the GBVM script system. They can be read into game variables or written from script values.

// From event JSON:
// EVENT_ENGINE_FIELD_STORE — reads a C variable into a game variable
// EVENT_ENGINE_FIELD_SET — writes a value to a C variable

Bridging C and GBVM Variables

// In C code: read a GBVM variable
INT16 val = *(script_memory + VAR_MY_VARIABLE);

// In GBVM: read a C variable
VM_GET_INT8   VAR_RESULT, _my_c_byte_var
VM_GET_INT16  VAR_RESULT, _my_c_word_var

// In GBVM: write to a C variable
VM_SET_INT8   _my_c_byte_var, VAR_SOURCE
VM_SET_INT16  _my_c_word_var, VAR_SOURCE

RPN Stack Calculations

; Calculate: result = (score * 2) + bonus
VM_RPN
  .R_REF  VAR_SCORE
  .R_INT16 2
  .R_OPERATOR .MUL
  .R_REF  VAR_BONUS
  .R_OPERATOR .ADD
  .R_REF_SET VAR_RESULT
  .R_STOP

WRAM Budget & Stack Overflow

GB Studio allocates a large script_memory[] array in WRAM that holds both the variable heap and all script context stacks. The stock default sizes are generous — often too generous for projects with multiple plugins, leaving dangerously little space for the hardware stack.

WRAM Layout

WRAM (0xC000–0xDFFF = 8192 bytes)
┌─────────────────────────────────────────┐ 0xC000
│ shadow_OAM                    (160B)    │
├─────────────────────────────────────────┤ 0xC0A0
│ _DATA segment                           │
│   script_memory[]                       │
│     heap: VM_HEAP_SIZE × 2 bytes        │
│     stacks: VM_MAX_CONTEXTS ×           │
│             VM_CONTEXT_STACK_SIZE × 2   │
│   actor structs (30 × ~39B)             │
│   triggers, UI, sprites, engine globals │
│   plugin static variables               │
├─────────────────────────────────────────┤
│ *** HARDWARE STACK (what's left) ***    │ ← grows downward
├─────────────────────────────────────────┤ 0xDF00
│ shadow_OAM2              (160B, fixed)  │
│ _BkgPalette, _vwf_tile_data  (fixed)   │
└─────────────────────────────────────────┘ 0xDFFF

Stock script_memory Size

With stock defaults:

script_memory = (VM_HEAP_SIZE + VM_MAX_CONTEXTS × VM_CONTEXT_STACK_SIZE) × 2 bytes
             = (768 + 16 × 64) × 2
             = (768 + 1024) × 2
             = 3584 bytes

Add actor structs (~1176B), engine globals (~1800B), and plugin static variables, and the _DATA segment can easily reach 7800+ bytes — leaving under 200 bytes for the hardware stack.

Stack Overflow Symptoms

Deceptive Failures
Stack overflow symptoms look like logic bugs, not memory issues:
  • Dialog boxes fail to appear or display corruption
  • Kernel panic (game freezes) on scene transitions
  • Random crashes that disappear when a plugin is removed
  • Works in simple scenes, breaks in complex ones (deeper call nesting)
  • A plugin with correct code breaks unrelated features

The hardware stack (SP register) grows downward from 0xDF00. When it collides with the top of _DATA, it overwrites engine variables, actor data, or script memory — causing symptoms anywhere in the game, not just in the code that triggered the overflow.

Diagnosing Stack Overflow

Step 1: Check the linker map file. After a build, open the .map file and find the end of the _DATA segment. Subtract from 0xDF00 to get available stack:

Available stack = 0xDF00 - end_of_DATA

Example from .map file:
  _DATA = 0xC0A0, size = 0x1D96
  End of _DATA = 0xC0A0 + 0x1D96 = 0xDE36
  Available stack = 0xDF00 - 0xDE36 = 202 bytes  ← dangerously low

Step 2: Estimate stack requirements. Each nested banked function call uses ~10–12 bytes of stack. Dialog rendering involves 4–5 levels of nesting (GBVM → vm_overlay_show → ui_draw_frame → tile functions). Budget at least 200–300 bytes for the deepest call chain.

Optimizing WRAM

Create a config plugin that overrides vm.h and/or data_manager.h to reduce oversized defaults:

// engine/include/vm.h override
#ifndef VM_H_INCLUDE
#define VM_H_INCLUDE

// ... stock vm.h content ...

// Override stock defaults (measure your actual usage first!)
#undef VM_HEAP_SIZE
#define VM_HEAP_SIZE            256    // stock: 768. Check your max variable index.

#undef VM_CONTEXT_STACK_SIZE
#define VM_CONTEXT_STACK_SIZE   32     // stock: 64. Check deepest RPN stack usage.

#endif
// engine/include/data_manager.h override
// ... stock data_manager.h content ...

#undef SCENE_STACK_SIZE
#define SCENE_STACK_SIZE        4      // stock: 8. Check deepest push-scene nesting.

Savings Calculator

SettingStockReducedSavings (bytes)How to Measure
VM_HEAP_SIZE7682561024Check highest variable index in project
VM_CONTEXT_STACK_SIZE64321024Check deepest RPN operand stack in any event
SCENE_STACK_SIZE8432Count max nested push-scene transitions
VM_MAX_CONTEXTS16(keep)0Count peak concurrent scripts (hard to reduce)
Audit Before Reducing
Before reducing these values, audit your project:
  • VM_HEAP_SIZE: Must be ≥ your highest variable index + 1
  • VM_CONTEXT_STACK_SIZE: Must be ≥ deepest RPN operand stack usage across all event scripts
  • SCENE_STACK_SIZE: Must be ≥ maximum nesting depth of push-scene transitions
  • Reducing too aggressively causes silent corruption that's even harder to debug than stack overflow

Plugin WRAM Awareness

Every static variable in a plugin's C code consumes WRAM. When designing plugins, be aware of your WRAM footprint:

// Each of these consumes WRAM permanently (all scenes, not just yours)
static UBYTE my_state;           // 1 byte
static UINT16 my_buffer[32];     // 64 bytes — significant!
static actor_t *my_actors[4];    // 8 bytes (4 × 2B pointers)
Static Variables Are Global
Plugin static variables are allocated at link time and consume WRAM in ALL scenes, even scenes that don't use your plugin's scene type. Minimize static allocations — use local variables where possible, and consider whether large buffers can be reduced or shared.