Skip to content

Commit

Permalink
Fix generation increment overflowing id
Browse files Browse the repository at this point in the history
  • Loading branch information
Ukendio committed Nov 14, 2024
1 parent ceb7ea9 commit b40af9f
Show file tree
Hide file tree
Showing 4 changed files with 384 additions and 3 deletions.
8 changes: 5 additions & 3 deletions src/init.luau
Original file line number Diff line number Diff line change
Expand Up @@ -148,7 +148,7 @@ local function ECS_GENERATION_INC(e: i53)
return id
end

return ECS_COMBINE(id, next_gen) + flags
return ECS_COMBINE(id, next_gen)
end
return ECS_COMBINE(e, 1)
end
Expand Down Expand Up @@ -1683,13 +1683,13 @@ function World.new()
self.ROOT_ARCHETYPE = archetype_create(self, {}, "")

for i = 1, HI_COMPONENT_ID do
local e = entity_index_new_id(entity_index, i)
local e = entity_index_new_id(entity_index)
world_add(self, e, EcsComponent)
end

for i = HI_COMPONENT_ID + 1, EcsRest do
-- Initialize built-in components
entity_index_new_id(entity_index, i)
entity_index_new_id(entity_index)
end

world_add(self, EcsName, EcsComponent)
Expand Down Expand Up @@ -1885,4 +1885,6 @@ return {
entity_index_try_get = entity_index_try_get,
entity_index_try_get_any = entity_index_try_get_any,
entity_index_is_alive = entity_index_is_alive,
entity_index_remove = entity_index_remove,
entity_index_new_id = entity_index_new_id
}
192 changes: 192 additions & 0 deletions test/gen.luau
Original file line number Diff line number Diff line change
@@ -0,0 +1,192 @@
type i53 = number
type i24 = number

type Ty = { i53 }
type ArchetypeId = number

type Column = { any }

type Map<K, V> = { [K]: V }

type GraphEdge = {
from: Archetype,
to: Archetype?,
prev: GraphEdge?,
next: GraphEdge?,
id: number,
}

type GraphEdges = Map<i53, GraphEdge>

type GraphNode = {
add: GraphEdges,
remove: GraphEdges,
refs: GraphEdge,
}

type ArchetypeRecord = {
count: number,
column: number,
}

export type Archetype = {
id: number,
node: GraphNode,
types: Ty,
type: string,
entities: { number },
columns: { Column },
records: { ArchetypeRecord },
}
type Record = {
archetype: Archetype,
row: number,
dense: i24,
}

type EntityIndex = {
dense_array: Map<i24, i53>,
sparse_array: Map<i53, Record>,
sparse_count: number,
alive_count: number,
max_id: number
}


local ECS_PAIR_FLAG = 0x8
local ECS_ID_FLAGS_MASK = 0x10
local ECS_ENTITY_MASK = bit32.lshift(1, 24)
local ECS_GENERATION_MASK = bit32.lshift(1, 16)

-- HIGH 24 bits LOW 24 bits
local function ECS_GENERATION(e: i53): i24
return if e > ECS_ENTITY_MASK then (e // ECS_ID_FLAGS_MASK) % ECS_GENERATION_MASK else 0
end

local function ECS_COMBINE(source: number, target: number): i53
return (source * 268435456) + (target * ECS_ID_FLAGS_MASK)
end

local function ECS_GENERATION_INC(e: i53)
if e > ECS_ENTITY_MASK then
local flags = e // ECS_ID_FLAGS_MASK
local id = flags // ECS_ENTITY_MASK
local generation = flags % ECS_GENERATION_MASK

local next_gen = generation + 1
if next_gen > ECS_GENERATION_MASK then
return id
end

return ECS_COMBINE(id, next_gen) + flags
end
return ECS_COMBINE(e, 1)
end
local function ECS_ENTITY_T_LO(e: i53): i24
return if e > ECS_ENTITY_MASK then (e // ECS_ID_FLAGS_MASK) // ECS_ENTITY_MASK else e
end


local function entity_index_try_get_any(entity_index: EntityIndex, entity: number): Record?
local r = entity_index.sparse_array[ECS_ENTITY_T_LO(entity)]
if not r then
return nil
end

if not r or r.dense == 0 then
return nil
end

return r
end

local function entity_index_try_get(entity_index: EntityIndex, entity: number): Record?
local r = entity_index_try_get_any(entity_index, entity)
if r then
local r_dense = r.dense
if r_dense > entity_index.alive_count then
return nil
end
if entity_index.dense_array[r_dense] ~= entity then
return nil
end
end
return r
end

local function entity_index_get_alive(entity_index: EntityIndex,
entity: number): number

local r = entity_index_try_get_any(entity_index, entity)
if r then
return entity_index.dense_array[r.dense]
end
return 0
end

local function entity_index_remove(entity_index: EntityIndex, entity: number)
local r = entity_index_try_get(entity_index, entity)
if not r then
return
end
local dense_array = entity_index.dense_array
local index_of_deleted_entity = r.dense
local last_entity_alive_at_index = entity_index.alive_count
entity_index.alive_count -= 1

local last_alive_entity = dense_array[last_entity_alive_at_index]
local r_swap = entity_index_try_get_any(
entity_index, last_alive_entity) :: Record
r_swap.dense = index_of_deleted_entity
r.archetype = nil :: any
r.row = nil :: any
r.dense = last_entity_alive_at_index

dense_array[index_of_deleted_entity] = last_alive_entity
dense_array[last_entity_alive_at_index] = ECS_GENERATION_INC(entity)
end

local function entity_index_new_id(entity_index: EntityIndex, data): i53
local dense_array = entity_index.dense_array
if entity_index.alive_count ~= #dense_array then
entity_index.alive_count += 1
local id = dense_array[entity_index.alive_count]
return id
end
entity_index.max_id +=1
local id = entity_index.max_id
entity_index.alive_count += 1

dense_array[entity_index.alive_count] = id
entity_index.sparse_array[id] = {
dense = entity_index.alive_count,
archetype = data
} :: Record

entity_index.sparse_count += 1

return id
end

local function entity_index_is_alive(entity_index: EntityIndex, entity: number)
return entity_index_try_get(entity_index, entity) ~= nil
end

local eidx = {
alive_count = 0,
max_id = 0,
sparse_array = {} :: { Record },
sparse_count = 0,
dense_array = {} :: { i53 }
}
local e1v0 = entity_index_new_id(eidx, "e1v0")
local e2v0 = entity_index_new_id(eidx, "e2v0")
local e3v0 = entity_index_new_id(eidx, "e3v0")
local e4v0 = entity_index_new_id(eidx, "e4v0")
local e5v0 = entity_index_new_id(eidx, "e5v0")
local t = require("@testkit")
local tprint = t.print
entity_index_remove(eidx, e5v0)
local e5v1 = entity_index_new_id(eidx, "e5v1")
entity_index_remove(eidx, e2v0)
tprint(eidx)
157 changes: 157 additions & 0 deletions test/lol.luau
Original file line number Diff line number Diff line change
@@ -0,0 +1,157 @@
local c = {
white_underline = function(s: any)
return `\27[1;4m{s}\27[0m`
end,

white = function(s: any)
return `\27[37;1m{s}\27[0m`
end,

green = function(s: any)
return `\27[32;1m{s}\27[0m`
end,

red = function(s: any)
return `\27[31;1m{s}\27[0m`
end,

yellow = function(s: any)
return `\27[33;1m{s}\27[0m`
end,

red_highlight = function(s: any)
return `\27[41;1;30m{s}\27[0m`
end,

green_highlight = function(s: any)
return `\27[42;1;30m{s}\27[0m`
end,

gray = function(s: any)
return `\27[30;1m{s}\27[0m`
end,
}


local ECS_PAIR_FLAG = 0x8
local ECS_ID_FLAGS_MASK = 0x10
local ECS_ENTITY_MASK = bit32.lshift(1, 24)
local ECS_GENERATION_MASK = bit32.lshift(1, 16)

type i53 = number
type i24 = number

local function ECS_ENTITY_T_LO(e: i53): i24
return if e > ECS_ENTITY_MASK then (e // ECS_ID_FLAGS_MASK) // ECS_ENTITY_MASK else e
end

local function ECS_GENERATION(e: i53): i24
return if e > ECS_ENTITY_MASK then (e // ECS_ID_FLAGS_MASK) % ECS_GENERATION_MASK else 0
end

local ECS_ID = ECS_ENTITY_T_LO

local function ECS_COMBINE(source: number, target: number): i53
return (source * 268435456) + (target * ECS_ID_FLAGS_MASK)
end

local function ECS_GENERATION_INC(e: i53)
if e > ECS_ENTITY_MASK then
local flags = e // ECS_ID_FLAGS_MASK
local id = flags // ECS_ENTITY_MASK
local generation = flags % ECS_GENERATION_MASK

local next_gen = generation + 1
if next_gen > ECS_GENERATION_MASK then
return id
end

return ECS_COMBINE(id, next_gen) + flags
end
return ECS_COMBINE(e, 1)
end

local function bl()
print("")
end

local function pe(e)
local gen = ECS_GENERATION(e)
return c.green(`e{ECS_ID(e)}`)..c.yellow(`v{gen}`)
end

local function dprint(tbl: { [number]: number })
bl()
print("--------")
for i, e in tbl do
print("| "..pe(e).." |")
print("--------")
end
bl()
end

local max_id = 0
local alive_count = 0
local dense = {}
local sparse = {}
local function alloc()
if alive_count ~= #dense then
alive_count += 1
print("*recycled", pe(dense[alive_count]))
return dense[alive_count]
end
max_id += 1
local id = max_id
alive_count += 1
dense[alive_count] = id
sparse[id] = {
dense = alive_count
}
print("*allocated", pe(id))
return id
end

local function remove(entity)
local id = ECS_ID(entity)
local r = sparse[id]
local index_of_deleted_entity = r.dense
local last_entity_alive_at_index = alive_count -- last entity alive
alive_count -= 1
local last_alive_entity = dense[last_entity_alive_at_index]
local r_swap = sparse[ECS_ID(last_alive_entity)]
r_swap.dense = r.dense
r.dense = last_entity_alive_at_index
dense[index_of_deleted_entity] = last_alive_entity
dense[last_entity_alive_at_index] = ECS_GENERATION_INC(entity)
end

local function alive(e)
local r = sparse[ECS_ID(e)]

return dense[r.dense] == e
end

local function pa(e)
print(`{pe(e)} is {if alive(e) then "alive" else "not alive"}`)
end

local tprint = require("@testkit").print
local e1v0 = alloc()
local e2v0 = alloc()
local e3v0 = alloc()
local e4v0 = alloc()
local e5v0 = alloc()
pa(e1v0)
pa(e4v0)
remove(e5v0)
pa(e5v0)

local e5v1 = alloc()
pa(e5v0)
pa(e5v1)
pa(e2v0)
print(ECS_ID(e2v0))

dprint(dense)
remove(e2v0)
dprint(dense)
Loading

0 comments on commit b40af9f

Please sign in to comment.