Optimize column access

Cleanup
This commit is contained in:
Ukendio 2025-06-25 13:31:25 +02:00
parent 24dddee82e
commit 1ee5790035
122 changed files with 3837 additions and 1716 deletions

0
.gitattributes vendored Normal file → Executable file
View file

0
.github/ISSUE_TEMPLATE/BUG-REPORT.md vendored Normal file → Executable file
View file

0
.github/ISSUE_TEMPLATE/DOCUMENTATION.md vendored Normal file → Executable file
View file

0
.github/ISSUE_TEMPLATE/FEATURE-REQUEST.md vendored Normal file → Executable file
View file

0
.github/PULL_REQUEST_TEMPLATE.md vendored Normal file → Executable file
View file

0
.github/workflows/analysis.yaml vendored Normal file → Executable file
View file

0
.github/workflows/dependabot.yml vendored Normal file → Executable file
View file

0
.github/workflows/deploy-docs.yaml vendored Normal file → Executable file
View file

0
.github/workflows/publish-npm.yml vendored Normal file → Executable file
View file

0
.github/workflows/release.yaml vendored Normal file → Executable file
View file

0
.github/workflows/unit-testing.yaml vendored Normal file → Executable file
View file

0
.gitignore vendored Normal file → Executable file
View file

0
.luaurc Normal file → Executable file
View file

0
.prettierrc Normal file → Executable file
View file

0
.stylua.toml Normal file → Executable file
View file

10
CHANGELOG.md Normal file → Executable file
View file

@ -2,6 +2,16 @@
## Unreleased
### Added
- `jecs.component_record` for retrieving the component_record of a component.
- `Column<T>` and `ColumnsMap<T>` types for typescript.
### Changed
- The fields `archetype.records[id]` and `archetype.counts[id` have been removed from the archetype struct and been moved to the component record `component_index[id].records[archetype.id]` and `component_index[id].counts[archetype.id]` respectively.
- Removed the metatable `jecs.World`. Use `jecs.world()` to create your World.
- Archetypes will no longer be garbage collected when invalidated, allowing them to be recycled to save a lot of performance during frequent deletion.
- Removed `jecs.entity_index_try_get_fast`. Use `jecs.entity_index_try_get` instead.
## 0.6.1
### Changed

0
LICENSE Normal file → Executable file
View file

0
README.md Normal file → Executable file
View file

0
addons/observers.luau Normal file → Executable file
View file

0
assets/image-1.png Normal file → Executable file
View file

Before

Width:  |  Height:  |  Size: 392 KiB

After

Width:  |  Height:  |  Size: 392 KiB

0
assets/image-2.png Normal file → Executable file
View file

Before

Width:  |  Height:  |  Size: 391 KiB

After

Width:  |  Height:  |  Size: 391 KiB

0
assets/image-3.png Normal file → Executable file
View file

Before

Width:  |  Height:  |  Size: 61 KiB

After

Width:  |  Height:  |  Size: 61 KiB

0
assets/image-4.png Normal file → Executable file
View file

Before

Width:  |  Height:  |  Size: 103 KiB

After

Width:  |  Height:  |  Size: 103 KiB

0
assets/image-5.png Normal file → Executable file
View file

Before

Width:  |  Height:  |  Size: 8.7 KiB

After

Width:  |  Height:  |  Size: 8.7 KiB

0
assets/jecs_darkmode.svg Normal file → Executable file
View file

Before

Width:  |  Height:  |  Size: 1.2 KiB

After

Width:  |  Height:  |  Size: 1.2 KiB

0
assets/jecs_lightmode.svg Normal file → Executable file
View file

Before

Width:  |  Height:  |  Size: 1.2 KiB

After

Width:  |  Height:  |  Size: 1.2 KiB

0
assets/logo_old.png Normal file → Executable file
View file

Before

Width:  |  Height:  |  Size: 68 KiB

After

Width:  |  Height:  |  Size: 68 KiB

0
bench.project.json Normal file → Executable file
View file

0
benches/cached.luau Normal file → Executable file
View file

34
benches/general.luau Normal file → Executable file
View file

@ -14,7 +14,7 @@ local pair = jecs.pair
do
TITLE("create")
local world = jecs.World.new()
local world = jecs.world()
BENCH("entity", function()
for i = 1, START(N) do
@ -35,7 +35,7 @@ end
do
TITLE("set")
local world = jecs.World.new()
local world = jecs.world()
local A = world:component()
local entities = table.create(N)
@ -69,7 +69,7 @@ end
do
TITLE("set relationship")
local world = jecs.World.new()
local world = jecs.world()
local A = world:component()
local entities = table.create(N)
@ -103,7 +103,7 @@ end
do
TITLE("get")
local world = jecs.World.new()
local world = jecs.world()
local A = world:component()
local B = world:component()
local C = world:component()
@ -147,7 +147,7 @@ do
TITLE("target")
BENCH("1st target", function()
local world = jecs.World.new()
local world = jecs.world()
local A = world:component()
local B = world:component()
local C = world:component()
@ -179,7 +179,7 @@ do
local function view_bench(n: number)
BENCH(`{n} entities per archetype`, function()
local world = jecs.World.new()
local world = jecs.world()
local A = world:component()
local B = world:component()
@ -205,7 +205,7 @@ do
end)
BENCH(`inlined query`, function()
local world = jecs.World.new()
local world = jecs.world()
local A = world:component()
local B = world:component()
local C = world:component()
@ -225,12 +225,22 @@ do
local archetypes = world:query(A, B, C, D):archetypes()
START()
-- for _, archetype in archetypes do
-- local columns, records = archetype.columns, archetype.records
-- local a = columns[records[A]]
-- local b = columns[records[B]]
-- local c = columns[records[C]]
-- local d = columns[records[D]]
-- for row in archetype.entities do
-- local _1, _2, _3, _4 = a[row], b[row], c[row], d[row]
-- end
-- end
for _, archetype in archetypes do
local columns, records = archetype.columns, archetype.records
local a = columns[records[A]]
local b = columns[records[B]]
local c = columns[records[C]]
local d = columns[records[D]]
local columns_map = archetype.columns_map
local a = columns_map[A]
local b = columns_map[B]
local c = columns_map[C]
local d = columns_map[D]
for row in archetype.entities do
local _1, _2, _3, _4 = a[row], b[row], c[row], d[row]
end

2
benches/query.luau Normal file → Executable file
View file

@ -15,7 +15,7 @@ type i53 = number
do
TITLE(testkit.color.white_underline("Jecs query"))
local ecs = jecs.World.new()
local ecs = jecs.world()
do
TITLE("one component in common")

5
benches/visual/despawn.bench.luau Normal file → Executable file
View file

@ -6,8 +6,7 @@ local Matter = require(ReplicatedStorage.DevPackages.Matter)
local ecr = require(ReplicatedStorage.DevPackages.ecr)
local jecs = require(ReplicatedStorage.Lib)
local pair = jecs.pair
local newWorld = Matter.World.new()
local ecs = jecs.World.new()
local ecs = jecs.world()
local mirror = require(ReplicatedStorage.mirror)
local mcs = mirror.World.new()
@ -26,8 +25,6 @@ mcs:add(E3, pair(jecs.OnDeleteTarget, jecs.Delete))
local E4 = mcs:entity()
mcs:add(E4, pair(jecs.OnDeleteTarget, jecs.Delete))
local registry2 = ecr.registry()
return {
ParameterGenerator = function()
local j = ecs:entity()

22
benches/visual/insertion.bench.luau Normal file → Executable file
View file

@ -2,30 +2,11 @@
--!native
local ReplicatedStorage = game:GetService("ReplicatedStorage")
local Matter = require(ReplicatedStorage.DevPackages.Matter)
local ecr = require(ReplicatedStorage.DevPackages.ecr)
local jecs = require(ReplicatedStorage.Lib:Clone())
local ecs = jecs.World.new()
local ecs = jecs.world()
local mirror = require(ReplicatedStorage.mirror:Clone())
local mcs = mirror.World.new()
local A1 = Matter.component()
local A2 = Matter.component()
local A3 = Matter.component()
local A4 = Matter.component()
local A5 = Matter.component()
local A6 = Matter.component()
local A7 = Matter.component()
local A8 = Matter.component()
local B1 = ecr.component()
local B2 = ecr.component()
local B3 = ecr.component()
local B4 = ecr.component()
local B5 = ecr.component()
local B6 = ecr.component()
local B7 = ecr.component()
local B8 = ecr.component()
local C1 = ecs:component()
local C2 = ecs:component()
@ -44,7 +25,6 @@ local E6 = mcs:component()
local E7 = mcs:component()
local E8 = mcs:component()
local registry2 = ecr.registry()
return {
ParameterGenerator = function()
return

45
benches/visual/query.bench.luau Normal file → Executable file
View file

@ -2,14 +2,14 @@
--!native
local ReplicatedStorage = game:GetService("ReplicatedStorage")
local Matter = require(ReplicatedStorage.DevPackages["_Index"]["matter-ecs_matter@0.8.1"].matter)
local ecr = require(ReplicatedStorage.DevPackages["_Index"]["centau_ecr@0.8.0"].ecr)
local Matter = require(ReplicatedStorage.DevPackages.matter)
local ecr = require(ReplicatedStorage.DevPackages.ecr)
local newWorld = Matter.World.new()
local jecs = require(ReplicatedStorage.Lib)
local mirror = require(ReplicatedStorage.mirror)
local mcs = mirror.World.new()
local ecs = jecs.World.new()
local jecs = require(ReplicatedStorage.Lib:Clone())
local mirror = require(ReplicatedStorage.mirror:Clone())
local mcs = mirror.world()
local ecs = jecs.world()
local A1 = Matter.component()
local A2 = Matter.component()
@ -38,14 +38,14 @@ local D6 = ecs:component()
local D7 = ecs:component()
local D8 = ecs:component()
local E1 = mcs:entity()
local E2 = mcs:entity()
local E3 = mcs:entity()
local E4 = mcs:entity()
local E5 = mcs:entity()
local E6 = mcs:entity()
local E7 = mcs:entity()
local E8 = mcs:entity()
local E1 = mcs:component()
local E2 = mcs:component()
local E3 = mcs:component()
local E4 = mcs:component()
local E5 = mcs:component()
local E6 = mcs:component()
local E7 = mcs:component()
local E8 = mcs:component()
local registry2 = ecr.registry()
@ -146,13 +146,18 @@ return {
end,
Functions = {
Matter = function()
for entityId, firstComponent in newWorld:query(A2, A4, A6, A8) do
end
end,
-- Matter = function()
-- for entityId, firstComponent in newWorld:query(A2, A4, A6, A8) do
-- end
-- end,
ECR = function()
for entityId, firstComponent in registry2:view(B2, B4, B6, B8) do
-- ECR = function()
-- for entityId, firstComponent in registry2:view(B2, B4, B6, B8) do
-- end
-- end,
--
Mirror = function()
for entityId, firstComponent in mcs:query(E2, E4, E6, E8) do
end
end,

2
benches/visual/remove.bench.luau Normal file → Executable file
View file

@ -6,7 +6,7 @@ local Matter = require(ReplicatedStorage.DevPackages.Matter)
local ecr = require(ReplicatedStorage.DevPackages.ecr)
local jecs = require(ReplicatedStorage.Lib)
local pair = jecs.pair
local ecs = jecs.World.new()
local ecs = jecs.world()
local mirror = require(ReplicatedStorage.mirror)
local mcs = mirror.World.new()

0
benches/visual/spawn.bench.luau Normal file → Executable file
View file

0
benches/visual/wally.toml Normal file → Executable file
View file

0
default.project.json Normal file → Executable file
View file

0
demo/.gitignore vendored Normal file → Executable file
View file

0
demo/README.md Normal file → Executable file
View file

0
demo/default.project.json Normal file → Executable file
View file

0
demo/src/ReplicatedStorage/collect.luau Normal file → Executable file
View file

0
demo/src/ReplicatedStorage/components.luau Normal file → Executable file
View file

0
demo/src/ReplicatedStorage/main.client.luau Normal file → Executable file
View file

0
demo/src/ReplicatedStorage/observers_add.luau Normal file → Executable file
View file

0
demo/src/ReplicatedStorage/remotes.luau Normal file → Executable file
View file

0
demo/src/ReplicatedStorage/schedule.luau Normal file → Executable file
View file

View file

0
demo/src/ReplicatedStorage/types.luau Normal file → Executable file
View file

0
demo/src/ServerScriptService/main.server.luau Normal file → Executable file
View file

View file

View file

0
demo/src/ServerScriptService/systems/poison_hurts.luau Normal file → Executable file
View file

0
demo/src/ServerScriptService/systems/replication.luau Normal file → Executable file
View file

0
demo/wally.toml Normal file → Executable file
View file

0
docs/.vitepress/config.mts Normal file → Executable file
View file

0
docs/api/jecs.md Normal file → Executable file
View file

0
docs/api/query.md Normal file → Executable file
View file

0
docs/api/world.md Normal file → Executable file
View file

0
docs/index.md Normal file → Executable file
View file

0
docs/learn/contributing/coverage.md Normal file → Executable file
View file

0
docs/learn/contributing/guidelines.md Normal file → Executable file
View file

0
docs/learn/contributing/issues.md Normal file → Executable file
View file

0
docs/learn/contributing/pull-requests.md Normal file → Executable file
View file

0
docs/learn/overview.md Normal file → Executable file
View file

0
docs/learn/public/jecs_logo.svg Normal file → Executable file
View file

Before

Width:  |  Height:  |  Size: 3.9 KiB

After

Width:  |  Height:  |  Size: 3.9 KiB

0
docs/public/coverage/ansi.luau.html vendored Normal file → Executable file
View file

0
docs/public/coverage/entity_visualiser.luau.html vendored Normal file → Executable file
View file

0
docs/public/coverage/index.html vendored Normal file → Executable file
View file

0
docs/public/coverage/jecs.luau.html vendored Normal file → Executable file
View file

0
docs/public/coverage/lifetime_tracker.luau.html vendored Normal file → Executable file
View file

0
docs/public/coverage/testkit.luau.html vendored Normal file → Executable file
View file

0
docs/public/coverage/tests.luau.html vendored Normal file → Executable file
View file

0
docs/public/jecs_logo.svg Normal file → Executable file
View file

Before

Width:  |  Height:  |  Size: 3.9 KiB

After

Width:  |  Height:  |  Size: 3.9 KiB

0
docs/resources.md Normal file → Executable file
View file

0
examples/README.md Normal file → Executable file
View file

0
examples/luau/entities/basics.luau Normal file → Executable file
View file

0
examples/luau/entities/hierarchy.luau Normal file → Executable file
View file

0
examples/luau/hooks/cleanup.luau Normal file → Executable file
View file

0
examples/luau/queries/basics.luau Normal file → Executable file
View file

0
examples/luau/queries/changetracking.luau Normal file → Executable file
View file

0
examples/luau/queries/spatial_grids.luau Normal file → Executable file
View file

0
examples/luau/queries/wildcards.luau Normal file → Executable file
View file

21
jecs.d.ts vendored Normal file → Executable file
View file

@ -41,16 +41,15 @@ type Nullable<T extends unknown[]> = { [K in keyof T]: T[K] | undefined };
type InferComponents<A extends Id[]> = { [K in keyof A]: InferComponent<A[K]> };
type ArchetypeId = number;
type Column = unknown[];
export type Column<T> = T[];
export type Archetype = {
export type Archetype<T extends unknown[]> = {
id: number;
types: number[];
type: string;
entities: number[];
columns: Column[];
records: number[];
counts: number[];
columns: Column<unknown>[];
columns_map: { [K in keyof T]: Column<T[K]> }
};
type Iter<T extends unknown[]> = IterableFunction<LuaTuple<[Entity, ...T]>>;
@ -65,7 +64,7 @@ export type CachedQuery<T extends unknown[]> = {
* Returns the matched archetypes of the query
* @returns An array of archetypes of the query
*/
archetypes(): Archetype[];
archetypes(): Archetype<T>[];
} & Iter<T>;
export type Query<T extends unknown[]> = {
@ -99,7 +98,7 @@ export type Query<T extends unknown[]> = {
* Returns the matched archetypes of the query
* @returns An array of archetypes of the query
*/
archetypes(): Archetype[];
archetypes(): Archetype<T>[];
} & Iter<T>;
export class World {
@ -310,3 +309,11 @@ export declare const Delete: Tag;
export declare const Remove: Tag;
export declare const Name: Entity<string>;
export declare const Rest: Entity;
export type ComponentRecord = {
records: Map<Id, number>,
counts: Map<Id, number>,
size: number,
}
export function component_record(world: World, id: Id): ComponentRecord

2378
jecs.luau Normal file → Executable file

File diff suppressed because it is too large Load diff

2943
mirror.luau Normal file → Executable file

File diff suppressed because it is too large Load diff

0
package-lock.json generated Normal file → Executable file
View file

0
package.json Normal file → Executable file
View file

0
rokit.toml Normal file → Executable file
View file

0
test/addons/observers.luau Normal file → Executable file
View file

0
test/lol.luau Normal file → Executable file
View file

0
test/stress.client.luau Normal file → Executable file
View file

0
test/stress.project.json Normal file → Executable file
View file

69
test/tests.luau Normal file → Executable file
View file

@ -64,7 +64,7 @@ TEST("world:add()", function()
end
do CASE("archetype move")
local world = jecs.World.new()
local world = jecs.world()
local d = dwi(world)
@ -609,7 +609,7 @@ TEST("world:delete()", function()
end
do CASE "fast delete"
local world = jecs.World.new()
local world = jecs.world()
local entities = {}
local Health = world:component()
@ -635,7 +635,7 @@ TEST("world:delete()", function()
end
do CASE "cycle"
local world = jecs.World.new()
local world = jecs.world()
local Likes = world:component()
world:add(Likes, pair(jecs.OnDeleteTarget, jecs.Delete))
local bob = world:entity()
@ -800,7 +800,7 @@ end)
TEST("world:has()", function()
do CASE "should find Tag on entity"
local world = jecs.World.new()
local world = jecs.world()
local Tag = world:entity()
@ -811,7 +811,7 @@ TEST("world:has()", function()
end
do CASE "should return false when missing one tag"
local world = jecs.World.new()
local world = jecs.world()
local A = world:entity()
local B = world:entity()
@ -866,7 +866,7 @@ TEST("world:query()", function()
CHECK(#q:archetypes() == 0)
end
do CASE "multiple iter"
local world = jecs.World.new()
local world = jecs.world()
local A = world:component() :: jecs.Entity<string>
local B = world:component() :: jecs.Entity<string>
local e = world:entity()
@ -883,7 +883,7 @@ TEST("world:query()", function()
CHECK(counter == 2)
end
do CASE "tag"
local world = jecs.World.new()
local world = jecs.world()
local A = world:entity()
local e = world:entity()
CHECK_EXPECT_ERR(function()
@ -897,7 +897,7 @@ TEST("world:query()", function()
CHECK(count == 1)
end
do CASE "pairs"
local world = jecs.World.new()
local world = jecs.world()
local C1 = world:component() :: jecs.Id<boolean>
local C2 = world:component() :: jecs.Id<boolean>
@ -969,7 +969,7 @@ TEST("world:query()", function()
do CASE "query single component"
do
local world = jecs.World.new()
local world = jecs.world()
local A = world:component()
local B = world:component()
@ -1125,7 +1125,7 @@ TEST("world:query()", function()
end
do CASE "should query all entities without B"
local world = jecs.World.new()
local world = jecs.world()
local A = world:component()
local B = world:component()
@ -1149,7 +1149,7 @@ TEST("world:query()", function()
end
do CASE "should allow querying for relations"
local world = jecs.World.new()
local world = jecs.world()
local Eats = world:component()
local Apples = world:component()
local bob = world:entity()
@ -1162,7 +1162,7 @@ TEST("world:query()", function()
end
do CASE "should allow wildcards in queries"
local world = jecs.World.new()
local world = jecs.world()
local Eats = world:component()
local Apples = world:entity()
local bob = world:entity()
@ -1181,7 +1181,7 @@ TEST("world:query()", function()
end
do CASE "should match against multiple pairs"
local world = jecs.World.new()
local world = jecs.world()
local Eats = world:component()
local Apples = world:entity()
local Oranges = world:entity()
@ -1213,7 +1213,7 @@ TEST("world:query()", function()
end
do CASE "should only relate alive entities"
local world = jecs.World.new()
local world = jecs.world()
local Eats = world:entity()
local Apples = world:component()
local Oranges = world:component()
@ -1241,7 +1241,7 @@ TEST("world:query()", function()
do
CASE("should error when setting invalid pair")
local world = jecs.World.new()
local world = jecs.world()
local Eats = world:component()
local Apples = world:component()
local bob = world:entity()
@ -1254,7 +1254,7 @@ TEST("world:query()", function()
do
CASE("should find target for ChildOf")
local world = jecs.World.new()
local world = jecs.world()
local ChildOf = jecs.ChildOf
local Name = world:component()
@ -1278,7 +1278,7 @@ TEST("world:query()", function()
do
CASE("despawning while iterating")
local world = jecs.World.new()
local world = jecs.world()
local A = world:component()
local B = world:component()
@ -1297,7 +1297,7 @@ TEST("world:query()", function()
end
do CASE "should not find any entities"
local world = jecs.World.new()
local world = jecs.world()
local Hello = world:component()
local Bob = world:component()
@ -1316,7 +1316,7 @@ TEST("world:query()", function()
do CASE "world:query():without()"
-- REGRESSION TEST
local world = jecs.World.new()
local world = jecs.world()
local _1, _2, _3 = world:component(), world:component(), world:component()
local counter = 0
@ -1330,7 +1330,7 @@ end)
TEST("world:remove()", function()
do
CASE("should allow remove a component that doesn't exist on entity")
local world = jecs.World.new()
local world = jecs.world()
local Health = world:component()
local Poison = world:component()
@ -1444,10 +1444,15 @@ TEST("world:target", function()
CHECK(jecs.pair_first(world, pair(B, C)) == B)
local r = (jecs.entity_index_try_get(world.entity_index :: any, e :: any) :: any) :: jecs.Record
local archetype = r.archetype
local records = archetype.records
local counts = archetype.counts
CHECK(counts[pair(A, __)] == 4)
CHECK(records[pair(B, C)] > records[pair(A, E)])
local function cdr(id)
return assert(jecs.component_record(world, id))
end
local idr_b_c = cdr(pair(B, C))
local idr_a_wc = cdr(pair(A, __))
local idr_a_e = cdr(pair(A, E))
CHECK(idr_a_wc.counts[archetype.id] == 4)
CHECK(idr_b_c.records[archetype.id] > idr_a_e.records[archetype.id])
CHECK(world:target(e, A, 0) == B)
CHECK(world:target(e, A, 1) == C)
CHECK(world:target(e, A, 2) == D)
@ -1457,10 +1462,10 @@ TEST("world:target", function()
CHECK(world:target(e, C, 0) == D)
CHECK(world:target(e, C, 1) == nil)
CHECK(archetype.records[pair(A, B):: any] == 1)
CHECK(archetype.records[pair(A, C):: any] == 2)
CHECK(archetype.records[pair(A, D):: any] == 3)
CHECK(archetype.records[pair(A, E):: any] == 4)
CHECK(cdr(pair(A, B)).records[archetype.id] == 1)
CHECK(cdr(pair(A, C)).records[archetype.id] == 2)
CHECK(cdr(pair(A, D)).records[archetype.id] == 3)
CHECK(cdr(pair(A, E)).records[archetype.id] == 4)
CHECK(world:target(e, C, 0) == D)
CHECK(world:target(e, C, 1) == nil)
@ -1582,9 +1587,13 @@ TEST("#repro", function()
local function getTargets(relation)
local tgts = {}
local pairwildcard = pair(relation, jecs.Wildcard)
local idr = assert(jecs.component_record(world, pairwildcard))
local counts = idr.counts
local records = idr.records
for _, archetype in world:query(pairwildcard):archetypes() do
local tr = archetype.records[pairwildcard]
local count = archetype.counts[pairwildcard]
local archetype_id = archetype.id
local count = counts[archetype_id]
local tr = records[archetype_id]
local types = archetype.types
for _, entity in archetype.entities do
for i = 0, count - 1 do

0
test/tools/entity_visualiser.luau Normal file → Executable file
View file

0
thesis/drafts/1/listings-rust.sty Normal file → Executable file
View file

0
thesis/drafts/1/paper.aux Normal file → Executable file
View file

0
thesis/drafts/1/paper.fdb_latexmk Normal file → Executable file
View file

0
thesis/drafts/1/paper.fls Normal file → Executable file
View file

0
thesis/drafts/1/paper.log Normal file → Executable file
View file

Some files were not shown because too many files have changed in this diff Show more