# emit_ecs.ludic โ€” ECS storage and the entity allocator. For each component: # a %Cmp_ layout, a dense @S_ store, and an @H_ "has this component" array. # Entities are integer handles; L_alloc reuses freed slots. Mirrors the ECS # parts of compiler/back/ir_decl.c. const MAX_ENT: int = 1024 fn has_systems() -> bool { var i = 0 while i < len(prog) { if prog[i].kind == N_SYS { return true }; i = i + 1 } return false } fn has_models() -> bool { var i = 0 while i < len(prog) { if prog[i].kind == N_ARCH { return true }; i = i + 1 } return false } # N5: does the program have an `entry` block? A game with both handlers and an # `entry` drives its own loop (calling tick_fixed/tick_render), instead of the # compiler's auto frame loop. A game with handlers and no entry uses the auto-loop. fn has_entry() -> bool { var i = 0 while i < len(prog) { if prog[i].kind == N_MAIN { return true }; i = i + 1 } return false } # Does this program run the ECS? A property alone no longer answers that โ€” the # same `property` keyword also declares plain `new`-allocated records (the merged # `struct`). A program uses the ECS when it has a handler or a model; a tool that # only declares record types and functions does not, and gets no entity storage, # allocator, snapshot or runtime splice. fn has_ecs() -> bool { return has_systems() or has_models() } fn emit_ecs_storage() -> void { emith("@L_running = internal global i32 1\n") emith("@L_key = internal global i32 0\n") if len(g_scenes) > 0 { emith("@L_scene = internal global i32 0\n") } # active base scene id emith("@L_entc = internal global i32 0\n") let me = itoa(MAX_ENT) emith(`@L_alive = internal global [{me} x i32] zeroinitializer\n`) emith(`@L_kind = internal global [{me} x i32] zeroinitializer\n`) emith(`@L_freelist = internal global [{me} x i32] zeroinitializer\n`) emith("@L_freen = internal global i32 0\n") # NETWORKING role registers (N3/N5): a runtime sets these; offline they hold the # single-player default โ€” @L_role=1 (this peer is the authority), local id 0. # Emitted only when a networking feature is used, so non-networked builds are # byte-identical (ยง8). @L_owner_arr is the per-entity network owner (N3, @Owned). if net_any() { emith("@L_role = internal global i32 1\n") # 1 = server/authority (offline default) emith("@L_localid = internal global i32 0\n") # this peer's id } if net_has_owned() { emith(`@L_owner_arr = internal global [{me} x i32] zeroinitializer\n`) } var i = 0 while i < len(prog) { let c = prog[i] # per-entity storage for a property (its %Cmp_ layout is emitted in the # header). Every property in an ECS program is a component today; a property # used only via `new` would not need these, but no such program mixes the two. if c.kind == N_COMP { emith(`@S_{c.s} = internal global [{me} x %Cmp_{c.s}] zeroinitializer\n`) emith(`@H_{c.s} = internal global [{me} x i8] zeroinitializer\n`) } # one enabled-flag global per model and per handler (default enabled) if c.kind == N_ARCH { emith(`@ME_{c.s} = internal global i32 1\n`) } if c.kind == N_SYS { emith(`@HE_{c.s} = internal global i32 1\n`) } i = i + 1 } # one enabled-flag global per toggled layer (default shown) var li = 0 while li < len(g_toggled_layers) { emith(`@LE_{g_toggled_layers[li]} = internal global i32 1\n`); li = li + 1 } } # L_reset(e): clear every has-flag and the archetype kind for entity e fn emit_ecs_allocator() -> void { let me = itoa(MAX_ENT) emit("define void @L_reset(i32 %e) {\nentry:\n") var i = 0 while i < len(prog) { if prog[i].kind == N_COMP { let hn = `%h{itoa(i)}` emit(" "); emit(hn); emit(" = getelementptr inbounds ["); emit(me); emit(" x i8], ptr @H_"); emit(prog[i].s); emit(", i32 0, i32 %e\n") emit(" store i8 0, ptr "); emit(hn); emit("\n") } i = i + 1 } emit(" %k = getelementptr inbounds ["); emit(me); emit(" x i32], ptr @L_kind, i32 0, i32 %e\n") emit(" store i32 0, ptr %k\n") # N3: reset an @Owned entity's network owner to -1 (unowned) on alloc/free if net_has_owned() { emit(" %ow = getelementptr inbounds ["); emit(me); emit(" x i32], ptr @L_owner_arr, i32 0, i32 %e\n") emit(" store i32 -1, ptr %ow\n") } emit(" ret void\n}\n\n") emit("define i32 @L_alloc() {\nentry:\n") emit(" %fn = load i32, ptr @L_freen\n") emit(" %has = icmp sgt i32 %fn, 0\n") emit(" br i1 %has, label %reuse, label %fresh\n") emit("reuse:\n") emit(" %fn1 = sub i32 %fn, 1\n") emit(" store i32 %fn1, ptr @L_freen\n") emit(" %fp = getelementptr inbounds ["); emit(me); emit(" x i32], ptr @L_freelist, i32 0, i32 %fn1\n") emit(" %re = load i32, ptr %fp\n") emit(" br label %done\n") emit("fresh:\n") emit(" %ec = load i32, ptr @L_entc\n") emit(" %ec1 = add i32 %ec, 1\n") emit(" store i32 %ec1, ptr @L_entc\n") emit(" br label %done\n") emit("done:\n") emit(" %e = phi i32 [ %re, %reuse ], [ %ec, %fresh ]\n") emit(" call void @L_reset(i32 %e)\n") emit(" %ap = getelementptr inbounds ["); emit(me); emit(" x i32], ptr @L_alive, i32 0, i32 %e\n") emit(" store i32 1, ptr %ap\n") emit(" ret i32 %e\n}\n\n") emit("define void @L_free_entity(i32 %e) {\nentry:\n") emit(" %ap = getelementptr inbounds ["); emit(me); emit(" x i32], ptr @L_alive, i32 0, i32 %e\n") emit(" store i32 0, ptr %ap\n") emit(" call void @L_reset(i32 %e)\n") emit(" %fn = load i32, ptr @L_freen\n") emit(" %fp = getelementptr inbounds ["); emit(me); emit(" x i32], ptr @L_freelist, i32 0, i32 %fn\n") emit(" store i32 %e, ptr %fp\n") emit(" %fn1 = add i32 %fn, 1\n") emit(" store i32 %fn1, ptr @L_freen\n") emit(" ret void\n}\n\n") }