From 8a3e97844448a298466daf369fad70f6582ecd0c Mon Sep 17 00:00:00 2001 From: Dusk-92 Date: Tue, 8 Sep 2026 09:33:48 +0200 Subject: [PATCH] gc: keep rootgc linked between B2 sweep chunks --- experiments/wp24b-gc/main.zig | 333 ++++++++++++++++------------------ 1 file changed, 158 insertions(+), 175 deletions(-) diff --git a/experiments/wp24b-gc/main.zig b/experiments/wp24b-gc/main.zig index 3b9c122..df6a72e 100644 --- a/experiments/wp24b-gc/main.zig +++ b/experiments/wp24b-gc/main.zig @@ -1,11 +1,17 @@ -//! WeirdPerformance 2.4-B GC safe-sweep companion. +//! WeirdPerformance 2.4-B2 GC in-place 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. +//! A = validated WeirdPerformance 2.4-A binary, unchanged. +//! B2 = 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. +//! B1 split rootgc by detaching swept survivors from the live rootgc chain +//! between GC calls. B2 keeps the complete rootgc chain linked whenever +//! control returns to WoW. Each chunk is isolated only for the duration of +//! lua_gc_remove_objects(), then reconnected before the detour returns. +//! +//! This preserves B1's bounded 50,000-object rootgc sweep while removing the +//! long-lived fragmented-list state suspected in delayed lua_table_set_value +//! corruption (0x006FA8E2). //! //! Target: WoW 1.12.1 build 5875, x86 only. @@ -16,7 +22,6 @@ const X86_FASTCALL: std.builtin.CallingConvention = .{ .x86_fastcall = .{} }; const LUA_COLLECT_GARBAGE_ADDR: usize = 0x6F7340; const LUA_CLOSE_ADDR: usize = 0x6F6EF0; -const BIRTH_MARK_ADDR: usize = 0x6F7B37; const IS_IN_WORLD_ADDR: u32 = 0x00B4B424; const GS_ROOTGC: u32 = 0x10; @@ -50,19 +55,14 @@ var installed = false; var in_gc = false; var closing_lua = false; +// B2 state. Unlike B1 there are no detached rootgc fragments. +// sweep_next is the first object not yet swept in the current native mark cycle. +// New luaC_link objects are prepended before it and are intentionally left for +// the next cycle rather than being exposed to the current sweep without a mark. var sweeping = false; -var swept_head: u32 = 0; -var swept_tail: u32 = 0; -var unswept_rest: u32 = 0; +var sweep_next: 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)).*; } @@ -96,15 +96,40 @@ fn validateClient() bool { return image_size == EXPECTED_IMAGE_SIZE; } -fn findNth(head: u32, n: u32) struct { obj: u32, count: u32 } { +fn resetSweepState() void { + sweeping = false; + sweep_next = 0; + saved_g = 0; +} + +// Locate the pointer field which currently references target. This is required +// because luaC_link prepends newly allocated objects to g->rootgc while a split +// sweep is in progress. We must skip those new objects instead of accidentally +// sweeping them with marks from the previous atomic phase. +fn findLinkTo(g: u32, target: u32) ?u32 { + if (target == 0) return null; + + var link_addr = g + GS_ROOTGC; + var obj = readU32(link_addr); + while (obj != 0) { + if (obj == target) return link_addr; + link_addr = obj + OBJ_NEXT; + obj = readU32(link_addr); + } + return null; +} + +fn findChunkTail(head: u32, limit: u32) struct { tail: u32, count: u32 } { + if (head == 0 or limit == 0) return .{ .tail = 0, .count = 0 }; + var obj = head; - var i: u32 = 0; - while (obj != 0 and i < n) : (i += 1) { + var count: u32 = 1; + while (count < limit) : (count += 1) { const next = readU32(obj + OBJ_NEXT); - if (next == 0) return .{ .obj = 0, .count = i + 1 }; + if (next == 0) return .{ .tail = obj, .count = count }; obj = next; } - return .{ .obj = obj, .count = i }; + return .{ .tail = obj, .count = count }; } fn findTail(head: u32) u32 { @@ -117,89 +142,83 @@ fn findTail(head: u32) u32 { } } -fn acquireBirthMark() bool { - if (birth_mark_owned) return readU8(BIRTH_MARK_ADDR) == 0x01; +const ChunkResult = enum { + more, + done, + invalid, +}; - const current = readU8(BIRTH_MARK_ADDR); - if (current != 0x00) return false; +// Sweep exactly one bounded sub-list using WoW's native sweep routine. +// The rootgc chain may be temporarily truncated while the native routine runs, +// but it is always fully reconnected before this function returns. +fn sweepOneChunk(L: u32, g: u32) ChunkResult { + if (!sweeping or sweep_next == 0) return .done; + if (g != saved_g) return .invalid; - birth_mark_original = current; - const patch = [1]u8{0x01}; - hook.writeProtected(BIRTH_MARK_ADDR, &patch); + const link_addr = findLinkTo(g, sweep_next) orelse return .invalid; + const head = readU32(link_addr); + if (head != sweep_next) return .invalid; - if (readU8(BIRTH_MARK_ADDR) != 0x01) return false; - birth_mark_owned = true; + const chunk = findChunkTail(head, CHUNK_SIZE); + if (chunk.tail == 0) return .done; + + const rest = readU32(chunk.tail + OBJ_NEXT); + + // Final chunk: no temporary split is necessary. Let the native routine + // update the real list link directly and complete the cycle. + if (rest == 0) { + _ = lua_gc_remove_objects(L, link_addr, 0); + sweep_next = 0; + return .done; + } + + // Isolate only the current chunk for the native sweep call. + writeU32(chunk.tail + OBJ_NEXT, 0); + _ = lua_gc_remove_objects(L, link_addr, 0); + + // Reconnect the untouched remainder before returning to gameplay. + // If every object in the chunk died, link_addr itself becomes the bridge. + const survivors = readU32(link_addr); + if (survivors == 0) { + writeU32(link_addr, rest); + } else { + const survivor_tail = findTail(survivors); + if (survivor_tail == 0) return .invalid; + writeU32(survivor_tail + OBJ_NEXT, rest); + } + + sweep_next = rest; + return .more; +} + +fn finishCycle(L: u32) void { + resetSweepState(); + lua_gc_shrink_memory(L); + luaCallUserDataGC(L); +} + +// Used only for transitions such as leaving the world while a B2 cycle is +// active. The list is fully linked, so we can finish remaining chunks without +// any B1-style fragment reconstruction. +fn finishSweepNow(L: u32, g: u32) bool { + while (sweeping) { + switch (sweepOneChunk(L, g)) { + .more => {}, + .done => { + finishCycle(L); + return true; + }, + .invalid => { + resetSweepState(); + return false; + }, + } + } return true; } -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{birth_mark_original}; - 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 nativeFallback(L: u32) void { - reconnectSweep(); + resetSweepState(); collect_hook.callOriginal(.{L}); } @@ -209,12 +228,6 @@ fn collectGarbageDetour(L: u32) callconv(X86_FASTCALL) void { return; } - // Keep GlueXML/login/character-select on WoW's native GC. - // The experiment is intentionally gameplay-only. - if (readU32(IS_IN_WORLD_ADDR) == 0) { - collect_hook.callOriginal(.{L}); - return; - } if (in_gc) return; if (L == 0) return; if (readU32(L + 0x60) == 0) return; @@ -228,96 +241,67 @@ fn collectGarbageDetour(L: u32) callconv(X86_FASTCALL) void { return; } - // Never carry private list state into another Lua global_State. - if (sweeping and g != saved_g) { - resetSweepState(); + // If gameplay ends mid-cycle, finish the already-marked sweep while the + // current Lua state is still valid, then return control to native GC. + if (readU32(IS_IN_WORLD_ADDR) == 0) { + if (sweeping and g == saved_g) { + if (!finishSweepNow(L, g)) { + collect_hook.callOriginal(.{L}); + return; + } + } else if (sweeping) { + 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)) { + // Never carry a cursor into a different Lua global_State. + if (sweeping and g != saved_g) { nativeFallback(L); return; } if (!sweeping) { - // Keep WoW's native mark, userdata sweep, and string sweep. + // Keep WoW's native atomic 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); - luaCallUserDataGC(L); - 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); - luaCallUserDataGC(L); - return; - } - - unswept_rest = readU32(result.obj + OBJ_NEXT); - writeU32(result.obj + OBJ_NEXT, 0); - sweeping = true; + sweep_next = readU32(g + GS_ROOTGC); saved_g = g; + sweeping = sweep_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); - 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); + if (!sweeping) { + finishCycle(L); + return; } - - 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); + switch (sweepOneChunk(L, g)) { + .done => { + finishCycle(L); + return; + }, + .invalid => { + // The live rootgc chain itself was never fragmented across calls, + // so falling back to WoW's complete collector is safe here. + nativeFallback(L); + return; + }, + .more => { + const totalbytes = readU32(g + GS_TOTALBYTES); + writeU32(g + GS_GCTHRESHOLD, totalbytes + BATCH_HEADROOM); + return; + }, + } } fn luaCloseDetour(L: u32) callconv(X86_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. + // B2 never leaves detached rootgc fragments. lua_close can therefore own + // teardown normally; just discard the cursor before the state is destroyed. closing_lua = true; - reconnectSweep(); + resetSweepState(); in_gc = false; lua_close_hook.callOriginal(.{L}); @@ -330,7 +314,6 @@ fn install() void { if (!validateClient()) return; // 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; if (collect_hook.attach(LUA_COLLECT_GARBAGE_ADDR, &collectGarbageDetour) != .ok) { @@ -341,13 +324,13 @@ fn install() void { installed = true; } -const version: [*:0]const u8 = "2.4-B1-gc-safe-sweep-abi"; +const version: [*:0]const u8 = "2.4-B2-gc-inplace-safe-sweep"; -pub export fn WeirdPerformanceGC24B_GetVersion() callconv(.c) [*:0]const u8 { +pub export fn WeirdPerformanceGC24B2_GetVersion() callconv(.c) [*:0]const u8 { return version; } -pub export fn WeirdPerformanceGC24B_IsActive() callconv(.c) i32 { +pub export fn WeirdPerformanceGC24B2_IsActive() callconv(.c) i32 { return if (installed) 1 else 0; }