Test exclusive relation perf

This commit is contained in:
Ukendio 2025-07-01 02:32:31 +02:00
parent 0f7fd78285
commit 7dc8bb5759
3 changed files with 90 additions and 53 deletions

View file

@ -4,21 +4,24 @@
local ReplicatedStorage = game:GetService("ReplicatedStorage") local ReplicatedStorage = game:GetService("ReplicatedStorage")
local Matter = require(ReplicatedStorage.DevPackages.Matter) local Matter = require(ReplicatedStorage.DevPackages.Matter)
local ecr = require(ReplicatedStorage.DevPackages.ecr) local ecr = require(ReplicatedStorage.DevPackages.ecr)
local jecs = require(ReplicatedStorage.Lib) local jecs = require(ReplicatedStorage.Lib:Clone())
local pair = jecs.pair local pair = jecs.pair
local ecs = jecs.world() local ecs = jecs.world()
local mirror = require(ReplicatedStorage.mirror) local mirror = require(ReplicatedStorage.mirror:Clone())
local mcs = mirror.world() local mcs = mirror.world()
local C1 = ecs:component() local C1 = ecs:component()
local C2 = ecs:entity() local C2 = ecs:entity()
ecs:add(C2, pair(jecs.OnDeleteTarget, jecs.Delete)) ecs:add(C2, pair(jecs.OnDeleteTarget, jecs.Delete))
ecs:add(C2, jecs.Exclusive)
local C3 = ecs:entity() local C3 = ecs:entity()
ecs:add(C3, pair(jecs.OnDeleteTarget, jecs.Delete)) ecs:add(C3, pair(jecs.OnDeleteTarget, jecs.Delete))
local C4 = ecs:entity() local C4 = ecs:entity()
ecs:add(C4, pair(jecs.OnDeleteTarget, jecs.Delete)) ecs:add(C4, pair(jecs.OnDeleteTarget, jecs.Delete))
local E1 = mcs:component() local E1 = mcs:component()
local E2 = mcs:entity() local E2 = mcs:entity()
mcs:add(E2, mirror.Exclusive)
mcs:add(E2, pair(jecs.OnDeleteTarget, jecs.Delete)) mcs:add(E2, pair(jecs.OnDeleteTarget, jecs.Delete))
local E3 = mcs:entity() local E3 = mcs:entity()
mcs:add(E3, pair(jecs.OnDeleteTarget, jecs.Delete)) mcs:add(E3, pair(jecs.OnDeleteTarget, jecs.Delete))
@ -33,16 +36,16 @@ return {
Mirror = function() Mirror = function()
local m = mcs:entity() local m = mcs:entity()
for i = 1, 1000 do for i = 1, 1000 do
mcs:add(m, E3) mcs:add(m, pair(E2, E3))
mcs:remove(m, E3) mcs:add(m, pair(E2, E4))
end end
end, end,
Jecs = function() Jecs = function()
local j = ecs:entity() local j = ecs:entity()
for i = 1, 1000 do for i = 1, 1000 do
ecs:add(j, C3) ecs:add(j, pair(C2, C3))
ecs:remove(j, C3) ecs:add(j, pair(C2, C4))
end end
end, end,
}, },

View file

@ -186,9 +186,10 @@ local ECS_ENTITY_MASK = bit32.lshift(1, 24)
local ECS_GENERATION_MASK = bit32.lshift(1, 16) local ECS_GENERATION_MASK = bit32.lshift(1, 16)
local ECS_PAIR_OFFSET = 2^48 local ECS_PAIR_OFFSET = 2^48
local ECS_ID_DELETE = 0b01 local ECS_ID_DELETE = 0b0001
local ECS_ID_IS_TAG = 0b10 local ECS_ID_IS_TAG = 0b0010
local ECS_ID_MASK = 0b00 local ECS_ID_IS_EXCLUSIVE = 0b0100
local ECS_ID_MASK = 0b0000
local HI_COMPONENT_ID = 256 local HI_COMPONENT_ID = 256
local EcsOnAdd = HI_COMPONENT_ID + 1 local EcsOnAdd = HI_COMPONENT_ID + 1
@ -204,7 +205,8 @@ local EcsRemove = HI_COMPONENT_ID + 10
local EcsName = HI_COMPONENT_ID + 11 local EcsName = HI_COMPONENT_ID + 11
local EcsOnArchetypeCreate = HI_COMPONENT_ID + 12 local EcsOnArchetypeCreate = HI_COMPONENT_ID + 12
local EcsOnArchetypeDelete = HI_COMPONENT_ID + 13 local EcsOnArchetypeDelete = HI_COMPONENT_ID + 13
local EcsRest = HI_COMPONENT_ID + 14 local EcsExclusive = HI_COMPONENT_ID + 14
local EcsRest = HI_COMPONENT_ID + 15
local NULL_ARRAY = table.freeze({}) :: Column local NULL_ARRAY = table.freeze({}) :: Column
local NULL = newproxy(false) local NULL = newproxy(false)
@ -439,6 +441,7 @@ end
local function archetype_move( local function archetype_move(
entity_index: EntityIndex, entity_index: EntityIndex,
entity: Entity,
to: Archetype, to: Archetype,
dst_row: i24, dst_row: i24,
from: Archetype, from: Archetype,
@ -452,48 +455,58 @@ local function archetype_move(
local id_types = from.types local id_types = from.types
local columns_map = to.columns_map local columns_map = to.columns_map
for i, column in src_columns do if src_row ~= last then
if column == NULL_ARRAY then
continue
end
-- Retrieves the new column index from the source archetype's record from each component
-- We have to do this because the columns are tightly packed and indexes may not correspond to each other.
local dst_column = columns_map[id_types[i]]
-- Sometimes target column may not exist, e.g. when you remove a component.
if dst_column then
dst_column[dst_row] = column[src_row]
end
-- If the entity is the last row in the archetype then swapping it would be meaningless. -- If the entity is the last row in the archetype then swapping it would be meaningless.
if src_row ~= last then
for i, column in src_columns do
if column == NULL_ARRAY then
continue
end
-- Retrieves the new column index from the source archetype's record from each component
-- We have to do this because the columns are tightly packed and indexes may not correspond to each other.
local dst_column = columns_map[id_types[i]]
-- Sometimes target column may not exist, e.g. when you remove a component.
if dst_column then
dst_column[dst_row] = column[src_row]
end
-- Swap rempves columns to ensure there are no holes in the archetype. -- Swap rempves columns to ensure there are no holes in the archetype.
column[src_row] = column[last] column[src_row] = column[last]
column[last] = nil
end end
column[last] = nil
end
local moved = #src_entities
-- Move the entity from the source to the destination archetype. -- Move the entity from the source to the destination archetype.
-- Because we have swapped columns we now have to update the records -- Because we have swapped columns we now have to update the records
-- corresponding to the entities' rows that were swapped. -- corresponding to the entities' rows that were swapped.
local e1 = src_entities[src_row]
local e2 = src_entities[moved]
if src_row ~= moved then local e2 = src_entities[last]
src_entities[src_row] = e2 src_entities[src_row] = e2
local sparse_array = entity_index.sparse_array
local record2 = sparse_array[ECS_ENTITY_T_LO(e2 :: number)]
record2.row = src_row
else
for i, column in src_columns do
if column == NULL_ARRAY then
continue
end
-- Retrieves the new column index from the source archetype's record from each component
-- We have to do this because the columns are tightly packed and indexes may not correspond to each other.
local dst_column = columns_map[id_types[i]]
-- Sometimes target column may not exist, e.g. when you remove a component.
if dst_column then
dst_column[dst_row] = column[src_row]
end
column[last] = nil
end
end end
src_entities[moved] = nil :: any src_entities[last] = nil :: any
dst_entities[dst_row] = e1 dst_entities[dst_row] = entity
local sparse_array = entity_index.sparse_array
local record1 = sparse_array[ECS_ENTITY_T_LO(e1 :: number)]
local record2 = sparse_array[ECS_ENTITY_T_LO(e2 :: number)]
record1.row = dst_row
record2.row = src_row
end end
local function archetype_append( local function archetype_append(
@ -526,7 +539,7 @@ local function entity_move(
local sourceRow = record.row local sourceRow = record.row
local from = record.archetype local from = record.archetype
local dst_row = archetype_append(entity, to) local dst_row = archetype_append(entity, to)
archetype_move(entity_index, to, dst_row, from, sourceRow) archetype_move(entity_index, entity, to, dst_row, from, sourceRow)
record.archetype = to record.archetype = to
record.row = dst_row record.row = dst_row
end end
@ -662,6 +675,7 @@ local function id_record_ensure(world: World, id: Entity): ComponentRecord
local is_pair = ECS_IS_PAIR(id :: number) local is_pair = ECS_IS_PAIR(id :: number)
local has_delete = false local has_delete = false
local is_exclusive = false
if is_pair then if is_pair then
relation = entity_index_get_alive(entity_index, ECS_PAIR_FIRST(id :: number)) :: i53 relation = entity_index_get_alive(entity_index, ECS_PAIR_FIRST(id :: number)) :: i53
@ -676,6 +690,10 @@ local function id_record_ensure(world: World, id: Entity): ComponentRecord
if cleanup_policy_target == EcsDelete then if cleanup_policy_target == EcsDelete then
has_delete = true has_delete = true
end end
if world_has_one_inline(world, relation, EcsExclusive) then
is_exclusive = true
end
else else
local cleanup_policy = world_target(world, relation, EcsOnDelete, 0) local cleanup_policy = world_target(world, relation, EcsOnDelete, 0)
@ -697,7 +715,8 @@ local function id_record_ensure(world: World, id: Entity): ComponentRecord
flags = bit32.bor( flags = bit32.bor(
flags, flags,
if has_delete then ECS_ID_DELETE else 0, if has_delete then ECS_ID_DELETE else 0,
if is_tag then ECS_ID_IS_TAG else 0 if is_tag then ECS_ID_IS_TAG else 0,
if is_exclusive then ECS_ID_IS_EXCLUSIVE else 0
) )
idr = { idr = {
@ -744,7 +763,7 @@ local function archetype_register(world: World, archetype: Archetype)
local columns = archetype.columns local columns = archetype.columns
for i, component_id in archetype.types do for i, component_id in archetype.types do
local idr = id_record_ensure(world, component_id) local idr = id_record_ensure(world, component_id)
local is_tag = bit32.band(idr.flags, ECS_ID_IS_TAG) ~= 0 local is_tag = bit32.btest(idr.flags, ECS_ID_IS_TAG)
local column = if is_tag then NULL_ARRAY else {} local column = if is_tag then NULL_ARRAY else {}
columns[i] = column columns[i] = column
@ -908,9 +927,22 @@ end
local function find_archetype_with(world: World, id: Id, from: Archetype): Archetype local function find_archetype_with(world: World, id: Id, from: Archetype): Archetype
local id_types = from.types local id_types = from.types
local dst = table.clone(id_types)
if ECS_IS_PAIR(id::number) then
local first = ECS_PAIR_FIRST(id::number)
local idr = world.component_index[ECS_PAIR(first, EcsWildcard)]
if idr and bit32.btest(idr.flags, EcsExclusive) then
local cr = idr.records[from.id]
if cr then
dst[cr] = id
return archetype_ensure(world, dst)
end
end
end
local at = find_insert(id_types :: { number } , id :: number) local at = find_insert(id_types :: { number } , id :: number)
local dst = table.clone(id_types)
table.insert(dst, at, id) table.insert(dst, at, id)
return archetype_ensure(world, dst) return archetype_ensure(world, dst)
@ -2394,7 +2426,7 @@ local function world_new()
return entity return entity
else else
for i = eindex_max_id + 1, index do for i = eindex_max_id + 1, index do
eindex_sparse_array[i] = { dense = i } :: Record eindex_sparse_array[i] = { dense = i } :: Record
eindex_dense_array[i] = i eindex_dense_array[i] = i
end end
entity_index.max_id = index entity_index.max_id = index
@ -2459,8 +2491,8 @@ local function world_new()
local idr_archetype = archetypes[archetype_id] local idr_archetype = archetypes[archetype_id]
local entities = idr_archetype.entities local entities = idr_archetype.entities
local n = #entities local n = #entities
table.move(entities, 1, n, count + 1, queue)
count += n count += n
table.move(entities, 1, n, #queue + 1, queue)
end end
for _, e in queue do for _, e in queue do
inner_world_remove(world, e, entity) inner_world_remove(world, e, entity)
@ -2572,7 +2604,7 @@ local function world_new()
if idr then if idr then
local flags = idr.flags local flags = idr.flags
if bit32.band(flags, ECS_ID_DELETE) ~= 0 then if bit32.btest(flags, ECS_ID_DELETE) then
for archetype_id in idr.records do for archetype_id in idr.records do
local idr_archetype = archetypes[archetype_id] local idr_archetype = archetypes[archetype_id]
@ -2647,8 +2679,8 @@ local function world_new()
end end
local id_record = component_index[id] local id_record = component_index[id]
local flags = id_record.flags local flags = id_record.flags
local flags_delete_mask: number = bit32.band(flags, ECS_ID_DELETE) local flags_delete_mask = bit32.btest(flags, ECS_ID_DELETE)
if flags_delete_mask ~= 0 then if flags_delete_mask then
for i = #entities, 1, -1 do for i = #entities, 1, -1 do
local child = entities[i] local child = entities[i]
inner_world_delete(world, child) inner_world_delete(world, child)
@ -2690,7 +2722,7 @@ local function world_new()
if idr_r then if idr_r then
local archetype_ids = idr_r.records local archetype_ids = idr_r.records
local flags = idr_r.flags local flags = idr_r.flags
if (bit32.band(flags, ECS_ID_DELETE) :: number) ~= 0 then if bit32.btest(flags, ECS_ID_DELETE) then
for archetype_id in archetype_ids do for archetype_id in archetype_ids do
local idr_r_archetype = archetypes[archetype_id] local idr_r_archetype = archetypes[archetype_id]
local entities = idr_r_archetype.entities local entities = idr_r_archetype.entities
@ -2868,7 +2900,7 @@ end
local function ecs_is_tag(world: World, entity: Entity): boolean local function ecs_is_tag(world: World, entity: Entity): boolean
local idr = world.component_index[entity] local idr = world.component_index[entity]
if idr then if idr then
return bit32.band(idr.flags, ECS_ID_IS_TAG) ~= 0 return bit32.btest(idr.flags, ECS_ID_IS_TAG)
end end
return not world_has_one_inline(world, entity, EcsComponent) return not world_has_one_inline(world, entity, EcsComponent)
end end
@ -2892,6 +2924,7 @@ return {
Delete = (EcsDelete :: any) :: Entity, Delete = (EcsDelete :: any) :: Entity,
Remove = (EcsRemove :: any) :: Entity, Remove = (EcsRemove :: any) :: Entity,
Name = (EcsName :: any) :: Entity<string>, Name = (EcsName :: any) :: Entity<string>,
Exclusive = EcsExclusive :: Entity,
Rest = (EcsRest :: any) :: Entity, Rest = (EcsRest :: any) :: Entity,
pair = (ECS_PAIR :: any) :: <P, O>(first: Id<P>, second: Id<O>) -> Pair<P, O>, pair = (ECS_PAIR :: any) :: <P, O>(first: Id<P>, second: Id<O>) -> Pair<P, O>,

View file

@ -159,6 +159,7 @@ TEST("repro", function()
end) end)
TEST("world:add()", function() TEST("world:add()", function()
print("-----")
do CASE "idempotent" do CASE "idempotent"
local world = jecs.world() local world = jecs.world()
local d = dwi(world) local d = dwi(world)