code wiki / _hdl_build / nx_entity_store.nx

nx_entity_store.nx source

↩ module page · 151 lines · 6453 B

1// nx_entity_store.nx -- the SOVEREIGN ENTITY STORE. nx_gamebench gap-queue rank 1 after save/load and rpg-stats 2// landed: entity-component-sim was PARTIAL and blocks 7 of 12 benchmarked titles (Doom actors, OpenXcom 3// units, Diablo monsters, Cataclysm, Veloren, Freeciv, NetHack). 4// 5// THE HAZARD THIS PART EXISTS TO KILL -- the stale handle. Every naive entity store hands out a raw slot 6// index. An entity dies, its slot is recycled for a new entity, and any handle still held by old code now 7// silently addresses a DIFFERENT entity: a homing missile retargets onto a random unit, a dead unit's buff 8// lands on its replacement. It is one of the nastiest bug classes in game code because it is invisible 9// until it is a bug report. Here every handle carries a GENERATION stamped into it, bumped on destroy, so a 10// stale handle is DETECTABLY invalid -- rejection is by construction, not by discipline. 11// 12// Other certified properties (each a gate tooth): 13// - O(1) spawn and destroy (free-list + swap-remove), no scanning 14// - DENSE iteration: visiting the alive set never walks holes 15// - REPLAY-DETERMINISTIC: the same operation sequence yields the same handles and the same iteration 16// order, so recorded input replays identically (the determinism exceed carried into simulation) 17// - CAPACITY-BOUNDED: spawning past capacity returns the null handle, never corrupts the arena 18// - Component values are plain i64 so entity state drops straight into gs_save/gs_load (nx_gamesave). 19// 20// Arena layout follows the nx_swgpu base+offset idiom: one mmap block, all offsets derived from cap/ncomp. 21// LIB ONLY -- no main() by ecosystem convention. 22// license_tier: ORIGINAL expect_exit: 0 23import "nx_syscalls.nx" 24 25const EN_GENMOD: i64 = 1048576 // 2^20 generations per slot before wrap 26const EN_NULL: i64 = 0 // the null handle; a real handle is always > 0 27const EN_HDR: i64 = 4 // cap, ncomp, alive_count, fresh_next 28 29// ---- header accessors ---- 30func en_cap(a: *i64) -> i64 { return a[0] } 31func en_ncomp(a: *i64) -> i64 { return a[1] } 32func en_count(a: *i64) -> i64 { return a[2] } 33 34// ---- array bases (in i64 words) ---- 35func en_o_gen(a: *i64) -> i64 { return EN_HDR } 36func en_o_alive(a: *i64) -> i64 { return EN_HDR + en_cap(a) } 37func en_o_free(a: *i64) -> i64 { return EN_HDR + 2*en_cap(a) } 38func en_o_dense(a: *i64) -> i64 { return EN_HDR + 3*en_cap(a) } 39func en_o_pos(a: *i64) -> i64 { return EN_HDR + 4*en_cap(a) } 40func en_o_comp(a: *i64) -> i64 { return EN_HDR + 5*en_cap(a) } 41 42// total words an arena needs 43func en_words(cap: i64, ncomp: i64) -> i64 { return EN_HDR + 5*cap + ncomp*cap + 8 } 44func en_bytes(cap: i64, ncomp: i64) -> i64 { return en_words(cap, ncomp) * 8 } 45 46// free list is a stack; free_top lives at a[3+...]? keep it in the header slot 3 alongside fresh_next 47// slot 3 packs: fresh_next (how many slots ever handed out) ; the free stack length is derived 48func en_init(a: *i64, cap: i64, ncomp: i64) -> i64 { 49 a[0] = cap 50 a[1] = ncomp 51 a[2] = 0 // alive_count 52 a[3] = 0 // fresh_next 53 var i: i64 = 0 54 while i < cap { 55 a[en_o_gen(a) + i] = 1 // generations start at 1 so a zeroed handle is never valid 56 a[en_o_alive(a) + i] = 0 57 a[en_o_dense(a) + i] = 0 58 a[en_o_pos(a) + i] = 0-1 59 i = i + 1 60 } 61 a[en_o_free(a)] = 0 // free stack length stored at index 0 of the free region 62 return 0 63} 64 65func en_freelen(a: *i64) -> i64 { return a[en_o_free(a)] } 66 67func en_make_handle(idx: i64, gen: i64) -> i64 { return (idx + 1) * EN_GENMOD + gen } 68func en_handle_idx(h: i64) -> i64 { return (h / EN_GENMOD) - 1 } 69func en_handle_gen(h: i64) -> i64 { return h % EN_GENMOD } 70 71// ---- validity: the stale-handle killer ---- 72func en_valid(a: *i64, h: i64) -> i64 { 73 if h <= EN_NULL { return 0 } 74 let idx: i64 = en_handle_idx(h) 75 if idx < 0 { return 0 } 76 if idx >= en_cap(a) { return 0 } 77 if a[en_o_alive(a) + idx] != 1 { return 0 } 78 if a[en_o_gen(a) + idx] != en_handle_gen(h) { return 0 } 79 return 1 80} 81 82// ---- spawn: reuse a freed slot if any, else take a fresh one ---- 83func en_spawn(a: *i64) -> i64 { 84 var idx: i64 = 0-1 85 let fl: i64 = en_freelen(a) 86 if fl > 0 { 87 idx = a[en_o_free(a) + fl] // stack grows at +1..+fl 88 a[en_o_free(a)] = fl - 1 89 } else { 90 let fresh: i64 = a[3] 91 if fresh >= en_cap(a) { return EN_NULL } 92 idx = fresh 93 a[3] = fresh + 1 94 } 95 a[en_o_alive(a) + idx] = 1 96 // clear components so a recycled slot never leaks the previous entity's data 97 var c: i64 = 0 98 while c < en_ncomp(a) { a[en_o_comp(a) + c*en_cap(a) + idx] = 0; c = c + 1 } 99 // append to the dense list 100 let n: i64 = a[2] 101 a[en_o_dense(a) + n] = idx 102 a[en_o_pos(a) + idx] = n 103 a[2] = n + 1 104 return en_make_handle(idx, a[en_o_gen(a) + idx]) 105} 106 107// ---- destroy: bump generation (invalidating every outstanding handle), swap-remove from dense ---- 108func en_destroy(a: *i64, h: i64) -> i64 { 109 if en_valid(a, h) == 0 { return 0 } 110 let idx: i64 = en_handle_idx(h) 111 a[en_o_alive(a) + idx] = 0 112 var g: i64 = a[en_o_gen(a) + idx] + 1 113 if g >= EN_GENMOD { g = 1 } 114 a[en_o_gen(a) + idx] = g 115 // swap-remove keeps the dense list hole-free 116 let p: i64 = a[en_o_pos(a) + idx] 117 let n: i64 = a[2] 118 let last: i64 = a[en_o_dense(a) + n - 1] 119 a[en_o_dense(a) + p] = last 120 a[en_o_pos(a) + last] = p 121 a[en_o_pos(a) + idx] = 0-1 122 a[2] = n - 1 123 // push the slot onto the free stack 124 let fl: i64 = en_freelen(a) + 1 125 a[en_o_free(a) + fl] = idx 126 a[en_o_free(a)] = fl 127 return 1 128} 129 130// ---- components ---- 131func en_set(a: *i64, h: i64, comp: i64, val: i64) -> i64 { 132 if en_valid(a, h) == 0 { return 0 } 133 if comp < 0 { return 0 } 134 if comp >= en_ncomp(a) { return 0 } 135 a[en_o_comp(a) + comp*en_cap(a) + en_handle_idx(h)] = val 136 return 1 137} 138func en_get(a: *i64, h: i64, comp: i64) -> i64 { 139 if en_valid(a, h) == 0 { return 0 } 140 if comp < 0 { return 0 } 141 if comp >= en_ncomp(a) { return 0 } 142 return a[en_o_comp(a) + comp*en_cap(a) + en_handle_idx(h)] 143} 144 145// ---- deterministic dense iteration: en_nth(k) for k in 0..en_count-1 ---- 146func en_nth(a: *i64, k: i64) -> i64 { 147 if k < 0 { return EN_NULL } 148 if k >= a[2] { return EN_NULL } 149 let idx: i64 = a[en_o_dense(a) + k] 150 return en_make_handle(idx, a[en_o_gen(a) + idx]) 151}