diff --git a/.luaurc b/.luaurc index 522396b..1f993a4 100644 --- a/.luaurc +++ b/.luaurc @@ -2,6 +2,5 @@ "aliases": { "jecs": "src", "testkit": "testkit", - "mirror": "mirror" } } diff --git a/benches/cached.luau b/benches/cached.luau new file mode 100644 index 0000000..2e3057f --- /dev/null +++ b/benches/cached.luau @@ -0,0 +1,161 @@ + +local jecs = require("@jecs") +local mirror = require("../mirror/init") + +type i53 = number + +do + TITLE(testkit.color.white_underline("Jecs query")) + local ecs = jecs.World.new() + do + TITLE("one component in common") + + local function view_bench(world: jecs.World, A: i53, B: i53, C: i53, D: i53, E: i53, F: i53, G: i53, H: i53) + BENCH("4 component", function() + for _ in world:query(D, C, B, A) do + end + end) + end + + local D1 = ecs:component() + local D2 = ecs:component() + local D3 = ecs:component() + local D4 = ecs:component() + local D5 = ecs:component() + local D6 = ecs:component() + local D7 = ecs:component() + local D8 = ecs:component() + + local function flip() + return math.random() >= 0.15 + end + + local added = 0 + local archetypes = {} + for i = 1, 2 ^ 16 - 2 do + local entity = ecs:entity() + + local combination = "" + + if flip() then + combination ..= "B" + ecs:set(entity, D2, {value = true}) + end + if flip() then + combination ..= "C" + ecs:set(entity, D3, {value = true}) + end + if flip() then + combination ..= "D" + ecs:set(entity, D4, {value = true}) + end + if flip() then + combination ..= "E" + ecs:set(entity, D5, {value = true}) + end + if flip() then + combination ..= "F" + ecs:set(entity, D6, {value = true}) + end + if flip() then + combination ..= "G" + ecs:set(entity, D7, {value = true}) + end + if flip() then + combination ..= "H" + ecs:set(entity, D8, {value = true}) + end + + if #combination == 7 then + added += 1 + ecs:set(entity, D1, {value = true}) + end + archetypes[combination] = true + end + + local a = 0 + for _ in archetypes do + a += 1 + end + + view_bench(ecs, D1, D2, D3, D4, D5, D6, D7, D8) + end +end + +do + TITLE(testkit.color.white_underline("Mirror query")) + local ecs = mirror.World.new() + do + TITLE("one component in common") + + local function view_bench(world: jecs.World, A: i53, B: i53, C: i53, D: i53, E: i53, F: i53, G: i53, H: i53) + BENCH("4 component", function() + for _ in world:query(D, C, B, A) do + end + end) + end + + local D1 = ecs:component() + local D2 = ecs:component() + local D3 = ecs:component() + local D4 = ecs:component() + local D5 = ecs:component() + local D6 = ecs:component() + local D7 = ecs:component() + local D8 = ecs:component() + + local function flip() + return math.random() >= 0.15 + end + + local added = 0 + local archetypes = {} + for i = 1, 2 ^ 16 - 2 do + local entity = ecs:entity() + + local combination = "" + + if flip() then + combination ..= "B" + ecs:set(entity, D2, {value = true}) + end + if flip() then + combination ..= "C" + ecs:set(entity, D3, {value = true}) + end + if flip() then + combination ..= "D" + ecs:set(entity, D4, {value = true}) + end + if flip() then + combination ..= "E" + ecs:set(entity, D5, {value = true}) + end + if flip() then + combination ..= "F" + ecs:set(entity, D6, {value = true}) + end + if flip() then + combination ..= "G" + ecs:set(entity, D7, {value = true}) + end + if flip() then + combination ..= "H" + ecs:set(entity, D8, {value = true}) + end + + if #combination == 7 then + added += 1 + ecs:set(entity, D1, {value = true}) + end + archetypes[combination] = true + end + + local a = 0 + for _ in archetypes do + a += 1 + end + + view_bench(ecs, D1, D2, D3, D4, D5, D6, D7, D8) + end +end diff --git a/benches/visual/query.bench.luau b/benches/visual/query.bench.luau index 3d00783..d579dce 100644 --- a/benches/visual/query.bench.luau +++ b/benches/visual/query.bench.luau @@ -52,7 +52,7 @@ local E8 = mcs:entity() local registry2 = ecr.registry() -local function flip() +local function flip() return math.random() >= 0.15 end @@ -61,80 +61,80 @@ local N = 2^16-2 local archetypes = {} local hm = 0 -for i = 1, N do +for i = 1, N do local id = registry2.create() local combination = "" local n = newWorld:spawn() local entity = ecs:entity() local m = mcs:entity() - if flip() then + if flip() then combination ..= "B" - registry2:set(id, B2, {value = true}) + registry2:set(id, B2, {value = true}) ecs:set(entity, D2, { value = true}) mcs:set(m, E2, { value = 2}) newWorld:insert(n, A2({value = true})) end - if flip() then + if flip() then combination ..= "C" - registry2:set(id, B3, {value = true}) + registry2:set(id, B3, {value = true}) ecs:set(entity, D3, { value = true}) mcs:set(m, E3, { value = 2}) newWorld:insert(n, A3({value = true})) end - if flip() then + if flip() then combination ..= "D" - registry2:set(id, B4, {value = true}) + registry2:set(id, B4, {value = true}) ecs:set(entity, D4, { value = true}) mcs:set(m, E4, { value = 2}) - newWorld:insert(n, A4({value = true})) + newWorld:insert(n, A4({value = true})) end - if flip() then + if flip() then combination ..= "E" - registry2:set(id, B5, {value = true}) + registry2:set(id, B5, {value = true}) ecs:set(entity, D5, { value = true}) mcs:set(m, E5, { value = 2}) - newWorld:insert(n, A5({value = true})) + newWorld:insert(n, A5({value = true})) end - if flip() then + if flip() then combination ..= "F" - registry2:set(id, B6, {value = true}) + registry2:set(id, B6, {value = true}) ecs:set(entity, D6, { value = true}) mcs:set(m, E6, { value = 2}) - newWorld:insert(n, A6({value = true})) + newWorld:insert(n, A6({value = true})) end - if flip() then + if flip() then combination ..= "G" - registry2:set(id, B7, {value = true}) + registry2:set(id, B7, {value = true}) ecs:set(entity, D7, { value = true}) mcs:set(m, E7, { value = 2}) - newWorld:insert(n, A7({value = true})) + newWorld:insert(n, A7({value = true})) end - if flip() then + if flip() then combination ..= "H" - registry2:set(id, B8, {value = true}) - newWorld:insert(n, A8({value = true})) + registry2:set(id, B8, {value = true}) + newWorld:insert(n, A8({value = true})) ecs:set(entity, D8, { value = true}) mcs:set(m, E8, { value = 2}) end - if #combination == 7 then + if #combination == 7 then combination = "A" .. combination common += 1 - registry2:set(id, B1, {value = true}) + registry2:set(id, B1, {value = true}) ecs:set(entity, D1, { value = true}) - newWorld:insert(n, A1({value = true})) + newWorld:insert(n, A1({value = true})) mcs:set(m, E1, { value = 2}) end - if combination:find("BCDF") then - if not archetypes[combination] then + if combination:find("BCDF") then + if not archetypes[combination] then print(combination) - end + end hm += 1 end archetypes[combination] = true @@ -149,7 +149,7 @@ local green = rgb.green local WALL = gray(" │ ") local numberOfArchetypes = 0 -for _ in archetypes do +for _ in archetypes do numberOfArchetypes += 1 end print(common) @@ -167,36 +167,22 @@ print( return { ParameterGenerator = function() return - end, + end, Functions = { - Mirror = function() + ECR = function() local matched = 0 - for entityId, firstComponent in mcs:query(E1, E4, E6, E8) do + for entityId, firstComponent in registry2:view(B1, B4) do matched += 1 end end, - Matter = function() + Jecs = function() local matched = 0 - for entityId, firstComponent in newWorld:query(A1, A4, A6, A8) do + for entityId, firstComponent in ecs:query(D1, D4) do matched += 1 end - end, - ECR = function() - local matched = 0 - for entityId, firstComponent in registry2:view(B1, B4, B6, B8) do - matched += 1 - end - end, - - Jecs = function() - local matched = 0 - for entityId, firstComponent in ecs:query(D1, D4, D6, D8) do - matched += 1 - end - end, }, diff --git a/src/init.luau b/src/init.luau index 05c636c..3dca1f3 100644 --- a/src/init.luau +++ b/src/init.luau @@ -142,7 +142,7 @@ end local ERROR_ENTITY_NOT_ALIVE = "Entity is not alive" local ERROR_GENERATION_INVALID = "INVALID GENERATION" -local function getAlive(index: EntityIndex, e: i24): i53 +local function entity_index_get_alive(index: EntityIndex, e: i24): i53 local denseArray = index.dense local id = denseArray[ECS_ENTITY_T_LO(e)] @@ -159,18 +159,18 @@ local function getAlive(index: EntityIndex, e: i24): i53 error(ERROR_ENTITY_NOT_ALIVE) end -local function sparseGet(entityIndex, id) - return entityIndex.sparse[getAlive(entityIndex, id)] +local function entity_index_sparse_get(entityIndex, id) + return entityIndex.sparse[entity_index_get_alive(entityIndex, id)] end -- ECS_PAIR_FIRST, gets the relationship target / obj / HIGH bits -local function ECS_PAIR_RELATION(entityIndex, e) - return getAlive(entityIndex, ECS_ENTITY_T_HI(e)) +local function ecs_pair_relation(entityIndex, e) + return entity_index_get_alive(entityIndex, ECS_ENTITY_T_HI(e)) end -- ECS_PAIR_SECOND gets the relationship / pred / LOW bits -local function ECS_PAIR_OBJECT(entityIndex, e) - return getAlive(entityIndex, ECS_ENTITY_T_LO(e)) +local function ecs_pair_object(entityIndex, e) + return entity_index_get_alive(entityIndex, ECS_ENTITY_T_LO(e)) end local function entity_index_new_id(entityIndex: EntityIndex, index: i24): i53 @@ -300,8 +300,8 @@ local function archetype_of(world: any, types: { i24 }, prev: Archetype?): Arche idr.size += 1 records[componentId] = i if ECS_IS_PAIR(componentId) then - local relation = ECS_PAIR_RELATION(world.entityIndex, componentId) - local object = ECS_PAIR_OBJECT(world.entityIndex, componentId) + local relation = ecs_pair_relation(world.entityIndex, componentId) + local object = ecs_pair_object(world.entityIndex, componentId) local r = ECS_PAIR(relation, EcsWildcard) local idr_r = id_record_ensure(componentIndex, r) @@ -375,7 +375,7 @@ local function world_target(world: World, entity: i53, relation: i24--[[, nth: n return nil end - return ECS_PAIR_OBJECT(entityIndex, archetype.types[archetypeRecord]) + return ecs_pair_object(entityIndex, archetype.types[archetypeRecord]) end local function world_parent(world: World, entity: i53) @@ -408,22 +408,22 @@ local function find_insert(types: { i53 }, toAdd: i53): number return #types + 1 end -local function find_archetype_with(world: World, node: Archetype, componentId: i53): Archetype +local function find_archetype_with(world: World, node: Archetype, id: i53): Archetype local types = node.types -- Component IDs are added incrementally, so inserting and sorting -- them each time would be expensive. Instead this insertion sort can find the insertion -- point in the types array. - local destinationType = table.clone(node.types) :: { i53 } - local at = find_insert(types, componentId) + local dst_type = table.clone(node.types) :: { i53 } + local at = find_insert(types, id) if at == -1 then -- If it finds a duplicate, it just means it is the same archetype so it can return it -- directly instead of needing to hash types for a lookup to the archetype. return node end - table.insert(destinationType, at, componentId) + table.insert(dst_type, at, id) - return archetype_ensure(world, destinationType, node) + return archetype_ensure(world, dst_type, node) end local function edge_ensure(archetype: Archetype, componentId: i53): ArchetypeEdge @@ -624,42 +624,49 @@ local function world_clear(world: World, entityId: i53) entity_move(world.entityIndex, entityId, record, ROOT_ARCHETYPE) end --- Keeping the function as small as possible to enable inlining -local function fetch(record: Record, componentId: i24): any - local archetype = record.archetype - if not archetype then - return nil - end +local world_get: (world: World, entityId: i53, a: i53, b: i53?, c: i53?, d: i53?, e: i53?) -> (...any) +do + -- Keeping the function as small as possible to enable inlining + local function fetch(id: i24, records, columns, row): any + local tr = records[id] - local archetypeRecord = archetype.records[componentId] + if not tr then + return nil + end - if not archetypeRecord then - return nil - end + return columns[tr][row] + end - return archetype.columns[archetypeRecord][record.row] -end + function world_get(world: World, entityId: i53, a: i53, b: i53?, c: i53?, d: i53?, e: i53?): ...any + local id = entityId + local record = world.entityIndex.sparse[id] + if not record then + return nil + end -local function world_get(world: World, entityId: i53, a: i53, b: i53?, c: i53?, d: i53?, e: i53?): ...any - local id = entityId - local record = world.entityIndex.sparse[id] - if not record then - return nil - end + local archetype = record.archetype + if not archetype then + return nil + end - local va = fetch(record, a) + local records = archetype.records + local columns = archetype.records + local row = record.row - if b == nil then - return va - elseif c == nil then - return va, fetch(record, b) - elseif d == nil then - return va, fetch(record, b), fetch(record, c) - elseif e == nil then - return va, fetch(record, b), fetch(record, c), fetch(record, d) - else - error("args exceeded") - end + local va = fetch(a, tr, columns, row) + + if b == nil then + return va + elseif c == nil then + return va, fetch(b, tr, columns, row) + elseif d == nil then + return va, fetch(b, tr, columns, row), fetch(c, tr, columns, row) + elseif e == nil then + return va, fetch(b, tr, columns, row), fetch(c, tr, columns, row), fetch(d, tr, columns, row) + else + error("args exceeded") + end + end end type Item = () -> (number, ...any) @@ -848,58 +855,66 @@ do end end + local cache + function world_query(world: World, ...: number): Query -- breaking? if (...) == nil then error("Missing components") end + indices = {} - compatibleArchetypes = {} length = 0 components = { ... } - local archetypes: { Archetype } = world.archetypes :: any - local firstArchetypeMap: ArchetypeMap - local componentIndex = world.componentIndex + if cache then + compatibleArchetypes = cache + else + compatibleArchetypes = {} + local archetypes: { Archetype } = world.archetypes :: any + local firstArchetypeMap: ArchetypeMap + local componentIndex = world.componentIndex - for _, componentId in components do - local map: ArchetypeMap = componentIndex[componentId] :: any - if not map then - return EmptyQuery - end + for _, componentId in components do + local map: ArchetypeMap = componentIndex[componentId] :: any + if not map then + return EmptyQuery + end - if (firstArchetypeMap :: any) == nil or firstArchetypeMap.size > map.size then - firstArchetypeMap = map - end - end + if (firstArchetypeMap :: any) == nil or firstArchetypeMap.size > map.size then + firstArchetypeMap = map + end + end - for id in firstArchetypeMap.cache do - local compatibleArchetype = archetypes[id] - local archetypeRecords = compatibleArchetype.records + for id in firstArchetypeMap.cache do + local compatibleArchetype = archetypes[id] + local archetypeRecords = compatibleArchetype.records - local records: { number } = {} - local skip = false + local records: { number } = {} + local skip = false - for i, componentId in components do - local index = archetypeRecords[componentId] - if not index then - skip = true - break - end - -- index should be index.offset - records[i] = index - end + for i, componentId in components do + local index = archetypeRecords[componentId] + if not index then + skip = true + break + end + -- index should be index.offset + records[i] = index + end - if skip then - continue - end + if skip then + continue + end - length += 1 - compatibleArchetypes[length] = compatibleArchetype - indices[length] = records - end + length += 1 + compatibleArchetypes[length] = compatibleArchetype + indices[length] = records + end + cache = compatibleArchetypes + end lastArchetype = 1 archetype = compatibleArchetypes[lastArchetype] @@ -1113,14 +1128,15 @@ return { Rest = EcsRest, + pair = (ECS_PAIR :: any) :: (pred: Entity, obj: Entity) -> number, + + -- Inwards facing API for testing IS_PAIR = ECS_IS_PAIR, ECS_ID = ECS_ENTITY_T_LO, - ECS_PAIR = ECS_PAIR, ECS_GENERATION_INC = ECS_GENERATION_INC, ECS_GENERATION = ECS_GENERATION, - ECS_PAIR_RELATION = ECS_PAIR_RELATION, - ECS_PAIR_OBJECT = ECS_PAIR_OBJECT, - pair = (ECS_PAIR :: any) :: (pred: Entity, obj: Entity) -> number, - getAlive = getAlive, + ecs_pair_relation = ecs_pair_relation, + ecs_pair_object = ecs_pair_object, + entity_index_get_alive = entity_index_get_alive, } diff --git a/test/tests.luau b/test/tests.luau index 9a90bd8..67559cb 100644 --- a/test/tests.luau +++ b/test/tests.luau @@ -4,10 +4,10 @@ local __ = jecs.Wildcard local ECS_ID, ECS_GENERATION = jecs.ECS_ID, jecs.ECS_GENERATION local ECS_GENERATION_INC = jecs.ECS_GENERATION_INC local IS_PAIR = jecs.IS_PAIR -local ECS_PAIR = jecs.ECS_PAIR -local getAlive = jecs.getAlive -local ECS_PAIR_RELATION = jecs.ECS_PAIR_RELATION -local ECS_PAIR_OBJECT = jecs.ECS_PAIR_OBJECT +local pair = jecs.pair +local getAlive = jecs.entity_index_get_alive +local ecs_pair_relation = jecs.ecs_pair_relation +local ecs_pair_object = jecs.ecs_pair_object local TEST, CASE, CHECK, FINISH, SKIP = testkit.test() local function CHECK_NO_ERR(s: string, fn: (T...) -> (), ...: T...) @@ -256,11 +256,11 @@ TEST("world", function() CHECK(IS_PAIR(world:entity()) == false) - local pair = ECS_PAIR(e2, e3) + local pair = pair(e2, e3) CHECK(IS_PAIR(pair) == true) - CHECK(ECS_PAIR_RELATION(world.entityIndex, pair) == e2) - CHECK(ECS_PAIR_OBJECT(world.entityIndex, pair) == e3) + CHECK(ecs_pair_relation(world.entityIndex, pair) == e2) + CHECK(ecs_pair_object(world.entityIndex, pair) == e3) end do CASE("should allow querying for relations") @@ -269,8 +269,8 @@ TEST("world", function() local Apples = world:entity() local bob = world:entity() - world:set(bob, ECS_PAIR(Eats, Apples), true) - for e, bool in world:query(ECS_PAIR(Eats, Apples)) do + world:set(bob, pair(Eats, Apples), true) + for e, bool in world:query(pair(Eats, Apples)) do CHECK(e == bob) CHECK(bool) end @@ -282,14 +282,14 @@ TEST("world", function() local Apples = world:entity() local bob = world:entity() - world:set(bob, ECS_PAIR(Eats, Apples), "bob eats apples") + world:set(bob, pair(Eats, Apples), "bob eats apples") local w = jecs.Wildcard - for e, data in world:query(ECS_PAIR(Eats, w)) do + for e, data in world:query(pair(Eats, w)) do CHECK(e == bob) CHECK(data == "bob eats apples") end - for e, data in world:query(ECS_PAIR(w, Apples)) do + for e, data in world:query(pair(w, Apples)) do CHECK(e == bob) CHECK(data == "bob eats apples") end @@ -303,12 +303,12 @@ TEST("world", function() local bob = world:entity() local alice = world:entity() - world:set(bob, ECS_PAIR(Eats, Apples), "bob eats apples") - world:set(alice, ECS_PAIR(Eats, Oranges), "alice eats oranges") + world:set(bob, pair(Eats, Apples), "bob eats apples") + world:set(alice, pair(Eats, Oranges), "alice eats oranges") local w = jecs.Wildcard local count = 0 - for e, data in world:query(ECS_PAIR(Eats, w)) do + for e, data in world:query(pair(Eats, w)) do count += 1 if e == bob then CHECK(data == "bob eats apples") @@ -320,7 +320,7 @@ TEST("world", function() CHECK(count == 2) count = 0 - for e, data in world:query(ECS_PAIR(w, Apples)) do + for e, data in world:query(pair(w, Apples)) do count += 1 CHECK(data == "bob eats apples") end @@ -337,21 +337,21 @@ TEST("world", function() local alice = world:entity() world:set(bob, Apples, "apples") - world:set(bob, ECS_PAIR(Eats, Apples), "bob eats apples") - world:set(alice, ECS_PAIR(Eats, Oranges), "alice eats oranges") + world:set(bob, pair(Eats, Apples), "bob eats apples") + world:set(alice, pair(Eats, Oranges), "alice eats oranges") world:delete(Apples) local Wildcard = jecs.Wildcard local count = 0 - for _, data in world:query(ECS_PAIR(Wildcard, Apples)) do + for _, data in world:query(pair(Wildcard, Apples)) do count += 1 end - world:delete(ECS_PAIR(Eats, Apples)) + world:delete(pair(Eats, Apples)) CHECK(count == 0) - CHECK(world:get(bob, ECS_PAIR(Eats, Apples)) == nil) + CHECK(world:get(bob, pair(Eats, Apples)) == nil) end do CASE("should error when setting invalid pair") @@ -362,13 +362,12 @@ TEST("world", function() world:delete(Apples) - world:set(bob, ECS_PAIR(Eats, Apples), "bob eats apples") + world:set(bob, pair(Eats, Apples), "bob eats apples") end do CASE("should find target for ChildOf") local world = jecs.World.new() local ChildOf = jecs.ChildOf - local pair = ECS_PAIR local Name = world:component() @@ -383,7 +382,7 @@ TEST("world", function() CHECK(world:parent(bob) == alice) -- O(1) local count = 0 - for _, name in world:query(Name, ECS_PAIR(ChildOf, alice)) do + for _, name in world:query(Name, pair(ChildOf, alice)) do count += 1 end CHECK(count == 2) @@ -521,11 +520,11 @@ TEST("world", function() local Bob = world:component() local helloBob = world:entity() - world:add(helloBob, ECS_PAIR(Hello, Bob)) + world:add(helloBob, pair(Hello, Bob)) world:add(helloBob, Bob) local withoutCount = 0 - for _ in world:query(ECS_PAIR(Hello, Bob)):without(Bob) do + for _ in world:query(pair(Hello, Bob)):without(Bob) do withoutCount += 1 end