Dirty Bit Upload
</blockquote>
Overview
The engine uploads only modified entities per frame rather than the full slab. A double-buffered cumulative dirty bit array (12.5KB for 100K entities — fits in L1) tracks which entities changed. The render thread reads this to build the minimal upload set.
Status: Operational since 2026-05. In steady state at 100K entities + 56 physics bodies, the render thread runs at ~0.88ms (1133 FPS) driven almost entirely by dirty-bit selective upload.
Three Dirty Bits
All three bits live in the entity's flags field (CacheSlotMeta):
| Bit | Name | Semantics |
|---|---|---|
| Bit 31 | Active | Entity is alive and should be rendered / simulated |
| Bit 30 | Dirty | Cumulative: this entity has been modified since last render acknowledgment |
| Bit 29 | DirtiedFrame | Per-frame: set on any write this logic tick, cleared unconditionally at frame start |
**DirtiedFrame (bit 29):** Cleared at the top of every logic frame. Set by FieldProxy::operator= on any write. Used to compute the current frame's dirty set.
**Dirty (bit 30):** Accumulates across frames until the render thread acknowledges via RenderAck atomic handshake. If the render thread falls behind N logic frames, Dirty accumulates all N frames' worth of changes. When the render thread finally catches up, it SIMDs OR the dirty arrays from all intermediate frames into one upload set.
<tt>FieldProxy</tt> Dirty Marking
Every FieldProxy::operator= marks both dirty bits atomically:
template<typename T, FieldWidth WIDTH> FieldProxy<T, WIDTH>& operator=(const T& value) { *ptr = value; flags |= TemporalFlagBits::Dirty | TemporalFlagBits::DirtiedFrame; return *this; }
The flags array is at a known offset from the field array base — always CurrFieldAddrs[0] by convention (SemFlags = 1, index 0). There is no indirection.
Per-Slab Bitplanes
There are 5 heap-allocated GPU dirty bitplanes — one per field slab slot. Each bitplane is a 12.5KB bit array (100K entities / 8 bits per byte).
Logic side: After the AVX2 entity sweep, the engine scans the slab flags array with AVX2, ORing the active+dirty bits into all 5 bitplanes that correspond to slabs currently writable by the render thread.
**PullActiveTransforms special case:** Jolt writes entity positions back into the slab after each physics step, bypassing FieldProxy. PullActiveTransforms manually marks dirty bits for all awake bodies after Jolt writeback.
**FirstSlabWrite:** The first write to each slab slot does a full memcpy to bootstrap GPU state, since there is no previous frame to delta against. Subsequent writes use dirty-bit selective upload.
<tt>RenderAck</tt> Handshake
The render thread atomically acknowledges each frame's dirty bits after uploading:
- Logic writes
DirtiedFramebits during the logic tick PublishCompletedFramecopiesDirtiedFrame→Dirtyfor the completed frame- Render thread reads
Dirtybits for the frame it is about to upload - Render thread uploads only the dirty entities
- Render thread writes
RenderAckfor that frame - On the next logic tick,
Dirtybits acknowledged byRenderAckare cleared
If the render thread is processing frame T and the logic thread is already on frame T+3, the logic thread will OR frames T+1, T+2, T+3's dirty bits into the cumulative set before the render thread picks up.
SIMD OR Path — Catching Up After Lag
When the render thread falls behind multiple logic frames, it cannot simply upload the most recent frame's data — entities modified in intermediate frames would be missed.
Logic frames: T T+1 T+2 T+3 (current) Render on: T-1 (just finished) → Must upload changes from T, T+1, T+2, T+3 SIMD OR: dirty[T] | dirty[T+1] | dirty[T+2] | dirty[T+3] → One combined upload set, no full-slab copy
The 12.5KB bitplane (100K entities) fits in L1 cache. OR-ing 4 frames is 4 AVX2 passes of 1562 256-bit operations — negligible cost.
Selective Scatter
After building the upload set, the scatter pass iterates only entities with set bits:
for (uint64_t word : dirty_bitplane) { while (word) { int bit = ctzll(word); // find lowest set bit uint32_t entityIdx = base + bit; UploadEntity(entityIdx); word &= word - 1; // clear lowest set bit } }
ctzll (count trailing zeros) is a single CPU instruction. For a typical frame where 1% of entities move, this processes ~1000 entities instead of 100K.
Dirty Bits and Rollback
The dirty set from a rollback correction propagates naturally through the resimulation: any entity that gets corrected writes via FieldProxy, which sets Dirty. After the resimulation, the upload set contains exactly the entities that changed during rollback — no manual dependency tracking required.
The same Dirty bit (bit 30) drives both GPU upload and rollback blast radius.
<tt>TemporalFlagBits</tt> Enum
All entity flags live in one place:
enum class TemporalFlagBits : uint32_t { Active = 1u << 31, // entity renders and simulates Dirty = 1u << 30, // cumulative: needs GPU upload DirtiedFrame = 1u << 29, // per-frame: wrote this tick Replicated = 1u << 28, // net: Authority has assigned a net handle Alive = 1u << 27, // in Alive state (spawned but not yet Active) Tombstone = 1u << 26, // deferred destruction pending NetConfirmedDead = 1u << 25, // despawn confirmed by Authority PrePhysSkip = 1u << 24, // skip PrePhysics sweep for this entity PostPhysSkip = 1u << 23, // skip PostPhysics sweep for this entity ScalarSkip = 1u << 22, // skip ScalarUpdate sweep for this entity ASleep = 1u << 21, // physics body is sleeping // bits [20:17] — 4-bit attachment depth (depthMask) };
Typed |= / &= operators on CacheSlotMeta prevent raw integer mixing.