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>
79 lines
2.9 KiB
Text
79 lines
2.9 KiB
Text
# 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
|
|
}
|