diff --git a/experiments/wp24b-gc/main.zig b/experiments/wp24b-gc/main.zig index 5836e96..ed645ec 100644 --- a/experiments/wp24b-gc/main.zig +++ b/experiments/wp24b-gc/main.zig @@ -1,15 +1,20 @@ -//! 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. +//! WeirdPerformance 2.4-B GC safe-sweep companion. +//! +//! A/B contract: +//! A = validated WeirdPerformance 2.4-A binary, unchanged. +//! B = the exact same 2.4-A binary + this companion DLL. +//! +//! This companion only changes Lua GC behavior: WoW's native mark/udata/string +//! work is kept, while the rootgc sweep is split into bounded chunks. +//! +//! Target: WoW 1.12.1 build 5875, x86 only. 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 LUA_CLOSE_ADDR: usize = 0x6F6EF0; const BIRTH_MARK_ADDR: usize = 0x6F7B37; const GS_ROOTGC: u32 = 0x10; @@ -21,8 +26,12 @@ 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; +// Crash logs for the supported client show 0x00400000..0x00D2B000. +const EXPECTED_IMAGE_BASE: usize = 0x00400000; +const EXPECTED_IMAGE_SIZE: u32 = 0x0092B000; + const CollectFn = fn (u32) callconv(hook.cc.fastcall) void; +const LuaCloseFn = 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; @@ -32,36 +41,59 @@ 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 lua_close_hook: hook.Detour(LuaCloseFn) = .{}; -var runtime_installed = false; -var experiment_enabled = false; -var restart_pending = true; +var installed = false; var in_gc = false; +var closing_lua = 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; +var birth_mark_original: u8 = 0; inline fn readU8(addr: usize) u8 { return @as(*volatile const u8, @ptrFromInt(addr)).*; } + +inline fn readU16(addr: usize) u16 { + return @as(*volatile const u16, @ptrFromInt(addr)).*; +} + inline fn readU32(addr: u32) u32 { return @as(*volatile const u32, @ptrFromInt(addr)).*; } + +inline fn readU32usize(addr: usize) 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 validateClient() bool { + if (readU16(EXPECTED_IMAGE_BASE) != 0x5A4D) return false; // MZ + + const pe_off = readU32usize(EXPECTED_IMAGE_BASE + 0x3C); + const pe = EXPECTED_IMAGE_BASE + pe_off; + if (readU32usize(pe) != 0x00004550) return false; // PE\0\0 + if (readU16(pe + 4) != 0x014C) return false; // IMAGE_FILE_MACHINE_I386 + if (readU16(pe + 24) != 0x010B) return false; // PE32 optional header + + const image_size = readU32usize(pe + 24 + 56); + return image_size == EXPECTED_IMAGE_SIZE; +} + fn findNth(head: u32, n: u32) struct { obj: u32, count: u32 } { var obj = head; var i: u32 = 0; @@ -85,9 +117,14 @@ fn findTail(head: u32) u32 { fn acquireBirthMark() bool { if (birth_mark_owned) return readU8(BIRTH_MARK_ADDR) == 0x01; - if (readU8(BIRTH_MARK_ADDR) != 0x00) return false; + + const current = readU8(BIRTH_MARK_ADDR); + if (current != 0x00) return false; + + birth_mark_original = current; 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; @@ -95,8 +132,10 @@ fn acquireBirthMark() bool { fn releaseBirthMark() void { if (!birth_mark_owned) return; + + // Restore only while we still own exactly the byte we installed. if (readU8(BIRTH_MARK_ADDR) == 0x01) { - const patch = [1]u8{0x00}; + const patch = [1]u8{birth_mark_original}; hook.writeProtected(BIRTH_MARK_ADDR, &patch); } birth_mark_owned = false; @@ -121,15 +160,22 @@ fn reconnectSweep() void { 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); + 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); + if (tail != 0) { + writeU32(tail + OBJ_NEXT, swept_head); + } else { + writeU32(g + GS_ROOTGC, swept_head); + } } + resetSweepState(); } @@ -145,23 +191,22 @@ fn detachSweptAndRestore(g: u32) void { swept_tail = tail; } } + writeU32(g + GS_ROOTGC, unswept_rest); unswept_rest = 0; } -fn fallbackNative(L: u32) void { +fn nativeFallback(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) { + if (closing_lua) { collect_hook.callOriginal(.{L}); return; } - + if (in_gc) return; if (L == 0) return; if (readU32(L + 0x60) == 0) return; @@ -174,18 +219,33 @@ fn collectGarbageDetour(L: u32) callconv(hook.cc.fastcall) void { return; } + // Never carry private list state into another Lua global_State. + if (sweeping and g != saved_g) { + resetSweepState(); + collect_hook.callOriginal(.{L}); + return; + } + + // If another module changed the birth byte during our split cycle, + // reconnect immediately and hand this collection back to WoW. if (sweeping and (!birth_mark_owned or readU8(BIRTH_MARK_ADDR) != 0x01)) { - fallbackNative(L); + nativeFallback(L); return; } if (!sweeping) { + // Keep WoW's native mark, userdata sweep, and string sweep. lua_gc_full_collection(L); _ = lua_gc_remove_objects(L, g + GS_ROOTUDATA, 0); lua_gc_sweep_all_lists(L, 0); + swept_head = 0; + swept_tail = 0; + const rootgc_head = readU32(g + GS_ROOTGC); const result = findNth(rootgc_head, CHUNK_SIZE); + + // Small rootgc lists remain one-shot, matching native behavior closely. if (result.obj == 0) { _ = lua_gc_remove_objects(L, g + GS_ROOTGC, 0); lua_gc_shrink_memory(L); @@ -193,6 +253,7 @@ fn collectGarbageDetour(L: u32) callconv(hook.cc.fastcall) void { return; } + // If the birth marker is unavailable, do not split this collection. if (!acquireBirthMark()) { _ = lua_gc_remove_objects(L, g + GS_ROOTGC, 0); lua_gc_shrink_memory(L); @@ -213,23 +274,20 @@ fn collectGarbageDetour(L: u32) callconv(hook.cc.fastcall) void { 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); + 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); @@ -238,6 +296,7 @@ fn collectGarbageDetour(L: u32) callconv(hook.cc.fastcall) void { unswept_rest = readU32(result.obj + OBJ_NEXT); writeU32(result.obj + OBJ_NEXT, 0); + _ = lua_gc_remove_objects(L, g + GS_ROOTGC, 0); detachSweptAndRestore(g); @@ -245,52 +304,32 @@ fn collectGarbageDetour(L: u32) callconv(hook.cc.fastcall) void { 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; +fn luaCloseDetour(L: u32) callconv(hook.cc.fastcall) void { + // lua_close may run native sweep paths that bypass luaC_collectgarbage. + // Reconnect every private fragment while the old Lua state is still valid. + closing_lua = true; reconnectSweep(); - shutdown_hook.callOriginal(.{}); + in_gc = false; + + lua_close_hook.callOriginal(.{L}); + + closing_lua = false; } -fn playerLoadDetour() callconv(hook.cc.stdcall) void { - player_load_hook.callOriginal(.{}); - reconnectSweep(); - restart_pending = false; - experiment_enabled = true; -} +fn install() void { + if (installed) return; + if (!validateClient()) return; -fn installRuntimeHooks() bool { - if (runtime_installed) return true; + // Transactional install: lua_close guard first, collector second. + // If the collector cannot attach, roll the close hook back immediately. + if (lua_close_hook.attach(LUA_CLOSE_ADDR, &luaCloseDetour) != .ok) return; - // 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; + lua_close_hook.detach(); + return; } - 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); + installed = true; } const version: [*:0]const u8 = "2.4-B-gc-safe-sweep"; @@ -300,7 +339,7 @@ pub export fn WeirdPerformanceGC24B_GetVersion() callconv(.c) [*:0]const u8 { } pub export fn WeirdPerformanceGC24B_IsActive() callconv(.c) i32 { - return if (runtime_installed and experiment_enabled and !restart_pending) 1 else 0; + return if (installed) 1 else 0; } pub export fn DllMain( @@ -308,8 +347,9 @@ pub export fn DllMain( reason: u32, _: ?*anyopaque, ) callconv(WINAPI) std.os.windows.BOOL { - if (reason == 1) installBootstrap(); + if (reason == 1) install(); - // No hot-unhook: this A/B companion is process-lifetime by design. + // Process-lifetime A/B companion. We intentionally do not hot-unhook + // detours during process teardown. return @enumFromInt(1); }