Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
27 changes: 20 additions & 7 deletions docs/api/archetype.md
Original file line number Diff line number Diff line change
Expand Up @@ -52,7 +52,7 @@ slot-ordered addresses).
| `world.addComponent<T>(e, v)` | **Create-or-update**: `T` present → overwrite in place (no move); absent → move `e` to the set `current ∪ {T}`, copying the shared components into the new row. Stale handle → `InvalidArgument` + warn-once; unregistered `T` → `InvalidArgument` + warn; set would exceed 32 components → `BudgetExhausted` + warn | O(tail × row-stride) bytes moved (tail = rows at/above the insertion point); **no heap allocation** — growth is a pre-reserved, accounted, logged reserve (below) |
| `world.removeComponent<T>(e)` | Remove `T`: no-op ok if absent; else move `e` to `current ∪ {T} \ {T}`, copying the remaining components into the new row. Same error rows as add | as add |
| `world.archetypeCount()` | Distinct component sets created so far (archetypes are never destroyed; empty sets stay) | O(1) |
| `world.archetypeStats()` | `ArchetypeStats` snapshot: `archetypeCount`, `rowsLive`, `rowsReserved`, `bytesReserved`, `totalAdds`, `totalRemoves`, `totalArchetypeGrowth`, `totalReservations` (the M1-PROF-01 / G-R4 feed) | O(256 × 32) cold pass, no allocation |
| `world.archetypeStats()` | `ArchetypeStats` snapshot: `archetypeCount`, `rowsLive`, `rowsReserved`, `bytesReserved`, `totalAdds`, `totalRemoves`, `totalArchetypeGrowth`, `totalReservations`, `totalRowShifts` (the M1-PROF-01 / G-R4 feed) | O(256 × 32) cold pass, no allocation |

The `World::destroy(e)` / `World::clear()` cost note now includes the
row detach: an entity with components leaves its archetype first —
Expand Down Expand Up @@ -127,12 +127,25 @@ warn-once + `rate_limited` drain.
| Debug (`-O0`) | 0.123 ms | 0.243 ms | 1.98 |
| Release (`-O2`) | 0.0021 ms | 0.0040 ms | 1.94 |

The suite asserts the flatness (`p99 < 3 × p50`), the zero
reservation delta, and the zero-allocation window, and prints the
machine-greppable line (`archetype-churn <stats>`) to the ctest
output on every run — the M1 baseline record for the G-R4 feed.
Numbers are machine-dependent; the *shape* (flat, no spike, no
allocation) is the tested property.
The wall-clock numbers above are a recorded baseline, not a gate:
they are machine-dependent, and the shared macOS CI runners'
wall-clock tail alone reaches ~5× the median (2026-09-13 runs:
p99/p50 = 4.9 on macos-14/macos-15 vs 1.98 on Linux), so a raw
time-ratio assertion does not travel across P0 platforms
(methodology §6). The suite gates the *work* instead, which is
identical on every platform: the window's total row-shift count is
a seed-independent deterministic constant — each op moving slot `s`
shifts exactly `2 × (9999 − s)` rows (s's rank in each archetype
sums to `s`), so the window shifts exactly
`10000 × 9999 = 99,990,000` rows for *any* seed — asserted as a KAT
against `totalRowShifts` — plus a per-row wall-clock floor
(≤ 200 ns/row; measured ~24 ns/row Linux Debug, ~7 ns/row macOS
Debug, and VM preemption spreads across the window). The zero
reservation delta and the zero-allocation window are asserted too.
The machine-greppable lines (`archetype-churn <stats>`,
`archetype-churn work: rows_shifted=… ns_per_row=…`) land in the
ctest output on every run — the M1 baseline record for the G-R4
feed.
- **Memory per entity (live, with components):** one row per archetype
column — `Σ component sizes` bytes (8 B for a pos+vel pair) plus the
2 B slot column slot; per-slot bookkeeping is 11 B (entity.md).
Expand Down
33 changes: 17 additions & 16 deletions laige-api.json
Original file line number Diff line number Diff line change
Expand Up @@ -401,15 +401,16 @@
{"name": "laige::kArchetypeColumnPad", "kind": "variable", "header": "src/laige-sim/include/laige/sim/archetype.h", "line": 202, "signature": "inline constexpr std::size_t kArchetypeColumnPad = kArchetypeColumnAlignment - 1", "summary": "Over-allocation that keeps the aligned base inside the raw block.", "budget": null, "experimental": false},
{"name": "laige::kInvalidColumnIndex", "kind": "variable", "header": "src/laige-sim/include/laige/sim/archetype.h", "line": 207, "signature": "inline constexpr std::uint32_t kInvalidColumnIndex = 0xFFFFFFFFu", "summary": "The \"column absent\" sentinel for columnIndexOf results (API-008: call sites never spell raw 0xFFFFFFFF).", "budget": null, "experimental": false},
{"name": "laige::kInvalidRowIndex", "kind": "variable", "header": "src/laige-sim/include/laige/sim/archetype.h", "line": 212, "signature": "inline constexpr std::uint32_t kInvalidRowIndex = 0xFFFFFFFFu", "summary": "The \"row absent\" sentinel for attachSlot failure results. Rows are 0-based and row 0 is a valid row, so 0 cannot be the failure value (API-008: call sites never spell raw 0xFFFFFFFF).", "budget": null, "experimental": false},
{"name": "laige::ArchetypeStats", "kind": "struct", "header": "src/laige-sim/include/laige/sim/archetype.h", "line": 234, "signature": "struct ArchetypeStats", "summary": "One archetype's storage accounting snapshot (PRD §10.4, FR-11.4, G-R4 feed; mirrors the M0-CORE-05 PoolStats shape). A plain value the M1 profiler (M1-PROF-01) and the churn/overflow checks pull:", "budget": null, "experimental": false},
{"name": "laige::ArchetypeStats::archetypeCount", "kind": "variable", "header": "src/laige-sim/include/laige/sim/archetype.h", "line": 235, "signature": "std::uint32_t archetypeCount{}", "summary": null, "budget": null, "experimental": false},
{"name": "laige::ArchetypeStats::rowsLive", "kind": "variable", "header": "src/laige-sim/include/laige/sim/archetype.h", "line": 236, "signature": "std::uint32_t rowsLive{}", "summary": null, "budget": null, "experimental": false},
{"name": "laige::ArchetypeStats::rowsReserved", "kind": "variable", "header": "src/laige-sim/include/laige/sim/archetype.h", "line": 237, "signature": "std::uint32_t rowsReserved{}", "summary": null, "budget": null, "experimental": false},
{"name": "laige::ArchetypeStats::bytesReserved", "kind": "variable", "header": "src/laige-sim/include/laige/sim/archetype.h", "line": 238, "signature": "std::uint64_t bytesReserved{}", "summary": null, "budget": null, "experimental": false},
{"name": "laige::ArchetypeStats::totalAdds", "kind": "variable", "header": "src/laige-sim/include/laige/sim/archetype.h", "line": 239, "signature": "std::uint64_t totalAdds{}", "summary": null, "budget": null, "experimental": false},
{"name": "laige::ArchetypeStats::totalRemoves", "kind": "variable", "header": "src/laige-sim/include/laige/sim/archetype.h", "line": 240, "signature": "std::uint64_t totalRemoves{}", "summary": null, "budget": null, "experimental": false},
{"name": "laige::ArchetypeStats::totalArchetypeGrowth", "kind": "variable", "header": "src/laige-sim/include/laige/sim/archetype.h", "line": 241, "signature": "std::uint64_t totalArchetypeGrowth{}", "summary": null, "budget": null, "experimental": false},
{"name": "laige::ArchetypeStats::totalReservations", "kind": "variable", "header": "src/laige-sim/include/laige/sim/archetype.h", "line": 242, "signature": "std::uint64_t totalReservations{}", "summary": null, "budget": null, "experimental": false},
{"name": "laige::ArchetypeStats", "kind": "struct", "header": "src/laige-sim/include/laige/sim/archetype.h", "line": 239, "signature": "struct ArchetypeStats", "summary": "One archetype's storage accounting snapshot (PRD §10.4, FR-11.4, G-R4 feed; mirrors the M0-CORE-05 PoolStats shape). A plain value the M1 profiler (M1-PROF-01) and the churn/overflow checks pull:", "budget": null, "experimental": false},
{"name": "laige::ArchetypeStats::archetypeCount", "kind": "variable", "header": "src/laige-sim/include/laige/sim/archetype.h", "line": 240, "signature": "std::uint32_t archetypeCount{}", "summary": null, "budget": null, "experimental": false},
{"name": "laige::ArchetypeStats::rowsLive", "kind": "variable", "header": "src/laige-sim/include/laige/sim/archetype.h", "line": 241, "signature": "std::uint32_t rowsLive{}", "summary": null, "budget": null, "experimental": false},
{"name": "laige::ArchetypeStats::rowsReserved", "kind": "variable", "header": "src/laige-sim/include/laige/sim/archetype.h", "line": 242, "signature": "std::uint32_t rowsReserved{}", "summary": null, "budget": null, "experimental": false},
{"name": "laige::ArchetypeStats::bytesReserved", "kind": "variable", "header": "src/laige-sim/include/laige/sim/archetype.h", "line": 243, "signature": "std::uint64_t bytesReserved{}", "summary": null, "budget": null, "experimental": false},
{"name": "laige::ArchetypeStats::totalAdds", "kind": "variable", "header": "src/laige-sim/include/laige/sim/archetype.h", "line": 244, "signature": "std::uint64_t totalAdds{}", "summary": null, "budget": null, "experimental": false},
{"name": "laige::ArchetypeStats::totalRemoves", "kind": "variable", "header": "src/laige-sim/include/laige/sim/archetype.h", "line": 245, "signature": "std::uint64_t totalRemoves{}", "summary": null, "budget": null, "experimental": false},
{"name": "laige::ArchetypeStats::totalArchetypeGrowth", "kind": "variable", "header": "src/laige-sim/include/laige/sim/archetype.h", "line": 246, "signature": "std::uint64_t totalArchetypeGrowth{}", "summary": null, "budget": null, "experimental": false},
{"name": "laige::ArchetypeStats::totalReservations", "kind": "variable", "header": "src/laige-sim/include/laige/sim/archetype.h", "line": 247, "signature": "std::uint64_t totalReservations{}", "summary": null, "budget": null, "experimental": false},
{"name": "laige::ArchetypeStats::totalRowShifts", "kind": "variable", "header": "src/laige-sim/include/laige/sim/archetype.h", "line": 248, "signature": "std::uint64_t totalRowShifts{}", "summary": null, "budget": null, "experimental": false},
{"name": "laige::ComponentTypeId", "kind": "struct", "header": "src/laige-sim/include/laige/sim/component.h", "line": 119, "signature": "struct ComponentTypeId", "summary": "The stable per-world component type id (FR-1.2). See the header preamble for the assignment and determinism contracts.", "budget": null, "experimental": false},
{"name": "laige::ComponentTypeId::value", "kind": "variable", "header": "src/laige-sim/include/laige/sim/component.h", "line": 120, "signature": "std::uint32_t value{}", "summary": null, "budget": null, "experimental": false},
{"name": "laige::kInvalidComponentTypeId", "kind": "variable", "header": "src/laige-sim/include/laige/sim/component.h", "line": 125, "signature": "inline constexpr ComponentTypeId kInvalidComponentTypeId{0}", "summary": "The never-assigned id (API-008: the invalid state is representable and checkable; call sites never spell raw 0s).", "budget": null, "experimental": false},
Expand Down Expand Up @@ -455,13 +456,13 @@
{"name": "laige::World::removeComponent", "kind": "method", "header": "src/laige-sim/include/laige/sim/entity.h", "line": 373, "signature": "template <typename T> [[nodiscard]] Status removeComponent(Entity entity) noexcept", "summary": "Take the component of type T from `entity` (a no-op ok Status when the entity lacks T or has no components). Otherwise the entity moves to the archetype of its component set minus T — same cost and allocation contract as addComponent. Stale/invalid handle or unregistered T -> InvalidArgument (+ warn).", "budget": null, "experimental": false},
{"name": "laige::World::archetypeCount", "kind": "method", "header": "src/laige-sim/include/laige/sim/entity.h", "line": 379, "signature": "[[nodiscard]] std::uint32_t archetypeCount() const noexcept", "summary": "The number of distinct component sets seen by this world so far (0 .. kMaxArchetypes; archetypes are never destroyed in M1). O(1), no side effects.", "budget": null, "experimental": false},
{"name": "laige::World::archetypeStats", "kind": "method", "header": "src/laige-sim/include/laige/sim/entity.h", "line": 384, "signature": "[[nodiscard]] ArchetypeStats archetypeStats() const noexcept", "summary": "Archetype storage accounting snapshot (ArchetypeStats): the profiler (M1-PROF-01) and the zero-overflow/zero-allocation checks read this. O(kMaxArchetypes), no allocation.", "budget": null, "experimental": false},
{"name": "laige::World::each", "kind": "method", "header": "src/laige-sim/include/laige/sim/entity.h", "line": 415, "signature": "template <typename... Ts, typename... Acc, typename F> [[nodiscard]] Status each(F&& fn, Acc... accesses) noexcept", "summary": "Iterate every entity having ALL of T1..TN (superset match: extra components do not exclude an entity), invoking `fn(Entity, R1, ..., RN)` — one reference per listed component, in template order: a `const T&` where the access tag is Read, a `T&` where it is Write. The access tags follow `fn`, one Read/Write tag per listed component, in the same order (checked at compile time — they come after the callable because a pack of parameters must be the last parameters to be deducible); `each<>` (no components, no tags) visits every live entity in ascending slot-id order with no component references.", "budget": null, "experimental": false},
{"name": "laige::World::clear", "kind": "method", "header": "src/laige-sim/include/laige/sim/entity.h", "line": 429, "signature": "[[nodiscard]] Status clear() noexcept", "summary": "Destroy every live entity (shutdown path, CONC-006). Every handle becomes stale; the capacity is unchanged and the world is immediately reusable. O(capacity + detached rows * row-stride), no allocation, idempotent. M1-ECS-03: each live entity is detached from its archetype first (the per-entity component data is released with its row); the archetypes themselves — and the component type registry — survive. M1-ECS-04: rejected with ErrorCode::InvalidArgument (+ one rate-limited warn) while an iteration is active and any matched archetype still holds live rows — the clear is skipped, never partial (assert in debug; query.h \"Iteration legality\"); an ok Status otherwise.", "budget": null, "experimental": false},
{"name": "laige::World::World", "kind": "constructor", "header": "src/laige-sim/include/laige/sim/entity.h", "line": 433, "signature": "World(World&& other) noexcept", "summary": "Move is an O(1) pointer swap; the source becomes a valid empty world (capacity 0: every create() fails, every handle invalid).", "budget": null, "experimental": false},
{"name": "laige::World::operator=", "kind": "method", "header": "src/laige-sim/include/laige/sim/entity.h", "line": 434, "signature": "World& operator=(World&& other) noexcept", "summary": null, "budget": null, "experimental": false},
{"name": "laige::World::World", "kind": "constructor", "header": "src/laige-sim/include/laige/sim/entity.h", "line": 435, "signature": "World(const World&) = delete", "summary": null, "budget": null, "experimental": false},
{"name": "laige::World::operator=", "kind": "method", "header": "src/laige-sim/include/laige/sim/entity.h", "line": 436, "signature": "World& operator=(const World&) = delete", "summary": null, "budget": null, "experimental": false},
{"name": "laige::World::~World", "kind": "destructor", "header": "src/laige-sim/include/laige/sim/entity.h", "line": 441, "signature": "~World() noexcept", "summary": "Detaches every live entity's component rows (clear()) and releases the backing storage (per-slot tables, archetype table with its column blocks, type-key index). Idempotent with clear().", "budget": null, "experimental": false},
{"name": "laige::World::each", "kind": "method", "header": "src/laige-sim/include/laige/sim/entity.h", "line": 419, "signature": "template <typename... Ts, typename... Acc, typename F> [[nodiscard]] Status each(F&& fn, Acc...) noexcept", "summary": "Iterate every entity having ALL of T1..TN (superset match: extra components do not exclude an entity), invoking `fn(Entity, R1, ..., RN)` — one reference per listed component, in template order: a `const T&` where the access tag is Read, a `T&` where it is Write. The access tags follow `fn`, one Read/Write tag per listed component, in the same order (checked at compile time — they come after the callable because a pack of parameters must be the last parameters to be deducible); `each<>` (no components, no tags) visits every live entity in ascending slot-id order with no component references.", "budget": null, "experimental": false},
{"name": "laige::World::clear", "kind": "method", "header": "src/laige-sim/include/laige/sim/entity.h", "line": 433, "signature": "[[nodiscard]] Status clear() noexcept", "summary": "Destroy every live entity (shutdown path, CONC-006). Every handle becomes stale; the capacity is unchanged and the world is immediately reusable. O(capacity + detached rows * row-stride), no allocation, idempotent. M1-ECS-03: each live entity is detached from its archetype first (the per-entity component data is released with its row); the archetypes themselves — and the component type registry — survive. M1-ECS-04: rejected with ErrorCode::InvalidArgument (+ one rate-limited warn) while an iteration is active and any matched archetype still holds live rows — the clear is skipped, never partial (assert in debug; query.h \"Iteration legality\"); an ok Status otherwise.", "budget": null, "experimental": false},
{"name": "laige::World::World", "kind": "constructor", "header": "src/laige-sim/include/laige/sim/entity.h", "line": 437, "signature": "World(World&& other) noexcept", "summary": "Move is an O(1) pointer swap; the source becomes a valid empty world (capacity 0: every create() fails, every handle invalid).", "budget": null, "experimental": false},
{"name": "laige::World::operator=", "kind": "method", "header": "src/laige-sim/include/laige/sim/entity.h", "line": 438, "signature": "World& operator=(World&& other) noexcept", "summary": null, "budget": null, "experimental": false},
{"name": "laige::World::World", "kind": "constructor", "header": "src/laige-sim/include/laige/sim/entity.h", "line": 439, "signature": "World(const World&) = delete", "summary": null, "budget": null, "experimental": false},
{"name": "laige::World::operator=", "kind": "method", "header": "src/laige-sim/include/laige/sim/entity.h", "line": 440, "signature": "World& operator=(const World&) = delete", "summary": null, "budget": null, "experimental": false},
{"name": "laige::World::~World", "kind": "destructor", "header": "src/laige-sim/include/laige/sim/entity.h", "line": 445, "signature": "~World() noexcept", "summary": "Detaches every live entity's component rows (clear()) and releases the backing storage (per-slot tables, archetype table with its column blocks, type-key index). Idempotent with clear().", "budget": null, "experimental": false},
{"name": "laige::Access", "kind": "enum", "header": "src/laige-sim/include/laige/sim/query.h", "line": 234, "signature": "enum class Access : std::uint8_t", "summary": "The declared per-component access of a query (FR-1.3). Read: the component is only read during the iteration; Write: the system mutates it (through the query's reference or an in-place addComponent<T> overwrite — both legal, see the preamble \"Iteration legality\"). M1-SYS-01's system I/O declarations reuse this value type.", "budget": null, "experimental": false},
{"name": "laige::Access::Read", "kind": "enumerator", "header": "src/laige-sim/include/laige/sim/query.h", "line": 235, "signature": "Read = 0", "summary": null, "budget": null, "experimental": false},
{"name": "laige::Access::Write", "kind": "enumerator", "header": "src/laige-sim/include/laige/sim/query.h", "line": 236, "signature": "Write = 1", "summary": null, "budget": null, "experimental": false},
Expand Down
Loading
Loading