Skip to content

[M1-ECS-01] Entity handle - #16

Merged
offdev merged 2 commits into
masterfrom
m1-ecs-01-entity-handle
Sep 13, 2026
Merged

offdev merged 2 commits into
masterfrom
m1-ecs-01-entity-handle

Conversation

@offdev

@offdev offdev commented Sep 13, 2026

Copy link
Copy Markdown
Owner

Implements M1-ECS-01 · Entity handle (roadmap/M1-heartbeat.md) — the first step of M1, and the first module beyond laige-core (laige-sim), per the milestone rules.

What lands

laige::Entity — the 32-bit handle (FR-1.2, CPP-007): 16-bit slot id + 16-bit generation, static_asserted to exactly 4 bytes. Generation 0 is reserved (the default Entity{} is never valid); the 16-bit generation wrap (one slot freed 2^16 times) is documented in the header and pinned by a test (WorldStale.GenerationWrapDocumentedCollision) rather than assumed — the same documented-limit pattern as the M0-CORE-05 Pool<T> 2^32 case.

laige::World — the pool-backed entity storage (create/destroy, no per-op heap):

  • slot table = per-slot uint16 generation + uint8 alive flag + pre-allocated LIFO free stack (5 B/slot); the factory is the only allocation; every op is O(1)
  • capacity is the declared scene budget (G-R3): create() beyond it → BudgetExhausted; a >65536 budget is rejected at construction with InvalidArgument (API-008 — the invalid state is unrepresentable)
  • stale-handle contract (FR-12.3, S-9): queries degrade safely in every build (isValid → false, check() → Status(InvalidArgument) + warn-once); uses of a stale handle (destroy) assert in debug and return Status(InvalidArgument) + warn-once in release — never silent. Warns go through the logging facade (stable events ecs/stale_entity_access / ecs/stale_entity_destroy; the facade's rate limit implements the warn-once semantics, LOG-004)
  • move-only, single owner thread (CONC-001), deterministic (ARCH-010): bit-identical handle sequences for a given operation sequence, so handles are replay state from M1 on
  • EntityStats accounting (G-R3 / M1-PROF-01 feed)

check() is the access-validation primitive M1-ECS-03's component access builds on — the stale-handle contract stated in this step's Scope is documented here so the next step inherits it.

Files

  • src/laige-sim/CMakeLists.txt, entity.cpp, include/laige/sim/entity.h (new module; static/shared, engine + SimMath policy, single PUBLIC link to laige-core)
  • tests/laige-sim/ — entity CTest entry, 19 cases (the three the step names — generation bump on reuse, stale detection, capacity limit — plus LIFO ordering, clear/move, the 65536 boundary, the forked-SIGABRT stale destroy, stats, and the warn-once rate-limit summary via a memory sink)
  • docs/api/entity.md (API contract incl. Performance section, misuse warnings, roadmap context); docs/README.md linked
  • root CMakeLists.txt + tests/CMakeLists.txt wiring; laige-api.json regenerated (32 new symbols); roadmap box + Progress Board (M1 1/25) + change log

Verify (all local, this branch)

Tree Configure Result
g++ Debug (canonical) cmake -S . -B build -DCMAKE_BUILD_TYPE=Debug zero warnings; ctest 34/34
ASan+UBSan … -B build-asan -DLAIGE_ASAN=ON zero warnings; ctest -R entity green; ctest 34/34 (the step's Verify clause)
TSan … -B build-tsan -DLAIGE_TSAN=ON zero warnings; ctest -R entity green
shared (NFR-8.9) … -B build-shared -DLAIGE_BUILD_SHARED=ON zero warnings; ctest 34/34
Release … -B build-release -DCMAKE_BUILD_TYPE=Release zero warnings; ctest -R entity green — exercises the #ifdef NDEBUG release path of the stale destroy (Status + warn-once, verified in output)
  • ctest -R entity: 19 cases — 18 passed, 1 skipped in debug (DestroyStaleReturnsInvalidArgumentInRelease, release-only by design; runs on the Release row above)
  • debug stale-handle assert: forked child dies on SIGABRT (S-9)
  • tools/laige-include-lint: OK (1/10 deps; laige-sim → laige-core is the only new internal edge, R2-compliant)
  • api-real-tree: green after manifest regeneration (407 symbols, 11 headers)

No ENGINE-RULE-EXCEPTION added. No new dependencies. No public API changes to existing modules (additive only: new module + new header).

Untested / carried to CI

  • MSVC (windows-msvc) and AppleClang (macOS) compiles land in the CI matrix (no MSVC/AppleClang locally); the fork test skips on Windows by design (POSIX-only pattern, same as pools_tests)
  • The standing zero-alloc assertion lands with M1-ALLOC-01; per the M1 milestone rules, ASan + pool accounting is the check until then

First module beyond laige-core (M1 milestone rules): the laige-sim module with the ECS foundation per roadmap/M1-heartbeat.md M1-ECS-01.

- laige::Entity: 32-bit handle (16-bit id + 16-bit generation; generation 0 reserved; 65536 slots; documented 2^16 wrap limit)
- laige::World: pool-backed entity storage (create/destroy/check/isValid/clear/stats; setup-only allocation; O(1) no-allocation ops; LIFO deterministic slot assignment)
- Stale-handle contract: debug assert / release Status(InvalidArgument) + warn-once via the logging facade (ecs/stale_entity_access, ecs/stale_entity_destroy) — never silent (FR-12.3, CORE-008)
- Capacity = declared scene budget (G-R3): BudgetExhausted on overflow; InvalidArgument for a >65536 budget at construction (API-008)
- entity CTest entry (19 cases incl. the forked-SIGABRT stale destroy and the pinned 2^16 generation wrap); docs/api/entity.md; laige-api.json regenerated

Verify: ctest -R entity + full suite green on fresh g++ Debug, ASan, TSan, shared, and Release trees, zero warnings; tools/laige-include-lint OK; api-real-tree green.
@offdev
offdev merged commit b16a671 into master Sep 13, 2026
10 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant