From bb6e6b92360da4052be726623efdbfded0c659b6 Mon Sep 17 00:00:00 2001 From: Dusk-92 Date: Wed, 2 Sep 2026 23:39:02 +0200 Subject: [PATCH] test: add 2.4-B GC safe-sweep companion --- experiments/wp24b-gc/main.zig | 315 ++++++++++++++++++++++++++++++++++ 1 file changed, 315 insertions(+) create mode 100644 experiments/wp24b-gc/main.zig diff --git a/experiments/wp24b-gc/main.zig b/experiments/wp24b-gc/main.zig new file mode 100644 index 0000000..5836e96 --- /dev/null +++ b/experiments/wp24b-gc/main.zig @@ -0,0 +1,315 @@ +//! WeirdPerformance 2.4-B GC companion experiment. +//! Load alongside the validated WeirdPerformance 2.4-A DLL. +//! The only experimental delta is Lua GC rootgc sweeping. + +const std = @import("std"); +const hook = @import("zhook"); +const WINAPI = std.os.windows.WINAPI; + +const ENGINE_INIT_ADDR: usize = 0x46A400; +const PLAYER_LOAD_SCRIPT_FUNCTIONS_ADDR: usize = 0x490250; +const CGGAMEUI_SHUTDOWN_ADDR: usize = 0x490BD0; +const LUA_COLLECT_GARBAGE_ADDR: usize = 0x6F7340; +const BIRTH_MARK_ADDR: usize = 0x6F7B37; + +const GS_ROOTGC: u32 = 0x10; +const GS_ROOTUDATA: u32 = 0x14; +const GS_GCTHRESHOLD: u32 = 0x24; +const GS_TOTALBYTES: u32 = 0x28; +const OBJ_NEXT: u32 = 0; + +const CHUNK_SIZE: u32 = 50_000; +const BATCH_HEADROOM: u32 = 128 * 1024; + +const VoidStdcallFn = fn () callconv(hook.cc.stdcall) void; +const CollectFn = fn (u32) callconv(hook.cc.fastcall) void; +const SweepAllFn = fn (u32, u32) callconv(hook.cc.fastcall) void; +const RemoveObjectsFn = fn (u32, u32, u32) callconv(hook.cc.fastcall) u32; + +const lua_gc_full_collection: *const CollectFn = @ptrFromInt(0x6F73E0); +const lua_gc_shrink_memory: *const CollectFn = @ptrFromInt(0x6F7370); +const luaCallUserDataGC: *const CollectFn = @ptrFromInt(0x6F7080); +const lua_gc_sweep_all_lists: *const SweepAllFn = @ptrFromInt(0x6F72F0); +const lua_gc_remove_objects: *const RemoveObjectsFn = @ptrFromInt(0x6F7210); + +var engine_init_hook: hook.Detour(VoidStdcallFn) = .{}; +var player_load_hook: hook.Detour(VoidStdcallFn) = .{}; +var shutdown_hook: hook.Detour(VoidStdcallFn) = .{}; +var collect_hook: hook.Detour(CollectFn) = .{}; + +var runtime_installed = false; +var experiment_enabled = false; +var restart_pending = true; +var in_gc = false; + +var sweeping = false; +var swept_head: u32 = 0; +var swept_tail: u32 = 0; +var unswept_rest: u32 = 0; +var saved_g: u32 = 0; +var birth_mark_owned = false; + +inline fn readU8(addr: usize) u8 { + return @as(*volatile const u8, @ptrFromInt(addr)).*; +} +inline fn readU32(addr: u32) u32 { + return @as(*volatile const u32, @ptrFromInt(addr)).*; +} +inline fn writeU32(addr: u32, value: u32) void { + @as(*volatile u32, @ptrFromInt(addr)).* = value; +} +inline fn getGlobalState(L: u32) u32 { + return readU32(L + 0x10); +} + +fn findNth(head: u32, n: u32) struct { obj: u32, count: u32 } { + var obj = head; + var i: u32 = 0; + while (obj != 0 and i < n) : (i += 1) { + const next = readU32(obj + OBJ_NEXT); + if (next == 0) return .{ .obj = 0, .count = i + 1 }; + obj = next; + } + return .{ .obj = obj, .count = i }; +} + +fn findTail(head: u32) u32 { + var obj = head; + if (obj == 0) return 0; + while (true) { + const next = readU32(obj + OBJ_NEXT); + if (next == 0) return obj; + obj = next; + } +} + +fn acquireBirthMark() bool { + if (birth_mark_owned) return readU8(BIRTH_MARK_ADDR) == 0x01; + if (readU8(BIRTH_MARK_ADDR) != 0x00) return false; + const patch = [1]u8{0x01}; + hook.writeProtected(BIRTH_MARK_ADDR, &patch); + if (readU8(BIRTH_MARK_ADDR) != 0x01) return false; + birth_mark_owned = true; + return true; +} + +fn releaseBirthMark() void { + if (!birth_mark_owned) return; + if (readU8(BIRTH_MARK_ADDR) == 0x01) { + const patch = [1]u8{0x00}; + hook.writeProtected(BIRTH_MARK_ADDR, &patch); + } + birth_mark_owned = false; +} + +fn resetSweepState() void { + sweeping = false; + swept_head = 0; + swept_tail = 0; + unswept_rest = 0; + saved_g = 0; + releaseBirthMark(); +} + +fn reconnectSweep() void { + if (!sweeping or saved_g == 0) { + resetSweepState(); + return; + } + + const g = saved_g; + var tail = findTail(readU32(g + GS_ROOTGC)); + + if (unswept_rest != 0) { + if (tail != 0) writeU32(tail + OBJ_NEXT, unswept_rest) + else writeU32(g + GS_ROOTGC, unswept_rest); + tail = findTail(unswept_rest); + } + + if (swept_head != 0) { + if (tail != 0) writeU32(tail + OBJ_NEXT, swept_head) + else writeU32(g + GS_ROOTGC, swept_head); + } + resetSweepState(); +} + +fn detachSweptAndRestore(g: u32) void { + const current_head = readU32(g + GS_ROOTGC); + if (current_head != 0) { + const tail = findTail(current_head); + if (swept_head == 0) { + swept_head = current_head; + swept_tail = tail; + } else { + writeU32(swept_tail + OBJ_NEXT, current_head); + swept_tail = tail; + } + } + writeU32(g + GS_ROOTGC, unswept_rest); + unswept_rest = 0; +} + +fn fallbackNative(L: u32) void { + reconnectSweep(); + collect_hook.callOriginal(.{L}); +} + +fn collectGarbageDetour(L: u32) callconv(hook.cc.fastcall) void { + if (in_gc) return; + + if (!experiment_enabled or restart_pending) { + collect_hook.callOriginal(.{L}); + return; + } + + if (L == 0) return; + if (readU32(L + 0x60) == 0) return; + + in_gc = true; + defer in_gc = false; + + const g = getGlobalState(L); + if (g == 0) { + collect_hook.callOriginal(.{L}); + return; + } + + if (sweeping and (!birth_mark_owned or readU8(BIRTH_MARK_ADDR) != 0x01)) { + fallbackNative(L); + return; + } + + if (!sweeping) { + lua_gc_full_collection(L); + _ = lua_gc_remove_objects(L, g + GS_ROOTUDATA, 0); + lua_gc_sweep_all_lists(L, 0); + + const rootgc_head = readU32(g + GS_ROOTGC); + const result = findNth(rootgc_head, CHUNK_SIZE); + if (result.obj == 0) { + _ = lua_gc_remove_objects(L, g + GS_ROOTGC, 0); + lua_gc_shrink_memory(L); + luaCallUserDataGC(L); + return; + } + + if (!acquireBirthMark()) { + _ = lua_gc_remove_objects(L, g + GS_ROOTGC, 0); + lua_gc_shrink_memory(L); + luaCallUserDataGC(L); + return; + } + + unswept_rest = readU32(result.obj + OBJ_NEXT); + writeU32(result.obj + OBJ_NEXT, 0); + sweeping = true; + saved_g = g; + + _ = lua_gc_remove_objects(L, g + GS_ROOTGC, 0); + detachSweptAndRestore(g); + + const totalbytes = readU32(g + GS_TOTALBYTES); + writeU32(g + GS_GCTHRESHOLD, totalbytes + BATCH_HEADROOM); + return; + } + + if (g != saved_g) { + // Never dereference bookkeeping belonging to a different/dead Lua state. + resetSweepState(); + collect_hook.callOriginal(.{L}); + return; + } + + const rootgc_head = readU32(g + GS_ROOTGC); + const result = findNth(rootgc_head, CHUNK_SIZE); + + if (result.obj == 0) { + _ = lua_gc_remove_objects(L, g + GS_ROOTGC, 0); + const current_head = readU32(g + GS_ROOTGC); + if (swept_head != 0) { + if (current_head != 0) writeU32(swept_tail + OBJ_NEXT, current_head); + writeU32(g + GS_ROOTGC, swept_head); + } + resetSweepState(); + lua_gc_shrink_memory(L); + luaCallUserDataGC(L); + return; + } + + unswept_rest = readU32(result.obj + OBJ_NEXT); + writeU32(result.obj + OBJ_NEXT, 0); + _ = lua_gc_remove_objects(L, g + GS_ROOTGC, 0); + detachSweptAndRestore(g); + + const totalbytes = readU32(g + GS_TOTALBYTES); + writeU32(g + GS_GCTHRESHOLD, totalbytes + BATCH_HEADROOM); +} + +fn shutdownDetour() callconv(hook.cc.stdcall) void { + // Runs for /reload and UI teardown. Lock before original Lua cleanup. + restart_pending = true; + experiment_enabled = false; + reconnectSweep(); + shutdown_hook.callOriginal(.{}); +} + +fn playerLoadDetour() callconv(hook.cc.stdcall) void { + player_load_hook.callOriginal(.{}); + reconnectSweep(); + restart_pending = false; + experiment_enabled = true; +} + +fn installRuntimeHooks() bool { + if (runtime_installed) return true; + + // Lifecycle guards first, GC hook last. No partially-installed GC. + if (shutdown_hook.attach(CGGAMEUI_SHUTDOWN_ADDR, &shutdownDetour) != .ok) return false; + if (player_load_hook.attach(PLAYER_LOAD_SCRIPT_FUNCTIONS_ADDR, &playerLoadDetour) != .ok) { + shutdown_hook.detach(); + return false; + } + if (collect_hook.attach(LUA_COLLECT_GARBAGE_ADDR, &collectGarbageDetour) != .ok) { + player_load_hook.detach(); + shutdown_hook.detach(); + return false; + } + + runtime_installed = true; + restart_pending = true; + experiment_enabled = false; + return true; +} + +fn engineInitDetour() callconv(hook.cc.stdcall) void { + engine_init_hook.callOriginal(.{}); + _ = installRuntimeHooks(); +} + +fn installBootstrap() void { + // Supported fixed-base WoW 1.12.1 build family only. + const image_base = @as(*volatile const u16, @ptrFromInt(0x00400000)); + if (image_base.* != 0x5A4D) return; + _ = engine_init_hook.attach(ENGINE_INIT_ADDR, &engineInitDetour); +} + +const version: [*:0]const u8 = "2.4-B-gc-safe-sweep"; + +pub export fn WeirdPerformanceGC24B_GetVersion() callconv(.c) [*:0]const u8 { + return version; +} + +pub export fn WeirdPerformanceGC24B_IsActive() callconv(.c) i32 { + return if (runtime_installed and experiment_enabled and !restart_pending) 1 else 0; +} + +pub export fn DllMain( + _: ?*anyopaque, + reason: u32, + _: ?*anyopaque, +) callconv(WINAPI) std.os.windows.BOOL { + if (reason == 1) installBootstrap(); + + // No hot-unhook: this A/B companion is process-lifetime by design. + return @enumFromInt(1); +}