Skip to content

feat/generational ids - #34

Merged
ragoune merged 4 commits into
masterfrom
feat/generational-ids
Sep 18, 2026
Merged

ragoune merged 4 commits into
masterfrom
feat/generational-ids

Conversation

@ragoune

@ragoune ragoune commented Sep 18, 2026

Copy link
Copy Markdown
Contributor

Generational entity handles

EntityId is now a generational handle: the slot lives in the low 32 bits, its generation in the high 32 bits. Generations start at 1, so EntityId(0) is a guaranteed null handle (IsNull()). Index() and Generation() expose the parts.

RemoveEntity bumps the slot's generation. A handle kept after its entity was removed is dead, even once the slot is recycled by a new entity: it never aliases the new occupant. Exists reports false, GetComponent returns nil, Add*/Remove* return an error, RemoveEntity is a no-op. The slot keeps a tombstone next to the generation, so a forged handle cannot resolve to a freed slot either.

Bug fix

An entity created bare (CreateEntity) and then given a component was appended to its new archetype without being swap-removed from the empty archetype. The stale entry was counted twice by optional-only queries, and a later swap-remove in the empty archetype rewrote the key of a live entity, aliasing its row with a neighbour's. Two tests reproduce it (both fail on v1.9.0).

Also

  • BenchmarkCreateRemoveVolt: full remove+recreate cycle, exercises slot recycling.
  • CI: golangci-lint v2.13.2 via golangci-lint-action v9 (the pinned v1.64.5 could not read the export data of the Go 1.25 toolchain).

Breaking change

Any code indexing a slice or array by a raw EntityId must use entityId.Index() instead, and == 0 sentinels become IsNull().

…tityId, null handle

EntityId now packs the slot (low 32 bits) and its generation (high 32 bits,
starting at 1). RemoveEntity bumps the slot's generation, so a handle kept
after its entity was removed is dead even once the slot is recycled: Exists
reports false, GetComponent nil, Add*/Remove* an error — never the slot's new
occupant. The zero value is the null handle. Index(), Generation() and
IsNull() expose the parts of a handle.

The record keeps a tombstone (key -1) next to the generation, so a forged
(index, generation+1) handle cannot resolve to a freed slot. The same
unplaced state fixes a latent bug: an entity created bare then given a
component was appended to its new archetype without being swap-removed from
the empty archetype (stale entry, counted twice by optional-only queries, and
whose later swap-remove rewrites the key of a live entity). A failed
CreateEntityWithComponentsN now gives its slot back and returns the null
handle.

Benchmarks (5800X, n=6, vs v1.9.0): create 15.6 ms vs 16.4, create+remove
6.5 ms vs 6.2 (median +1.6 %), add/remove/iterate within ±2 % — inside the
±5 % budget. Tests: default and -race, all green.
The pinned golangci-lint v1.64.5 is built with Go 1.24 and cannot type-check
the Go 1.25 toolchain that `go-version: stable` now resolves to, which fails
the lint job. golangci-lint-action v9 runs on Node 24 (Node 20 is deprecated
on GitHub runners) and requires golangci-lint v2. No configuration file:
the default linters report 0 issues locally with v2.
@ragoune
ragoune merged commit fb20a30 into master Sep 18, 2026
4 checks passed
@ragoune
ragoune deleted the feat/generational-ids branch September 18, 2026 13:47
@mlange-42

Copy link
Copy Markdown

@ragoune I highly recommend to handle dead entities more strictly, i.e. always panic when the user tries to do anything about them (incl. getting component or removing entity).

Experience has shown that keeping a dead entity somewhere is one of the most common sources of bugs. If you let things slip through silently, these bugs will be hard to notice and to track down.

@ragoune

ragoune commented Sep 18, 2026

Copy link
Copy Markdown
Contributor Author

Hello, indeed I hesitated to panic in this case, so it confirms it is the best way to handle this case. I will do it ASAP, before the next release.

Thanks for the feedback !

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.

2 participants