World detail — terrain, voxels, streaming, LOD
Four systems decide how much of a world stands at any one moment:
- terrain — a heightfield drawn through a quadtree LOD cut that refines under the camera
- voxels — a
VoxelWorldsplit into chunks, each meshed on the GPU into its own child entity - spatial streaming — a cell store that stands and releases groups of entities as sources move
- mesh LOD — a level chosen per entity from how much of the viewport it spans
Reading a Terrain, VoxelWorld, StreamingSource or MeshLod component's
fields back tells you what was asked for. The streaming namespace answers
the other question: what is standing, what it costs, and for a piece that is
not on screen, which of a closed set of reasons it is not.
What is resident right now
local reading = streaming.observe()
reading.totals --> voxelChunks / voxelChunksMeshed / voxelChunksFailed /
-- voxelGpuBytes / terrainEntities / terrainBound /
-- terrainIndexCount / cellsTotal / cellsResident /
-- cellsPending / cellBytesResident / lodReceivers
reading.terrain --> per Terrain entity: bound, nodeCount, indexCount,
-- vertexCapacity, meshGuid, and the eye the cut refined under
reading.voxel --> per VoxelWorld: chunksTotal / meshed / queued / failed /
-- empty, gpuBytes, and one row per chunk
reading.cells --> the store's config, its counters, and one row per cell
reading.levels --> per LOD receiver: the level it draws at and the screen
-- fraction that selection was measured from
Each axis reads on its own too: streaming.terrain(), streaming.voxel(),
streaming.cells(), streaming.levels().
The same document is served at /zero/runtime/observations/streaming. It is
built by the call and published as its last act, so the namespace and the VFS
node carry one reading rather than two derivations of it:
local served = require("@builtin::modules.json").decode(
vfs.read("/zero/runtime/observations/streaming"))
served.reading.totals.voxelChunksFailed
Nothing is computed per frame. An engine nobody is observing pays nothing, and
streaming.observe() is the only thing that builds the reading.
Which pool a number belongs to
voxelGpuBytes is what the voxel chunk builds reserved on the device — the
vertex pool at the renderer's own stride plus one u32 per reserved index. It
is that pool alone. The terrain pool is reported as vertexCapacity per
terrain entity, the streaming store's bytes as cellBytesResident, and every
resource the device holds under any system is renderer.gpuMemory() and
asset.observe().
Why a piece is not on screen
streaming.whyNotDrawn(entityOrName) -- whichever system holds it
streaming.whyNotDrawn({ entity = "Vox", chunk = "0_0_0" }) -- one voxel chunk
streaming.whyNotDrawn({ entity = rock, level = 0 }) -- one mesh-LOD level
streaming.whyNotDrawn({ cell = "3_-2" }) -- one streaming cell
The answer is { kind, reason, detail }. reason is one of the set
streaming.reasons() enumerates, and detail names what it is about:
| reason | what it means |
|---|---|
drawn | the engine holds this subject's geometry and is drawing it |
notManaged | no terrain, voxel, streaming or mesh-LOD system holds it |
notBuilt | nothing has built this subject's geometry |
buildQueued | a build has been asked for and has not run yet |
buildFailed | a build ran and the engine did not carry it through — detail is the engine's own message |
builtEmpty | a build ran over a region holding nothing to draw |
emittedNothing | the build ran and the shader wrote no geometry into the capacity it reserved |
outsideRadius | no streaming source is close enough to want it resident |
evicted | it was resident, the store released it, and its content now reads from the file the release wrote |
coarserLevel | a coarser LOD level is the one selected, so this level is not the one drawn |
finerLevel | a finer LOD level is the one selected, so this level is not the one drawn |
noSuchLevel | the LOD chain holds no level with this number |
hidden | the entity carrying the geometry is switched off |
A voxel world that stands and draws nothing
A chunk's state is the engine's verdict on the build, not the component's
record of having asked for one. VoxelWorld:dirtyChunks() empties as soon as
each chunk has been attempted; VoxelWorld:chunks() says what became of the
attempt:
for _, chunk in ipairs(world:chunks()) do
print(chunk.key, chunk.state, chunk.detail)
end
-- 0_0_0 buildFailed the compute shader never compiled within 10000ms, so the dispatch was dropped
Each row carries state, detail, dispatches and failures from the
engine's dispatch record, the block count the build read (solidVoxels), what
it reserved (reservedVertices, reservedIndices, gpuBytes), and the entity
and mesh the chunk draws through.
The join is the buffer prefix: every buffer of a build is created under one, and
the engine's dispatch record names it — so chunk.buildPrefix is what holds a
chunk against compute.program.status(shaderGuid) directly.
Reserved is not emitted, and the row carries both
reservedVertices / reservedIndices are the capacity the build asked the
device for, sized off an upper bound before the shader ran. emittedVertices /
emittedIndices are what the shader actually wrote, read back off the counters
it increments as it emits.
They are different numbers and the difference is a whole class of failure. A
dispatch the device carried through that wrote nothing leaves a chunk holding a
MeshHandle, a plausible gpuBytes, failures = 0 — and no geometry. That
chunk reads emittedNothing, and meshed is only ever reported for a chunk
whose counters say geometry is there.
The read is issued once the engine has recorded the dispatch reaching the
device — a read issued before that returns the zeros the build wrote into the
counters, which is indistinguishable from a shader that emitted nothing. Until
it lands, the chunk reads queued with a detail saying the answer is not in
yet; emittedVertices and emittedIndices are absent from the row.
Gpu.emitted(chunk.buildPrefix) waits for that read rather than reporting the
absence, for a caller that wants the number and not the state.
Publishing a reading of your own
/zero/runtime/observations is generic. Any Luau system can put a reading on
the engine's shelf and gain a VFS leaf without a change to the engine:
__observations.publish("mySystem", { resident = 12, pending = 3 })
-- served at /zero/runtime/observations/mySystem as
-- { name = "mySystem", publishedAtMs = ..., reading = { ... } }
__observations.names() --> { "mySystem", "streaming" }
The document is serialised at the moment of the call, so the bytes the VFS hands out are the bytes of the table the publishing call built.