From df2f7f0f1b2d70c111e6ca5d973ee0d47fe959c3 Mon Sep 17 00:00:00 2001 From: jacob Date: Thu, 13 Nov 2025 18:27:49 -0600 Subject: [PATCH] build ent tree every frame --- src/proto/pp_sim/pp_sim_core.c | 288 +++++++++++++++++++++++---------- src/proto/pp_sim/pp_sim_core.h | 87 ++++++---- src/proto/pp_vis/pp_vis_core.c | 94 +++++++++-- src/proto/pp_vis/pp_vis_core.h | 4 +- 4 files changed, 349 insertions(+), 124 deletions(-) diff --git a/src/proto/pp_sim/pp_sim_core.c b/src/proto/pp_sim/pp_sim_core.c index 4e33eef0..ece4cea8 100644 --- a/src/proto/pp_sim/pp_sim_core.c +++ b/src/proto/pp_sim/pp_sim_core.c @@ -57,6 +57,17 @@ b32 S_MatchKey(S_Key a, S_Key b) return a.v.hi == b.v.hi && a.v.lo == b.v.lo; } +//////////////////////////////////////////////////////////// +//~ Key helpers + +S_Key S_RandKey(void) +{ + /* TODO: Don't use true randomness for entity keys. It's overkill & non-deterministic. */ + S_Key result = ZI; + TrueRand(StringFromStruct(&result)); + return result; +} + //////////////////////////////////////////////////////////// //~ Shape helpers @@ -93,31 +104,57 @@ Vec2 S_SupportPointFromShape(S_Shape shape, Vec2 dir) //////////////////////////////////////////////////////////// //~ Lookup helpers -S_Ent *S_EntFromKey(S_World *world, S_Key key) +S_Lookup S_LookupFromWorld(Arena *arena, S_World *world) { - S_Ent *ent = &S_nil_ent; + S_Lookup lookup = ZI; + + lookup.bins_count = 4096; + lookup.bins = PushStructs(arena, S_LookupEntNode *, lookup.bins_count); + for (i64 ent_idx = 0; ent_idx < world->ents_count; ++ent_idx) + { + S_Ent *ent = &world->ents[ent_idx]; + if (ent->active) + { + S_Key key = ent->key; + S_LookupEntNode *n = PushStruct(arena, S_LookupEntNode); + n->ent = ent; + S_LookupEntNode **bin = &lookup.bins[ent->key.v.lo % lookup.bins_count]; + SllStackPush(*bin, n); + } + } + + return lookup; +} + +S_Ent *S_EntFromKey(S_Lookup *lookup, S_Key key) +{ + S_Ent *result = &S_nil_ent; if (!S_IsKeyNil(key)) { - S_EntLookupNode *n = world->ent_bins[key.v.lo % world->ent_bins_count]; - for (; n; n = n->next) + i64 bins_count = lookup->bins_count; + if (bins_count > 0) { - if (S_MatchKey(n->ent->key, key)) + S_LookupEntNode *n = lookup->bins[key.v.lo % bins_count]; + for (; n; n = n->next) { - ent = n->ent; - break; + if (S_MatchKey(n->ent->key, key)) + { + result = n->ent; + break; + } } } } - return ent; + return result; } //////////////////////////////////////////////////////////// //~ Iteration helpers -void S_ResetIter(Arena *arena, S_Iter *iter, S_World *world, S_Key key, S_IterKind kind) +void S_ResetIter(Arena *arena, S_Iter *iter, S_Lookup *lookup, S_Key key, S_IterKind kind) { iter->kind = kind; - iter->world = world; + iter->lookup = lookup; for (S_IterDfsNode *n = iter->first_dfs; n; n = n->next) { n->next = iter->first_free_dfs; @@ -143,7 +180,7 @@ void S_ResetIter(Arena *arena, S_Iter *iter, S_World *world, S_Key key, S_IterKi S_Ent *S_NextEnt(Arena *arena, S_Iter *iter) { S_Ent *result = &S_nil_ent; - S_World *world = iter->world; + S_Lookup *lookup = iter->lookup; b32 is_post_order = iter->kind == S_IterKind_Post; b32 stop = 0; @@ -152,11 +189,11 @@ S_Ent *S_NextEnt(Arena *arena, S_Iter *iter) if (iter->first_dfs) { S_IterDfsNode *n = iter->first_dfs; - S_Ent *ent = S_EntFromKey(iter->world, n->ent_key); + S_Ent *ent = S_EntFromKey(iter->lookup, n->ent_key); if (!n->visited) { /* Push children to dfs stack */ - for (S_Ent *child = S_EntFromKey(world, ent->last); !S_IsEntNil(child); child = S_EntFromKey(world, ent->prev)) + for (S_Ent *child = S_EntFromKey(lookup, ent->last); !S_IsEntNil(child); child = S_EntFromKey(lookup, child->prev)) { S_IterDfsNode *child_n = iter->first_free_dfs; if (child_n) @@ -202,32 +239,38 @@ S_Ent *S_NextEnt(Arena *arena, S_Iter *iter) S_World *S_WorldFromSnapshot(Arena *arena, S_Snapshot *snapshot) { - TempArena scratch = BeginScratch(arena); S_World *world = PushStruct(arena, S_World); /* Copy ents */ world->ents = PushStructsNoZero(arena, S_Ent, snapshot->ents_count); CopyStructs(world->ents, snapshot->ents, snapshot->ents_count); - world->allocated_ents_count = snapshot->ents_count; + world->ents_count = snapshot->ents_count; world->tick = snapshot->tick; - /* Init lookup */ - world->ent_bins_count = 4096; - world->ent_bins = PushStructs(arena, S_EntLookupNode *, world->ent_bins_count); - for (i64 ent_idx = 0; ent_idx < world->allocated_ents_count; ++ent_idx) - { - S_Ent *ent = &world->ents[ent_idx]; - S_Key key = ent->key; - S_EntLookupNode *n = PushStruct(arena, S_EntLookupNode); - n->ent = ent; - S_EntLookupNode **bin = &world->ent_bins[ent->key.v.lo % world->ent_bins_count]; - SllStackPush(*bin, n); - } - - EndScratch(scratch); return world; } +//////////////////////////////////////////////////////////// +//~ Sorting + +MergesortCompareFuncDef(S_SortEntsByKeyCmp, arg_a, arg_b, _) +{ + S_Ent *a = *(S_Ent **)arg_a; + S_Ent *b = *(S_Ent **)arg_b; + S_Key a_key = a->key; + S_Key b_key = b->key; + i32 result = 0; + if (result == 0) + { + result = ((a_key.v.lo < b_key.v.lo) - (a_key.v.lo > b_key.v.lo)); + } + if (result == 0) + { + result = ((a_key.v.hi < b_key.v.hi) - (a_key.v.hi > b_key.v.hi)); + } + return result; +} + //////////////////////////////////////////////////////////// //~ Sim worker @@ -238,50 +281,10 @@ JobDef(S_SimWorker, _, __) Arena *perm = PermArena(); //- World data - Arena *world_arena = AcquireArena(Gibi(64)); - S_World *world = 0; - - { - S_Snapshot *empty_ss = PushStruct(frame_arena, S_Snapshot); - { - empty_ss->ents = PushStructs(frame_arena, S_Ent, 1024); - /* Create root ent */ - S_Ent *root_ent = &empty_ss->ents[empty_ss->ents_count++]; - { - *root_ent = S_nil_ent; - root_ent->key = S_RootKey; - } - /* Create test ent */ - S_Ent *test_ent = &empty_ss->ents[empty_ss->ents_count++]; - { - *test_ent = S_nil_ent; - // test_ent->shape.points_count = 1; - // test_ent->shape.radius = 0.25; - test_ent->key = ((S_Key) { .v.hi = 0x66444f20b7e41f3d, .v.lo = 0x5a2df684b9430943 }); - - { - S_Shape *shape = &test_ent->local_shape; - shape->points_count = 4; - shape->points[0] = VEC2(100, 100); - shape->points[1] = VEC2(200, 100); - shape->points[2] = VEC2(150, 200); - shape->points[3] = VEC2(125, 200); - // shape->radius = 1; - } - - - - - - - test_ent->parent = root_ent->key; - root_ent->first = test_ent->key; - root_ent->last = test_ent->key; - ++root_ent->count; - } - } - world = S_WorldFromSnapshot(world_arena, empty_ss); - } + Arena *ents_arena = AcquireArena(Gibi(64)); + S_World *world = PushStruct(perm, S_World); + world->ents = ArenaFirst(ents_arena, S_Ent); + i64 first_free_ent_num = 0; ////////////////////////////// //- Sim loop @@ -290,13 +293,27 @@ JobDef(S_SimWorker, _, __) while (!shutdown) { ResetArena(frame_arena); - S_Iter ent_iter = ZI; + S_Iter iter = ZI; + S_Lookup lookup = ZI; ////////////////////////////// //- Begin sim frame i64 frame_begin_ns = TimeNs(); + ////////////////////////////// + //- Create root ent + + if (world->tick == 0) + { + S_Ent *root_ent = PushStruct(ents_arena, S_Ent); + *root_ent = S_nil_ent; + root_ent->key = S_RootKey; + root_ent->active = 1; + ++world->ents_count; + } + lookup = S_LookupFromWorld(frame_arena, world); + ////////////////////////////// //- Pop sim commands @@ -313,25 +330,132 @@ JobDef(S_SimWorker, _, __) UnlockTicketMutex(&shared->input_back_tm); ////////////////////////////// - //- Process sim commands + //- Spawn entities for (S_CmdNode *cmd_node = input->first_cmd_node; cmd_node; cmd_node = cmd_node->next) { S_Cmd cmd = cmd_node->cmd; - switch (cmd.kind) + if (cmd.kind == S_CmdKind_Spawn) { - case S_CmdKind_Ent: + for (S_EntListNode *src_n = cmd.ents.first; src_n; src_n = src_n->next) { - LogInfoF("Ent cmd"); - } break; + S_Ent *src = &src_n->ent; + if (S_IsKeyNil(src->parent)) + { + src->parent = S_RootKey; + } + S_Key key = src->key; + if (!S_MatchKey(key, S_RootKey) && !S_IsKeyNil(key)) + { + S_Ent *dst = S_EntFromKey(&lookup, key); + if (S_IsEntNil(dst)) + { + if (first_free_ent_num > 0) + { + dst = &world->ents[first_free_ent_num - 1]; + first_free_ent_num = dst->next_free_ent_num; + } + else + { + dst = PushStructNoZero(ents_arena, S_Ent); + } + *dst = S_nil_ent; + dst->key = key; + ++world->ents_count; + } + *dst = *src; + dst->active = 1; + } + + LogInfoF("RAAAH %F", FmtSint(world->ents_count)); + } } } + lookup = S_LookupFromWorld(frame_arena, world); + + ////////////////////////////// + //- Rebuild entity tree + + { + /* Reset tree links */ + for (i64 ent_idx = 0; ent_idx < world->ents_count; ++ent_idx) + { + S_Ent *ent = &world->ents[ent_idx]; + if (ent->active) + { + ent->first = S_NilKey; + ent->last = S_NilKey; + ent->next = S_NilKey; + ent->prev = S_NilKey; + ent->count = 0; + } + } + + { + /* Sort ents by key before tree-build (for deterministic child order in parents) */ + S_Ent **sorted_ents = 0; + { + sorted_ents = PushStructsNoZero(frame_arena, S_Ent *, world->ents_count); + for (i64 ent_idx = 0; ent_idx < world->ents_count; ++ent_idx) + { + sorted_ents[ent_idx] = &world->ents[ent_idx]; + } + Mergesort(sorted_ents, world->ents_count, sizeof(*sorted_ents), S_SortEntsByKeyCmp, 0); + } + + /* Build tree */ + for (i64 ent_idx = 0; ent_idx < world->ents_count; ++ent_idx) + { + S_Ent *ent = sorted_ents[ent_idx]; + if (ent->active) + { + S_Key key = ent->key; + S_Ent *parent = S_EntFromKey(&lookup, ent->parent); + if (parent->active) + { + S_Ent *prev = S_EntFromKey(&lookup, parent->last); + if (S_IsEntNil(prev)) + { + parent->first = key; + } + else + { + prev->next = key; + } + ent->prev = prev->key; + parent->last = key; + ++parent->count; + } + } + } + } + + /* Prune dangling ents */ + /* NOTE: If only the top level of a multi-level ent tree is + * dangling, the children may be marked inactive one level per tick + * since iteration is linear over the array. */ + for (i64 ent_idx = 0; ent_idx < world->ents_count; ++ent_idx) + { + S_Ent *ent = &world->ents[ent_idx]; + if (ent->active && !S_MatchKey(ent->key, S_RootKey)) + { + S_Ent *parent = S_EntFromKey(&lookup, ent->parent); + if (!parent->active) + { + ent->active = 0; + ent->next_free_ent_num = first_free_ent_num; + first_free_ent_num = ent_idx + 1; + } + } + } + } + lookup = S_LookupFromWorld(frame_arena, world); ////////////////////////////// //- Update entities from user control - S_ResetIter(frame_arena, &ent_iter, world, S_RootKey, S_IterKind_Post); - for (S_Ent *ent = S_NextEnt(frame_arena, &ent_iter); !S_IsEntNil(ent); ent = S_NextEnt(frame_arena, &ent_iter)) + S_ResetIter(frame_arena, &iter, &lookup, S_RootKey, S_IterKind_Pre); + for (S_Ent *ent = S_NextEnt(frame_arena, &iter); !S_IsEntNil(ent); ent = S_NextEnt(frame_arena, &iter)) { } @@ -345,7 +469,7 @@ JobDef(S_SimWorker, _, __) S_Snapshot *snapshot = &snapshot_node->snapshot; SllQueuePush(output->first_snapshot_node, output->last_snapshot_node, snapshot_node); ++output->snapshots_count; - snapshot->ents_count = world->allocated_ents_count; + snapshot->ents_count = world->ents_count; snapshot->tick = world->tick; snapshot->ents = PushStructsNoZero(output->arena, S_Ent, snapshot->ents_count); for (i64 ent_idx = 0; ent_idx < snapshot->ents_count; ++ent_idx) diff --git a/src/proto/pp_sim/pp_sim_core.h b/src/proto/pp_sim/pp_sim_core.h index ab3485fd..b4779896 100644 --- a/src/proto/pp_sim/pp_sim_core.h +++ b/src/proto/pp_sim/pp_sim_core.h @@ -22,40 +22,55 @@ Struct(S_Shape) //////////////////////////////////////////////////////////// //~ Ent types -////////////////////////////// -//- Ent roperties - -Enum(S_EntProp) -{ - S_EntProp_None, -}; - -////////////////////////////// -//- Ent +/* TODO: Move boolean fields into bitwise property flags */ Struct(S_Ent) { - //- Tree data + ////////////////////////////// + //- Tree links + S_Key parent; S_Key first; S_Key last; S_Key next; S_Key prev; - i64 count; + u64 count; + ////////////////////////////// //- Persistent data + + b32 active; S_Key key; - //- Build data + ////////////////////////////// + //- Pre-solve data + + S_Key follow; + S_Key camera; + S_Shape local_shape; Xform local_to_parent_xf; - //- Final data + ////////////////////////////// + //- Post-solve data + Xform final_local_to_world_xf; + + ////////////////////////////// + //- Internal sim data + + i64 next_free_ent_num; + } extern Readonly S_nil_ent; -////////////////////////////// -//- Ent containers +//////////////////////////////////////////////////////////// +//~ Ent container types + +Struct(S_EntArray) +{ + u64 count; + S_Ent *ents; +}; Struct(S_EntListNode) { @@ -70,24 +85,29 @@ Struct(S_EntList) u64 count; }; -Struct(S_EntLookupNode) +//////////////////////////////////////////////////////////// +//~ Lookup types + +Struct(S_LookupEntNode) { - S_EntLookupNode *next; + S_LookupEntNode *next; S_Ent *ent; }; +Struct(S_Lookup) +{ + S_LookupEntNode **bins; + i64 bins_count; +}; + //////////////////////////////////////////////////////////// //~ World types Struct(S_World) { i64 tick; - S_Ent *ents; - i64 allocated_ents_count; - - S_EntLookupNode **ent_bins; - i64 ent_bins_count; + i64 ents_count; }; Struct(S_Snapshot) @@ -122,7 +142,7 @@ Struct(S_IterDfsNode) Struct(S_Iter) { S_IterKind kind; - S_World *world; + S_Lookup *lookup; S_IterDfsNode *first_dfs; S_IterDfsNode *first_free_dfs; }; @@ -133,13 +153,13 @@ Struct(S_Iter) Enum(S_CmdKind) { S_CmdKind_Nop, - S_CmdKind_Ent, + S_CmdKind_Spawn, }; Struct(S_Cmd) { S_CmdKind kind; - S_Ent ent; + S_EntList ents; }; Struct(S_CmdNode) @@ -201,6 +221,11 @@ b32 S_IsKeyNil(S_Key key); b32 S_IsEntNil(S_Ent *ent); b32 S_MatchKey(S_Key a, S_Key b); +//////////////////////////////////////////////////////////// +//~ Key helpers + +S_Key S_RandKey(void); + //////////////////////////////////////////////////////////// //~ Shape helpers @@ -210,12 +235,13 @@ Vec2 S_SupportPointFromShape(S_Shape shape, Vec2 dir); //////////////////////////////////////////////////////////// //~ Lookup helpers -S_Ent *S_EntFromKey(S_World *world, S_Key key); +S_Lookup S_LookupFromWorld(Arena *arena, S_World *world); +S_Ent *S_EntFromKey(S_Lookup *lookup, S_Key key); //////////////////////////////////////////////////////////// //~ Iteration helpers -void S_ResetIter(Arena *arena, S_Iter *iter, S_World *world, S_Key key, S_IterKind kind); +void S_ResetIter(Arena *arena, S_Iter *iter, S_Lookup *lookup, S_Key key, S_IterKind kind); S_Ent *S_NextEnt(Arena *arena, S_Iter *iter); //////////////////////////////////////////////////////////// @@ -223,6 +249,11 @@ S_Ent *S_NextEnt(Arena *arena, S_Iter *iter); S_World *S_WorldFromSnapshot(Arena *arena, S_Snapshot *snapshot); +//////////////////////////////////////////////////////////// +//~ Sorting + +MergesortCompareFuncDef(S_SortEntsByKeyCmp, arg_a, arg_b, _); + //////////////////////////////////////////////////////////// //~ Sim worker diff --git a/src/proto/pp_vis/pp_vis_core.c b/src/proto/pp_vis/pp_vis_core.c index 4383ac3e..a7614fb3 100644 --- a/src/proto/pp_vis/pp_vis_core.c +++ b/src/proto/pp_vis/pp_vis_core.c @@ -22,6 +22,55 @@ void V_Shutdown(void) YieldOnFence(&shared->worker_completion_fence, shared->workers_count); } +//////////////////////////////////////////////////////////// +//~ Test ents + +void V_PushTestEnts(Arena *arena, S_EntList *list) +{ + S_Key player_key = S_RandKey(); + S_Key camera_key = S_RandKey(); + + i32 count = 2; + for (u64 i = 0; i < count; ++i) + { + S_EntListNode *n = PushStruct(arena, S_EntListNode); + SllQueuePush(list->first, list->last, n); + ++list->count; + S_Ent *ent = &n->ent; + *ent = S_nil_ent; + switch (i) + { + /* Test player */ + case 0: + { + ent->key = player_key; + ent->camera = camera_key; + + { + S_Shape *shape = &ent->local_shape; + shape->points_count = 4; + shape->points[0] = VEC2(100, 100); + shape->points[1] = VEC2(200, 100); + shape->points[2] = VEC2(150, 200); + shape->points[3] = VEC2(125, 200); + // shape->radius = 1; + } + } break; + + /* Test camera */ + case 1: + { + ent->key = camera_key; + } break; + + default: + { + Assert(0); + } break; + } + } +} + //////////////////////////////////////////////////////////// //~ Vis worker @@ -95,13 +144,11 @@ JobDef(V_VisWorker, _, __) String swap_encoded = SwappedStateFromName(scratch.arena, Lit("pp_vis")); BB_Buff bb = BB_BuffFromString(swap_encoded); BB_Reader br = BB_ReaderFromBuff(&bb); - String swap_str = BB_ReadString(scratch.arena, &br); if (swap_str.len == sizeof(Persist)) { CopyBytes(&persist, swap_str.text, swap_str.len); } - window_restore = BB_ReadString(perm, &br); } EndScratch(scratch); @@ -112,12 +159,20 @@ JobDef(V_VisWorker, _, __) Arena *world_arena = AcquireArena(Gibi(64)); S_World *world = PushStruct(world_arena, S_World); + S_Lookup lookup = ZI; b32 shutdown = 0; while (!shutdown) { ResetArena(frame_arena); - S_Iter ent_iter = ZI; + S_Iter iter = ZI; + + S_EntList spawn_ents = ZI; + + if (world->tick == 0) + { + V_PushTestEnts(frame_arena, &spawn_ents); + } ////////////////////////////// //- Begin vis frame @@ -175,6 +230,7 @@ JobDef(V_VisWorker, _, __) { ResetArena(world_arena); world = S_WorldFromSnapshot(world_arena, &sim_output->last_snapshot_node->snapshot); + lookup = S_LookupFromWorld(world_arena, world); } ////////////////////////////// @@ -260,8 +316,6 @@ JobDef(V_VisWorker, _, __) ////////////////////////////// //- Process vis commands - S_EntList spawn_ents = ZI; - for (V_CmdNode *cmd_node = first_cmd_node; cmd_node; cmd_node = cmd_node->next) { String cmd_name = cmd_node->cmd.name; @@ -332,13 +386,29 @@ JobDef(V_VisWorker, _, __) LockTicketMutex(&sim_shared->input_back_tm); { S_InputState *v2s = &sim_shared->input_states[sim_shared->input_back_idx]; - for (S_EntListNode *ent_node = spawn_ents.first; ent_node; ent_node = ent_node->next) + + /* Submit spawn cmds */ + if (spawn_ents.count > 0) { S_CmdNode *cmd_node = PushStruct(v2s->arena, S_CmdNode); - cmd_node->cmd.kind = S_CmdKind_Ent; - cmd_node->cmd.ent = ent_node->ent; - SllQueuePush(v2s->first_cmd_node, v2s->last_cmd_node, cmd_node); - ++v2s->cmds_count; + { + SllQueuePush(v2s->first_cmd_node, v2s->last_cmd_node, cmd_node); + ++v2s->cmds_count; + } + S_Cmd *cmd = &cmd_node->cmd; + { + cmd->kind = S_CmdKind_Spawn; + } + S_EntList *dst = &cmd->ents; + for (S_EntListNode *src_n = spawn_ents.first; src_n; src_n = src_n->next) + { + S_EntListNode *dst_n = PushStruct(v2s->arena, S_EntListNode); + { + SllQueuePush(dst->first, dst->last, dst_n); + ++dst->count; + } + dst_n->ent = src_n->ent; + } } } UnlockTicketMutex(&sim_shared->input_back_tm); @@ -347,8 +417,8 @@ JobDef(V_VisWorker, _, __) //- Build render data /* Build shapes */ - S_ResetIter(frame_arena, &ent_iter, world, S_RootKey, S_IterKind_Pre); - for (S_Ent *ent = S_NextEnt(frame_arena, &ent_iter); !S_IsEntNil(ent); ent = S_NextEnt(frame_arena, &ent_iter)) + S_ResetIter(frame_arena, &iter, &lookup, S_RootKey, S_IterKind_Pre); + for (S_Ent *ent = S_NextEnt(frame_arena, &iter); !S_IsEntNil(ent); ent = S_NextEnt(frame_arena, &iter)) { Xform xf = ent->final_local_to_world_xf; S_Shape shape = S_MulXformShape(ent->final_local_to_world_xf, ent->local_shape); diff --git a/src/proto/pp_vis/pp_vis_core.h b/src/proto/pp_vis/pp_vis_core.h index b8a3ca9a..04e8580b 100644 --- a/src/proto/pp_vis/pp_vis_core.h +++ b/src/proto/pp_vis/pp_vis_core.h @@ -88,9 +88,9 @@ void V_Startup(void); void V_Shutdown(void); //////////////////////////////////////////////////////////// -//~ Hotkey helpers +//~ Test ents -String V_StringFromHotkey(Arena *arena, V_Hotkey hotkey); +void V_PushTestEnts(Arena *arena, S_EntList *list); //////////////////////////////////////////////////////////// //~ Vis worker