Compare commits
26 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 67f831d524 | |||
| 5832b8d0cb | |||
| fdf4f0a13b | |||
| 5997a91d51 | |||
| 8a3e978444 | |||
| 83d85f5d5c | |||
| 5c61928260 | |||
| 722406fef2 | |||
| c9ba29f60b | |||
| 650825d793 | |||
| ce63965502 | |||
| dd04a4f480 | |||
| b5662b64ec | |||
| 3d821c41c3 | |||
| 39b0097ed2 | |||
| 232f37d762 | |||
| 762034301a | |||
| 9d5d7bd0d6 | |||
| f72686f27a | |||
| 3e5628f198 | |||
| 1cc0ca7e29 | |||
| cfc584ebfa | |||
| 1a0aad3223 | |||
| 368ae01c64 | |||
| b4f9cca516 | |||
| bb6e6b9236 |
@@ -0,0 +1,121 @@
|
||||
name: Build WeirdPerformance 2.4-B1 GC test
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- test/wp24b-gc-safe-sweep
|
||||
paths:
|
||||
- 'experiments/wp24b-gc/**'
|
||||
- '.github/workflows/wp24b-gc.yml'
|
||||
workflow_dispatch:
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
jobs:
|
||||
build:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: mlugg/setup-zig@v2
|
||||
with:
|
||||
version: '0.17.0-dev.1970+67f39b551'
|
||||
- name: Build and ABI-validate x86 DLL
|
||||
working-directory: experiments/wp24b-gc
|
||||
run: |
|
||||
set -euo pipefail
|
||||
zig build --fetch
|
||||
python3 - <<'PY'
|
||||
from pathlib import Path
|
||||
matches = list(Path("zig-pkg").glob("zhook-*/src/zhook.zig"))
|
||||
if len(matches) != 1:
|
||||
raise SystemExit(f"expected one pinned zhook source, found {len(matches)}")
|
||||
p = matches[0]
|
||||
text = p.read_text(encoding="utf-8")
|
||||
old = "var patch: [MAX_STOLEN]u8 = .{0x90} ** MAX_STOLEN;"
|
||||
new = "var patch: [MAX_STOLEN]u8 = @splat(0x90);"
|
||||
if old not in text:
|
||||
raise SystemExit("expected pinned zhook repeat initializer not found")
|
||||
p.write_text(text.replace(old, new), encoding="utf-8")
|
||||
PY
|
||||
|
||||
zig build -Doptimize=small
|
||||
DLL=zig-out/bin/weirdperformance_gc24b1.dll
|
||||
test -s "$DLL"
|
||||
file "$DLL" | tee BINARY_INFO.txt
|
||||
file "$DLL" | grep -Eq 'PE32.*Intel (80386|i386)'
|
||||
|
||||
if command -v llvm-objdump >/dev/null 2>&1; then
|
||||
llvm-objdump -d --x86-asm-syntax=intel "$DLL" > ABI_DISASM.txt
|
||||
else
|
||||
objdump -d -M intel "$DLL" > ABI_DISASM.txt
|
||||
fi
|
||||
|
||||
python3 - <<'PY'
|
||||
import re
|
||||
from pathlib import Path
|
||||
|
||||
asm = Path("ABI_DISASM.txt").read_text(encoding="utf-8", errors="replace").lower()
|
||||
|
||||
def require(pattern, label):
|
||||
if not re.search(pattern, asm, re.S):
|
||||
raise SystemExit(f"ABI validation failed: {label}")
|
||||
|
||||
# zhook must jump to callbacks compiled with L arriving in ECX.
|
||||
m = re.search(r"mov\s+edx,\s*0x6f7340.{0,240}?push\s+0x([0-9a-f]+)", asm, re.S)
|
||||
if not m:
|
||||
raise SystemExit("ABI validation failed: collector callback address not found")
|
||||
cb = m.group(1).lstrip("0") or "0"
|
||||
pos = asm.find(cb + ":")
|
||||
if pos < 0 or not re.search(r"mov\s+esi,\s*ecx", asm[pos:pos+500]):
|
||||
raise SystemExit("ABI validation failed: collector callback does not consume L from ECX")
|
||||
|
||||
m = re.search(r"mov\s+edx,\s*0x6f6ef0.{0,240}?push\s+0x([0-9a-f]+)", asm, re.S)
|
||||
if not m:
|
||||
raise SystemExit("ABI validation failed: lua_close callback address not found")
|
||||
cb = m.group(1).lstrip("0") or "0"
|
||||
pos = asm.find(cb + ":")
|
||||
if pos < 0 or not re.search(r"mov\s+esi,\s*ecx", asm[pos:pos+400]):
|
||||
raise SystemExit("ABI validation failed: lua_close callback does not consume L from ECX")
|
||||
|
||||
# Native helper ABI observed in WoW 5875:
|
||||
# ECX=L, EDX=&list, stack arg=all for 0x6F7210.
|
||||
require(
|
||||
r"lea\s+edx,\s*\[edi\s*\+\s*0x14\].{0,160}?"
|
||||
r"mov\s+eax,\s*0x6f7210.{0,120}?"
|
||||
r"mov\s+ecx,\s*esi.{0,120}?"
|
||||
r"push\s+0x0.{0,80}?call\s+eax",
|
||||
"lua_gc_remove_objects register/stack ABI",
|
||||
)
|
||||
|
||||
require(
|
||||
r"mov\s+eax,\s*0x6f72f0.{0,120}?"
|
||||
r"mov\s+ecx,\s*esi.{0,100}?"
|
||||
r"xor\s+edx,\s*edx.{0,80}?call\s+eax",
|
||||
"lua_gc_sweep_all_lists fastcall ABI",
|
||||
)
|
||||
|
||||
for addr, label in (
|
||||
("6f73e0", "lua_gc_full_collection"),
|
||||
("6f7370", "lua_gc_shrink_memory"),
|
||||
("6f7080", "luaCallUserDataGC"),
|
||||
):
|
||||
require(
|
||||
rf"mov\s+eax,\s*0x{addr}.{{0,120}}?mov\s+ecx,\s*esi.{{0,80}}?call\s+eax",
|
||||
f"{label} ECX ABI",
|
||||
)
|
||||
|
||||
print("ABI validation: PASS")
|
||||
PY
|
||||
|
||||
sha256sum "$DLL" | tee SHA256SUMS.txt
|
||||
- uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: WeirdPerformance-2.4-B1-GC-Safe-Sweep
|
||||
path: |
|
||||
experiments/wp24b-gc/zig-out/bin/weirdperformance_gc24b1.dll
|
||||
experiments/wp24b-gc/SHA256SUMS.txt
|
||||
experiments/wp24b-gc/BINARY_INFO.txt
|
||||
experiments/wp24b-gc/ABI_DISASM.txt
|
||||
experiments/wp24b-gc/README.md
|
||||
if-no-files-found: error
|
||||
@@ -0,0 +1,148 @@
|
||||
name: Build WeirdPerformance 2.4-B2 in-place GC test
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- test/wp24b2-gc-inplace-sweep
|
||||
paths:
|
||||
- 'experiments/wp24b-gc/**'
|
||||
- '.github/workflows/wp24b2-inplace-gc.yml'
|
||||
workflow_dispatch:
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
jobs:
|
||||
build:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: mlugg/setup-zig@v2
|
||||
with:
|
||||
version: '0.17.0-dev.1970+67f39b551'
|
||||
- name: Build and ABI-validate x86 DLL
|
||||
working-directory: experiments/wp24b-gc
|
||||
run: |
|
||||
set -euo pipefail
|
||||
zig build --fetch
|
||||
python3 - <<'PY'
|
||||
from pathlib import Path
|
||||
matches = list(Path("zig-pkg").glob("zhook-*/src/zhook.zig"))
|
||||
if len(matches) != 1:
|
||||
raise SystemExit(f"expected one pinned zhook source, found {len(matches)}")
|
||||
p = matches[0]
|
||||
text = p.read_text(encoding="utf-8")
|
||||
old = "var patch: [MAX_STOLEN]u8 = .{0x90} ** MAX_STOLEN;"
|
||||
new = "var patch: [MAX_STOLEN]u8 = @splat(0x90);"
|
||||
if old not in text:
|
||||
raise SystemExit("expected pinned zhook repeat initializer not found")
|
||||
p.write_text(text.replace(old, new), encoding="utf-8")
|
||||
PY
|
||||
|
||||
zig build -Doptimize=small
|
||||
DLL=zig-out/bin/weirdperformance_gc24b2.dll
|
||||
test -s "$DLL"
|
||||
file "$DLL" | tee BINARY_INFO.txt
|
||||
file "$DLL" | grep -Eq 'PE32.*Intel (80386|i386)'
|
||||
strings "$DLL" | grep -q '2.4-B2-gc-inplace-safe-sweep'
|
||||
|
||||
if command -v llvm-objdump >/dev/null 2>&1; then
|
||||
llvm-objdump -d --x86-asm-syntax=intel "$DLL" > ABI_DISASM.txt
|
||||
else
|
||||
objdump -d -M intel "$DLL" > ABI_DISASM.txt
|
||||
fi
|
||||
|
||||
python3 - <<'PY'
|
||||
import re
|
||||
from pathlib import Path
|
||||
|
||||
asm = Path("ABI_DISASM.txt").read_text(encoding="utf-8", errors="replace").lower()
|
||||
|
||||
def require(pattern, label):
|
||||
if not re.search(pattern, asm, re.S):
|
||||
raise SystemExit(f"ABI validation failed: {label}")
|
||||
|
||||
def callback_snippet(target, window):
|
||||
m = re.search(rf"mov\s+edx,\s*0x{target}.{{0,240}}?push\s+0x([0-9a-f]+)", asm, re.S)
|
||||
if not m:
|
||||
raise SystemExit(f"ABI validation failed: callback for 0x{target} not found")
|
||||
cb = m.group(1).lstrip("0") or "0"
|
||||
pos = asm.find(cb + ":")
|
||||
if pos < 0:
|
||||
raise SystemExit(f"ABI validation failed: callback body {cb} not found")
|
||||
return asm[pos:pos+window]
|
||||
|
||||
# Collector callback: same proven B1 code shape, L copied from ECX.
|
||||
collector = callback_snippet("6f7340", 900)
|
||||
if not re.search(r"mov\s+esi,\s*ecx", collector):
|
||||
raise SystemExit("ABI validation failed: collector callback does not consume L from ECX")
|
||||
|
||||
# B2 lua_close has no helper call before callOriginal: Zig can legally
|
||||
# leave the incoming fastcall ECX untouched and call the trampoline
|
||||
# directly. Accept either explicit preservation or verified no write
|
||||
# to ECX before the first call instruction.
|
||||
close = callback_snippet("6f6ef0", 800)
|
||||
first_call = re.search(r"\bcall\b", close)
|
||||
if not first_call:
|
||||
raise SystemExit("ABI validation failed: lua_close trampoline call missing")
|
||||
before_call = close[:first_call.start()]
|
||||
ecx_saved = re.search(
|
||||
r"(?:mov\s+(?:esi|edi|ebx|ebp|eax|edx),\s*ecx|push\s+ecx|mov\s+(?:dword ptr\s*)?\[[^\]]+\],\s*ecx)",
|
||||
before_call,
|
||||
)
|
||||
ecx_written = re.search(
|
||||
r"\b(?:mov|lea|xor|and|or|add|sub|imul|inc|dec|pop)\s+ecx\b",
|
||||
before_call,
|
||||
)
|
||||
if not ecx_saved and ecx_written:
|
||||
print("lua_close callback snippet:")
|
||||
print(close)
|
||||
raise SystemExit("ABI validation failed: lua_close clobbers incoming ECX before trampoline")
|
||||
|
||||
# Native helper ABI observed directly in WoW 5875:
|
||||
# lua_gc_remove_objects: ECX=L, EDX=&list, stack arg all=0.
|
||||
require(
|
||||
r"lea\s+edx,\s*\[edi\s*\+\s*0x14\].{0,200}?"
|
||||
r"mov\s+eax,\s*0x6f7210.{0,160}?"
|
||||
r"mov\s+ecx,\s*esi.{0,160}?"
|
||||
r"push\s+0x0.{0,100}?call\s+eax",
|
||||
"rootudata lua_gc_remove_objects ABI",
|
||||
)
|
||||
|
||||
# At least one dynamic-list remove call must load L into ECX.
|
||||
require(
|
||||
r"mov\s+eax,\s*0x6f7210.{0,240}?mov\s+ecx,\s*e[a-z]{2}.{0,200}?call\s+eax",
|
||||
"chunk lua_gc_remove_objects fastcall callsite",
|
||||
)
|
||||
|
||||
require(
|
||||
r"mov\s+eax,\s*0x6f72f0.{0,160}?"
|
||||
r"mov\s+ecx,\s*esi.{0,120}?"
|
||||
r"xor\s+edx,\s*edx.{0,100}?call\s+eax",
|
||||
"lua_gc_sweep_all_lists fastcall ABI",
|
||||
)
|
||||
|
||||
for addr, label in (
|
||||
("6f73e0", "lua_gc_full_collection"),
|
||||
("6f7370", "lua_gc_shrink_memory"),
|
||||
("6f7080", "luaCallUserDataGC"),
|
||||
):
|
||||
require(
|
||||
rf"mov\s+eax,\s*0x{addr}.{{0,180}}?mov\s+ecx,\s*e[a-z]{{2}}.{{0,120}}?call\s+eax",
|
||||
f"{label} ECX ABI",
|
||||
)
|
||||
|
||||
print("ABI validation: PASS")
|
||||
PY
|
||||
|
||||
sha256sum "$DLL" | tee SHA256SUMS.txt
|
||||
- uses: actions/upload-artifact@v4
|
||||
if: always()
|
||||
with:
|
||||
name: WeirdPerformance-2.4-B2-GC-InPlace-Safe-Sweep
|
||||
path: |
|
||||
experiments/wp24b-gc/zig-out/bin/weirdperformance_gc24b2.dll
|
||||
experiments/wp24b-gc/SHA256SUMS.txt
|
||||
experiments/wp24b-gc/BINARY_INFO.txt
|
||||
experiments/wp24b-gc/ABI_DISASM.txt
|
||||
if-no-files-found: warn
|
||||
@@ -0,0 +1,46 @@
|
||||
# WeirdPerformance 2.4-B1 — GC Safe Sweep ABI-fixed test
|
||||
|
||||
Strict A/B experiment for WoW 1.12.1 build 5875.
|
||||
|
||||
## Baseline
|
||||
|
||||
Keep the validated 2.4-A binary **unchanged**:
|
||||
|
||||
- `weirdperformance.dll`
|
||||
- SHA-256: `d35168ae06c19087ef9b7c68918a054640396dfbfcef0664e18e9372284b5eef`
|
||||
|
||||
## B1 variant
|
||||
|
||||
Add:
|
||||
|
||||
- `weirdperformance_gc24b1.dll`
|
||||
|
||||
The companion changes **only Lua GC behavior**. It does not replace or rebuild the validated 2.4-A DLL.
|
||||
|
||||
The first 2.4-B build is invalid and must not be used. It was built with an i386 Windows fastcall ABI mismatch and crashed at startup in WoW's native `lua_gc_remove_objects`. B1 is built with a pinned Zig 0.17 development toolchain and is binary-disassembly checked for the expected register ABI before distribution.
|
||||
|
||||
The implementation is based on the historical pre-generational incremental sweep: WoW keeps its native mark, userdata/string sweep, memory shrink and finalizers. Only the main `rootgc` sweep is split into 50,000-object chunks.
|
||||
|
||||
## Safety scope
|
||||
|
||||
- hooks only `luaC_collectgarbage` and `lua_close`;
|
||||
- no overlap with the lifecycle hooks referenced by the validated 2.4-A binary;
|
||||
- any fragmented sweep is reconnected before native `lua_close` destroys the Lua state;
|
||||
- GC calls made from inside `lua_close` stay native;
|
||||
- the birth-mark byte is ownership checked and restored only while still owned;
|
||||
- a changed Lua `global_State` forces native fallback rather than reconnecting stale pointers;
|
||||
- hook installation is transactional;
|
||||
- no generational age bitmap, no write barriers, no GC tuning, no profiling/RDTSC.
|
||||
|
||||
## Installation for the test
|
||||
|
||||
Keep both DLLs next to WoW and list both in `dlls.txt`:
|
||||
|
||||
```text
|
||||
weirdperformance.dll
|
||||
weirdperformance_gc24b1.dll
|
||||
```
|
||||
|
||||
Delete the old `weirdperformance_gc24b.dll` if it is still present.
|
||||
|
||||
Removing `weirdperformance_gc24b1.dll` returns you exactly to the validated 2.4-A baseline.
|
||||
@@ -0,0 +1,30 @@
|
||||
const std = @import("std");
|
||||
|
||||
pub fn build(b: *std.Build) void {
|
||||
const target = b.resolveTargetQuery(.{
|
||||
.cpu_arch = .x86,
|
||||
.os_tag = .windows,
|
||||
.abi = .msvc,
|
||||
.cpu_features_add = std.Target.x86.featureSet(&.{ .sse, .sse2 }),
|
||||
});
|
||||
const optimize = b.option(std.builtin.OptimizeMode, "optimize", "Optimization mode") orelse .small;
|
||||
|
||||
const zhook_dep = b.dependency("zhook", .{
|
||||
.target = target,
|
||||
.optimize = optimize,
|
||||
});
|
||||
|
||||
const lib = b.addLibrary(.{
|
||||
.name = "weirdperformance_gc24b2",
|
||||
.linkage = .dynamic,
|
||||
.root_module = b.createModule(.{
|
||||
.root_source_file = b.path("main.zig"),
|
||||
.target = target,
|
||||
.optimize = optimize,
|
||||
.imports = &.{
|
||||
.{ .name = "zhook", .module = zhook_dep.module("zhook") },
|
||||
},
|
||||
}),
|
||||
});
|
||||
b.installArtifact(lib);
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
.{
|
||||
.name = .weirdperformance_gc24b,
|
||||
.version = "2.4.0",
|
||||
.fingerprint = 0x95e8be635406d50f,
|
||||
.dependencies = .{
|
||||
.zhook = .{
|
||||
.url = "https://codeberg.org/marcelinevq/zhook/archive/f1b252ed61ad839f00310c386761d068f293ad0f.tar.gz",
|
||||
.hash = "zhook-0.1.0-pFkSYC6FAACAnkqu0k_DJBWdL0gJjrM22IfXeQPJAMov",
|
||||
},
|
||||
},
|
||||
.paths = .{
|
||||
"build.zig",
|
||||
"build.zig.zon",
|
||||
"main.zig",
|
||||
},
|
||||
}
|
||||
@@ -0,0 +1,347 @@
|
||||
//! WeirdPerformance 2.4-B2 GC in-place safe-sweep companion.
|
||||
//!
|
||||
//! A/B contract:
|
||||
//! A = validated WeirdPerformance 2.4-A binary, unchanged.
|
||||
//! B2 = the exact same 2.4-A binary + this companion DLL.
|
||||
//!
|
||||
//! 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.
|
||||
|
||||
const std = @import("std");
|
||||
const hook = @import("zhook");
|
||||
const WINAPI = std.builtin.CallingConvention.winapi;
|
||||
const X86_FASTCALL: std.builtin.CallingConvention = .{ .x86_fastcall = .{} };
|
||||
|
||||
const LUA_COLLECT_GARBAGE_ADDR: usize = 0x6F7340;
|
||||
const LUA_CLOSE_ADDR: usize = 0x6F6EF0;
|
||||
const IS_IN_WORLD_ADDR: u32 = 0x00B4B424;
|
||||
|
||||
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;
|
||||
|
||||
// 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(X86_FASTCALL) void;
|
||||
const LuaCloseFn = fn (u32) callconv(X86_FASTCALL) void;
|
||||
const SweepAllFn = fn (u32, u32) callconv(X86_FASTCALL) void;
|
||||
const RemoveObjectsFn = fn (u32, u32, u32) callconv(X86_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 collect_hook: hook.Detour(CollectFn) = .{};
|
||||
var lua_close_hook: hook.Detour(LuaCloseFn) = .{};
|
||||
|
||||
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 sweep_next: u32 = 0;
|
||||
var saved_g: u32 = 0;
|
||||
|
||||
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 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 count: u32 = 1;
|
||||
while (count < limit) : (count += 1) {
|
||||
const next = readU32(obj + OBJ_NEXT);
|
||||
if (next == 0) return .{ .tail = obj, .count = count };
|
||||
obj = next;
|
||||
}
|
||||
return .{ .tail = obj, .count = count };
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
const ChunkResult = enum {
|
||||
more,
|
||||
done,
|
||||
invalid,
|
||||
};
|
||||
|
||||
// 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;
|
||||
|
||||
const link_addr = findLinkTo(g, sweep_next) orelse return .invalid;
|
||||
const head = readU32(link_addr);
|
||||
if (head != sweep_next) return .invalid;
|
||||
|
||||
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 nativeFallback(L: u32) void {
|
||||
resetSweepState();
|
||||
collect_hook.callOriginal(.{L});
|
||||
}
|
||||
|
||||
fn collectGarbageDetour(L: u32) callconv(X86_FASTCALL) void {
|
||||
if (closing_lua) {
|
||||
collect_hook.callOriginal(.{L});
|
||||
return;
|
||||
}
|
||||
|
||||
if (in_gc) 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 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;
|
||||
}
|
||||
|
||||
// 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 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);
|
||||
|
||||
sweep_next = readU32(g + GS_ROOTGC);
|
||||
saved_g = g;
|
||||
sweeping = sweep_next != 0;
|
||||
|
||||
if (!sweeping) {
|
||||
finishCycle(L);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
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 {
|
||||
// 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;
|
||||
resetSweepState();
|
||||
in_gc = false;
|
||||
|
||||
lua_close_hook.callOriginal(.{L});
|
||||
|
||||
closing_lua = false;
|
||||
}
|
||||
|
||||
fn install() void {
|
||||
if (installed) return;
|
||||
if (!validateClient()) return;
|
||||
|
||||
// Transactional install: lua_close guard first, collector second.
|
||||
if (lua_close_hook.attach(LUA_CLOSE_ADDR, &luaCloseDetour) != .ok) return;
|
||||
|
||||
if (collect_hook.attach(LUA_COLLECT_GARBAGE_ADDR, &collectGarbageDetour) != .ok) {
|
||||
lua_close_hook.detach();
|
||||
return;
|
||||
}
|
||||
|
||||
installed = true;
|
||||
}
|
||||
|
||||
const version: [*:0]const u8 = "2.4-B2-gc-inplace-safe-sweep";
|
||||
|
||||
pub export fn WeirdPerformanceGC24B2_GetVersion() callconv(.c) [*:0]const u8 {
|
||||
return version;
|
||||
}
|
||||
|
||||
pub export fn WeirdPerformanceGC24B2_IsActive() callconv(.c) i32 {
|
||||
return if (installed) 1 else 0;
|
||||
}
|
||||
|
||||
pub export fn DllMain(
|
||||
_: ?*anyopaque,
|
||||
reason: u32,
|
||||
_: ?*anyopaque,
|
||||
) callconv(WINAPI) std.os.windows.BOOL {
|
||||
if (reason == 1) install();
|
||||
|
||||
// Process-lifetime A/B companion. We intentionally do not hot-unhook
|
||||
// detours during process teardown.
|
||||
return @enumFromInt(1);
|
||||
}
|
||||
Reference in New Issue
Block a user