Repository navigation
Add an archetype storage next to the sparse set - #169
Merged
Merged
Conversation
ctreffs
force-pushed
the
2.0/archetype-storage
branch
7 times, most recently
from
October 26, 2026 12:45
d96873f to
857ce7f
Compare
An ArchetypeTable holds the entities of one component set with a hand-managed column per component type. ArchetypeStorage keeps the tables, the entity-to-(table,row) index, swap-remove row moves and cached add/remove edges between tables. Nothing selects it yet. Claude-Session: https://claude.ai/code/session_011ppjd3fe8ytKkn4EL3337N
Nexus(storage: .archetype) now selects ArchetypeStorage. Components are added, replaced and removed by moving the entity's row between tables along cached edges; get, modify, scoped access and withComponents work on the columns. Iteration and spans still serve the sparse-set storage only. The default stays the sparse set. Claude-Session: https://claude.ai/code/session_011ppjd3fe8ytKkn4EL3337N
The archetype storage calls the bulk write body once per table that holds the component type, so the event sums the calls instead of reporting the last one. Claude-Session: https://claude.ai/code/session_011ppjd3fe8ytKkn4EL3337N
…type storage Every fixed arity overload and the parameter pack fallback switch on the storage once per call. The archetype path resolves the tables a family matches from a cached query, resolves the columns once per table and loops over the rows. The pack fallback keeps the never-specialize attribute on its helpers. Claude-Session: https://claude.ai/code/session_011ppjd3fe8ytKkn4EL3337N
forEachComponentSpan passes one batch per matching table, viewing the table's columns. The single-span componentSpan(reading:) and (writing:) view the column when one table holds all components of the type. When several tables hold them the span views a gathered copy, and the writing variant stores it back when the body returns, also if it throws. Claude-Session: https://claude.ai/code/session_011ppjd3fe8ytKkn4EL3337N
The tests that create a Nexus take the storage kind as an argument (@test(arguments: Nexus.StorageKind.allCases)), so one body checks the sparse-set and the archetype storage, including the exit tests for traps. The direct storage tests run on the storage enum. New cases cover churn across component sets against a model, forEach and bulk writes over several tables, single and per-table spans, and table reuse. Claude-Session: https://claude.ai/code/session_011ppjd3fe8ytKkn4EL3337N
The index of a component type is a protocol witness. Reading it inside the loop over tables put a witness call into the loop that check-sil.sh inspects, so the archetype paths of forEach(reading:writing:) and forEachComponentSpan read the indices before the loop. Claude-Session: https://claude.ai/code/session_011ppjd3fe8ytKkn4EL3337N
The archetype storage caches the tables a family matches. Building the list on the first iteration allocated a query and grew its array inside that iteration; creating the family does it instead, so every iteration starts with a cached list. Claude-Session: https://claude.ai/code/session_011ppjd3fe8ytKkn4EL3337N
A call site switches on the storage once, so its SIL holds the loops of both storages. The script now also fails when a site's loops do not reach the archetype code, and covers forEachComponentSpan with three more sites. Claude-Session: https://claude.ai/code/session_011ppjd3fe8ytKkn4EL3337N
…e cases The IterOnly benchmarks run on the sparse-set and the archetype storage, named with the storage in brackets. StorageBenchmarks.swift ports the S2 spike cases: iteration at several arities, filtered and fragmented families, 100k entities in permuted order, string components, random access, add/remove churn and entity creation. Claude-Session: https://claude.ai/code/session_011ppjd3fe8ytKkn4EL3337N
Adds a DocC article that describes both storages, the flecs design the archetype storage follows, the benchmark numbers and the differences a program can observe (iteration order, single-type spans, per-table span batches). Claude-Session: https://claude.ai/code/session_011ppjd3fe8ytKkn4EL3337N
ctreffs
force-pushed
the
2.0/archetype-storage
branch
from
October 26, 2026 12:50
857ce7f to
23fd144
Compare
ctreffs
marked this pull request as ready for review
October 26, 2026 12:50
ctreffs
enabled stacked auto-merge (squash)
October 26, 2026 13:00
stacked auto-merge was automatically disabled
October 26, 2026 13:01
Replaced by a newer auto-merge request
ctreffs
enabled stacked auto-merge (squash)
October 26, 2026 13:01
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Step 13 of FirebladeECS 2.0. Stacked on #168.
Nexus(storage: .archetype)adds a second storage beside the sparse set, modeled on flecs:forEachand the pack fallback, spans,Single, FSM, cloning and snapshots. About 37 test files now run on both (@Test(arguments: Nexus.StorageKind.allCases))..defaultis still the sparse set. Step 14 switches it.Scripts/check-sil.shnow cover both storages.Numbers (10k entities, release, p50 µs, 0 mallocs in every iteration case)
for, 1 componentfor, 2 componentsforEach, 1 readforEach, 2 reads + 1 writeforEach, 2 reads, 100k shuffledforEachComponentSpan, read + writeget(10k lookups)On archetype, add/remove costs about 2.3x what it does on the sparse set, and create/destroy about 1.45x.
Behavior to review
componentSpan(reading:/writing:)views a gathered copy, and the writing variant scatters it back, even when the body throws. The cost is one allocation and an O(N) copy. With one table the view is zero-copy, the same as on the sparse set.forEachComponentSpanyields one batch per non-empty matching table.clear()drops all tables. A family builds its table query when it is created, which keeps the first iteration at 0 mallocs.Sequenceiteration still switches on the storage once per entity. It still beats 1.x by about 2.7x.Prior art (flecs)
In flecs, archetype tables are the default storage. The entity index maps each entity to its table and row, cached queries iterate per table, and sparse storage is an opt-in trait: "Sparse components are stored outside of tables, which means they do not have to be moved." Sources:
Verification
make test: 392 tests pass in debug (596 parameterized cases).make lintandmake docs-check-coveragepass, andScripts/check-sil.shexits 0.https://claude.ai/code/session_011ppjd3fe8ytKkn4EL3337N