diff --git a/packages/ludic.base/ecs_hooks.ludic b/packages/ludic.base/ecs_hooks.ludic new file mode 100644 index 00000000..b79f8656 --- /dev/null +++ b/packages/ludic.base/ecs_hooks.ludic @@ -0,0 +1,18 @@ +# ludic.base/ecs_hooks.ludic - observers on a table: told each handle as its row is made, and before +# it is removed, so what depends on a table (a drawing, a message) follows it without a scan +# an observer: a function handed an entity's handle (its states are supplied, like a system's) +export property TbHook { + run: fn(int) -> void = null +} + +# f is told every handle added from here on, and every handle about to be removed +export function tb_on_add(tb: Table, f: fn(int) -> void) -> void { + let hk = new TbHook + hk.run = f + push(tb.on_add, hk) +} +export function tb_on_remove(tb: Table, f: fn(int) -> void) -> void { + let hk = new TbHook + hk.run = f + push(tb.on_remove, hk) +} diff --git a/packages/ludic.base/ecs_table.ludic b/packages/ludic.base/ecs_table.ludic index 42cd5b32..83f18337 100644 --- a/packages/ludic.base/ecs_table.ludic +++ b/packages/ludic.base/ecs_table.ludic @@ -12,14 +12,18 @@ export property Table { 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 + blk: words = null # 64-row block -> the newest tick any row in it was added or written 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 + on_add: []TbHook = null # observers: told a handle as its row is made, and before it goes + on_remove: []TbHook = null } + # a table of nf float and ni int columns, every value 0 in a new row export function table_new(nf: int, ni: int) -> Table { let tb = new Table @@ -31,11 +35,14 @@ export function table_new(nf: int, ni: int) -> Table { for c in 0 .. ni { push(tb.i, words(0)) } tb.added = words(0) tb.changed = words(0) + tb.blk = words(0) tb.slot_gen = words(0) tb.slot_row = words(0) tb.free = words(0) tb.grids = new []Grid tb.idx = new []IntIndex + tb.on_add = new []TbHook + tb.on_remove = new []TbHook return tb } @@ -75,5 +82,15 @@ export function tb_add(tb: Table, r: T) -> int { 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_stamp(tb, tb.n - 1) + for k in 0 .. len(tb.on_add) { tb.on_add[k].run(h) } return h } + +# the row's tick, and its block's: what change detection reads +function tb_stamp(tb: Table, row: int) -> void { + tb.changed[row] = tb.tick + let b = row >> 6 + while len(tb.blk) <= b { push(tb.blk, 0) } + tb.blk[b] = tb.tick +} diff --git a/packages/ludic.base/ecs_table_remove.ludic b/packages/ludic.base/ecs_table_remove.ludic index 51b8a592..d7e0ccc7 100644 --- a/packages/ludic.base/ecs_table_remove.ludic +++ b/packages/ludic.base/ecs_table_remove.ludic @@ -1,6 +1,8 @@ # ludic.base/ecs_table_remove.ludic - removing a row moves the last one into its place, so the # rows stay dense; every index is told, and the slot's generation moves on export function tb_remove(tb: Table, h: int) -> bool { + if tb_row(tb, h) < 0 { return false } + for k in 0 .. len(tb.on_remove) { tb.on_remove[k].run(h) } let r = tb_row(tb, h) if r < 0 { return false } for g in 0 .. len(tb.grids) { grid_unlink(tb.grids[g], r) } @@ -23,6 +25,8 @@ function tb_move(tb: Table, from: int, to: int) -> void { for c in 0 .. len(tb.f) { tb.f[c][to] = tb.f[c][from] } for c in 0 .. len(tb.i) { tb.i[c][to] = tb.i[c][from] } tb.slot_row[tb.ent[to] & ECS_SLOT] = to + let b = to >> 6 + if tb.blk[b] < tb.changed[to] { tb.blk[b] = tb.changed[to] } for g in 0 .. len(tb.grids) { grid_moved(tb.grids[g], from, to) } for k in 0 .. len(tb.idx) { ix_moved(tb.idx[k], from, to) } } diff --git a/packages/ludic.base/ecs_table_set.ludic b/packages/ludic.base/ecs_table_set.ludic index 33f98f61..8edb8bf8 100644 --- a/packages/ludic.base/ecs_table_set.ludic +++ b/packages/ludic.base/ecs_table_set.ludic @@ -5,7 +5,7 @@ export function tb_i(tb: Table, c: int, row: int) -> int { return tb.i[c][ export function tb_set_f(tb: Table, c: int, row: int, v: float) -> void { tb.f[c][row] = v - tb.changed[row] = tb.tick + tb_stamp(tb, row) for g in 0 .. len(tb.grids) { let gr = tb.grids[g] if gr.cx == c or gr.cz == c { tb_grid_refile(tb, gr, row) } @@ -16,7 +16,7 @@ export function tb_set_f(tb: Table, c: int, row: int, v: float) -> void { export function tb_set_xz(tb: Table, cx: int, cz: int, row: int, x: float, z: float) -> void { tb.f[cx][row] = x tb.f[cz][row] = z - tb.changed[row] = tb.tick + tb_stamp(tb, row) for g in 0 .. len(tb.grids) { let gr = tb.grids[g] if gr.cx == cx or gr.cz == cz or gr.cx == cz or gr.cz == cx { tb_grid_refile(tb, gr, row) } @@ -25,7 +25,7 @@ export function tb_set_xz(tb: Table, cx: int, cz: int, row: int, x: float, export function tb_set_i(tb: Table, c: int, row: int, v: int) -> void { tb.i[c][row] = v - tb.changed[row] = tb.tick + tb_stamp(tb, row) for g in 0 .. len(tb.grids) { if tb.grids[g].gate == c { tb_grid_refile(tb, tb.grids[g], row) } } for k in 0 .. len(tb.idx) { let ix = tb.idx[k] @@ -35,12 +35,12 @@ export function tb_set_i(tb: Table, c: int, row: int, v: int) -> void { # a row written without a setter (tb.f / tb.i directly, for speed) is refiled everywhere by this export function tb_refile(tb: Table, row: int) -> void { - tb.changed[row] = tb.tick + tb_stamp(tb, row) for g in 0 .. len(tb.grids) { tb_grid_refile(tb, tb.grids[g], row) } for k in 0 .. len(tb.idx) { tb_ix_refile(tb, tb.idx[k], row) } } -export function tb_mark(tb: Table, row: int) -> void { tb.changed[row] = tb.tick } +export function tb_mark(tb: Table, row: int) -> void { tb_stamp(tb, row) } function tb_gate_on(tb: Table, gate: int, row: int) -> bool { return gate < 0 or tb.i[gate][row] != 0 } @@ -55,12 +55,22 @@ function tb_ix_refile(tb: Table, ix: IntIndex, row: int) -> void { # change detection: the rows a setter wrote after `since`, into `out` (cleared first); no allocation export function tb_changed_since(tb: Table, since: int, out: words) -> int { List.clear(out) - for r in 0 .. tb.n { if tb.changed[r] > since { push(out, r) } } + for b in 0 .. len(tb.blk) { + 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) } } + } return len(out) } export function tb_added_since(tb: Table, since: int, out: words) -> int { List.clear(out) - for r in 0 .. tb.n { if tb.added[r] > since { push(out, r) } } + for b in 0 .. len(tb.blk) { + 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.added[r] > since { push(out, r) } } + } return len(out) } diff --git a/packages/ludic.base/index.ludic b/packages/ludic.base/index.ludic index b3a92b9f..0d7aa63f 100644 --- a/packages/ludic.base/index.ludic +++ b/packages/ludic.base/index.ludic @@ -9,6 +9,7 @@ import "save.ludic" import "save_fields.ludic" import "system.ludic" import "ecs_table.ludic" +import "ecs_hooks.ludic" import "ecs_table_remove.ludic" import "ecs_table_set.ludic" import "ecs_grid.ludic" diff --git a/packages/ludic.base/tests/ecs_test.ludic b/packages/ludic.base/tests/ecs_test.ludic index 91def3f2..328e0f1f 100644 --- a/packages/ludic.base/tests/ecs_test.ludic +++ b/packages/ludic.base/tests/ecs_test.ludic @@ -10,6 +10,17 @@ program EcsTest { const KIND: int = 0 const ON: int = 1 + state HookState { + added: int = 0 + gone: int = 0 + last: int = -1 + } + function seen_add(hook_st: mut HookState, h: int) -> void { + hook_st.added += 1 + hook_st.last = h + } + function seen_gone(hook_st: mut HookState, h: int) -> void { hook_st.gone += 1 } + function toy(name: string) -> Toy { let t = new Toy t.name = name @@ -108,6 +119,41 @@ program EcsTest { expect_eq(imap_get(m, 14, -1), 9) } + function hooks_case(hook_st: HookState) -> void { + let tb: Table = table_new(2, 2) + tb_on_add(tb, fn seen_add) + tb_on_remove(tb, fn seen_gone) + let a = put(tb, "a", 0.0, 0.0, 1) + expect_eq(hook_st.last, a) + put(tb, "b", 0.0, 0.0, 1) + tb_remove(tb, a) + tb_remove(tb, a) + expect_eq(hook_st.added, 2) + expect_eq(hook_st.gone, 1) + } + + # 300 rows over five blocks; a change in one block is found, the untouched blocks skipped + function blocks_case() -> void { + let tb: Table = table_new(2, 2) + for i in 0 .. 300 { put(tb, "x", float(i), 0.0, 1) } + let seen = tb_tick(tb) + tb_advance(tb) + let out = words(0) + expect_eq(tb_changed_since(tb, seen, out), 0) + tb_set_f(tb, CX, 200, 9.0) + tb_set_f(tb, CX, 7, 9.0) + expect_eq(tb_changed_since(tb, seen, out), 2) + expect_eq(out[0], 7) + expect_eq(out[1], 200) + let before = tb_tick(tb) + tb_advance(tb) + tb_remove(tb, tb_handle(tb, 3)) + expect_eq(tb_changed_since(tb, before, out), 0) # a row moved into a gap was not written + expect_eq(tb_changed_since(tb, seen, out), 2) # a removal is told by tb_on_remove + } + + 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() } 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() }