Reference

API Reference

Every keyword, type, phase, builtin, namespace method, annotation and color in Ludic — each on its own page. Search, or browse by section. In any code sample across this site, hover a token for a summary and click to jump to its page.

Program structure

The shape of a Ludic program: one program block holding declarations.

Entities & the ECS

Entities are ids; properties are their data; queries walk them.

spawnCreate a model instance carrying the listed properties, seeding their fields.despawnRemove a model instance from the world, firing any @OnDespawn hooks.queryMatch every model instance carrying all the listed properties.selfThe model instance currently bound by the enclosing query loop.enableRe-activate a disabled property, model, or handler — its data is intact.disableDeactivate a property, model, or handler without destroying its data.attachStructurally add a property to a live model instance, seeding its fields.detachStructurally remove a property from a live model instance.world_attach_dynAttach a property to an instance by numeric id at runtime (dynamic ECS).world_countThe total number of live model instances in the world.world_detach_dynDetach a property from an instance by numeric id at runtime (dynamic ECS).world_field_idResolve a field name within a property to its numeric index.world_getRead one field of a model instance by numeric id (reflection ABI).world_hasTest whether a model instance currently carries a property.world_kindThe model id of an instance — which kind of thing it is.world_loadRestore the whole world from a serialized byte buffer.world_model_idResolve a model's name to its stable numeric id.world_prop_idResolve a property's name to its stable numeric id.world_query_nextStep to the next instance carrying a property, walking the world by id.world_register_propRegister a brand-new property at runtime and get its id (dynamic ECS).world_saveSerialize the whole world into a buffer; returns the number of bytes written.world_setWrite one field of a model instance by numeric id (reflection ABI).world_sizeThe number of live model instances in the world.world_spawnSpawn an instance of a model chosen by numeric id at runtime.

Control flow

Branches, loops, and state machines.

Scenes & layers

One active scene at a time, each grouping handlers into layers.

Events

Decoupled, named messages between handlers.

Screen — drawing

The 2D drawing surface. Every call takes named arguments; draw during the Render phase, then Screen.show().

Screen.clearFill the whole framebuffer with one color to start a fresh frame.Screen.fill_rectangleDraw a solid, filled rectangle at a pixel position.Screen.draw_rectangleDraw a one-pixel-thick rectangle outline (not filled).Screen.put_pixelSet a single pixel at a pixel coordinate to one color.Screen.draw_textDraw a string with the built-in font at an integer scale.Screen.draw_numberDraw an integer directly, with no string allocation or conversion.Screen.showPresent the finished frame — copy everything drawn this frame to the window.Screen.widthThe framebuffer width in pixels.Screen.heightThe framebuffer height in pixels.Screen.statusSet the persistent one-line status/HUD string shown by the runtime.Screen.lineDraw a straight line between two points.Screen.circleDraw a circle outline.Screen.fill_circleDraw a filled disc.Screen.triangleDraw a triangle outline.Screen.fill_triangleDraw a filled triangle.Screen.spriteBlit a sprite at its natural size.Screen.sprite_scaledBlit a sprite at an integer scale.Screen.ovalDraw an axis-aligned ellipse outline.Screen.cameraSet the world-space camera offset for the draw path.Screen.clipRestrict drawing to a screen-space rectangle.Screen.clip_resetLift the clip rectangle (draw to the whole screen again).Screen.blend_modeChoose replace or additive pixel blending.Screen.measure_textThe pixel advance width of text in the built-in font.Screen.pixelRead a framebuffer pixel (0x00RRGGBB), or 0 if off-screen.Screen.barA filled meter: value of max over a track.

Clock

A game-controlled simulated clock: a single seconds counter the game owns. Unlike Time.now / Time.since, it never reads the wall clock, so any gameplay that reads Clock.now() is deterministic and replay-safe. Set it outright, or advance it by a Duration each tick to run time at whatever rate the simulation wants. The clock is a plain instant (seconds since 1970), so all the DateTime.* readers work on it directly.

Collision

2D overlap tests on integer coordinates (pixels or tiles). Rectangles are (x, y, w, h) from the top-left; circles are (x, y, r). Each returns a bool. Squared distances are computed in 64-bit so large coordinates never overflow. These are static boolean tests. For a *moving* body with real collision resolution, the engine ships a physics-lite movement system built on the same integer math — declare the well-known components and the engine advances them for you each frame, no handler wired: - property Body { vx, vy, gravity, max_fall, rx, ry, policy, on_ground, hit_wall, hit_ceiling } — velocity/accel state in Q16.16 fixed-point (vx/vy/gravity/max_fall), engine-owned sub-pixel accumulators (rx/ry), an integration policy (0 platformer with gravity, 1 top-down), and the derived contact flags the engine sets each frame. - property Collider { w, h, offx, offy, is_trigger, one_way, layer, mask, hit, entered, exited } — an AABB shape offset from the entity's Position { x, y }, with layer/mask filtering, one-way-platform and trigger flags, and per-frame trigger outputs. - property Solids { tile, wall, oneway } — an optional single config entity that turns on the tile-grid broadphase over the Map tilemap (tile px size, the solid wall glyph, and an optional one-way oneway glyph). The engine-owned esys_move system (Update phase) integrates velocity and gravity into a tentative move, then resolves it with a per-axis **swept AABB** — against both solid Collider entities and the tile grid — so a fast body never tunnels through a wall. It handles one-way platforms (which block only a downward landing), reports trigger/sensor overlaps without resolving them, and sets on_ground / hit_wall / hit_ceiling for a controller to read. Everything is integer and deterministic, so movement reproduces exactly under replay, lockstep and world_save snapshots. Contact is surfaced as polled flags (the SpriteAnim.event_fired shape) rather than engine-emitted events, so a game raises its own CollisionResolved / TriggerEntered events from its handler with no coupling. This is the shared foundation the built-in gameplay controllers build on. Related: Grid, Map, Motion, @EngineSystem.

Color functions

Building and blending colors at runtime. Colors are 0x00RRGGBB ints; these pack channels and transform an existing color. The named palette constants (Color.Charcoal, …) are a separate compile-time set.

Crypto

Secure, test-vector-backed hashing for the handful of security-sensitive things a game actually does: signing a save or leaderboard payload so casual tampering is detectable, and verifying that a network message or token was not forged by someone who does not hold the key. This is the deliberate counterpart to the fast Hash library — same idea, opposite trade-off. Hash is fast and reversible and must never guard anything; Crypto is SHA-256 and HMAC-SHA256 implemented to the standard, so the algorithms are the ones with published known-answer tests rather than anything home-grown. Digests are returned as lowercase hex strings, not raw bytes — a str is null-terminated and a raw digest can contain a zero byte, so hex is the form you can print, store, and compare directly. Alongside hashing, this library exposes the OS cryptographically-secure random generator — random_bytes, random_hex, and random_u32 — for tokens, nonces, and Uuid generation, plus base64 for moving bytes through text-only channels. The secure-random helpers are deliberately non-deterministic and must never seed the lockstep simulation RNG (Random). What this is not: it is not DRM and it is not unbeatable anti-cheat. A client-side game cannot keep a secret from the machine it runs on — a determined owner can always read the key out of the binary. Use it to make *casual* tampering detectable and to authenticate messages between parties who share a key. To verify a MAC always use Crypto.verify_hmac (a constant-time check), never ==, which leaks how much of a guessed MAC was correct.

Date

Calendar days on the proleptic Gregorian calendar (UTC). A Date is stored as a plain int: the count of days since 1970-01-01, so shifting a date by whole days is ordinary integer arithmetic and two dates subtract to a day count. Build one with Date.new, read its parts with year/month/day/weekday. Every operation is deterministic integer math — no leap seconds, no timezone, no floating point.

DateTime

Instants on the wall-clock timeline (UTC), stored as a plain int: the count of seconds since 1970-01-01, matching Time.now. Build one with DateTime.from, shift it by a Duration with DateTime.add, and pull it apart with the year/month/day/hour/minute/second readers. Deterministic integer math throughout; v1 is UTC-only with no leap seconds, and assumes instants at or after 1970.

Duration

Spans of real time, measured in whole seconds and carried in a plain int — so a duration adds, subtracts and compares with the ordinary operators (Duration.minutes(5) + Duration.seconds(30), away > Duration.hours(3)). The constructors build a span from a unit; the as_* readers convert a span back to whole units (truncating toward zero). Integer-only, so no floating-point drift.

Ease

Tween curves — the "juice" layer. Each takes a normalized amount t in 0.0..1.0 and returns an eased fixed, ready to feed to Math.lerp. All deterministic fixed-point.

Fs

The filesystem, wrapped into one safe, ergonomic API — no file descriptors, no byte buffers, no half-written files. The foundation for saves, config, mods, and asset loading: read and write whole files as text, check existence, make directories, copy, and list a folder. The bare file_* builtins remain underneath for streaming; Fs.* is the everyday layer. Fallible calls surface failure as a value — null, false, or -1 — that you branch on, never a crash (a first-class try/else lands with the error-handling work). Writes are atomic (written to a temporary file and renamed into place), so a crash mid-write never corrupts the previous file. list returns entries sorted, so a directory walk is reproducible across runs and platforms. Paths are /-separated over the native (macOS/BSD) filesystem — the fully supported target today; a sandboxed virtual filesystem for wasm and recursive directory copy are follow-ups. Text helpers are UTF-8 (they pair with Unicode); use Path to build the paths and Os for per-user locations.

Hash

Fast, non-cryptographic hashing for everyday game needs: turning string IDs into integer handles, mixing a few numbers into one deterministic seed, and checksumming data to catch corruption. Every function is plain 32-bit integer arithmetic with a defined byte order and fixed constants, so a given input hashes to exactly the same value on every platform and every run — the guarantee that makes it safe for procedural generation and lockstep networking. Results are 32-bit and print as signed integers. Arguments are positional. Not for security. These hashes are fast and reversible — never use them for passwords, tokens, or tamper-proofing. For secure hashing reach for the cryptography library instead.

Input

Reading input. At the base, Input.key gives this frame's key and Input.poll is the single per-frame read that also drives deterministic record/replay. Over that sit named actions — Input.bind / Input.down / Input.pressed / Input.rebind — so gameplay reads rebindable actions, not physical keys. The device layer adds everything past one key per frame: multiple simultaneous held keys (Input.key_down / key_pressed / key_released), analog Input.axis and normalized Input.vector, the mouse (position, delta, buttons, wheel), gamepads and touch. The held set is fed by the platform when windowed and by the Input.press / Input.set_* injection on every target — the same idea as Godot's action_press, and what a replay, an AI, or the network feeds. Everything is integer and deterministic, and record/replay snapshots the whole per-frame state.

Input.keyThe key pressed this frame as a character code (0 when nothing is pressed).Input.bindBind a physical key to a named action, so gameplay reads the action, not the key.Input.rebindRemap an action from one key to another at runtime (rebinding menus).Input.pollAdvance one frame of input; the single per-frame read behind actions and replay.Input.downIs a named action held on the frame last polled?Input.pressedDid a named action go down this frame (a one-shot edge)?Input.recordStart recording polled input each frame (for deterministic replay).Input.replayReplay recorded input; poll then reads the tape, not the device.Input.key_downIs a physical key held this frame?Input.key_pressedDid a key go down this frame (edge)?Input.key_releasedDid a key go up this frame (edge)?Input.key_labelThe name to show a player for a key - in their own keyboard layout.Input.pressInject a held key (AI, tutorial, testing, network).Input.releaseRelease an injected key.Input.axisA -1..+1 axis from two keys.Input.axis_iA -1/0/1 directional intent as a plain int, no bool->int glue.Input.vectorA normalized 2D vector from four keys.Input.strength0..1 strength of a named action.Input.mouse_xThe mouse x position this frame.Input.mouse_yThe mouse y position this frame.Input.mouse_dxMouse x movement since the last poll.Input.mouse_dyMouse y movement since the last poll.Input.mouse_downIs a mouse button held?Input.wheelScroll-wheel delta this frame.Input.set_mouseInject the mouse state (headless / AI / testing).Input.pad_connectedIs a gamepad connected?Input.pad_buttonIs a gamepad button held?Input.pad_axisA gamepad analog axis, -1..+1.Input.set_padInject a gamepad's whole state.Input.touch_countHow many touch points are active?Input.touch_xThe x of a touch point.Input.touch_yThe y of a touch point.Input.set_touchInject a touch point.Input.actionRegister a default key binding for an action (kept if already bound).Input.bind_padAlso fire a named action from a gamepad button (device-agnostic).Input.activeIs a named action active right now (any bound key held, or pad button down)?Input.just_pressedDid a named action go active this frame (the deterministic on-press)?Input.just_releasedDid a named action go inactive this frame (the on-release edge)?Input.cursor_modeHide / lock / confine the OS mouse cursor to the window (windowed).Input.move_iThe standard top-down movement intent, -1/0/1 per axis.

List

Operations over []T slices — length, ends access, push/pop, swap, search, and in-place reverse. A slice is a shared growable buffer, so these mutate it in place and every holder sees the change. Arguments are positional.

Log

Levelled, structured logging — the default way to answer "what is my game doing?" and "why did that break?", and a real step up from scattering print calls through your code. Each message carries a level, from trace (most verbose) through debug, info, and warn to error, and a runtime threshold set with Log.set_level decides which ones actually appear — so a development build can be chatty and a release build quiet, without touching the call sites. Messages go to standard error, kept separate from a program's real stdout, tagged with their level. Beyond the message you can pass **structured fields** as trailing key/value pairs — Log.warn("missing texture", "path", p, "id", n) prints [WARN] missing texture path=... id=... — cheap to write and easy to grep. Numeric values (int, long) are formatted for you; the level tag is chosen at compile time, so a filtered-out level costs only a threshold comparison at runtime. Logging never touches the simulation — it writes to stderr and returns — so it has no effect on gameplay determinism or replays. This first version ships the console (stderr) sink; a rotating-file sink and an in-engine overlay sink are planned follow-ups.

Math

Deterministic fixed-point math. Every function is computed in Q16.16 with plain integer arithmetic, so results are bit-identical on every platform and every run — the same guarantee the rest of the runtime gives. Arguments are positional. Given a float or double argument, the same functions compute in that type instead (using the platform's math library) and return it — floor, ceil and round included, which return an int only for fixed. sign returns an int either way. trunc and atan exist only for floats.

Math.minThe smaller of two values.Math.maxThe larger of two values.Math.absThe magnitude of a value, dropping its sign.Math.clampConstrain a value to the range [lo, hi].Math.signThe sign of a value as -1, 0, or 1.Math.floorRound a fixed value down to the nearest whole int.Math.ceilRound a fixed value up to the nearest whole int.Math.roundRound a fixed value to the nearest whole int.Math.lerpBlend between two values by a 0..1 amount.Math.inverse_lerpFind where a value sits between two endpoints as a 0..1 fraction.Math.remapRescale a value from one range into another.Math.sqrtThe square root of a fixed value.Math.sinSine of an angle in radians.Math.cosCosine of an angle in radians.Math.tanTangent of an angle in radians.Math.hypotLength of the vector (x, y).Math.distDistance between two points.Math.dist2Squared distance between two points.Math.deg_to_radConvert degrees to radians.Math.rad_to_degConvert radians to degrees.Math.posmodModulo that is always non-negative.Math.wrapWrap a value into the range [lo, hi).Math.ping_pongBounce a counter back and forth in [0, len].Math.snappedRound a value to the nearest multiple of step.Math.move_towardStep from one value toward another by at most delta.Math.smoothstepA smooth 0..1 ramp between two edges.Math.atan2The angle of the vector (x, y), in radians.Math.asinThe arcsine of a value, in radians.Math.acosThe arccosine of a value, in radians.Math.expe raised to the power x.Math.logThe natural logarithm of x.Math.powbase raised to a fixed exponent.

Memory

Raw memory — allocate byte and word buffers, copy and fill regions, and peek/poke individual bytes. The low-level escape hatch.

Mime

Content-type detection for assets and downloads: map a filename to its MIME type, or refine the guess by peeking at a file's leading bytes. Useful for deciding how to load a dropped-in mod file, labelling an export, or picking a decoder. of is a pure lookup by extension; sniff reads the first bytes and recognises a few well-known signatures (PNG, JPEG, GIF, PDF), falling back to the extension when nothing matches. The extension table is deliberately compact and documented; richer magic-byte coverage is a follow-up.

Network

The low-level networking seam — send and poll datagrams, serialize and apply entity state, and read ownership and role. Multiplayer is normally a compile-time property of the ECS, not glue you thread by hand: mark fields @Sync and models @Owned and the compiler generates the replication. These Network.* primitives are what that sugar lowers to, for when you drive the transport yourself. Offline they collapse to single-player defaults.

Noise

Procedural noise — the primitive that terrain, caves, biomes, textures, clouds, wind, and object placement are built on. Every generator is implemented in Q16.16 **fixed point** over an integer permutation hash seeded from an explicit seed, so a given seed reproduces the *exact* same field on every platform and every run: native, headless, and (later) wasm all agree bit-for-bit. That is a real edge over float-based engines, whose worlds can drift subtly across CPUs and break shared-seed multiplayer or replays. Coordinates are fixed values. The integer part of a coordinate selects a lattice cell and the fraction interpolates within it, so you scale feature size by sampling at a fractional *frequency* (e.g. multiply coordinates by 1/64). Outputs are fixed normalised to [-1, 1]; Noise.unit remaps that to [0, 1] when you want a height or a probability. Pick a generator by feel: value2 is cheap and blocky; perlin2 is the classic gradient noise; simplex2 is the organic default with fewer directional artifacts; fbm2 stacks octaves of simplex for natural, detailed fields; and cellular2 (Worley) gives Voronoi-cell structure for stone, cracks, and biome boundaries. Every sampler is a pure function of (x, y, seed) — no global state, no allocation — so it is safe to call across a whole worldgen pass or a per-pixel fill. Seed worldgen from its own seed (or a dedicated Random stream), kept separate from gameplay RNG, so generating the world never desyncs the simulation.

Os

The operating-system interface — the environment *around* the game rather than the game itself: the command line, environment variables, the standard streams, the process exit code, the host platform, and the per-user folders a game writes its saves, config, and cache into. It rounds the bare System builtins (arg, getenv, exit) into one coherent, Go-flavored namespace. It is deliberately small and game-scoped: there is no process spawning, no signals, and no user/permission APIs. Reach for it in launchers, asset pipelines, and dev tools — parsing launch flags like --level 3, reading config from the environment, or resolving where a save file belongs — far more than in the simulation itself. **Determinism:** args, the env family, and platform/arch are non-deterministic host input. Read them once at startup to configure the game, and keep them out of the replayable simulation so a shared seed still reproduces. **Platform coverage:** this first version targets the native macOS/BSD host — the fully supported target today. platform is portable (the uname system name is available on every Unix); arch and the known-folder layout (save_dir/config_dir/cache_dir) follow the macOS conventions. Linux/Windows/wasm folder resolution and a target-aware arch are documented follow-ups.

Path

Path manipulation as pure string operations — no disk access, no allocation surprises, just the joining and splitting every save system, mod loader, and asset pipeline needs. Separate a filename from its folder, get an extension, join segments without worrying about doubled or missing separators, and collapse ./../duplicate slashes into a canonical form. Paths use / as the separator, matching the native (macOS/BSD) filesystem — the fully supported target today; Windows-style separators are a follow-up. Pair these with Os (which supplies per-user directory *locations*) and Fs (which does the actual reading and writing).

Save

Whole-world save and restore — write a binary snapshot of the ECS world and read it back.

System

Low-level file and process access — raw file handles, reading a character, and running a shell command. Outside the deterministic simulation. For command-line arguments, environment variables, the exit code, and the standard streams, use the Os namespace.

Text

String queries and building over string values — length, character access, substrings, search, prefix/suffix tests, and int conversion. Arguments are positional. Strings are byte sequences, so indices and lengths count bytes.

Time

The frame clock. frame/elapsed/delta are deterministic (driven by a per-frame counter at a fixed 60 fps), so a replay reproduces every value; now is the wall clock and is explicitly non-deterministic.

Unicode

Correct-by-default text over UTF-8. Player names, translated menus, and chat arrive as UTF-8 bytes, and counting *bytes* gets non-ASCII text wrong — the wrong length, and truncation that slices a character in half into mojibake. The Unicode namespace works in code points and (approximately) grapheme clusters instead, so measuring, indexing, truncating, and case-mapping behave for every language. This is the correctness layer, not a replacement: the byte-oriented Text operations stay for speed on ASCII and for raw byte work. Reach for Unicode.* whenever the text came from a human — a name, a message, a localized string. Three notions of "length" matter, and the API keeps them distinct: byte_len (storage), len (code points — Unicode scalar values), and grapheme_len (user-perceived characters, where a base letter plus its combining accent, or a ZWJ emoji sequence, count as one). **Coverage (v1):** decoding and validation cover the full UTF-8 range. Case mapping covers ASCII and the Latin-1 letters — correct for Western-European text; wider scripts (Latin-Extended, Greek, Cyrillic), locale rules (Turkish i, German ß), and NFC normalization are follow-ups. grapheme_len approximates UAX#29 for the cases real player text hits — combining marks, variation selectors, ZWJ sequences, and flag pairs.

Uuid

Universally-unique identifiers — stable IDs that don't collide, generated locally with no central authority handing out numbers. Reach for a UUID whenever something needs an identity that survives being saved, shared, or sent over a network: players and sessions in multiplayer, user-created content (levels, items, mods) that has to merge cleanly across installs, or a per-install / per-run ID for analytics and bug reports. Two versions are provided. Uuid.new makes a **v4** (random) UUID — 122 bits of entropy, effectively never colliding. Uuid.new_v7 makes a **v7** (time-ordered) UUID whose leading bits are a millisecond timestamp, so a batch of v7 IDs sorts by creation time — friendly to database indexes and append logs. Both set the RFC 4122 version and variant bits correctly. A UUID is represented as its canonical lowercase 36-character text form (8-4-4-4-12), the same shape you store, print, send, and compare — so there is no conversion at each boundary. Validate untrusted input with Uuid.is_valid or normalise it with Uuid.parse, and compare with Uuid.equals, which ignores case. **Determinism caveat.** v4 and the random tail of v7 come from the OS cryptographically-secure RNG (Crypto), which is non-deterministic by design. Minting a UUID inside the lockstep simulation will desync replays and networked peers — generate IDs at the edges (on connect, on save, on spawn-from-input), never per tick in reproducible gameplay code.

World

Runtime reflection over the ECS world — read and write component fields by id, spawn and inspect entities, and register components at runtime. The introspection layer a mod uses.

Audio

Sound effects and music. A sound is loaded once with Audio.load into a handle; Audio.play fires it one-shot and Audio.play_music loops it on a single music channel. Playback is out-of-band — the audio device is real-time, not part of the deterministic simulation — but every trigger here is an ordinary frame-driven call, so a recorded run replays the same sounds at the same frames. Headless builds carry the whole API as no-ops (no audio device needed), so the same game code runs under the test harness.

Random

A seeded, deterministic RNG — same seed, same sequence, every run and every platform.

Vector

2D vector math for positions, velocities, and directions. A Vector is a pair of Q16.16 fixed components packed into one value, so it is copied by value and never allocates. Every operation is deterministic integer fixed-point — bit-identical on every platform, the same guarantee the rest of the runtime gives. Arguments are positional; angles are in radians.

Http

A poll-based HTTP / HTTPS client for out-of-band data — leaderboards, cloud saves, remote config, telemetry, downloads. Open a request with Http.get / Http.post (or Http.open to add headers first), then each frame call Http.poll — it returns -1 while pending, 0 on error, or the status code — so the frame never blocks. Read the reply with Http.status / ok / text / header, and pair it with Json.* for (de)serialization. TLS is the system's, on by default. A large download goes to a file instead of memory with Http.save_to, and Http.received / expected drive a progress bar while it is pending. HTTP depends on the network and the wall clock, so — like Net.* and Time.now — it is out-of-band and must never feed the deterministic lockstep/replay simulation. The transport is NSURLConnection / NSURLSession on macOS and WinHTTP on Windows; the raw-response parser (Http.parse) is pure and portable.

IVec2

Integer 2D vector math for tile and grid coordinates, cell offsets, and integer sizes. An IVec2 is a pair of whole-number int components (x, y) packed into one value, so it is copied by value and never allocates. Every operation is exact integer arithmetic — no rounding, and bit-identical on every platform. Use it wherever a fractional part would be meaningless; reach for Vector when you need sub-pixel precision. Arguments are positional.

Map — tilemap

A character grid the game paints and reads.

Rect

Axis-aligned rectangles for HUD layout boxes, hitboxes, and camera regions. A Rect is four Q16.16 fixed components — position (x, y) (its top-left corner) and size (w, h) — packed into a single value that is copied by value and never allocates. It offers fast point-in-rect and rectangle-overlap tests. Every operation is deterministic fixed-point, bit-identical on every platform. Arguments are positional.

Udp

Plain IPv4 datagrams, polled — the transport under a game's own netcode: peer-to-peer play, a LAN lobby, a STUN query. Open a socket with Udp.open, send with Udp.send, and each frame drain what arrived with Udp.recv, which returns 0 when nothing is waiting and never blocks. The sender of the last datagram read is Udp.from_ip / from_port. An address is an int: a.b.c.d is (a << 24) | (b << 16) | (c << 8) | d, converted by Udp.ip and Udp.ip_text. Datagrams can be lost, repeated and reordered; the protocol a game builds on top says what to do about that. Like Http.* it is out-of-band and must never feed the deterministic lockstep/replay simulation directly. BSD sockets on macOS, Winsock on Windows.

BigInt

Arbitrary-precision integers with no upper bound, for idle/incremental counters, exact huge currencies, and score arithmetic that a 32- or 64-bit int would overflow. A BigInt is a sign-magnitude number stored as base-1e9 limbs, so every operation is exact and therefore deterministic — bit-identical on every platform, with no binary floating point. Build one with BigInt.from (an int) or BigInt.parse (decimal text), combine with add / sub / mul / pow / div / mod, compare with cmp / eq / is_zero, and render with str. Arguments are positional. The runtime is spliced in only when a program mentions BigInt.*.

Color — the named palette

Color.Name lowers to a plain 0xRRGGBB integer at compile time — no runtime cost, identical to writing the hex by hand, but readable. 221 names are built in; a custom shade is any hex literal or a named const. The full palette:

Process

Child processes, started and polled — what a launcher is made of. Process.spawn starts a program directly, with no shell, and returns at once; each frame Process.poll answers -1 while the child runs and its exit code once it has ended, so a crash is a non-zero code the game can act on. Process.kill ends a child and Process.free lets a handle go. Pair it with App.window_hide to step aside while the child runs. The child inherits the environment and the working directory. posix_spawn on macOS; CreateProcessW on Windows, with each argument quoted by the MSVC rules and no console window. Like Http.* it is out-of-band and must never feed the deterministic lockstep/replay simulation.

Decimal

Exact base-10 fixed-point numbers for game economies — prices, balances and taxes on values like 0.10 that binary floating point cannot represent, so they always add up exactly. A Decimal is a BigInt mantissa with a decimal scale (the number of digits after the point), giving unbounded range and exact add / sub / mul. Build one with Decimal.from (an int) or Decimal.parse (text like "19.99"), compare with cmp / eq, change precision with rescale (truncates toward zero), read the current precision with scale, and render with str. Every operation is exact and deterministic. The runtime is spliced in only when a program mentions Decimal.* (or BigInt.*).

Types

Ludic is statically typed; most code uses just int.

Dict

A hash map from string keys to int values — the everyday lookup a game needs, like resource counts or an id registry by name. A Dict is backed by an open-addressing hash table (FNV-1a, linear probing, tombstone deletes, growing at load factor 0.7), so get/set/has are O(1) on average rather than the linear scan a list would give. Create one with Dict.new, then set / get / get_or / has / remove / size / clear / keys. Values are int (which also holds an entity or any small id); for richer values use the Value.* tree. Arguments are positional. The runtime is spliced in only when a program mentions Dict.* (or Set.*).

Operators & tokens

The symbols the grammar recognizes.

Builtin functions

Global functions available anywhere, beyond the namespaced APIs above.

printPrint an int or string followed by a newline — for headless tests and debugging.stringConvert an int, bool, or fixed value to text; a string passes through unchanged.quitStop the game loop cleanly after the current frame finishes.saveSerialize the whole world — every model instance, property, and program var — in one call.loadRestore the world from a snapshot previously written by save().lenThe number of elements in a slice, or the byte length of a string.pushAppend one element to the end of a growable slice.absThe absolute value of an integer (its magnitude, never negative).clampConstrain a value to the inclusive range [lo, hi].panicAbort with a located error message instead of crashing.assertAbort with a located message when an invariant is false.okWrap a success payload in a result — the happy half of try/else.errWrap a failure message in a result — the sad half, recovered by try/else.is_okTrue when a result carries a success payload.is_errTrue when a result carries a failure.someWrap a present value in an option.noneThe empty option — a missing value.is_someTrue when an option holds a value.is_noneTrue when an option is empty.unwrap_orThe option value, or a fallback if empty.exitTerminate the process immediately with a status code.runRun a string as a shell command.getenvRead an environment variable, returning empty when it is unset.read_charRead one byte from standard input, or -1 at end of input.argThe i-th command-line argument as a string.arg_countThe number of command-line arguments, counting the program name.bytesAllocate a raw buffer of n bytes and return a pointer to it.file_stderrThe standard-error stream handle for use with file_write.file_stdoutThe standard-output stream handle for use with file_write.file_writeWrite a run of raw bytes to a stream handle.fixedConvert an int (or a float) into a Q16.16 fixed-point value.floorFloor a fixed-point value down to the nearest integer.maxThe larger of two integers.minThe smaller of two integers.ui_buildBuild the declared UI tree so it can be opened and rendered.wordsAllocate a raw buffer of n 32-bit words, indexable with [i].floatConvert any number to a float (or double).intConvert a number to a whole number, truncating toward zero.float_bitsA float's IEEE bit pattern as an int, and back.

Set

A set of string members — membership tests for tags, unlocked achievements, or visited tiles. A Set shares the same open-addressing hash table as Dict, so add / has / remove are O(1) on average and duplicates are ignored. Create one with Set.new, then add / has / remove / size / clear / members. Arguments are positional. The runtime is spliced in only when a program mentions Set.* (or Dict.*).

Annotations

@Name(...) decorators attach compile-time behavior to a handler, property, or function.

@QueriesDeclare the properties a handler operates on, binding their fields by name in the body.@ComputedA derived field expanded inline wherever it is read, never stored.@OnRegister a compile-time listener that runs whenever an event is emitted.@exportExpose a function as a native symbol so a host can call it.@SyncMark fields as replicated and models as owned so the compiler generates networking.@HandlesDeclarative hint naming the event or subsystem a handler is responsible for.@OnAttachRun a handler when a property is structurally attached to a live instance.@OnDespawnRun a handler when a model instance is torn down, optionally knowing why.@OnDetachRun a handler when a property is structurally detached from a live instance.@OnDisableRun a handler when a property is paused (disabled) on an instance.@OnEnableRun a handler when a paused property is re-enabled on an instance.@OnQuitRun a handler once at shutdown, after the last frame.@OnSpawnRun a handler once each time an instance of a model is spawned.@OnStartRun a handler once at boot instead of assigning it a frame phase.@OwnedGive a model a network owner slot so its instances can be assigned to a peer.@PredictedRun a control handler on the owning client speculatively and on the server authoritatively.@PublicPromote a lifecycle hook to a public event other modules can listen for.@ReadsDeclare that a handler reads a property — an analysis and scheduling hint.@ServerRun a handler only on the authority; clients receive the result via @Sync.@ToClientsA remote event broadcast from the server to clients — a server→clients notification.@ToServerA remote event sent from a client to the server — a client→server request.@WritesDeclare that a handler writes a property — an analysis and scheduling hint.@EngineSystemRegister a package's engine-owned system so the frame loop runs it each phase.@NamespaceLet a package provide a Name.method(…) namespace that dispatches to name_method."@ClearColor"Declare a clear colour so the Render phase auto-clears + auto-presents for you.@SystemRegister a prebuilt binary module's system with the host at load, per phase.

Huge

Idle/incremental big numbers — magnitudes far past what a 32- or 64-bit integer can hold, for prestige currencies and exponential growth. A Huge is a normalized mantissa (a Q16.16 fixed in [1, 10)) times 10^exponent, so it can represent 10^100 or 10^1000 while staying one small value. It is a display-scale number (about four significant digits), not a lockstep-exact one — keep it out of the deterministic simulation and reach for BigInt / Decimal when exactness matters. Build one with Huge.from, combine with add / sub / mul / neg, compare with cmp / sign, read mantissa / exp, and render as 1.23e45 with str. Arguments are positional. Spliced in only when a program mentions Huge.*.

Angle

An auto-wrapping angle in radians, so you never juggle % TAU by hand. Every Angle.* result is normalized into [-pi, pi), which makes diff the shortest signed rotation between two headings and lerp turn the short way around. Build from degrees with Angle.from_degrees (read back with to_degrees), take sin / cos, combine with add, and normalize any raw value with wrap. It builds on the deterministic fixed-point Math.* trig, so results are bit-identical on every platform. Arguments are positional; angles are fixed radians. Spliced in only when a program mentions Angle.*.

Percent

A value clamped to [0, 1] — health fractions, volumes, and the t of an interpolation, without stray values slipping below 0 or above 1. Percent.clamp pins any fixed into range; Percent.of forms a clamped ratio num / den; Percent.lerp interpolates a..b by a clamped t; and Percent.apply scales a value by a clamped fraction. Every operation is deterministic fixed-point. Arguments are positional. Spliced in only when a program mentions Percent.*.

Regex

Regular expressions with PCRE/PECL-compatible syntax over a **linear-time** Thompson NFA (a Pike VM), so a bad pattern from a modder can never trigger catastrophic backtracking — matching is O(n·m), never exponential. Compile a pattern once and reuse it; an invalid pattern is an error value, never a crash. Supported: literals, ., classes [...] with ranges/negation and \d \w \s (and their negations), anchors ^ $, alternation |, capturing and (?:…) groups, and the quantifiers * + ? and {n,m} in greedy or lazy form. Backreferences and look-around are out of scope for a linear engine.

Grid

Tile geometry and pathfinding over the Map tilemap. A cell is passable unless it is out of bounds or holds the caller's wall tile (a char code, e.g. '#'), so any impassable glyph works. Everything is integer and deterministic — same map and query reproduce the same path every run. Grid.line/Grid.flood/Grid.a_star return Cell slices (index them with len / [i]; each cell has .x and .y).

Anim

Spritesheet frame animation off the fixed frame clock. Store an elapsed timer (seconds, a fixed) on a component and each frame ask Anim.frame / Anim.once / Anim.pingpong which cell to draw; Anim.cell_x/Anim.cell_y turn a frame index into a source rectangle on the sheet. Everything is integer/fixed and deterministic — the same timer reproduces the same frame every run, so replays and lockstep netcode match exactly. For the ECS engine-owned SpriteAnim component there is also an ergonomic layer: register named clips with Anim.clip and play them by name with Anim.play, and arm frame events with Anim.on_frame / Anim.fired.

Motion

Ergonomic control of the engine-owned Motion component — the value tween the engine advances each tick (see the ECS engine-systems). Motion.to starts a tween on an entity from one value to another over a number of ticks with an easing curve, rewinding it so it plays from the start, instead of setting the component's from/to/dur/ease fields by hand. The tween is integer and deterministic, so motion reproduces exactly under replay and lockstep. For a standalone, sequenced tween not tied to a component, see the fluent Tween handles. Related: Anim, Ease, Tween.

Tween

Value interpolation over a timeline, off the fixed frame clock. The timeline helpers Tween.progress/Tween.loop/Tween.yoyo turn an elapsed timer and a duration into a normalized amount; Tween.ease shapes that amount through an easing curve (shared with Ease); and the typed blends Tween.number/Tween.round/Tween.point/Tween.tint interpolate a fixed, int, Vector, or color. All deterministic fixed-point, so a replay reproduces every eased value exactly. Beyond these pure interpolators there are fluent handles the engine advances for you: Tween.to starts one and returns a handle, Tween.chain and Tween.delay sequence more segments, and Tween.value / Tween.done / Tween.parallel / Tween.stop read and control them.

Query

Spatial and set queries over the live entities that carry a property, built on the World reflection ABI. Query.count and Query.first ask how many bearers there are and which is first; Query.nearest and Query.within add a position — read from a coordinate property's two int fields — to find the closest entity to a point or every entity inside a radius. A property id comes from World.prop_id and a field id from World.field_id. Everything is integer and deterministic: the same world reproduces the same answers, entity order included, every run. The scan is linear over the entity table — ample for the entity counts Ludic targets — and a game that uses Query.* gets the reflection ABI emitted automatically, no @event required.

Reflect

Runtime reflection over the world schema — inspect properties, fields, and entity state by name and by index. It powers the conveniences game devs consume without writing reflection code: automatic save/load, data-driven tools, and debug/inspector overlays. Enumerate with Reflect.prop_count/Reflect.prop_name and Reflect.field_count/Reflect.field_name/Reflect.field_type; resolve ids with Reflect.prop/Reflect.field; read and write an entity's fields with Reflect.get/Reflect.set/Reflect.has; and identify its model with Reflect.kind/Reflect.model. The metadata tables are generated at compile time (the same reflection ABI a foreign mod binds), so introspection is a table walk, not heavy runtime machinery. This is an advanced/tooling surface — most developers get its benefits through built-in features. Related: World, Query.

Camera

A world-space camera — a draw offset threaded through the render path (the same offset Screen.camera sets). Camera.set places it, Camera.follow eases it toward a target, and Camera.shake jitters it from the seeded RNG for impact and explosions. Everything is integer and deterministic — driven off the same seed and inputs, a replay reproduces the exact camera path, shake included. The camera moves everything drawn; reset it to (0, 0) to draw a fixed HUD.

Light

A software 2D light-accumulation pass over the framebuffer, run in a render phase after drawing the scene and before Screen.show. Light.ambient multiplies the whole scene toward a tint — the night/cave modulate that darkens everything so lights add mood back on top. Light.point accumulates a radial glow that falls off with distance and clamps per channel, and Light.occlude registers rectangles that block a light's rays to cast hard shadows (cleared each frame with Light.clear_occluders). Lighting is a rendering concern only — it never touches game state or replays — and it is fully deterministic (integer and Q16.16 fixed), so the same scene lights identically every run and in a headless render, keeping screenshots diffable. Colours are 0x00RRGGBB. Beyond the radial core, the pass carries the render-quality tiers: Light.spot cones, a Light.falloff exponent, Light.soft shadows (penumbra), Light.gel colour cookies, normal-mapped surfaces (Light.normal + Light.height) that shade by facing, and a Light.time_of_day day/night ramp — every one deterministic. This is the imperative surface; the engine also consumes Light2D/Occluder components automatically (a Light2D may carry optional direction/spread/falloff/softness/gel fields). Related: Screen, Color, Camera.

Sprite

A namespaced spritesheet / atlas API. Sprite.sheet loads one image and remembers its cell grid; Sprite.cell addresses a cell by grid coords and Sprite.cell_span a sprite that spans more than one cell; Sprite.define / Sprite.named name and look up a cell; Sprite.draw / Sprite.draw_scaled blit it (through the camera / zoom / clip). It stands on the variable-size image loader, so a cell can be any size — not just 16x16. (Distinct from the Sprite engine component of the sprite-render system.)

Assets

Loading and lookup for whole-file assets, paired with the Sprite.* atlas API. Assets.image (and its alias Assets.load) loads a whole image file as one sprite; Assets.get looks a named sprite up.

Testing

A built-in testing framework in the spirit of Go's go test: tests live next to the code, run with one command, and report pass/fail — no harness to wire up. A test "name" { … } block is discovered automatically and run by a synthetic entry point that prints ok - name or FAIL - name for each, a == N passed, M failed == summary, and exits non-zero if anything failed (so CI and the bin/ludic runner catch it). Inside a test, the expect, expect_eq and expect_near assertions check a condition and, on failure, print file:line: … failed (got …, want …) and mark the test failed — without aborting, so one run reports every failure. expect_near takes a tolerance, which is what fixed-point and accumulated-integer game math need. Everything is deterministic and compiles to a native binary, so a suite runs in the same C-free toolchain as the rest of Ludic. Coverage instrumentation is a planned follow-up. Related: function, print.

Pool

Entity-pool statistics. Ludic's ECS is already pool-based: the allocator recycles freed entity slots through a freelist (a despawned slot is reused by the next spawn before any new slot is taken), and component storage is fixed per-entity arrays — so spawning and despawning many entities per frame does no per-spawn heap allocation and cannot fragment. Pool.live / Pool.free / Pool.reserved / Pool.capacity read those counters so a bullet-hell or horde game can watch reuse and budget against the cap.

Value

A generic, self-describing value tree — the node type reflection serializes into and JSON round-trips through. A node is one of null, int, fixed, bool, str, list, or object (see Value.kind). Build one with the constructors (Value.int, Value.object, …) and the builders Value.add/Value.put; read it with Value.get/Value.at/Value.count and the as_* accessors. Related: Json, Reflect.

Json

The text bridge over the Value tree: Json.encode turns a value tree into compact, stable JSON text and Json.parse reads it back. Together with Reflect.serialize/Reflect.apply this is a one-call, bit-exact save/load for entities, and a diffable on-disk format for tooling. Determinism holds: the same tree always encodes to the same bytes.

Job

Background work that stays out of the frame. A Job is a future — a handle to a result that lands later. Kick one off with Job.run (a background compute that advances a little each Job.pump and finishes after enough frames, so heavy work never hitches) or Job.defer (a future you resolve yourself with Job.fulfill / Job.fail). Poll it with done / ok / failed / cancelled, read result / error, and always collect on the main thread — a Job must never touch the ECS world directly. The scheduler is deterministic and cooperative, so the same jobs and the same budget reproduce byte-for-byte, every run and every target. For work that should use every core now, Job.parallel_for(count, fn work, ctx) runs work(i, ctx) across a pool of real OS threads and returns when all of it is done; a worker computes on what it was handed and never changes the world. Arguments are positional. Spliced in only when a program mentions Job.*.

Promise

Combine several Job futures and resolve the group on the main thread. Promise.all succeeds once every member has, Promise.race once the first does; both return an ordinary job handle you poll like any other. For a loading screen, Promise.count_done over the same handles is the bar's numerator and len the denominator, and Promise.all_done is the ready check. Ludic has no closures, so progress is polled rather than chained through a then callback. Build the handle list with new []int + push. Spliced in only when a program mentions Promise.*.

Sync

The advanced, opt-in tier — here be dragons. Raw building blocks for engine-level systems that pass data around: a mutex (cooperative lock), an atomic counter (get / set / add / cas) and a bounded channel (send / recv / can_recv / len). On today's single-threaded deterministic runtime these are cooperative — correct, ordered, replayable and impossible to deadlock — and exist so message-passing code reads the same now as it will when a preemptive OS-thread backend lands behind this same API. Beginners never need this; reach for Job.* / Promise.* instead. Spliced in only when a program mentions Sync.*.

Xml

A minimal, deterministic XML reader for the element/attribute/CDATA subset the native Tiled formats (TMX/TSX/TX) use. Xml.parse turns a document into an element tree; the accessors read a node's tag, text, attributes (Xml.attr/Xml.attr_int/Xml.has) and children (Xml.child/Xml.find/Xml.count). It expands the five predefined entities and numeric character references, skips the <?xml?> prolog, comments and <!DOCTYPE>, and is best-effort rather than validating — the same contract as Json. Spliced on demand when a program mentions Xml.*.

Base64

Standard base64 (RFC 4648) — Base64.decode reads base64 text into the bytes it stands for, and Base64.encode is the inverse. The decoder ignores ASCII whitespace, so it reads the newline-wrapped base64 that Tiled writes inside a <data> element; that output then feeds the DEFLATE inflater for zlib/gzip-compressed layer data. Deterministic; spliced on demand when a program mentions Base64.*. (The security-sensitive encoder Crypto.base64 is a separate, hardened path.)

Tiled

Load and draw Tiled maps. Both native format families — TMX/TSX/TX (XML) and TMJ/TSJ/TJ (JSON) — read onto one intermediate, a Value tree in Tiled's JSON schema, with layer data decoded to a dense GID list; so a CSV .tmx and a base64+zlib .tmj of the same map read identically. Tiled.read returns that intermediate tree for a map file and Tiled.read_tsx for a tileset file. Spliced on demand when a program mentions Tiled.*, alongside the Xml, Base64 and Value runtimes it builds on.

Tiled.readRead a map file into the intermediate value tree.Tiled.read_tsxRead a tileset file into a value object.Tiled.loadLoad a map file into the runtime map model.Tiled.gidThe raw GID at a cell in a tile layer.Tiled.resolveDecode a GID into tileset, local id and flip flags.Tiled.widthThe map width in tiles.Tiled.heightThe map height in tiles.Tiled.layer_countHow many layers the map has.Tiled.layer_nameA layer's name by index.Tiled.drawDraw every visible tile layer.Tiled.projectProject a collision layer to the byte tilemap.Tiled.treeThe underlying intermediate value tree.Tiled.tile_propWhether a GID's tile carries a bool property.Tiled.collideDrive collision from per-tile metadata.Tiled.collision_kindClassify a GID's collision from its tile metadata.Tiled.tile_shapesWhether a GID's tile has a collision shape.Tiled.draw_animDraw with animated tiles advanced to a frame.Tiled.frame_gidThe current GID of an animated tile at a frame.Tiled.animatedWhether a GID's tile is animated.Tiled.object_countHow many objects an object layer holds.Tiled.objectAn object on an object layer by index.Tiled.object_shapeClassify an object's shape.Tiled.propA custom property's value, with class-default fallback.Tiled.prop_intA custom property parsed as an integer.Tiled.prop_typeA custom property's declared type.Tiled.load_typesLoad a project custom-type table.Tiled.templateRead a template file into an object value.Tiled.spawnSpawn a Ludic entity from an object.Tiled.spawn_layerSpawn every object on a layer.Tiled.cell_xThe screen x of a tile cell for the map orientation.Tiled.cell_yThe screen y of a tile cell for the map orientation.Tiled.layer_kindA layer's kind.Tiled.layer_opacityA layer's opacity.Tiled.layer_tintA layer's tint colour.Tiled.layer_offsetxA layer's horizontal offset.Tiled.layer_offsetyA layer's vertical offset.Tiled.worldRead a .world file.Tiled.world_countHow many maps a world stitches.Tiled.world_mapA world member map by index.

Ui

The retained-mode menu API over a ui block. Ui.build constructs every declared widget tree once (after fonts and skins are loaded); Ui.open makes one menu active and Ui.close deactivates it; the frame loop ticks navigation on its own, an activation fires the UiClicked event, and Ui.clicked is the polled form; Ui.set_text updates a label or button, and Ui.render draws the active menu (call it from an Overlay handler so it paints over the world). Every id: Name in a ui block mints a UI_Name handle.

File

Raw file access over the C stdio calls, for programs that read and write their own formats. File.open returns a handle (or null), File.read / File.write move bytes through a bytes buffer, File.seek / File.tell position the cursor, and File.close releases the handle. For text and structured data prefer the higher-level Fs, Json and Prefs APIs.

Font

TrueType fonts for the retained UI and for Screen.draw_text-style drawing. Font.load reads a .ttf / .ttc file and returns a handle that a ui block's font: property or a text call takes.

Fx

Engine-owned transient effects. A game asks for a burst of sparks or a floating number and the engine owns the rest: it moves and ages them every Update, draws them every Render after the sprites (through the camera, shake and clip), and drops them when they expire. Nothing is an entity, so a hit effect needs no component, model, handler or draw call. Velocities and lifetimes come from the seeded RNG, so a replay produces the same sparks.

Prefab

A prefab is a model with preset component fields — prefab Grunt: Creature { Stats { hp: 30 }, Weapon { def_id: 1 } } — spawned with spawn Grunt { Position { x: 40 } }, where the spawn's own fields override the presets. Prefabs chain (prefab Grunt: Foe, where Foe is itself a prefab) so shared presets live once. spawn is also an expression yielding the new entity (let e = spawn Grunt { … }), and Prefab.spawn spawns a prefab chosen at runtime by name.

App

The running application, as distinct from its window. Today that is the boot splash: the runtime raises it before main from the game's asset pack, and App.splash_hide takes it down when the game has something to show instead. App.window_hide and App.window_show take the game's own window off the screen and bring it back without closing it — a launcher stepping aside while the game it started runs.

Gl

OpenGL for Ludic. Gl.* binds the whole OpenGL 4.1 core API: every gl* entry point of the platform gl3.h is a method named by its snake case (glBindBuffer → Gl.bind_buffer, glTexImage2D → Gl.tex_image2d, glDrawElementsInstanced → Gl.draw_elements_instanced), with every GL_* constant available as written. Parameters keep the header's names as labels (glVertexAttribPointer's pointer is called offset, program is prog); GLfloat/GLdouble parameters take fixed, pointer parameters take the raw bytes/words buffers Ludic already has, and 64-bit sizes take long (an int widens). The binding is generated by ludic-dev glgen from the header, with one ABI thunk per entry point. A context comes from Gl.open(width, height, title): windowed, an NSOpenGLContext on the game's window at the display's backing resolution (2× on Retina — Gl.width()/Gl.height() are the drawable's pixels); headless, an offscreen context with a framebuffer standing in for the screen (Gl.screen_fbo()), so the same program renders and screenshots under the test harness. Gl.swap() presents, Gl.screenshot(path) writes the screen as a PPM, Gl.check(tag) prints any pending error. The glue: Gl.program(vs, fs) / Gl.program5(vs, tcs, tes, gs, fs) compile and link (logs on failure), Gl.uniform(prog, name), Gl.vao(), Gl.buffer(), Gl.texture(), Gl.framebuffer() allocate, and float data is filled from Q16.16 with Gl.floats(n) / Gl.put(buf, i, v) / Gl.bytes_of(n) or from IEEE bits with Gl.put_bits; Gl.f32(v) and Gl.fixed(bits) convert single values. The bare f_add/f_mul/… helpers do IEEE single-precision arithmetic on those bit patterns for programs that want real floats on the CPU. Using Gl.* links gl.ll, the thunks and OpenGL.framework; a program that does not is byte-identical to before. The ludic.render3d package is a physically based 3D renderer written on this surface (see examples/rendering/smooth.ludic here, and Maroon Lake for a game built on it). ``ludic program Triangle { property Marker { on: int = 1 } model Anchor { Marker } var prog: int = 0 var vao: int = 0 handler Boot phase Start { spawn Anchor {} if not Gl.open(width: 640, height: 360, title: "GL") { quit() } prog = Gl.program(vs: "#version 410 core\nvoid main(){ gl_Position = vec4(float(gl_VertexID == 1) * 2.0 - 0.5, float(gl_VertexID == 2) * 2.0 - 0.5, 0.0, 1.0); }\n", fs: "#version 410 core\nout vec4 o; void main(){ o = vec4(1.0, 0.5, 0.2, 1.0); }\n") vao = Gl.vao() } handler Draw phase Render { Gl.clear_color(red: 0.1, green: 0.1, blue: 0.15, alpha: 1.0) Gl.clear(mask: GL_COLOR_BUFFER_BIT) Gl.use_program(prog: prog) Gl.bind_vertex_array(array: vao) Gl.draw_arrays(mode: GL_TRIANGLES, first: 0, count: 3) Gl.swap() } } ``

Vk

Vulkan for Ludic. Vk.* binds Vulkan 1.0–1.4 and the extensions a modern renderer is built around — swapchain and HDR colour spaces, ray query and acceleration structures, opacity micromaps, mesh shaders, variable rate shading, memory budget, pipeline libraries, NVIDIA low latency, and portability enumeration for MoltenVK. Every command is a method named by its snake case (vkCreateInstance → Vk.create_instance, vkCmdDrawIndexedIndirectCount → Vk.cmd_draw_indexed_indirect_count), every VK_* constant is available as written, and every struct has its size and field offsets as constants: VkDeviceCreateInfo_sizeof, VkDeviceCreateInfo_queueCreateInfoCount. The binding is generated by ludic-dev vkgen from the Vulkan registry (vk.xml), and every size and offset is checked against the SDK's C headers. A struct is plain memory filled by field name: bytes(Vk…_sizeof), Vk.zero(p, n), then Vk.put_i32 / Vk.put_i64 / Vk.put_ptr(p, offset, value), read back with Vk.get_i32 / get_i64 / get_ptr, and Vk.at(p, offset) for a nested struct or an inline array. Numbers follow the C types: a float is its IEEE bit pattern in an int, and uint64_t, VkDeviceSize and non-dispatchable handles are long. A negative long has to come from a long variable — an int literal passed straight to a long parameter is zero-extended. The loader is opened at run time by Vk.open(), never linked: vulkan-1.dll on Windows, libvulkan.1.dylib (the LunarG loader and MoltenVK) on macOS — beside the executable, in the library paths, or under $VULKAN_SDK. It returns 0 on a machine without Vulkan, and the program carries on; Vk.has(name) says whether one command is there. Using Vk.* links the generated thunks and the loader; a program that does not is unchanged. See examples/rendering/vk_probe.ludic (what a machine's Vulkan can do) and vk_compute.ludic (a Slang compute shader, dispatched and read back). ``ludic program Probe { property Marker { on: int = 1 } model Anchor { Marker } handler Boot phase Start { spawn Anchor {} if Vk.open() == 0 { print("no Vulkan here"); quit() } let app = bytes(VkApplicationInfo_sizeof) Vk.zero(app, VkApplicationInfo_sizeof) Vk.put_i32(app, VkApplicationInfo_sType, VK_STRUCTURE_TYPE_APPLICATION_INFO) Vk.put_i32(app, VkApplicationInfo_apiVersion, (1 << 22) | (3 << 12)) let ci = bytes(VkInstanceCreateInfo_sizeof) Vk.zero(ci, VkInstanceCreateInfo_sizeof) Vk.put_i32(ci, VkInstanceCreateInfo_sType, VK_STRUCTURE_TYPE_INSTANCE_CREATE_INFO) Vk.put_ptr(ci, VkInstanceCreateInfo_pApplicationInfo, app) let out = bytes(8) if Vk.create_instance(ci, null, out) == VK_SUCCESS { print("a Vulkan instance") Vk.destroy_instance(Vk.get_ptr(out, 0), null) } quit() } } ``

Phases