feat(ecs): ludic.base Table<T> - dense rows of hot columns, generational handles, a spatial grid, kind indexes and an id map kept current by the setters; ludic.things on it
A mechanic that keeps many of something keeps them as rows of a Table<T> rather than a list it scans. Removal swaps the last row in; a handle (22-bit slot, 9-bit generation) goes stale when its entity is removed. tb_set_f / tb_set_xz / tb_set_i stamp a change tick and refile the row in every index over that column in O(1): tb_grid (a doubly linked spatial hash, rings outward for nearest, rehashing as it grows), tb_index (a cached query: the rows of each value of a kind column, gated by an active column), tb_nearest_of / tb_within_of (a rare kind from its own list), tb_nearest_where (a predicate on the record), tb_within_recs (into the caller's list), tb_changed_since / tb_added_since, IntMap. No question allocates or writes: Ludic frees nothing, and the old lists' per-call copies leaked every frame. ludic.things keeps its Things as a Table<Thing> with x, z, kind and active as columns; every verb writes the record and the row together (a Thing carries its handle and table, so thing_hide(t) still needs no state), thing_set_on / thing_set_xz / thing_place_at are the silent forms the game used to do by assignment, and things_verify holds the columns against the records. things_near(_of) fill a caller's list, thing_of_kind walks a kind, things_count_of counts one, and things_tick visits only the kinds that tick. thing_find answers the first-placed by uid. Against a []Record scanned (M4 Pro): nearest 0.37 / 1.5 / 7.2 us at 10k / 100k / 1M (list 30 / 307 / 3075), by id 0.09 us at 100k (list 17), a move refiled in 11-44 ns. ecs_fuzz_test holds the grid, the kind index and the record queries against a scan through 9000 random changes. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
parent
e3813b1ea4
commit
f802bdd3ec
23 changed files with 1367 additions and 125 deletions
79
packages/ludic.base/ecs_table.ludic
Normal file
79
packages/ludic.base/ecs_table.ludic
Normal file
|
|
@ -0,0 +1,79 @@
|
|||
# ludic.base/ecs_table.ludic - a Table<T>: entities as dense rows of hot columns (floats, words)
|
||||
# and one record each for what is cold. A handle is a slot and a generation, so a stale one misses.
|
||||
export const ECS_SLOT: int = 4194303 # the low 22 bits of a handle: its slot
|
||||
export const ECS_GEN: int = 511 # the next 9: how many times the slot was reused
|
||||
export const ECS_NONE: int = -1
|
||||
|
||||
export property Table<T> {
|
||||
n: int = 0
|
||||
rec: []T = null
|
||||
ent: words = null # row -> handle
|
||||
f: []floats = null # hot float columns, a value per row each
|
||||
i: []words = null # hot int columns
|
||||
added: words = null # row -> the tick it was added at
|
||||
changed: words = null # row -> the tick a setter last wrote it at
|
||||
tick: int = 1
|
||||
slot_gen: words = null # slot -> generation
|
||||
slot_row: words = null # slot -> row, -1 while free
|
||||
free: words = null
|
||||
grids: []Grid = null
|
||||
idx: []IntIndex = null
|
||||
}
|
||||
|
||||
# a table of nf float and ni int columns, every value 0 in a new row
|
||||
export function table_new<T>(nf: int, ni: int) -> Table<T> {
|
||||
let tb = new Table<T>
|
||||
tb.rec = new []T
|
||||
tb.ent = words(0)
|
||||
tb.f = new []floats
|
||||
tb.i = new []words
|
||||
for c in 0 .. nf { push(tb.f, floats(0)) }
|
||||
for c in 0 .. ni { push(tb.i, words(0)) }
|
||||
tb.added = words(0)
|
||||
tb.changed = words(0)
|
||||
tb.slot_gen = words(0)
|
||||
tb.slot_row = words(0)
|
||||
tb.free = words(0)
|
||||
tb.grids = new []Grid
|
||||
tb.idx = new []IntIndex
|
||||
return tb
|
||||
}
|
||||
|
||||
export function tb_len<T>(tb: Table<T>) -> int { return tb.n }
|
||||
export function tb_rec<T>(tb: Table<T>, row: int) -> T { return tb.rec[row] }
|
||||
export function tb_handle<T>(tb: Table<T>, row: int) -> int { return tb.ent[row] }
|
||||
export function tb_tick<T>(tb: Table<T>) -> int { return tb.tick }
|
||||
# the next tick: what is written from here on is newer than anything before
|
||||
export function tb_advance<T>(tb: Table<T>) -> void { tb.tick += 1 }
|
||||
|
||||
# the row a handle names, or -1 when it was removed (or the slot has been reused since)
|
||||
export function tb_row<T>(tb: Table<T>, h: int) -> int {
|
||||
if h < 0 { return -1 }
|
||||
let s = h & ECS_SLOT
|
||||
if s >= len(tb.slot_gen) or tb.slot_gen[s] != ((h >> 22) & ECS_GEN) { return -1 }
|
||||
return tb.slot_row[s]
|
||||
}
|
||||
|
||||
export function tb_alive<T>(tb: Table<T>, h: int) -> bool { return tb_row(tb, h) >= 0 }
|
||||
|
||||
# a new row holding `r`, every column 0 and filed in no index until a setter says where it is
|
||||
export function tb_add<T>(tb: Table<T>, r: T) -> int {
|
||||
var s = 0
|
||||
if len(tb.free) > 0 { s = List.pop(tb.free) } else {
|
||||
s = len(tb.slot_gen)
|
||||
push(tb.slot_gen, 0)
|
||||
push(tb.slot_row, -1)
|
||||
}
|
||||
tb.slot_row[s] = tb.n
|
||||
let h = s | (tb.slot_gen[s] << 22)
|
||||
push(tb.rec, r)
|
||||
push(tb.ent, h)
|
||||
push(tb.added, tb.tick)
|
||||
push(tb.changed, tb.tick)
|
||||
for c in 0 .. len(tb.f) { push(tb.f[c], 0.0) }
|
||||
for c in 0 .. len(tb.i) { push(tb.i[c], 0) }
|
||||
tb.n += 1
|
||||
for g in 0 .. len(tb.grids) { grid_grow(tb.grids[g]) }
|
||||
for k in 0 .. len(tb.idx) { ix_grow(tb.idx[k]) }
|
||||
return h
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue