Runtime and program lifecycle
The TinyFX runtime owns frame scheduling, input, environment updates, GPU submission, resource lifetime, and the platform boundary. The same public program model is used by the native Rust VM, browser-WASM VM, and generated TypeScript/WebGPU execution paths.
Program lifecycle
An entry program may have optional setup() and loop() functions:
let angle = 0.0;
fn setup() {
env.configureCamera(distance: 500);
}
fn loop() {
angle += env.deltaTime;
draw(shader { frag { out.color = (in.uv, 0.5, 1.0); } });
}setup() runs once before the first frame. loop() runs once per frame after setup completes. Either may be omitted: a static program can draw in setup or initialization and has no need for a loop. Both are CPU entry points; neither may be declared gpu.
The runtime retains an internal render target and then presents it. This is why setup-only programs can preserve a static image instead of clearing it on a later empty frame.
Metadata and program information
A source file can begin with one metadata block before imports/items:
metadata {
title: "Particle study",
version: "0.2.0",
description: "A compact simulation.",
category: "Simulation",
tags: ["example", "compute"],
credits: "TinyFX contributors",
}All fields are optional, but the vocabulary is closed: title, version, description, category, tags, and credits. Fields/tags cannot repeat; unknown names are errors. Multiline string values preserve their content after common indentation is removed.
The compiler retains metadata per file and exposes an entry/per-file ProgramInfo view through tfx info and a successful browser check(). Metadata is for display and tooling, not an application ID, storage namespace, or compatibility version.
Persistent storage
storage is a small asynchronous JSON key-value API for state that should
survive later executions:
struct Preferences {
pub theme: string,
pub scale: float,
}
fn setup() {
let saved = storage::set(
"preferences",
json::fromValue(Preferences { theme: "night", scale: 1.25 }),
);
let loaded = storage::get("preferences");
if (saved.ok && loaded.ok && loaded.found) {
let preferences: Preferences = json::toValue(loaded.value);
print(preferences.theme);
}
}storage::get, storage::set, and storage::delete return StorageResult.
ok distinguishes success from a recoverable platform failure. found says
whether get or delete found the key. A successful get places the stored
JSON tree in value; errorCode and errorMessage are populated on failure.
Failures such as unavailable, permission-denied, invalid-data, io, and
quota-exceeded are values rather than failed setup/loop tasks.
The first storage version deliberately stores JSON rather than exposing a raw
filesystem or general database. Asset-like data can use a typed JSON envelope,
including generated images represented by dimensions plus an RGBA uint[].
This provides a portable save/restore path while keeping binary handles out of
the language. Raw byte/blob storage is not part of version 1.
The host owns the namespace; TinyFX source cannot choose another application's
data. Native tfx run hashes the canonical entry path by default and accepts
--storage-namespace NAME when a deployment needs an identity that survives a
move. Browser embeddings pass an immutable application identity:
await run(canvas, source, {
application: {
schema: "tinyfx.application-identity/1.0",
applicationId: "com.example.paint",
storageNamespace: "com.example.paint",
},
});Without a browser application identity, storage returns recoverable
unavailable; it never creates an ephemeral namespace and calls it persistent.
Both the browser-WASM and generated TypeScript/WebGPU hosts use the same queued
IndexedDB adapter, so promise settlement never re-enters a running program.
The native host uses atomic files in the platform data directory. The
Playground persists a distinct identity for each local draft or saved project;
bundled examples use a stable slug-derived namespace.
Keys are limited to 256 UTF-8 bytes, one JSON value to 4 MiB, and one namespace to 16 MiB. Backend schema version 1 is independent of application data. Programs should store their own data version inside a value (or versioned key), read the old shape, migrate it, and only then overwrite it. Display metadata or an application release version never silently changes the namespace.
The env global
The runtime updates env before each user frame:
env.time // seconds since start
env.deltaTime // elapsed seconds since prior frame
env.frame // frame count
env.screenSize // int2 pixel dimensions
env.mouse // screen/uv/velocity/buttons/wheel
env.keyboard // tracked held-key fields
env.camera // 3D camera state
env.camera2d // 2D camera state
env.stats // latest completed profiling snapshotMouse and keyboard fields are regular typed values. Mouse positions are available in screen pixels and normalized UV coordinates. The environment is also shader-capturable: the compiler narrows a shader's reads to the fields it actually uses rather than uploading the entire structure.
Cameras
Camera controllers are opt-in. Without one, the runtime keeps the program's chosen camera values (apart from dimensions). Configure an orbit/free 3D camera or a pan/zoom 2D camera from CPU code:
env.configureCamera(
target: (0.0, 0.0, 0.0),
distance: 500.0,
yaw: 0.0,
pitch: 0.4,
fov: 60.0,
);
env.configureCamera2d(position: (0.0, 0.0), zoom: 1.0);3D orbit mode uses left drag to orbit, middle drag to pan, and the wheel to zoom. Right-drag enters free movement (WASD/E/Q and wheel speed controls). 2D mode pans with left drag and zooms around the cursor with the wheel.
env.camera supplies view, perspective, combined, orthographic, transform, and ray helpers. Its projection helpers take an optional DepthConvention; pass the same convention to drawAdvanced when working with reverse-Z. env.camera.getPerspectiveMatrixReverseZ() is a deprecated compatibility wrapper.
Runtime metrics
env.stats contains the latest completed profiling snapshot:
env.stats.gpuFrameMs
env.stats.renderMs
env.stats.computeMs
env.stats.drawCalls
env.stats.computeDispatches
env.stats.renderPasses
env.stats.computePasses
env.stats.computeLoweredSteps
env.stats.gpuTimingAvailabledrawCalls and computeDispatches are logical TinyFX requests. Render and compute pass counters/timings describe included physical work: authored draws and kernels, compute-lowered feedback, mip/reduction work, and color/depth texture clears. Presentation blits, copies/readbacks, paint/UI composition, and the overlay are excluded from both the count and timing totals. Timestamp results are asynchronous; millisecond fields are -1 until a measurement exists or when the adapter has no timestamp support. Counts remain available either way.
env.showStats(true) displays the built-in overlay. stat(label, value) adds application metric lines independently of that visibility setting.
UI
The ui:: functions form an immediate-mode panel API:
if (ui::button("reset")) { reset(); }
speed = ui::slider("speed", speed, 0.0, 5.0);
enabled = ui::checkbox("enabled", enabled);
ui::group("advanced");
ui::label("tuning");
ui::pop();There are controls for labels/headings, separators, buttons, toggles, checkboxes, float/integer sliders, drag values, progress, text, combos, radios, colors, groups, pucks, and draggable points. Consult the generated operation reference for exact signatures.
Native execution renders the panel with egui and masks camera/environment input while UI interaction owns a press. Browser execution renders DOM controls above the canvas, so their events never reach canvas listeners. In both cases program values win when they change; an actively edited browser control keeps its own value until that edit completes.
Native window configuration
Native CLI programs can request changes to their primary window:
env.configureWindow(
title: "TinyFX demo",
size: (960, 540),
resizable: true,
fullscreen: false,
);The request is applied after the TinyFX frame. An empty title or (0, 0) size leaves that property unchanged; nonzero dimensions must be positive. Browser hosts report the window capability as unavailable rather than modifying the surrounding page.
Platform boundaries
The runtime uses opaque handles and platform contracts for browser-only services such as DOM UI, assets, input, camera/microphone permission, media, and persistent storage. Native host implementations provide the equivalent public TinyFX behavior when available. See assets and media and renderer for the exact ownership split.