feat(ecs): chunks as a grouping - IntIndex takes sparse values; ludic.things' chunk column and things_drop_chunk

A streamed world loads and lets go of a chunk's entities together, so a chunk is a first-class
group: chunk_of(x, z, size) packs a chunk's cell into an int, tb_remove_all(tb, ix, v) removes every
row an index files under one value (the observers told of each). IntIndex kept one list per value
up to the largest, which a packed chunk number (hundreds of millions) turned into hundreds of
millions of empty lists and a 12 GB test run; a value past 1024 now gets its slot through an IntMap,
so a sparse value costs one list.

ludic.things files every Thing under its 256 m chunk (TH_CHUNK), kept by every move;
things_chunk_count and things_drop_chunk (the port and the facts told of each removal, as
thing_remove tells them) are what the memory budget's streamed chunks ask. The moves are their own
file (moves.ludic). Tests: ludic.base 35, ludic.things 8, all under a 2 GB cap (24 MB peak).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Orkun ÇAKILKAYA 2026-09-27 22:07:26 +03:00
parent 7834b26ef8
commit f3fec88888
11 changed files with 186 additions and 65 deletions

View file

@ -0,0 +1,17 @@
# chunks.ludic - the Things of one chunk of the world, counted and let go together: what a streamed
# world asks when a chunk is loaded or dropped. A chunk is chunk_of(x, z, TH_CHUNK_M).
export function things_chunk(x: float, z: float) -> int { return chunk_of(x, z, TH_CHUNK_M) }
export function things_chunk_count(things_st: ThingsState, chunk: int) -> int { return ix_count(things_st.th.chunks, chunk) }
# every Thing standing in the chunk removed for good, the port and the facts told of each as
# thing_remove tells them; how many went
export function things_drop_chunk(things_st: mut ThingsState, chunk: int) -> int {
var n = 0
let rows = ix_rows(things_st.th.chunks, chunk)
while len(rows) > 0 {
thing_remove(things_st, things_st.th.tab.rec[rows[len(rows) - 1]])
n += 1
}
return n
}

View file

@ -7,7 +7,9 @@ import "kinds.ludic"
import "port.ludic"
import "store.ludic"
import "verbs.ludic"
import "moves.ludic"
import "queries.ludic"
import "near.ludic"
import "chunks.ludic"
import "dispatch.ludic"
import "system.ludic"

View file

@ -0,0 +1,55 @@
# moves.ludic - a Thing moved, shown or hidden: the record and its row together, so the grid, the
# kind indexes and the chunk index never answer for where it was or what it was
export function thing_move(t: Thing, x: float, z: float, yaw: float) -> void {
thing_move_to(t, x, ThingsWorld.ground(x, z), z, yaw)
}
export function thing_move_to(t: Thing, x: float, y: float, z: float, yaw: float) -> void {
thing_place_at(t, x, y, z, yaw)
ThingsWorld.moved(t)
}
# moved without telling the port: a stand-in the game draws itself, or nothing draws
export function thing_place_at(t: Thing, x: float, y: float, z: float, yaw: float) -> void {
t.x = x
t.y = y
t.z = z
t.yaw = yaw
let r = th_row(t)
if r >= 0 { th_moved(t.tab, r, x, z) }
}
export function thing_set_active(t: Thing, on: bool) -> void {
if t == null { return }
thing_set_on(t, on)
ThingsWorld.shown(t)
}
export function thing_hide(t: Thing) -> void { thing_set_active(t, false) }
export function thing_show(t: Thing) -> void { thing_set_active(t, true) }
# active or not without telling the port
export function thing_set_on(t: Thing, on: bool) -> void {
if t == null { return }
t.active = on
let r = th_row(t)
if r < 0 { return }
var v = 0
if on { v = 1 }
tb_set_i(t.tab, TH_ON, r, v)
}
# where it stands on the ground plane, without telling the port (its height is the caller's)
export function thing_set_xz(t: Thing, x: float, z: float) -> void {
if t == null { return }
t.x = x
t.z = z
let r = th_row(t)
if r >= 0 { th_moved(t.tab, r, x, z) }
}
# where a row now stands: its columns, its cell in the grid and its chunk
function th_moved(tb: Table<Thing>, r: int, x: float, z: float) -> void {
tb_set_xz(tb, TH_X, TH_Z, r, x, z)
let c = chunk_of(x, z, TH_CHUNK_M)
if tb.i[TH_CHUNK][r] != c { tb_set_i(tb, TH_CHUNK, r, c) }
}

View file

@ -5,6 +5,8 @@ export const TH_X: int = 0 # float columns
export const TH_Z: int = 1
export const TH_KIND: int = 0 # int columns
export const TH_ON: int = 1 # 1 while active
export const TH_CHUNK: int = 2 # the chunk of the world it stands in (chunk_of, TH_CHUNK_M)
export const TH_CHUNK_M: float = 256.0
const TH_CELL: float = 16.0
# the table and the indexes kept over it, made together
@ -13,6 +15,7 @@ export property ThingsTable {
grid: Grid = null
kinds: IntIndex = null # active Things by kind
every: IntIndex = null # every Thing by kind, active or not
chunks: IntIndex = null # every Thing by the chunk it stands in: loaded and let go together
}
export state ThingsState {
@ -27,11 +30,12 @@ function th_facts__new() -> Queue<ThingFact> { return queue_new("things.facts")
function th__new() -> ThingsTable {
let th = new ThingsTable
let tb: Table<Thing> = table_new(2, 2)
let tb: Table<Thing> = table_new(2, 3)
th.tab = tb
th.grid = tb_grid(tb, TH_X, TH_Z, TH_ON, TH_CELL)
th.kinds = tb_index(tb, TH_KIND, TH_ON)
th.every = tb_index(tb, TH_KIND, -1)
th.chunks = tb_index(tb, TH_CHUNK, -1)
return th
}
@ -82,7 +86,7 @@ export function things_verify(things_st: ThingsState) -> int {
let t = tb.rec[r]
var on = 0
if t.active { on = 1 }
if tb.f[TH_X][r] != t.x or tb.f[TH_Z][r] != t.z or tb.i[TH_KIND][r] != t.kind or tb.i[TH_ON][r] != on or t.ent != tb.ent[r] { bad += 1 }
if tb.f[TH_X][r] != t.x or tb.f[TH_Z][r] != t.z or tb.i[TH_KIND][r] != t.kind or tb.i[TH_ON][r] != on or tb.i[TH_CHUNK][r] != chunk_of(t.x, t.z, TH_CHUNK_M) or t.ent != tb.ent[r] { bad += 1 }
}
return bad
}

View file

@ -178,4 +178,23 @@ program ThingsTest {
keep.x = 1.0
expect_eq(things_verify(things_st), 1)
}
test "a chunk's Things are counted and let go together; a move carries a Thing to its new chunk" (things_st: mut ThingsState, things_test_st: ThingsTestState) {
things_clear(things_st)
put(things_st, TH_BRANCH, 10.0, 10.0)
put(things_st, TH_BRANCH, 20.0, 30.0)
let far = put(things_st, TH_BRANCH, 600.0, 10.0)
let here = things_chunk(10.0, 10.0)
expect_eq(things_chunk_count(things_st, here), 2)
expect_eq(things_chunk_count(things_st, things_chunk(600.0, 10.0)), 1)
thing_move(far, 15.0, 15.0, 0.0)
expect_eq(things_chunk_count(things_st, here), 3)
facts(things_st)
let gone = things_test_st.gone
expect_eq(things_drop_chunk(things_st, here), 3)
expect_eq(things_count(things_st), 0)
expect_eq(things_test_st.gone, gone + 3)
expect_eq(len(facts(things_st)), 3)
expect_eq(things_verify(things_st), 0)
}
}

View file

@ -35,6 +35,7 @@ function th_columns(t: Thing) -> void {
var on = 0
if t.active { on = 1 }
tb.i[TH_ON][r] = on
tb.i[TH_CHUNK][r] = chunk_of(t.x, t.z, TH_CHUNK_M)
tb_refile(tb, r)
}
@ -66,50 +67,3 @@ export function things_drop(things_st: mut ThingsState, gone: fn(Thing) -> bool)
r -= 1
}
}
export function thing_move(t: Thing, x: float, z: float, yaw: float) -> void {
thing_move_to(t, x, ThingsWorld.ground(x, z), z, yaw)
}
export function thing_move_to(t: Thing, x: float, y: float, z: float, yaw: float) -> void {
thing_place_at(t, x, y, z, yaw)
ThingsWorld.moved(t)
}
# moved without telling the port: a stand-in the game draws itself, or nothing draws
export function thing_place_at(t: Thing, x: float, y: float, z: float, yaw: float) -> void {
t.x = x
t.y = y
t.z = z
t.yaw = yaw
let r = th_row(t)
if r >= 0 { tb_set_xz(t.tab, TH_X, TH_Z, r, x, z) }
}
export function thing_set_active(t: Thing, on: bool) -> void {
if t == null { return }
thing_set_on(t, on)
ThingsWorld.shown(t)
}
export function thing_hide(t: Thing) -> void { thing_set_active(t, false) }
export function thing_show(t: Thing) -> void { thing_set_active(t, true) }
# active or not without telling the port
export function thing_set_on(t: Thing, on: bool) -> void {
if t == null { return }
t.active = on
let r = th_row(t)
if r < 0 { return }
var v = 0
if on { v = 1 }
tb_set_i(t.tab, TH_ON, r, v)
}
# where it stands on the ground plane, without telling the port (its height is the caller's)
export function thing_set_xz(t: Thing, x: float, z: float) -> void {
if t == null { return }
t.x = x
t.z = z
let r = th_row(t)
if r >= 0 { tb_set_xz(t.tab, TH_X, TH_Z, r, x, z) }
}