test: isolate 2.4-B GC hooks from 2.4-A lifecycle

This commit is contained in:
Dusk-92
2026-09-02 23:44:14 +02:00
parent 1cc0ca7e29
commit 3e5628f198
+114 -74
View File
@@ -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);
}