Veduta

Veduta game API

A Veduta game is a Go program whose main calls veduta.Run(&game.Game{}). The same binary plays on the console (a linux/arm64 board drawing on its panel's framebuffer and reading a gamepad) and, with -headless, renders, simulates and answers queries for the veduta tool on any machine, including a server with no display. Game logic lives only in the game binary; the tool never contains game code.

package main

import (
	"github.com/riftbane/veduta-engine"
	"mygame/game"
)

func main() { veduta.Run(&game.Game{}) }

Interfaces

type Game interface {
	Init(ctx *veduta.Context) error              // once, after the first scene is loaded
	Update(ctx *veduta.Context, in veduta.Input) // exactly once per tick, before behaviours
	Draw(ctx *veduta.Context, dl *gfx.DrawList)  // once per rendered frame, after the scene
}

type Behaviour interface {
	Update(ctx *veduta.Context, e *scene.Entity, in veduta.Input)
}

func RegisterKind(name string, ctor func(*scene.Entity) veduta.Behaviour)

Context

MemberMeaning
Scene *scene.Scenethe world: Find(name), Get(id), Tagged(tag), Entities() (id order)
Tick uint640 during Init, then 1, 2, … during Update
RNG *sim.RNGseeded xoshiro256**: Float32(), Range(lo, hi), Intn(n), Bool(), Chance(p) — the only allowed randomness
DT float32seconds per tick (1 / tick_rate, 1/20 by default)
Headless booltrue when run by the tool
Project *asset.Projectthe parsed veduta.json
Width, Height inta frame size in pixels, never 0 once Init runs. In Draw: the size of the frame being drawn (the panel's size divided by VEDUTA_SCALE in the player, --width×--height in render, the screenshot tile in simulate). In Init, Update and behaviours: the project's resolution (the frame the game is designed for) in every mode (player, render, simulate, snapshot), so logic that reads them gives the same trace everywhere and HUD layout can be computed during Update; in the player it can differ from the frame Draw receives.
Font, FontTexturebuilt-in 8×8 font for HUD text
Trace(event, fields)emit a game event into the current tick's trace
Invariant(name, pred)register a predicate checked after every tick when listed
RegisterState(codec)save/restore the game's own state in snapshots
Spawn(tmpl) *scene.Entityadd an entity (next id); its behaviour starts next tick
SpawnPrefab(name, origin, rotation, prefix)add a prefab's entities with the min corner of its footprint at origin, turned by 0, 90, 180 or 270 degrees about +Y as a world places it, named <prefix>_<entity> (the prefab's name when prefix is empty) and parented as the prefab says; returns them in prefab order
Despawn(e)remove e and its children at the end of the tick
LoadScene(name)replace the scene (ids restart at 1), e.g. to reset a level
SetModel(name, m) erroradd or replace a model the game builds at runtime (a voxel chunk's mesh): entities name it like an asset model, with culling, LODs and DrawDistance; uploaded at the next frame. The name contains : and does not start with world:; pass a new *asset.Model to change it (the engine re-uploads when the pointer changes). Not saved in snapshots and kept across LoadScene
RemoveModel(name), Model(name)forget a runtime model; look one up
Map() *tilemap.Mapthe scene's tile map (docs/map.md), nil without one: Get(layer, x, y), Set(layer, x, y, terrain) error (a map_set event), Has(layer, x, y, tag) (layer −1: any), CellAt(x, y), Center(x, y), Layer(name), Size(), Source() (its objects)
LoadMap(name) errorreplace the scene's tile map with a map as its file describes it ("" removes it); the entities stay; a map_load event
Clip(e, name) *asset.Clip, Clips(e)a clip of the textures e draws with, and their names; play one with e.SetAnim(name) (unless it plays) or e.Play(name) (from the start): the engine sets e.Frame every tick
Overlapping(e)live entities whose AABB overlaps e's (last tick's bounds)
Texture(name)a texture asset's handle and size in texels, for Batch.Image and Batch.NineSlice in Draw (ok false for an unknown texture)
ReadSave(name), WriteSave(name, data), RemoveSave(name), SaveNames()the game's saves: JSON objects or arrays of at most 1 MiB under valid names. The player keeps them as files in VEDUTA_SAVE_DIR (default out/saves of the project; the console sets it to the card); headless runs keep them in memory, starting from the scenario's saves. Writes and removals are trace events save_write and save_remove
HUD(dl) *sprite.Batchstart a HUD batch covering the frame; call End()
Text(b, x, y, scale, s, color)draw text with the built-in font
type StateCodec interface {
	SaveState(enc *gob.Encoder) error
	LoadState(dec *gob.Decoder) error
}

Input

veduta.Input is a value type, identical whether it comes from the console or a script.

Field / methodMeaning
Pressed, Held, Releasedbutton sets (sim.Buttons)
Down(b), JustPressed(b), JustReleased(b)button queries, b one of the constants below
DPad() gmath.Vec2the D-pad as a direction: X −1 (left), 0 or +1 (right), Y −1 (down), 0 or +1 (up); opposite directions cancel; a diagonal is (±1, ±1)

A button pressed and released within one tick appears in Pressed and Released but not in Held.

The console's controls

The console has a D-pad, A, B, Select, Cancel and Home. A game sees eight buttons; Home is the console's, and no game can see or swallow it: pressed, it opens the console's overlay over the game (the game keeps running, with no button held); held for a second, it leaves the game, even one that has stopped answering.

ButtonConstantScenario nameMeant forKeyboardGamepad
D-padveduta.ButtonUp, ButtonDown, ButtonLeft, ButtonRightup, down, left, rightmoving, choosingarrows, W A S DD-pad, hat, or a stick where the D-pad is not four buttons
Aveduta.ButtonAathe main action, confirmSpace, ZA (BTN_SOUTH)
Bveduta.ButtonBbthe second actionX, ShiftB (BTN_EAST)
Selectveduta.ButtonSelectselectthe game's menuEnter, TabStart
Cancelveduta.ButtonCancelcancelback, close a menuEscape, BackspaceSelect, KEY_BACK (the handheld's Cancel)
Homenonenonethe console's overlay; held, leave the gameHome (Ctrl+Q leaves at once)Home (BTN_MODE, KEY_HOMEPAGE), or Select and Start together

A USB pad has no Home button, so Select and Start pressed together are its Home; alone, its Start is the game's menu and its Select is Cancel. A pad's other buttons (X, Y, shoulders) and sticks beside a four-button D-pad are ignored. A program that plays games without an overlay (veduta run) leaves on a press of Home.

Cameras

ctx.Scene.Camera is the camera the player and render --camera scene draw with; a game may replace it during Update.

Entities

scene.Entity fields: ID, Name, Kind, Transform (Position, Rotation quaternion, Scale, relative to Parent), Model, Material, Tags, AABB (world bounds of the hitbox, else of the model, recomputed after every tick), Visible, State, Parent, Hitbox *gmath.AABB (the scene file's hitbox: a local-space box that replaces the model bounds as the source of AABB, so collisions, Overlapping, no_overlap and the trace's aabb use it; nil for none; Spawn copies it), Layer int (the scene file's layer: the first key of the draw order, lower layers drawn first; within a layer opaque parts in id order, then blended parts back to front by depth along the view axis; opaque and cutout parts write depth, so among them the nearest is in front whatever the layer, while blended parts write none, so an opaque part on a higher layer is drawn over a blended part on a lower one whatever their depth). Useful methods: WorldPosition(), World(), HasTag(t), Alive(). Use gmath for math: its Sin/Cos/Atan2 are deterministic on every platform; never use math.Sin in game logic.

Coordinates: right-handed, Y up, −Z forward, meters. Angles are degrees in files and radians in code. rotation_deg: [x, y, z] means R = Ry·Rx·Rz.

Tick timeline

  1. Tick 0: the scene is loaded (ids 1…n in scene-file order), Init runs, then the engine records tick 0 (events scene_load and any spawns from Init).
  2. Each tick t = 1, 2, …: Game.Update(ctx, in), then every live entity's behaviour in id order (entities spawned during the tick start next tick), then despawns are applied, transforms and AABBs are recomputed, collisions and invariants are checked, and tick t is recorded.

Input events of a script at tick t are part of the Input of tick t; events at tick 0 apply before the first update and appear in tick 1. The simulation runs at tick_rate (20 Hz by default); there is no interpolation and the player renders once per tick. With VEDUTA_PERF=<seconds> the player writes its frame times (below). The player collects garbage less often than a Go program does by default, at a heap five times what is live (GOGC=400; GOGC in the environment overrides it), and right after a scene or world loads, where the load already stalls a frame.

Frame times on a console

With VEDUTA_PERF=<seconds> (a console's dashboard sets 10 for every game) the player writes, that often, one line like this one to stderr, which a console copies to its kernel log:

veduta: perf: gpu (VC4 V3D 2.1), 200 frames in 10.0 s, update 2.10 ms, render 18.40 ms, present 6.20 ms, worst 41.00 ms, 0 over 50 ms, stepped 200, rendered 200, build 3.10 ms, submit 4.00 ms, readpixels 9.00 ms, convert 2.30 ms, keep 0.20 ms, overlay 0.01 ms, pack 2.00 ms, write 3.99 ms, commands 42.0, gl calls 812.0, gc 3, alloc 7.5 KB, self user 55.0%, self system 10.0%, cpu user 56.0%, cpu system 20.0%, cpu softirq 3.0%, cpu idle 20.0%

Fields are separated by ,, each a name and a value; the first ones, up to over, are the line as it has always been, and keep their order. Times are means per frame over the period, in milliseconds.

FieldMeaning
software, gpu (…)what drew the frames: the processor, or the GPU through its GL implementation
frames in … sframes drawn in the period, and its length
updatethe tick: Game.Update, behaviours, physics, invariants
renderthe frame drawn: build + submit + readpixels + convert
presentthe frame shown: keep + overlay + pack + write
worstthe slowest frame, update, render and present together
over … msframes slower than a tick (1000 / tick_rate ms)
stepped, renderedticks run and frames drawn in the period
buildthe camera, uploading the models the game changed, the draw list of the scene and of game.draw, the console's overlay
submitthe draw commands sent: on the GPU, the GL calls (made on the GL thread in End, in one handoff a frame); in software, all of the rasterization
readpixelson the GPU, the wait for it to finish and the read of the pixels back (the driver untiles and copies them)
converton the GPU, the pixels read turned into the frame's colors
keepthe copy of the frame kept to show under an error
overlaythe player's own overlay: F1, notices, the error
packthe frame packed into the panel's format (RGB565), where the panel says how long it took; 0 elsewhere, where write has it
writethe frame written to the panel's device. An SPI panel's driver sends it on later, in a kernel thread of its own
commandsdraw commands per frame
gl callsOpenGL ES calls per frame; 0 in software
gcgarbage collections in the period
allocKB allocated per frame
self user, self systemthe player's processor time in user space and in the kernel, as a share of all the processors' time in the period (/proc/self/stat; on Linux only, like the next four)
cpu user, cpu system, cpu softirq, cpu idlethe whole machine's (/proc/stat): cpu system above self system is kernel work done for others, such as sending the frames to an SPI panel

Measuring a draw

internal/drawcost in the engine's repository is a Lua game that draws N small textured quads, each an entity and so one draw command of two triangles: 25, then 100, then 200, 400 ticks (20 s) each, and again. Its scenes n25, n100 and n200 draw one count only, for bench, which checks the counts but not a console's times. On a console:

  1. Put the folder on the card as games/drawcost (veduta deploy from the folder, the card plugged in, or a plain copy).
  2. Start Draw cost from the dashboard, let it run a few minutes, leave it and power off.
  3. Take its veduta: perf: lines from the card's vedutaos/kernel.log. Keep those whose commands is a count plus the hud's one command (26, 101, 201): a period across two counts has a mean between them.
  4. Fit a straight line through render against commands: its slope is the milliseconds a draw costs, its intercept what a frame costs with nothing in it. The parts (submit, readpixels) tell where the draws' cost lands.

Determinism rules

Trace

One canonical JSON object per tick in trace.jsonl:

{"entities":[{"aabb":{"max":[0.5,1,-0.75],"min":[-0.5,0,-1.75]},"id":2,"kind":"player","material":"hero","model":"hero","name":"player","parent":"","position":[0,0,-1.25],"rotation_deg":[0,180,0],"scale":[1,1,1],"state":{"score":1},"tags":["player"],"visible":true}],"events":[{"event":"gem_collected","gem":"gem_1","score":1}],"tick":75}

Keys are sorted, no whitespace, floats use the shortest form that round-trips (float32 values as float32), non-finite numbers are the strings "NaN", "+Inf", "-Inf". Entity summaries hold: id, name, kind, position (world), rotation_deg (local Euler angles, R = Ry·Rx·Rz, pitch in [-90, 90]), scale (local), visible, tags, model, material, parent (name, "" for none), aabb ({min, max}, world; only entities with a model or a hitbox, and the hitbox's bounds when one is set) and state (only when set). Scenario expectation paths address these keys (position.z, aabb.max.y, state.score). The trace hash is the SHA-256 of the file's bytes.

Built-in events: scene_load (scene, entities), spawn and despawn (id, name, kind), collision (a, b, a_id, b_id: two AABBs started overlapping this tick; touching does not count; two static entities never collide), invariant_violation (name, detail: an invariant started failing this tick). Game events come from ctx.Trace.

Invariants

Checked after every tick (including tick 0). A scenario's invariants list is used when present, else the project's. Built-ins: finite_positions, within_bounds (project bounds), entity_count_max:N, no_overlap:tagA,tagB. Any other name must be registered by the game with ctx.Invariant(name, pred). A violation is reported when an invariant starts failing, not again while it keeps failing.

Headless subcommands

Every game binary accepts -project DIR (default .), -version, and:

game -headless render   --scene S --tick T --seed N --camera P --mode M --width W --height H --out F [--bundle] [--input F]
game -headless simulate --scenario F | --scene S --ticks N --seed N [--input F] [--screenshots 0,60] [--invariants a,b] --out DIR
game -headless query    --frame F --at x,y | --coverage
game -headless snapshot --scene S --tick T --out F [--input F]
game -headless snapshot --restore F --ticks N [--input F] [--out trace.jsonl]
game -headless describe
game -headless bench    --scenario F | --scene S --ticks N --seed N [--input F] [--width W --height H] [--cpus N]