Archetype deletion (#126)

* initial commit

* cleanup edges

* rename ptr to edge
This commit is contained in:
Marcus 2024-09-20 21:29:50 +02:00 committed by GitHub
parent ca00d4c0c1
commit 2ed869ba93
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
3 changed files with 300 additions and 105 deletions

View file

@ -11,16 +11,30 @@ type ArchetypeId = number
type Column = { any } type Column = { any }
type ArchetypeEdge = { type Map<K, V> = {[K]: V}
add: Archetype,
remove: Archetype, type GraphEdge = {
from: Archetype,
to: Archetype?,
prev: GraphEdge?,
next: GraphEdge?,
id: number
}
type GraphEdges = Map<i53, GraphEdge>
type GraphNode = {
add: GraphEdges,
remove: GraphEdges,
add_ref: GraphEdge?,
remove_ref: GraphEdge?
} }
export type Archetype = { export type Archetype = {
id: number, id: number,
edges: { [i53]: ArchetypeEdge }, node: GraphNode,
types: Ty, types: Ty,
type: string | number, type: string,
entities: { number }, entities: { number },
columns: { Column }, columns: { Column },
records: { ArchetypeRecord }, records: { ArchetypeRecord },
@ -28,27 +42,26 @@ export type Archetype = {
type Record = { type Record = {
archetype: Archetype, archetype: Archetype,
row: number, row: number,
dense: i24, dense: i24
componentRecord: ArchetypeMap,
} }
type EntityIndex = { dense: { [i24]: i53 }, sparse: { [i53]: Record } } type EntityIndex = {
dense: Map<i24, i53>,
sparse: Map<i53, Record>
}
type ArchetypeRecord = { type ArchetypeRecord = {
count: number, count: number,
column: number, column: number,
} }
type ArchetypeMap = { type IdRecord = {
cache: { ArchetypeRecord }, cache: { ArchetypeRecord },
flags: number, flags: number,
first: ArchetypeMap,
second: ArchetypeMap,
parent: ArchetypeMap,
size: number, size: number,
} }
type ComponentIndex = { [i24]: ArchetypeMap } type ComponentIndex = Map<i53, IdRecord>
type Archetypes = { [ArchetypeId]: Archetype } type Archetypes = { [ArchetypeId]: Archetype }
@ -420,7 +433,7 @@ local function ECS_ID_IS_WILDCARD(e: i53): boolean
return first == EcsWildcard or second == EcsWildcard return first == EcsWildcard or second == EcsWildcard
end end
local function id_record_ensure(world: World, id: number): ArchetypeMap local function id_record_ensure(world: World, id: number): IdRecord
local componentIndex = world.componentIndex local componentIndex = world.componentIndex
local idr = componentIndex[id] local idr = componentIndex[id]
@ -453,14 +466,20 @@ local function id_record_ensure(world: World, id: number): ArchetypeMap
size = 0, size = 0,
cache = {}, cache = {},
flags = flags, flags = flags,
} :: ArchetypeMap } :: IdRecord
componentIndex[id] = idr componentIndex[id] = idr
end end
return idr return idr
end end
local function archetype_append_to_records(idr: ArchetypeMap, archetype_id, records, id, index) local function archetype_append_to_records(
idr: IdRecord,
archetype_id: number,
records: Map<i53, ArchetypeRecord>,
id: number,
index: number
)
local tr = idr.cache[archetype_id] local tr = idr.cache[archetype_id]
if not tr then if not tr then
tr = { column = index, count = 1} tr = { column = index, count = 1}
@ -472,7 +491,7 @@ local function archetype_append_to_records(idr: ArchetypeMap, archetype_id, reco
end end
end end
local function archetype_create(world: World, types: { i24 }, prev: Archetype?): Archetype local function archetype_create(world: World, types: { i24 }, prev: i53?): Archetype
local ty = hash(types) local ty = hash(types)
local archetype_id = (world.nextArchetypeId :: number) + 1 local archetype_id = (world.nextArchetypeId :: number) + 1
@ -507,7 +526,7 @@ local function archetype_create(world: World, types: { i24 }, prev: Archetype?):
local archetype: Archetype = { local archetype: Archetype = {
columns = columns, columns = columns,
edges = {}, node = { add = {}, remove = {} },
entities = {}, entities = {},
id = archetype_id, id = archetype_id,
records = records, records = records,
@ -531,7 +550,7 @@ local function world_parent(world: World, entity: i53)
return world_target(world, entity, EcsChildOf, 0) return world_target(world, entity, EcsChildOf, 0)
end end
local function archetype_ensure(world: World, types, prev): Archetype local function archetype_ensure(world: World, types): Archetype
if #types < 1 then if #types < 1 then
return world.ROOT_ARCHETYPE return world.ROOT_ARCHETYPE
end end
@ -542,7 +561,7 @@ local function archetype_ensure(world: World, types, prev): Archetype
return archetype return archetype
end end
return archetype_create(world, types, prev) return archetype_create(world, types)
end end
local function find_insert(types: { i53 }, toAdd: i53): number local function find_insert(types: { i53 }, toAdd: i53): number
@ -572,32 +591,125 @@ local function find_archetype_with(world: World, node: Archetype, id: i53): Arch
end end
table.insert(dst, at, id) table.insert(dst, at, id)
return archetype_ensure(world, dst, node) return archetype_ensure(world, dst)
end end
local function edge_ensure(archetype: Archetype, id: i53): ArchetypeEdge local function find_archetype_without(world: World, node: Archetype, id: i53): Archetype
local edges = archetype.edges local types = node.types
local edge = edges[id] local at = table.find(types, id)
if not edge then if at == nil then
edge = {} :: any return node
edges[id] = edge
end end
return edge
local dst = table.clone(types)
table.remove(dst, at)
return archetype_ensure(world, dst)
end end
local function archetype_traverse_add(world: World, id: i53, from: Archetype): Archetype local function archetype_init_edge(archetype: Archetype,
from = from or world.ROOT_ARCHETYPE edge: GraphEdge, id: i53, to: Archetype)
edge.from = archetype
edge.to = to
edge.id = id
end
local edge = edge_ensure(from, id) local function archetype_ensure_edge(world, edges, id): GraphEdge
local add = edge.add local edge = edges[id]
if not add then if not edge then
-- Save an edge using the component ID to the archetype to allow edge = {
-- faster traversals to adjacent archetypes. from = nil :: any,
add = find_archetype_with(world, from, id) to = nil :: any,
edge.add = add :: never id = id,
end prev = nil,
next = nil,
} :: GraphEdge
edges[id] = edge
end
return add return edge
end
local function init_edge_for_add(world, archetype, edge, id, to)
archetype_init_edge(archetype, edge, id, to)
archetype_ensure_edge(world, archetype.node.add, id)
if archetype ~= to then
local to_add_ref = to.node.add_ref
edge.next = to_add_ref
edge.prev = nil
if to_add_ref then
to_add_ref.prev = edge
end
to.node.add_ref = edge
end
end
local function init_edge_for_remove(world, archetype, edge, id, to)
archetype_init_edge(archetype, edge, id, to)
archetype_ensure_edge(world, archetype.node.remove, id)
if archetype ~= to then
local to_remove_ref = to.node.remove_ref
local prev
if to_remove_ref then
prev = to_remove_ref.prev
to_remove_ref.prev = edge
edge.next = to_remove_ref
else
to.node.remove_ref = edge
edge.next = nil
end
edge.prev = prev
if prev then
prev.next = edge
end
end
end
local function create_edge_for_add(world: World, node: Archetype,
edge: GraphEdge, id: i53): Archetype
local to = find_archetype_with(world, node, id)
init_edge_for_add(world, node, edge, id, to)
return to
end
local function create_edge_for_remove(world: World, node: Archetype,
edge: GraphEdge, id: i53): Archetype
local to = find_archetype_without(world, node, id)
init_edge_for_remove(world, node, edge, id, to)
return to
end
local function archetype_traverse_add(world: World, id: i53,
from: Archetype): Archetype
from = from or world.ROOT_ARCHETYPE
local edge = archetype_ensure_edge(
world, from.node.add, id)
local to = edge.to
if not to then
to = create_edge_for_add(world, from, edge, id)
end
return to :: Archetype
end
local function archetype_traverse_remove(world: World, id: i53, from: Archetype): Archetype
from = from or world.ROOT_ARCHETYPE
local edge = archetype_ensure_edge(
world, from.node.add, id)
local to = edge.to
if not to then
to = create_edge_for_remove(world, from, edge, id)
end
return to :: Archetype
end end
local function invoke_hook(world: World, hook_id: number, id: i53, entity: i53, data: any?) local function invoke_hook(world: World, hook_id: number, id: i53, entity: i53, data: any?)
@ -700,24 +812,6 @@ local function world_component(world: World): i53
return id return id
end end
local function archetype_traverse_remove(world: World, id: i53, from: Archetype): Archetype
local edge = edge_ensure(from, id)
local remove = edge.remove
if not remove then
local to = table.clone(from.types) :: { i53 }
local at = table.find(to, id)
if not at then
return from
end
table.remove(to, at)
remove = archetype_ensure(world, to, from)
edge.remove = remove :: any
end
return remove
end
local function world_remove(world: World, entity: i53, id: i53) local function world_remove(world: World, entity: i53, id: i53)
local entity_index = world.entityIndex local entity_index = world.entityIndex
local record = entity_index.sparse[entity] local record = entity_index.sparse[entity]
@ -755,30 +849,118 @@ local function world_clear(world: World, entity: i53)
entity_move(world.entityIndex, entity, record, ROOT_ARCHETYPE) entity_move(world.entityIndex, entity, record, ROOT_ARCHETYPE)
end end
local world_delete: (world: World, entity: i53) -> () local function archetype_fast_delete_last(columns: { Column },
column_count: number, types: { i53 }, entity: i53)
for i, column in columns do
if column ~= NULL_ARRAY then
column[column_count] = nil
end
end
end
local function archetype_fast_delete(columns: { Column },
column_count: number, row, types, entity)
for i, column in columns do
if column ~= NULL_ARRAY then
column[row] = column[column_count]
column[column_count] = nil
end
end
end
local function archetype_disconnect_edge(edge: GraphEdge)
local edge_next = edge.next
local edge_prev = edge.prev
if edge_next then
edge_next.prev = edge_prev
end
if edge_prev then
edge_prev.next = edge_next
end
end
local function archetype_remove_edge(edges: Map<i53, GraphEdge>,
id: i53, edge: GraphEdge)
archetype_disconnect_edge(edge)
edges[id] = nil
end
local function archetype_clear_edges(archetype: Archetype)
local node = archetype.node
local add = node.add
local remove = node.remove
for _, edge in add do
archetype_disconnect_edge(edge)
end
for _, edge in remove do
archetype_disconnect_edge(edge)
end
local node_add_ref = node.add_ref
if node_add_ref then
local current = node_add_ref.next
while current do
local edge = current
current = current.next
local node_add = edge.from.node.add
if node_add then
archetype_remove_edge(node_add, edge.id, edge)
end
end
end
local node_remove_ref = node.remove_ref
if node_remove_ref then
local current = node_remove_ref.prev
while current do
local edge = current
current = current.prev
local node_remove = edge.from.node.remove
if node_remove then
archetype_remove_edge(node_remove, edge.id, edge)
end
end
end
node.add = nil :: any
node.remove = nil :: any
node.add_ref = nil :: any
node.remove_ref = nil :: any
end
local function archetype_destroy(world: World, archetype: Archetype)
local component_index = world.componentIndex
archetype_clear_edges(archetype)
local archetype_id = archetype.id
world.archetypes[archetype_id] = nil
world.archetypeIndex[archetype.type] = nil
local records = archetype.records
for id in records do
local idr = component_index[id]
idr.cache[archetype_id] = nil
idr.size -= 1
records[id] = nil
if idr.size == 0 then
component_index[id] = nil
end
end
end
local function world_cleanup(world)
for _, archetype in world.archetypes do
if #archetype.entities == 0 then
archetype_destroy(world, archetype)
end
end
end
local world_delete: (world: World, entity: i53, destruct: boolean?) -> ()
do do
local function archetype_fast_delete_last(columns: { Column },
column_count: number, types: { i53 }, entity: i53)
for i, column in columns do
if column ~= NULL_ARRAY then
column[column_count] = nil
end
end
end
local function archetype_fast_delete(columns: { Column },
column_count: number, row, types, entity)
for i, column in columns do
if column ~= NULL_ARRAY then
column[row] = column[column_count]
column[column_count] = nil
end
end
end
local function archetype_delete(world: World, local function archetype_delete(world: World,
archetype: Archetype, row: number) archetype: Archetype, row: number, destruct: boolean?)
local entityIndex = world.entityIndex local entityIndex = world.entityIndex
local columns = archetype.columns local columns = archetype.columns
@ -812,33 +994,31 @@ do
end end
local component_index = world.componentIndex local component_index = world.componentIndex
local archetypes = world.archetypes local archetypes = world.archetypes
local idr = component_index[delete] local idr = component_index[delete]
if idr then if idr then
local children = {} local children = {}
for archetype_id in idr.cache do for archetype_id in idr.cache do
local idr_archetype = archetypes[archetype_id] local idr_archetype = archetypes[archetype_id]
for i, child in idr_archetype.entities do for i, child in idr_archetype.entities do
table.insert(children, child) table.insert(children, child)
end end
end end
local flags = idr.flags local flags = idr.flags
if bit32.band(flags, ECS_ID_DELETE) ~= 0 then if bit32.band(flags, ECS_ID_DELETE) ~= 0 then
for _, child in children do for _, child in children do
-- Cascade deletion to children -- Cascade deletion to children
world_delete(world, child) world_delete(world, child)
end end
else else
for _, child in children do for _, child in children do
world_remove(world, child, delete) world_remove(world, child, delete)
end end
end end
end
component_index[delete] = nil
end
-- TODO: iterate each linked record. -- TODO: iterate each linked record.
-- local r = ECS_PAIR(delete, EcsWildcard) -- local r = ECS_PAIR(delete, EcsWildcard)
-- local idr_r = component_index[r] -- local idr_r = component_index[r]
@ -890,7 +1070,7 @@ do
if bit32.band(flags, ECS_ID_DELETE) ~= 0 then if bit32.band(flags, ECS_ID_DELETE) ~= 0 then
for _, child in children do for _, child in children do
-- Cascade deletions of it has Delete as component trait -- Cascade deletions of it has Delete as component trait
world_delete(world, child) world_delete(world, child, destruct)
end end
else else
local object = ECS_ENTITY_T_LO(id) local object = ECS_ENTITY_T_LO(id)
@ -903,11 +1083,10 @@ do
end end
end end
end end
component_index[o] = nil
end end
end end
function world_delete(world: World, entity: i53) function world_delete(world: World, entity: i53, destruct: boolean?)
local entityIndex = world.entityIndex local entityIndex = world.entityIndex
local record = entityIndex.sparse[entity] local record = entityIndex.sparse[entity]
@ -921,11 +1100,12 @@ do
if archetype then if archetype then
-- In the future should have a destruct mode for -- In the future should have a destruct mode for
-- deleting archetypes themselves. Maybe requires recycling -- deleting archetypes themselves. Maybe requires recycling
archetype_delete(world, archetype, row) archetype_delete(world, archetype, row, destruct)
end end
record.archetype = nil :: any record.archetype = nil :: any
entityIndex.sparse[entity] = nil entityIndex.sparse[entity] = nil
end end
end end
@ -1368,7 +1548,7 @@ local function world_query(world: World, ...)
local archetypes = world.archetypes local archetypes = world.archetypes
local idr: ArchetypeMap local idr: IdRecord
local componentIndex = world.componentIndex local componentIndex = world.componentIndex
for _, id in ids do for _, id in ids do
@ -1435,6 +1615,7 @@ World.has = world_has
World.target = world_target World.target = world_target
World.parent = world_parent World.parent = world_parent
World.contains = world_contains World.contains = world_contains
World.cleanup = world_cleanup
if _G.__JECS_DEBUG then if _G.__JECS_DEBUG then
-- taken from https://github.com/centau/ecr/blob/main/src/ecr.luau -- taken from https://github.com/centau/ecr/blob/main/src/ecr.luau

14
test/memory.luau Normal file
View file

@ -0,0 +1,14 @@
local testkit = require("@testkit")
local jecs = require("@jecs")
local world = jecs.World.new()
local A = world:component()
local B = world:component()
local e = world:entity()
world:add(e, A)
world:add(e, B)
local archetype_id = world.archetypeIndex["1_2"].id
world:delete(e)
testkit.print(world)

View file

@ -842,7 +842,7 @@ TEST("world:delete", function()
CHECK(world:get(id1, Health) == 50) CHECK(world:get(id1, Health) == 50)
end end
do CASE "delete entities using another Entity as component" do CASE "delete entities using another Entity as component with Delete cleanup action"
local world = jecs.World.new() local world = jecs.World.new()
local Health = world:entity() local Health = world:entity()