Builtin Library
README.md
layers
Top-level scene-management global + Scene proxy. § 17 step 9 of
docs/plans/2026-05-01-player-camera-unification.md. Renamed from the
legacy scene.* global to match /runtime/layers/ and to give
scene-management state (active root, additives, callback registries)
a single anchor distinct from per-scene authoring.
Global surface
| Symbol | Type | Notes |
|---|---|---|
layers.active | Scene | nil | Current root non-additive scene as a proxy. Live read; updates on every load/unload. |
layers.list() | { Scene } | Every loaded scene (root + additives) as proxies. |
layers.find(ref) | Scene | nil | Lookup by AssetRef<scene> or layer-id string. |
layers.is_loaded(ref) | boolean | |
layers.load(ref, opts?) | — | Loads ref. opts.additive = true layers on top; default = non-additive (replaces root). |
layers.unload(ref?) | — | Unloads ref; nil = active root. |
layers.reload(ref?) | — | Equivalent to unload + load with same ref. |
layers.onLoad(cb) -> handle | function | Subscribe to any scene-load event. Returns handle for offLoad. |
layers.offLoad(handle) | function | |
layers.onUnload(cb) -> handle / offUnload(h) | function | Same shape for unload events. |
layers.observe() | SceneObservation | What every load did and what each loaded scene costs, in one read. |
layers.lastLoad() | SceneLoadReport | nil | The last load's report; nil when nothing has loaded. |
layers.lastUnload() | SceneUnloadReport | nil | The last unload's, under the name the layer was loaded with. |
layers.loadHistory() | { SceneLoadReport } | The completed load reports still held, oldest first. |
layers.problems(ref?) | { SceneLoadFailure } | What a layer failed to produce, and why. |
layers.whyPartial(ref?) | (reason?, detail?) | The nearest cause a layer is not whole, from a closed set. |
layers.inventory() | { SceneLayerInventory } | What the engine attributes to each loaded layer. |
layers.cost() | { SceneLayerCost } | Each layer's per-frame entrypoint tick, summed across the window. |
layers.resetCostWindow() | — | Open a new cost window. |
Scene proxy surface
A Scene proxy is a sealed table with fields and :method callables.
| Field / method | Notes |
|---|---|
s.asset | AssetRef<scene> |
s.name, s.path | informational (logging, debug) |
s.guid | the scene asset's guid — the layer's identity |
s.sceneIdentity | the identity of the scene asset the layer was loaded from (e.g. @builtin::scenes.test_arena), whichever VM loaded it |
s.additive | boolean — additive overlay vs root scene |
s.parent, s.children | additive ownership chain |
s:root() | walks .parent to the non-additive root; returns self for roots |
s.players, s.camera | populated by §17 step 10 (player + camera spawners) |
s.entrypoint | AssetRef<script> | nil |
s.visible | boolean — read via field, toggle via s:set_visible(b) |
s.state | "loading" / "loaded" / "ready" / "partial" / "unloading" — the layer's lifecycle state. Both "ready" and "partial" are terminal: the load has settled. |
s.ready, s.loaded | booleans in lock-step with state. ready is true once the layer has settled, in either terminal state — it is the flag to wait on. |
s.ok | boolean — whether everything the scene declared was produced and the layer has run clean since. False whenever state is "partial", including when the tick starts raising long after the load settled. |
s.failures | { SceneLoadFailure } — every distinct failure the load and the layer's per-frame tick produced, each with the count of how often it repeated. |
s:unload() | unload this scene; additives use layers.unload(layerId), roots use layers.active:reload() |
s:reload() | unload + reload same ref |
s:set_visible(b) | show/hide without unloading |
s:load_additive(ref) | load a sibling additive overlay; returns the new proxy |
Example
-- Switch root scene
layers.load(asset.ref("@builtin::scenes.baseline_visual"))
-- Subscribe to all loads
local h = layers.onLoad(function(scene)
print("loaded:", scene.name)
end)
-- Layer an HUD overlay on top
layers.load(asset.ref("@builtin::scenes.hud"), { additive = true })
-- Iterate every loaded scene
for _, scn in ipairs(layers.list()) do
print(scn.name, scn.additive and "(additive)" or "(root)")
end
layers.unload() -- unload the active root
layers.offLoad(h)
What a load did, and what a scene costs
Every load writes one record at its own completion point; the readings below
compose that record, so a caller that did not make the load can still ask what
it did. A load that produced failures settles into "partial" rather than
"ready", and names the nearest cause from a closed set: loaderRaised,
entrypointCompileFailed, entrypointBodyRaised, entrypointRaised,
buildRaised, entityFailed, parentMissing, parentRefused,
parentAbandoned, componentUnresolved, componentRefused,
subscriberRaised, updateRaised.
layers.load(asset.ref("scenes.arena", "scene"))
-- Did it come up whole?
if not layers.active.ok then
local reason, detail = layers.whyPartial()
print(reason, detail)
for _, f in layers.problems() do
-- `count` rises when the same failure happens again, so a per-frame
-- tick that keeps raising stays one line.
print(f.phase, f.reason, f.entity, f.component, f.hook, f.count)
end
end
-- What did the swap do?
local r = layers.lastLoad()
print(r.name, r.outcome, r.durationMs)
print("replaced", r.replaced and r.replaced.name) -- the root that went
print("cascaded", #r.cascaded) -- the overlays with it
print(r.entities.added, "arrived,", r.entities.removed, "left")
print(r.phases.teardown, r.phases.instantiate, r.phases.settle)
-- Which loaded scene is expensive? `totalMs` is a SUM across the window, so
-- reset it first and read `avgMs` for the per-tick figure.
layers.resetCostWindow()
task.wait(1)
for _, c in layers.cost() do
print(c.name, c.avgMs, "ms/tick over", c.calls, "ticks")
end
-- What does each layer hold, and what does no layer claim?
for _, l in layers.inventory() do print(l.name, l.entities, l.ok) end
print(layers.observe().totals.unattributed)
The scene toolbox carries the same two readings as scene.observe and
scene.whyPartial.
Authoring surface
layers.* is the sole public scene-management surface. It accepts
AssetRefs (not paths), drives a callback registry, and constructs
Scene proxies that carry players / camera / hooks. The previous
scene.* global has been removed — its low-level FFI bindings now
live behind layers.* and the Scene proxy methods.