On this page
  1. tecs.Application
  2. Lifecycle
  3. Checkpoints and crashes
  4. Module contents
    1. Constructors
    2. Types
    3. Functions
    4. Values
  5. Constructors
    1. newApplication
  6. Types
    1. Config
    2. EntryPlugin
  7. Functions
    1. Application:checkpointPath
    2. Application:clearCrash
    3. Application:crashed
    4. Application:readCheckpoint
    5. Application:stageCheckpoint
  8. Values
    1. audio
    2. device
    3. elapsed
    4. frame
    5. input
    6. mcp
    7. quitRequested
    8. renderer
    9. suspended
    10. window
    11. world

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.

  1. The application starts the input frame and drains every platform event SDL delivered since the previous iteration. A quit request ends the iteration here.
  2. tecs.runtime.poll settles process-wide asynchronous work: finished asset loads, child processes, dialogs, generic I/O, HTTP clients, and file changes.
  3. The debug server answers, world:update(dt) runs the phases, and the renderer records and submits one frame.
  4. 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
): Application

Arguments

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
end

tecs.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.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: boolean

tecs.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.presentMode field

Caller-writable. Selects "vsync", "immediate" or "mailbox" presentation. The device defaults to "vsync".

tecs.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.audio field

Caller-writable. Configures sound output. Omitted takes the defaults, which open the platform's default device.

tecs.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.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.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.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: integer

tecs.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.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.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: boolean

tecs.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.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.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.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.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.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.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.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.ShadowOptions

tecs.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.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.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.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): string

Arguments

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, string

Arguments

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): string

Arguments

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): string

Arguments

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.device variable

Read-only. Exposes the GPU device.

tecs.Application.device: Device

tecs.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: number

tecs.Application.frame variable

Engine-owned. Reports completed application iterations.

tecs.Application.frame: integer

tecs.Application.input variable

Read-only. Exposes current input state.

tecs.Application.mcp variable

Read-only. Exposes the debug server when configuration requested one.

tecs.Application.quitRequested variable

Caller-writable. Set to true to leave the loop at the end of the current iteration.

tecs.Application.renderer variable

Read-only. Exposes the renderer that extracts this world.

tecs.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: boolean

tecs.Application.window variable

Read-only. Exposes the application window.

tecs.Application.world variable

Read-only. Exposes the world driven by this application.

tecs.Application.world: types.World