ludic.base: tb_reserve and imap_reserve size a table up front; HandlePool (a slot and a generation, nothing made after hd_pool_new, past its capacity a panic naming it); kept_push is the one declared grow, and every growth path left says its bound in @alloc_ok
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
parent
f2dd27f443
commit
2d1d06899e
17 changed files with 199 additions and 26 deletions
|
|
@ -29,6 +29,7 @@ function grid_moved(g: Grid, from: int, to: int) -> void {
|
|||
}
|
||||
|
||||
# twice the buckets, every filed row linked again where its key now hashes
|
||||
@alloc_ok("past grid_reserve only: the buckets double once past the most rows filed, and are kept")
|
||||
function grid_rehash(g: Grid) -> void {
|
||||
g.mask = g.mask * 2 + 1
|
||||
g.head = words(g.mask + 1)
|
||||
|
|
|
|||
|
|
@ -63,7 +63,7 @@ export function tb_within_recs<T>(tb: Table<T>, g: Grid, x: float, z: float, r:
|
|||
let cells = (ix1 - ix0 + 1) * (iz1 - iz0 + 1)
|
||||
if cells > g.count or cells < 0 {
|
||||
for row in 0 .. len(g.key) {
|
||||
if g.key[row] >= 0 and (mcol == null or mcol[row] == mv) and grid_d2(xs, zs, row, x, z) <= r * r { push(out, tb.rec[row]) }
|
||||
if g.key[row] >= 0 and (mcol == null or mcol[row] == mv) and grid_d2(xs, zs, row, x, z) <= r * r { kept_push(out, tb.rec[row]) }
|
||||
}
|
||||
return len(out)
|
||||
}
|
||||
|
|
@ -78,7 +78,7 @@ function grid_recs_cell<T>(tb: Table<T>, g: Grid, xs: floats, zs: floats, mcol:
|
|||
var r = g.head[grid_bucket(g, ix, iz)]
|
||||
while r > 0 {
|
||||
let row = r - 1
|
||||
if g.key[row] == k and (mcol == null or mcol[row] == mv) and grid_d2(xs, zs, row, x, z) <= r2 { push(out, tb.rec[row]) }
|
||||
if g.key[row] == k and (mcol == null or mcol[row] == mv) and grid_d2(xs, zs, row, x, z) <= r2 { kept_push(out, tb.rec[row]) }
|
||||
r = g.next[row]
|
||||
}
|
||||
}
|
||||
|
|
@ -95,7 +95,7 @@ export function tb_within_of_recs<T>(tb: Table<T>, g: Grid, ix: IntIndex, x: flo
|
|||
for k in 0 .. n {
|
||||
let row = rows[k]
|
||||
if g.gate >= 0 and tb.i[g.gate][row] == 0 { continue }
|
||||
if grid_d2(xs, zs, row, x, z) <= r * r { push(out, tb.rec[row]) }
|
||||
if grid_d2(xs, zs, row, x, z) <= r * r { kept_push(out, tb.rec[row]) }
|
||||
}
|
||||
return len(out)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -55,7 +55,7 @@ function ix_file(ix: IntIndex, r: int, v: int) -> void {
|
|||
if s < 0 { return }
|
||||
ix.pos[r] = len(ix.lists[s])
|
||||
ix.val[r] = s
|
||||
push(ix.lists[s], r)
|
||||
kept_push(ix.lists[s], r)
|
||||
}
|
||||
|
||||
# out of its list: the list's last row takes its place
|
||||
|
|
|
|||
|
|
@ -4,6 +4,7 @@
|
|||
const IX_DENSE: int = 1024 # a value below this is its own slot; above, one is given it
|
||||
const IX_CAP: int = 16 # rows a value's list holds before it grows: a list grown in play is a new block
|
||||
|
||||
@alloc_ok("once per distinct value: its list is made the first time the value is filed, and kept")
|
||||
function ix_list_new() -> words {
|
||||
let l = words(IX_CAP)
|
||||
List.clear(l)
|
||||
|
|
@ -16,15 +17,15 @@ function ix_slot(ix: IntIndex, v: int, make: bool) -> int {
|
|||
if v < IX_DENSE {
|
||||
if v >= len(ix.lists) {
|
||||
if not make { return -1 }
|
||||
while len(ix.lists) <= v { push(ix.lists, ix_list_new()) }
|
||||
while len(ix.lists) <= v { kept_push(ix.lists, ix_list_new()) }
|
||||
}
|
||||
return v
|
||||
}
|
||||
var s = imap_get(ix.sparse, v, -1)
|
||||
if s < 0 and make {
|
||||
while len(ix.lists) < IX_DENSE { push(ix.lists, ix_list_new()) }
|
||||
while len(ix.lists) < IX_DENSE { kept_push(ix.lists, ix_list_new()) }
|
||||
s = len(ix.lists)
|
||||
push(ix.lists, ix_list_new())
|
||||
kept_push(ix.lists, ix_list_new())
|
||||
imap_put(ix.sparse, v, s)
|
||||
}
|
||||
return s
|
||||
|
|
|
|||
|
|
@ -69,6 +69,18 @@ export function imap_del(m: IntMap, k: int) -> bool {
|
|||
export function imap_len(m: IntMap) -> int { return m.n }
|
||||
export function imap_clear(m: IntMap) -> void { imap_alloc(m, m.mask + 1) }
|
||||
|
||||
# room for `n` keys before the map grows, made now
|
||||
export function imap_reserve(m: IntMap, n: int) -> void {
|
||||
if (m.mask + 1) * 3 >= n * 4 { return }
|
||||
var cap = m.mask + 1
|
||||
while cap * 3 < n * 4 { cap = cap * 2 }
|
||||
let ok = m.keys
|
||||
let ov = m.vals
|
||||
imap_alloc(m, cap)
|
||||
for i in 0 .. len(ok) { if ok[i] >= 0 { imap_put(m, ok[i], ov[i]) } }
|
||||
}
|
||||
|
||||
@alloc_ok("past imap_reserve only: doubles when half full; the old arrays are not given back")
|
||||
function imap_grow(m: IntMap) -> void {
|
||||
let ok = m.keys
|
||||
let ov = m.vals
|
||||
|
|
|
|||
|
|
@ -66,22 +66,10 @@ export function tb_alive<T>(tb: Table<T>, h: int) -> bool { return tb_row(tb, h)
|
|||
# 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)
|
||||
}
|
||||
if len(tb.free) > 0 { s = List.pop(tb.free) } else { s = tb_new_slot(tb) }
|
||||
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]) }
|
||||
tb_push_row(tb, r, h)
|
||||
tb_stamp(tb, tb.n - 1)
|
||||
for k in 0 .. len(tb.on_add) { tb.on_add[k].run(h) }
|
||||
return h
|
||||
|
|
@ -91,6 +79,6 @@ export function tb_add<T>(tb: Table<T>, r: T) -> int {
|
|||
function tb_stamp<T>(tb: Table<T>, row: int) -> void {
|
||||
tb.changed[row] = tb.tick
|
||||
let b = row >> 6
|
||||
while len(tb.blk) <= b { push(tb.blk, 0) }
|
||||
while len(tb.blk) <= b { kept_push(tb.blk, 0) }
|
||||
tb.blk[b] = tb.tick
|
||||
}
|
||||
|
|
|
|||
|
|
@ -13,7 +13,7 @@ export function tb_remove<T>(tb: Table<T>, h: int) -> bool {
|
|||
let s = h & ECS_SLOT
|
||||
tb.slot_row[s] = -1
|
||||
tb.slot_gen[s] = (tb.slot_gen[s] + 1) & ECS_GEN
|
||||
push(tb.free, s)
|
||||
kept_push(tb.free, s)
|
||||
return true
|
||||
}
|
||||
|
||||
|
|
|
|||
43
packages/ludic.base/ecs_table_reserve.ludic
Normal file
43
packages/ludic.base/ecs_table_reserve.ludic
Normal file
|
|
@ -0,0 +1,43 @@
|
|||
# ludic.base/ecs_table_reserve.ludic - room made at the start: a table sized for its busiest moment
|
||||
# never grows in play, and nothing that grows is ever given back. What does grow past it is here
|
||||
export function tb_reserve<T>(tb: Table<T>, rows: int) -> void {
|
||||
let k = rows - tb.n
|
||||
if k <= 0 { return }
|
||||
for j in 0 .. k { tb_push_row(tb, null, 0) }
|
||||
for j in 0 .. k { tb_pop(tb) }
|
||||
let s0 = len(tb.slot_gen)
|
||||
for j in s0 .. rows {
|
||||
push(tb.slot_gen, 0)
|
||||
push(tb.slot_row, -1)
|
||||
push(tb.free, 0)
|
||||
}
|
||||
for j in s0 .. rows {
|
||||
List.pop(tb.slot_gen)
|
||||
List.pop(tb.slot_row)
|
||||
List.pop(tb.free)
|
||||
}
|
||||
while len(tb.blk) <= (rows >> 6) { push(tb.blk, 0) }
|
||||
for g in 0 .. len(tb.grids) { grid_reserve(tb.grids[g], rows) }
|
||||
}
|
||||
|
||||
# a row on every column, grid and index
|
||||
@alloc_ok("past tb_reserve's rows only: a table grows to its high-water once and keeps it")
|
||||
function tb_push_row<T>(tb: Table<T>, r: T, h: int) -> void {
|
||||
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]) }
|
||||
}
|
||||
|
||||
@alloc_ok("past tb_reserve's rows only: a slot is made once and reused through the free list")
|
||||
function tb_new_slot<T>(tb: Table<T>) -> int {
|
||||
let s = len(tb.slot_gen)
|
||||
push(tb.slot_gen, 0)
|
||||
push(tb.slot_row, -1)
|
||||
return s
|
||||
}
|
||||
|
|
@ -59,7 +59,7 @@ export function tb_changed_since<T>(tb: Table<T>, since: int, out: words) -> int
|
|||
if tb.blk[b] <= since { continue }
|
||||
var end = b * 64 + 64
|
||||
if end > tb.n { end = tb.n }
|
||||
for r in b * 64 .. end { if tb.changed[r] > since { push(out, r) } }
|
||||
for r in b * 64 .. end { if tb.changed[r] > since { kept_push(out, r) } }
|
||||
}
|
||||
return len(out)
|
||||
}
|
||||
|
|
|
|||
62
packages/ludic.base/handle.ludic
Normal file
62
packages/ludic.base/handle.ludic
Normal file
|
|
@ -0,0 +1,62 @@
|
|||
# ludic.base/handle.ludic - ids a pool hands out, each a slot and a generation (Table's encoding): a
|
||||
# freed id's slot is reused under a new generation, so an id kept past its free misses with -1
|
||||
export property HandlePool {
|
||||
name: string = ""
|
||||
gen: words = null # slot -> generation
|
||||
live: words = null # slot -> 1 while handed out
|
||||
free: words = null
|
||||
n: int = 0 # slots made so far
|
||||
count: int = 0 # handed out now
|
||||
refused: int = 0 # asks past the capacity
|
||||
strict: bool = true # past the capacity is a panic naming the pool, else -1
|
||||
}
|
||||
|
||||
# everything the pool will ever hold, made now: nothing after this allocates
|
||||
export function hd_pool_new(name: string, cap: int) -> HandlePool {
|
||||
let p = new HandlePool
|
||||
p.name = name
|
||||
p.gen = words(cap)
|
||||
p.live = words(cap)
|
||||
p.free = words(cap)
|
||||
List.clear(p.free)
|
||||
return p
|
||||
}
|
||||
|
||||
export function hd_new(p: HandlePool) -> int {
|
||||
var s = -1
|
||||
if len(p.free) > 0 { s = List.pop(p.free) }
|
||||
else if p.n < len(p.gen) {
|
||||
s = p.n
|
||||
p.n += 1
|
||||
} else { return hd_refuse(p) }
|
||||
p.live[s] = 1
|
||||
p.count += 1
|
||||
return s | (p.gen[s] << 22)
|
||||
}
|
||||
|
||||
function hd_refuse(p: HandlePool) -> int {
|
||||
p.refused += 1
|
||||
if p.strict { panic(`handle pool {p.name} is full at {len(p.gen)}`) }
|
||||
return ECS_NONE
|
||||
}
|
||||
|
||||
# the slot a handle names, or -1 when it was freed (or its slot has been handed out again since)
|
||||
export function hd_slot(p: HandlePool, h: int) -> int {
|
||||
if h < 0 { return -1 }
|
||||
let s = h & ECS_SLOT
|
||||
if s >= p.n or p.live[s] == 0 or p.gen[s] != ((h >> 22) & ECS_GEN) { return -1 }
|
||||
return s
|
||||
}
|
||||
|
||||
export function hd_free(p: HandlePool, h: int) -> bool {
|
||||
let s = hd_slot(p, h)
|
||||
if s < 0 { return false }
|
||||
p.live[s] = 0
|
||||
p.gen[s] = (p.gen[s] + 1) & ECS_GEN
|
||||
kept_push(p.free, s)
|
||||
p.count -= 1
|
||||
return true
|
||||
}
|
||||
|
||||
export function hd_live(p: HandlePool) -> int { return p.count }
|
||||
export function hd_cap(p: HandlePool) -> int { return len(p.gen) }
|
||||
|
|
@ -3,6 +3,8 @@
|
|||
module ludic_base uses
|
||||
numbers float
|
||||
import "tick.ludic"
|
||||
import "kept.ludic"
|
||||
import "handle.ludic"
|
||||
import "queue.ludic"
|
||||
import "text_buf.ludic"
|
||||
import "text_intern.ludic"
|
||||
|
|
@ -14,6 +16,7 @@ import "system.ludic"
|
|||
import "ecs_table.ludic"
|
||||
import "ecs_hooks.ludic"
|
||||
import "ecs_table_remove.ludic"
|
||||
import "ecs_table_reserve.ludic"
|
||||
import "ecs_table_set.ludic"
|
||||
import "ecs_grid.ludic"
|
||||
import "ecs_grid_file.ludic"
|
||||
|
|
|
|||
4
packages/ludic.base/kept.ludic
Normal file
4
packages/ludic.base/kept.ludic
Normal file
|
|
@ -0,0 +1,4 @@
|
|||
# ludic.base/kept.ludic - the one push the frame is allowed: into a list its owner keeps and clears,
|
||||
# which grows to the most it has ever held and stays there (a queue's halves, a query's out list)
|
||||
@alloc_ok("a kept list: it grows to the most it has held once, is cleared and reused, never dropped")
|
||||
export function kept_push<T>(xs: []T, v: T) -> void { push(xs, v) }
|
||||
|
|
@ -30,7 +30,7 @@ export function queue_new<T>(name: string) -> Queue<T> {
|
|||
}
|
||||
|
||||
export function q_push<T>(q: Queue<T>, v: T) -> void {
|
||||
push(q.items, v)
|
||||
kept_push(q.items, v)
|
||||
q.tag.pending = len(q.items)
|
||||
}
|
||||
|
||||
|
|
@ -65,7 +65,7 @@ export function q_unheld<T>(q: Queue<T>, pool: []T, out: []T) -> int {
|
|||
var held = false
|
||||
for j in 0 .. len(q.items) { if q.items[j] == r { held = true } }
|
||||
for j in 0 .. len(q.spare) { if q.spare[j] == r { held = true } }
|
||||
if not held { push(out, r) }
|
||||
if not held { kept_push(out, r) }
|
||||
}
|
||||
return len(out)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -17,6 +17,8 @@ export property System {
|
|||
export open registry Systems of System as SYS
|
||||
|
||||
|
||||
# the declared systems; a registry is not filled when a state's defaults are made, so on first ask
|
||||
@alloc_ok("once: the runner's list is made the first time it is asked, and kept")
|
||||
function cs_all(base_st: mut BaseState) -> []System {
|
||||
if base_st.cs_list == null {
|
||||
base_st.cs_list = new []System
|
||||
|
|
|
|||
|
|
@ -179,8 +179,26 @@ program EcsTest {
|
|||
test "observers hear each add, and a remove once" (hook_st: HookState) { hooks_case(hook_st) }
|
||||
test "change detection skips the blocks nobody wrote" () { blocks_case() }
|
||||
test "a removed handle misses, rows stay dense, a reused slot is a new handle" () { handles_case() }
|
||||
function reserve_case() -> void {
|
||||
let tb: Table<Toy> = table_new(2, 2)
|
||||
let g = tb_grid(tb, CX, CZ, ON, 8.0)
|
||||
let ix = tb_index(tb, KIND, ON)
|
||||
put(tb, "a", 1.0, 1.0, 3)
|
||||
tb_reserve(tb, 200)
|
||||
expect_eq(tb_len(tb), 1)
|
||||
var hs = new []int
|
||||
for i in 0 .. 150 { push(hs, put(tb, "t", float(i), 2.0, i % 4)) }
|
||||
expect_eq(tb_len(tb), 151)
|
||||
expect_eq(ix_count(ix, 3), 38)
|
||||
expect(tb_row(tb, hs[10]) >= 0)
|
||||
expect(tb_remove(tb, hs[10]))
|
||||
expect_eq(tb_row(tb, hs[10]), -1)
|
||||
expect_eq(grid_count(g), 150)
|
||||
}
|
||||
|
||||
test "the grid answers nearest and within, gated, after moves" () { grid_case() }
|
||||
test "the kind index follows the setters, the gate and removals" () { index_case() }
|
||||
test "change ticks name the rows written since" () { ticks_case() }
|
||||
test "IntMap puts, finds, deletes and grows" () { map_case() }
|
||||
test "a reserved table holds its rows and indexes as an unreserved one does" () { reserve_case() }
|
||||
}
|
||||
|
|
|
|||
38
packages/ludic.base/tests/handle_test.ludic
Normal file
38
packages/ludic.base/tests/handle_test.ludic
Normal file
|
|
@ -0,0 +1,38 @@
|
|||
# handle_test.ludic - a pool's ids: a freed one misses once its slot is reused, the generation wraps,
|
||||
# and past the capacity a pool that is not strict refuses rather than grows
|
||||
import "ludic.base"
|
||||
program HandleTest {
|
||||
numbers float
|
||||
test "a freed handle misses, and its slot comes back under a new generation" {
|
||||
let p = hd_pool_new("bufs", 4)
|
||||
p.strict = false
|
||||
let a = hd_new(p)
|
||||
let b = hd_new(p)
|
||||
expect_eq(hd_slot(p, a), 0)
|
||||
expect_eq(hd_slot(p, b), 1)
|
||||
expect(hd_free(p, a))
|
||||
expect_eq(hd_slot(p, a), -1)
|
||||
expect(not hd_free(p, a))
|
||||
let c = hd_new(p)
|
||||
expect_eq(hd_slot(p, c), 0)
|
||||
expect(c != a)
|
||||
expect_eq(hd_slot(p, a), -1)
|
||||
expect_eq(hd_live(p), 2)
|
||||
}
|
||||
test "the generation wraps after 512 reuses and the pool never grows past its capacity" {
|
||||
let p = hd_pool_new("once", 2)
|
||||
p.strict = false
|
||||
var h = hd_new(p)
|
||||
let first = h
|
||||
for i in 0 .. 511 {
|
||||
hd_free(p, h)
|
||||
h = hd_new(p)
|
||||
}
|
||||
hd_free(p, h)
|
||||
expect_eq(hd_new(p), first)
|
||||
hd_new(p)
|
||||
expect_eq(hd_new(p), -1)
|
||||
expect_eq(p.refused, 1)
|
||||
expect_eq(hd_cap(p), 2)
|
||||
}
|
||||
}
|
||||
|
|
@ -34,6 +34,7 @@ function sb_same(sb: StrBuf, s: string) -> bool {
|
|||
}
|
||||
|
||||
# the table's string for the buffer's bytes
|
||||
@alloc_ok("intern: one string per distinct text, up to the table's most, then kept")
|
||||
export function sb_intern(sb: StrBuf, tb: StrTable) -> string {
|
||||
let h = sb_hash(sb)
|
||||
let mask = len(tb.hs) - 1
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue