Keyboard shortcuts

Press ← or → to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Memory and Arenas

J3 has no garbage collector and no malloc/free pairs to match up. Values live on the stack, and anything bigger or longer-lived comes from an arena.

Arenas

An Arena (j3.memory) is a bump allocator: it hands out memory by moving a cursor forward, and frees everything at once by moving it back.

use j3.memory.{ Arena, List };

fn main() {
    let mem = Arena.create(16 * 1024 * 1024);   // 16 MB to start
    defer mem.destroy();

    let pos = mem.alloc::<Vec2>(256);           // []Vec2, 256 long
    let vel = mem.alloc::<Vec2>(256);
}

When an arena fills up it adds another chunk. Old pointers stay valid when it grows.

Keep the arena itself in one let and pass &Arena around. Never copy an Arena by value into a struct: store &Arena.

#![allow(unused)]
fn main() {
struct Game {
    mem: &Arena;
    world: World;
}
}

Checkpoint and rewind

checkpoint() remembers where the cursor is. rewind(mark) moves it back, freeing everything allocated since. This is how a game throws away per-frame scratch memory for free:

#![allow(unused)]
fn main() {
fn tick(g: &Game, dt: f32) {
    let frame = g.mem.checkpoint();
    defer g.mem.rewind(frame);

    // anything allocated from g.mem here is gone after this frame
}
}

Memory allocated before the checkpoint is untouched, so long-lived data and per-frame scratch can share one arena as long as the long-lived data comes first.

The scratch arena

memory.scratch(cap) gives a process-wide scratch arena (one per thread), and memory.rewindScratch() resets it. j3.app resets it every frame for you.

Lists

List<T> is a growable array that allocates from an arena:

#![allow(unused)]
fn main() {
let hits = List<int>.new(&mem);
hits.push(3);
hits.push(7);
hits.remove(0);       // keeps order
hits.swapRemove(0);   // O(1): moves the last item into the hole

for i in 0..hits.len {
    printLn(`{hits.items[i]}`);
}
}

items and len are public fields; hits.items[0..hits.len] is the live slice. When the arena rewinds past where the list allocated, the list’s memory goes with it.

Entities are indices

The J3 style for game objects is struct of arrays + integer ids: keep each field in its own slice, and refer to an entity by its index rather than a pointer. Indices survive reallocation and compaction; pointers don’t.

#![allow(unused)]
fn main() {
struct World {
    pos: []Vec2;
    vel: []Vec2;
    count: int;
}

fn spawn(world: &World, p: Vec2, v: Vec2): int {
    let id = world.count;
    world.pos[id] = p;
    world.vel[id] = v;
    world.count += 1;
    return id;
}
}

j3.game.ecs builds on the same idea with generational ids, typed columns, and deferred commands.