running

Resource Lifetime

GPU buffers, textures, and other engine objects are handles owned by scope — they die deterministically, and the runtime warns about leaks.

A GPUBuffer or GPUTexture wrapper holds a u32 handle; the real resource lives host-side behind a reference count. There are no finalizers anywhere — the collector frees a dead wrapper’s few bytes of wasm heap and runs nothing of yours. Lifetime instead follows scope:

The reap piggybacks on the frame collector (--runtime frame, what Rive projects bake with). Every wrapper records the handle it mints against its own pointer in an owner table; an explicit release() removes the entry. At the frame boundary the host runs a collection, normally a minor one over the nursery, a major when the old region outgrows its budget: wrappers that are no longer reachable die. The stdlib then walks the owner table and asks __resolveForward about each owner; survivors answer with their address, old or moved, and a dead owner answers zero — its handle is released right there. Deterministic, same frame, every time. The stub runtime tracks no ownership, so under it a forgotten per-frame resource simply leaks.

particles.as
export class Particles extends Layout {
    tex: GPUTexture;                       // persistent: created in init

    override init(context: Context): bool {
        this.tex = new GPUTexture(desc);   // yours until release()/dispose()
        return true;
    }

    override advance(seconds: f64): bool {
        let staging = new GPUBuffer(BufferUsage.vertex, size, data);
        // ... upload, draw ...
        staging.release();                 // best: prompt release
        // forgetting it is safe: the boundary collection frees it this frame
        return true;
    }

    dispose(): void {
        this.tex.release();                // instance teardown hook
    }
}

Watch a frame play out

Step through the machinery: wasm linear memory on top, the owner table the wrappers write to, and the host’s handle table. The interesting frame is the one where a buffer’s release is forgotten.

wasm linear memory — old region | nursery
owner table
host handle table → gpu resources

Instance teardown

When the host drops a script instance — its artboard is deleted, or the editor reloads the module — the dispose() hook runs once, with the Context still valid. Release persistent resources there. Anything you miss is swept when the file itself goes away, which for a long-lived file is far too late for GPU memory.

The leak warnings

Two watchdogs run in development hosts. Each re-arms after it fires, every 8 MB of growth or 512 handles, so a leaking session keeps hearing about it:

The full design — why scope-proof beats finalizers, and what the Luau lane does instead — lives with the host-binding notes in the repo.