Dirty tracking#
Tecs tracks value dirtiness per archetype and component. One mark means that some row in the column may have changed. Structural residue separately records bounded append and swap-pop ranges for incremental consumers.
Incremental consumers such as text layout and hierarchy composition use that signal to skip unchanged columns. A write that Tecs cannot see leaves those consumers with stale data.
Declare write intent#
Read through get and write through getMut:
for archetype, length in movers:iter() do
local transforms =
assert(archetype:getMut(tecs.ecs.Transform2D))
local velocities = assert(archetype:get(Velocity))
unsafe do
for row = 1, length as integer do
local transform = transforms[row]
local velocity = velocities[row]
transform.x = transform.x + velocity.x * dt
transform.y = transform.y + velocity.y * dt
end
end
endgetMut marks the entire component column dirty before returning it. Never call it speculatively in a loop that might not write.
For a conditional write, read first and mark only when the condition succeeds:
local transforms = assert(archetype:get(tecs.ecs.Transform2D))
local changed = false
unsafe do
for row = 1, length as integer do
if needsCorrection(transforms[row]) then
correct(transforms[row])
changed = true
end
end
end
if changed then
archetype:markComponentDirty(tecs.ecs.Transform2D)
endA value read does not declare write intent. A field assignment through world:get or archetype:get changes memory without marking it. Prefer getMut; otherwise call world:markComponentDirty or the archetype marker after the write.
Automatic marks#
These paths maintain dirty state:
getMutandworld:set- spawn placement
- movement into another archetype
- swap-pop after removal
Spawn and structural movement mark every component on the archetype, because a row moving in has every column newly written at that row.
Each changed column is marked on its archetype.
world:update clears marks after the pipeline runs. A consumer must inspect the columns it reads before those marks clear.
world:dirtyArchetypes() visits the first-dirtied queue, not the world's whole archetype registry. Each archetype enters that queue once per dirty window. Use structuralCount() and the structural residue methods to decide whether a retained consumer can update ranges or must rebuild. trackValueCount(Component) opts a column's explicit writes into a monotonic aggregate counter; structural changes have their own counter.
Retained rendering#
The renderer also keeps monotonic write counters. Those counters survive the end-of-frame dirty clear. Unchanged renderable archetype runs keep their packed instances and GPU buffers; a changed run rewrites and uploads its affected range. Structural changes that move packed runs rebuild the layout. getMut, set and explicit dirty marks therefore reach the next render even when extraction skipped a frame.
Camera movement updates the view and culling while world-space instance data stays resident. Screen-space geometry and fixed-step interpolation also track the view and interpolation fraction that affect their presented positions.