On this page
tecs.Application
The application that an entry chunk returns.
The host owns the loop and calls the application to initialize, process events, iterate, and shut down. Game code registers systems and observers instead of calling a blocking run function:
return tecs.newApplication({
window = {
title = "Game",
width = 1280,
height = 720,
},
plugin = function(world: tecs.World, app: tecs.Application)
local transforms <const> = world:newQuery({
include = {tecs.Transform2D},
})
world:addSystem({
name = "game.Move",
phase = tecs.ecs.phases.Update,
run = function(dt: number)
for archetype, length in transforms:iter() do
local column <const> = archetype:getMut(
tecs.Transform2D
)
for row = 1, length do
column[row].x = column[row].x + 120 * dt
end
end
end,
})
world:spawn(tecs.Transform2D(100, 100))
end,
})Lifecycle
The entry plugin receives the world and application after engine subsystem installation and before startup phases. It registers the game. The world then owns the scheduled lifecycle:
| Stage | Game API |
|---|---|
| Startup | PreStartup, Startup, and PostStartup |
| Event | world:observe(0, tecs.platform.events.on.<kind>, handler) |
| Frame | The phases driven by world:update(dt) |
| Shutdown | PreShutdown, Shutdown, and PostShutdown |
Every iteration runs the same eight steps in the same order, and the order is the contract: a system reads what the steps above it settled during this frame rather than one frame later.
- The application starts the input frame and drains every platform event SDL delivered since the previous iteration. A quit request ends the iteration here.
tecs.runtime.pollsettles process-wide asynchronous work: finished asset loads, child processes, dialogs, generic I/O, HTTP clients, and file changes.- The debug server answers,
world:update(dt)runs the phases, and the renderer records and submits one frame. - The audio mixer updates, which reaps the voices it has finished with.
Steps 2 and 4 run whatever else is true of the process. A decode that finished, a child that exited, a transfer that completed and a file that changed have all already happened, and a voice the mixer is done with has to be reaped either way, so a suspended run and a crashed one keep settling them. Step 7 is the one the guards hold back. A suspended application skips all of it, and a crashed one skips the world update and the rendering while still answering the debug server, so an agent that was watching gets the traceback.
Event observers run inside step 1, before the world update and outside every phase. They see input state after the application folds the event in. Store event state in a component or a resource when a scheduled system has to react to it.
Checkpoints and crashes
Mobile platforms may suspend a process before another frame runs. Prepare a snapshot during an ordinary frame and stage its bytes before that deadline:
local save <const> = world:saveSnapshot().buffer
app:stageCheckpoint(tostring(save))The application writes the latest staged bytes when the platform backgrounds or terminates it. The next entry plugin can call readCheckpoint and load the result.
The application catches errors from game systems, observers, plugins, and startup phases. It records the first traceback while the host continues to serve platform events and the debug connection. The guard restores engine-owned scopes, not partially changed game state. Use clearCrash only as a development reload aid.
Methods whose names begin with an underscore belong to the host contract.
Module contents
Constructors
| Constructor | Description |
|---|---|
newApplication |
Builds an application. |
Types
| Type | Kind | Description |
|---|---|---|
Config |
record | Caller-writable. Sets the window to open, as Options describes it. |
EntryPlugin |
type | Configures the world owned by an application. |
Functions
| Function | Kind | Description |
|---|---|---|
checkpointPath |
Instance | Returns the checkpoint path, or nil when the config names none. |
clearCrash |
Instance | Clears a recoverable gameplay crash so development can render another frame. |
crashed |
Instance | Returns the gameplay traceback, or nil while the game is healthy. |
readCheckpoint |
Instance | Reads the bytes the last run left, or returns nil when there are none. |
stageCheckpoint |
Instance | Hands the engine the bytes to write when the platform backgrounds us. |
Values
| Value | Type | Description |
|---|---|---|
audio |
Audio |
Read-only. Exposes the audio mixer. |
device |
Device |
Read-only. Exposes the GPU device. |
elapsed |
number |
Engine-owned. Reports seconds of simulated time. |
frame |
integer |
Engine-owned. Reports completed application iterations. |
input |
Input |
Read-only. Exposes current input state. |
mcp |
Server |
Read-only. Exposes the debug server when configuration requested one. |
quitRequested |
boolean |
Caller-writable. Set to true to leave the loop at the end of the current iteration. |
renderer |
Renderer |
Read-only. Exposes the renderer that extracts this world. |
suspended |
boolean |
Engine-owned. Reports true while the platform has the application in the background. |
window |
Window |
Read-only. Exposes the application window. |
world |
World |
Read-only. Exposes the world driven by this application. |
Constructors
tecs.Application.newApplication Static
Builds an application. Return the result from the entry chunk.
Reached as tecs.newApplication, at the root rather than under a module, because an application is not a subsystem: it owns the window, the device, the input, the renderer and the audio. Tecs hands it to the entry plugin beside the world.
function tecs.Application.newApplication(
config: Application.Config
): ApplicationArguments
| Name | Type | Description |
|---|---|---|
config |
Application.Config |
Tecs reads this table here and ignores later changes. This call opens nothing. |
Returns
| Type | Description |
|---|---|
Application |
The application, at rest until the host's first callback. |
Types
tecs.Application.Config record
record tecs.Application.Config
window: Window.Options
debug: boolean
framesInFlight: integer
presentMode: string
ambientLight: {number}
audio: Audio.Config
logFile: string
logLevel: integer
checkpoint: string
mcpPort: integer
watch: watcher.Config
capacity: integer
sprites: boolean
meshes: Renderer.MeshOptions
timestep: number
fixedMaxSteps: integer
fixedOverload: ecs.FixedOverload
maxEntities: integer
reserveRuns: boolean
partialRewrites: boolean
packImages: boolean
shadows: Deferred.ShadowOptions
debugMaxFrames: integer
quitOnEscape: boolean
plugin: EntryPlugin
endtecs.Application.Config.window field
Caller-writable. Sets the window to open, as Options describes it. The application passes the table through whole.
tecs.Application.Config.window: Window.Optionstecs.Application.Config.debug field
Caller-writable. Marks this run as a development run.
Tecs creates the device with validation layers enabled and writes the log file below by default. This setting also joins the three settings clearCrash looks at, beside mcpPort and watch, to decide whether resuming after a gameplay crash is something this build does at all.
tecs.Application.Config.debug: booleantecs.Application.Config.framesInFlight field
Caller-writable. Sets how many frames the CPU may run ahead of the GPU. The device defaults to two.
tecs.Application.Config.framesInFlight: integertecs.Application.Config.presentMode field
Caller-writable. Selects "vsync", "immediate" or "mailbox" presentation. The device defaults to "vsync".
tecs.Application.Config.presentMode: stringtecs.Application.Config.ambientLight field
Caller-writable. Sets the light every surface receives before any light entity contributes, as red, green and blue. Defaults to white.
White rather than a dim gray, because a scene that has placed no lights is the first thing anyone builds and it should look like what it is: sprites at their own color, with lights adding on top of them. A game doing its own lighting turns this down to the level it wants the unlit parts of the scene to sit at.
tecs.Application.Config.ambientLight: {number}tecs.Application.Config.audio field
Caller-writable. Configures sound output. Omitted takes the defaults, which open the platform's default device.
tecs.Application.Config.audio: Audio.Configtecs.Application.Config.logFile field
Caller-writable. Sets a JSON Lines log file under the writable root. Omitted creates one when mcpPort or debug is set and otherwise keeps logging at the platform destination.
tecs.Application.Config.logFile: stringtecs.Application.Config.logLevel field
Caller-writable. Sets the lowest priority that reaches the log, as one of log.TRACE, log.VERBOSE, log.DEBUG, log.INFO, log.WARN, log.ERROR or log.CRITICAL. Omitted leaves the platform default. The level applies to every category and does not change whether logFile stores the accepted messages.
tecs.Application.Config.logLevel: integertecs.Application.Config.checkpoint field
Caller-writable. Sets the file name under the writable root that receives a checkpoint when the platform backgrounds or terminates the application. Omitted means no checkpoint, and stageCheckpoint says so.
See Application:stageCheckpoint for what a game has to do, which is the part that matters.
tecs.Application.Config.checkpoint: stringtecs.Application.Config.mcpPort field
Caller-writable. Sets the MCP server port. After the server starts, the application appends [MCP :port] to the window title. Omitted means no server, since a game should not open a socket nobody asked for.
tecs.Application.Config.mcpPort: integertecs.Application.Config.watch field
Caller-writable. Watches loaded content files and reloads them when they change. Omitted means no watcher, on the same footing as the server above: a poll of the filesystem is not something to start because a build happened to be able to. A release refuses it.
tecs.Application.Config.watch: watcher.Configtecs.Application.Config.capacity field
Caller-writable. Sets the renderer buffer capacity in instances. Defaults to 65536.
An instance is a row the GPU draws: every renderable entity is one, and so is every glyph of every text, which an instance producer writes into a run of its own rather than spawning an entity per glyph. This is a ceiling rather than a hint. Rows past it are dropped rather than growing a buffer mid-frame, and renderer.sprites.dropped counts them.
Sized independently of maxEntities beside it, because the two count different things: a world full of entities that carry no Renderable2D needs no instances at all, and one text entity needs one instance per character.
tecs.Application.Config.capacity: integertecs.Application.Config.sprites field
Caller-writable. Enables the 2D sprite domain unless set to false. Defaults to true. Disabling it creates no sprite image array, packet, buffers, or world queries, and makes renderer.sprites nil.
tecs.Application.Config.sprites: booleantecs.Application.Config.meshes field
Caller-writable. Enables and configures the 3D mesh domain. Nil, the default, loads no mesh renderer and allocates no mesh buffers.
tecs.Application.Config.timestep field
Caller-writable. Sets the seconds one fixed step covers. Must be greater than zero. Defaults to 1/60.
tecs.Application.Config.timestep: numbertecs.Application.Config.fixedMaxSteps field
Caller-writable. Sets the most fixed steps one update runs before the overload policy applies. Must be a positive integer. Defaults to 10.
tecs.Application.Config.fixedMaxSteps: integertecs.Application.Config.fixedOverload field
Caller-writable. Selects what becomes of catch-up that did not fit. Defaults to "drop", which bounds a frame's work and reports the loss through world:getStats. "accumulate" keeps every second instead.
tecs.Application.Config.fixedOverload: ecs.FixedOverloadtecs.Application.Config.maxEntities field
Caller-writable. Sets the world's entity slot capacity. Defaults to 2^20, and 2^22 - 1 is the ceiling the packed id format allows.
Concurrent slots rather than lifetime spawns: a slot is given back when the world despawns the entity, then the next entity reuses it. The world preallocates this many slots, so it is a memory decision as much as a limit, and a game that knows its population sets it rather than paying for a million slots it will not use.
tecs.Application.Config.maxEntities: integertecs.Application.Config.reserveRuns field
Caller-writable. Gives each archetype a run with room to grow rather than packing the runs end to end. Defaults to false, which packs.
Packed, a run cannot grow without shifting every run after it, so a spawn anywhere rewrites every instance in the scene. Reserved, it grows into its own slack and the archetypes around it are left alone. The cull also dispatches over the slack as well as over the rows, and that reserving needs headroom: a capacity sized to exactly the population leaves nothing to reserve out of and the setting does nothing.
Worth it for a scene that spawns into one archetype while most of the world sits in others, which is what a game with a level and projectiles in it looks like. Worth nothing for a scene held in a single archetype, where there is no other run to leave alone.
Not named for debugging, unlike debugMaxFrames below, because it is not a debugging setting: it is a tuning one, measured at 15.4x on a mixed-archetype spawner scene. A name with debug in it would warn off exactly the shipped games that should turn it on.
tecs.Application.Config.reserveRuns: booleantecs.Application.Config.partialRewrites field
Caller-writable. Rewrites the rows a spawn or a despawn wrote rather than the archetype's whole run. Defaults to false, which rewrites the run.
A row moving into an archetype has every column newly written at that row, so a structural change dirties every column of the archetype it lands on and the run is resynced whole. The rest of the run did not change, and the ECS records which rows did, so this makes one spawn cost the rows it wrote rather than the archetype's population. It composes with reserveRuns above rather than replacing it: reserving keeps a spawn from moving the archetypes around it, and this keeps it from rewriting the one it landed in.
Two things are deliberately left out of it. A value write through archetype:getMut names a column and not a row, so it still rewrites the run; that includes a batchSpawn fill callback, which declares intent on the whole column it writes new rows into. And an archetype carrying PreviousTransform2D is interpolated, so its drawn positions move on every frame that falls at a new point in the fixed step whatever the world did.
tecs.Application.Config.partialRewrites: booleantecs.Application.Config.packImages field
Caller-writable. Fits many images into each layer of the renderer's image array rather than one. Defaults to false, where the ceiling on distinct images is the array's layer count and a small image costs a whole cell. On, the ceiling is the array's area instead.
tecs.Application.Config.packImages: booleantecs.Application.Config.shadows field
Caller-writable. Enables entity shadows and configures their cost. Nil, the default, means an Occluder2D or a DropShadow2D on an entity draws the entity and casts nothing, because the targets that would hold a shadow are never built. An empty table turns them on with every default; see Deferred.ShadowOptions for the seven numbers.
tecs.Application.Config.shadows: Deferred.ShadowOptionstecs.Application.Config.debugMaxFrames field
Caller-writable. Stops after this many iterations. This lets an automated run drive a real window to completion without a human closing it.
tecs.Application.Config.debugMaxFrames: integertecs.Application.Config.quitOnEscape field
Caller-writable. Requests a clean exit when the platform delivers an Escape key press. Defaults to false. The check is independent of gameplay input layers and remains available after a gameplay crash.
tecs.Application.Config.quitOnEscape: booleantecs.Application.Config.plugin field
Caller-writable. Sets the game plugin that receives the world and this application.
Called at the end of initialization, after every engine subsystem is installed and before the startup phases run, so it can register a Startup system and have it run this initialization rather than the next one.
One entry point rather than a list, because composing plugins is something the world already does: world:addPlugin takes an tecs.Plugin and is how the engine installs its own, so a game with several calls it from in here and needs no second mechanism.
The world comes first because every plugin the world takes is function(world), so this reads as that shape with one more thing and code moves between here and a delegated plugin without a silent argument swap.
tecs.Application.Config.plugin: EntryPlugintecs.Application.EntryPlugin type
Configures the world owned by an application.
type tecs.Application.EntryPlugin = function(types.World, Application)Functions
tecs.Application:checkpointPath Instance
Returns the checkpoint path, or nil when the config names none.
function tecs.Application.checkpointPath(self): stringArguments
| Name | Type | Description |
|---|---|---|
self |
Application |
Returns
| Type | Description |
|---|---|
string |
tecs.Application:clearCrash Instance
Clears a recoverable gameplay crash so development can render another frame. Reload the world before trusting it.
The method works only when debug, mcpPort or watch marks the run for development. It refuses to continue after incomplete graphics recovery.
function tecs.Application.clearCrash(self): boolean, stringArguments
| Name | Type | Description |
|---|---|---|
self |
Application |
Returns
| Type | Description |
|---|---|
boolean |
Returns whether the loop resumed. |
string |
Returns the reason when the loop could not resume. |
tecs.Application:crashed Instance
Returns the gameplay traceback, or nil while the game is healthy.
function tecs.Application.crashed(self): stringArguments
| Name | Type | Description |
|---|---|---|
self |
Application |
Returns
| Type | Description |
|---|---|
string |
tecs.Application:readCheckpoint Instance
Reads the bytes the last run left, or returns nil when there are none.
Read it while building the world, which is what the plugin and the startup phases are for. Nil covers every reason there is nothing to resume from: a first run, a game that never staged anything, a file the player deleted. None of those is an error, so none of them raises.
function tecs.Application.readCheckpoint(self): stringArguments
| Name | Type | Description |
|---|---|---|
self |
Application |
Returns
| Type | Description |
|---|---|
string |
tecs.Application:stageCheckpoint Instance
Hands the engine the bytes to write when the platform backgrounds us.
Call this from an ordinary system, on an ordinary frame, whenever the state worth keeping has changed. Nothing is written here; the bytes are held, and _willEnterBackground writes them at the one moment the platform gives.
What this takes is bytes rather than a function that produces them, and that is the whole design rather than an inconvenience. iOS allows roughly five seconds from the backgrounding callback returning and Android rather less, and at this project's scale a world is not serializable inside any of it: the platform could kill a callback part way through four million entities through and leave nothing behind. A function would let a game postpone the serializing into exactly that callback while looking like it had prepared something. A string cannot: by the time it exists, the expensive half has already happened on a frame that could afford it. The host times the hook and says so past 250 ms, which is the check on the rest.
Staging again replaces the previous bytes; there is one checkpoint, not a queue. Staging and then backgrounding twice writes once, because a write no changes only gives the platform another chance to interrupt the write for nothing.
The counterpart is readCheckpoint, which is how the next run gets it back. Raises when the config names no checkpoint, because silently holding bytes that will never be written is the one failure a game would not notice.
function tecs.Application.stageCheckpoint(self, bytes: string)Arguments
| Name | Type | Description |
|---|---|---|
self |
Application |
|
bytes |
string |
Returns
None.
Values
tecs.Application.audio variable
Read-only. Exposes the audio mixer.
tecs.Application.audio: Audiotecs.Application.device variable
Read-only. Exposes the GPU device.
tecs.Application.device: Devicetecs.Application.elapsed variable
Engine-owned. Reports seconds of simulated time. The frame delta advances it so a replay reproduces the value exactly.
tecs.Application.elapsed: numbertecs.Application.frame variable
Engine-owned. Reports completed application iterations.
tecs.Application.frame: integertecs.Application.input variable
Read-only. Exposes current input state.
tecs.Application.input: Inputtecs.Application.mcp variable
Read-only. Exposes the debug server when configuration requested one.
tecs.Application.mcp: mcp.Servertecs.Application.quitRequested variable
Caller-writable. Set to true to leave the loop at the end of the current iteration.
tecs.Application.quitRequested: booleantecs.Application.renderer variable
Read-only. Exposes the renderer that extracts this world.
tecs.Application.renderer: Renderertecs.Application.suspended variable
Engine-owned. Reports true while the platform has the application in the background. The loop still runs, but simulation and rendering do not.
tecs.Application.suspended: booleantecs.Application.window variable
Read-only. Exposes the application window.
tecs.Application.window: Windowtecs.Application.world variable
Read-only. Exposes the world driven by this application.
tecs.Application.world: types.World