Compare commits

...

24 Commits

Author SHA1 Message Date
Dusk-92 a2b8d2a7e0 ci: build and ABI-gate WeirdPerformance 2.4-B2 2026-09-07 18:11:22 +02:00
Dusk-92 5dde1f641e docs: add B2 reload-guard test protocol 2026-09-07 18:10:54 +02:00
Dusk-92 d6262135a8 test: add B2 new-Lua-state GC grace guard 2026-09-07 18:10:36 +02:00
Dusk-92 83d85f5d5c ci: accept Intel 80386 PE32 label 2026-09-03 11:00:45 +02:00
Dusk-92 5c61928260 safety: keep GC native until player is in world 2026-09-03 10:49:40 +02:00
Dusk-92 722406fef2 ci: pin Zig and enforce x86 GC ABI gate 2026-09-03 10:47:47 +02:00
Dusk-92 c9ba29f60b docs: mark broken B invalid and document B1 2026-09-03 10:47:12 +02:00
Dusk-92 650825d793 build: rename ABI-fixed companion to gc24b1 2026-09-03 10:46:54 +02:00
Dusk-92 ce63965502 ci: patch pinned zhook syntax for Zig 0.17 dev 2026-09-03 10:43:01 +02:00
Dusk-92 dd04a4f480 ci: use Zig 0.17 optimize spelling 2026-09-03 10:38:59 +02:00
Dusk-92 b5662b64ec build: adapt optimize mode to Zig 0.17 dev 2026-09-03 10:38:54 +02:00
Dusk-92 3d821c41c3 ci: build 2.4-B1 with Zig 0.17 dev fastcall fix 2026-09-03 10:36:39 +02:00
Dusk-92 39b0097ed2 build: declare x86 fastcall convention explicitly 2026-09-03 10:35:20 +02:00
Dusk-92 232f37d762 fix: force verified x86 fastcall ABI for 2.4-B1 GC 2026-09-03 10:34:29 +02:00
Dusk-92 762034301a build: use Zig 0.16 winapi calling convention 2026-09-02 23:45:52 +02:00
Dusk-92 9d5d7bd0d6 docs: document strict 2.4-A plus GC companion A/B 2026-09-02 23:44:19 +02:00
Dusk-92 f72686f27a build: add Zig 0.16 package fingerprint 2026-09-02 23:44:16 +02:00
Dusk-92 3e5628f198 test: isolate 2.4-B GC hooks from 2.4-A lifecycle 2026-09-02 23:44:14 +02:00
Dusk-92 1cc0ca7e29 ci: compile 2.4-B before format cleanup 2026-09-02 23:39:51 +02:00
Dusk-92 cfc584ebfa ci: build 2.4-B GC test artifact 2026-09-02 23:39:11 +02:00
Dusk-92 1a0aad3223 docs: document 2.4-B GC A/B test 2026-09-02 23:39:09 +02:00
Dusk-92 368ae01c64 test: pin zhook for 2.4-B GC build 2026-09-02 23:39:07 +02:00
Dusk-92 b4f9cca516 test: add standalone 2.4-B GC build 2026-09-02 23:39:04 +02:00
Dusk-92 bb6e6b9236 test: add 2.4-B GC safe-sweep companion 2026-09-02 23:39:02 +02:00
8 changed files with 840 additions and 0 deletions
+121
View File
@@ -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
+131
View File
@@ -0,0 +1,131 @@
name: Build WeirdPerformance 2.4-B2 GC reload-guard test
on:
push:
branches:
- test/wp24b2-gc-reload-guard
paths:
- 'experiments/wp24b-gc/**'
- '.github/workflows/wp24b2-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
python3 b2_patch.py
grep -q 'NATIVE_GRACE_COLLECTIONS: u32 = 3' main.zig
grep -q '2.4-B2-gc-safe-sweep-reload-guard' main.zig
grep -q 'weirdperformance_gc24b2' build.zig
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-safe-sweep-reload-guard'
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+700]):
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+500]):
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
sha256sum main.zig build.zig b2_patch.py > PATCHED_SOURCE_SHA256.txt
- uses: actions/upload-artifact@v4
with:
name: WeirdPerformance-2.4-B2-GC-Reload-Guard
path: |
experiments/wp24b-gc/zig-out/bin/weirdperformance_gc24b2.dll
experiments/wp24b-gc/SHA256SUMS.txt
experiments/wp24b-gc/PATCHED_SOURCE_SHA256.txt
experiments/wp24b-gc/BINARY_INFO.txt
experiments/wp24b-gc/ABI_DISASM.txt
experiments/wp24b-gc/README-B2.md
experiments/wp24b-gc/b2_patch.py
if-no-files-found: error
+53
View File
@@ -0,0 +1,53 @@
# WeirdPerformance 2.4-B2 — GC Safe Sweep Reload Guard
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`
## B2 variant
Add:
- `weirdperformance_gc24b2.dll`
B2 is generated from the ABI-fixed B1 source with one isolated behavioral change:
- whenever a new Lua `global_State` is observed (initial world entry, `/reload`, logout/relog), the first **3 GC collections stay fully native**;
- after those 3 native collections, the exact B1 incremental rootgc safe-sweep behavior resumes.
Nothing else is changed: same 50,000-object chunk size, same native mark/userdata/string work, same birth-mark ownership checks, same `lua_close` reconnect safety, same `IS_IN_WORLD` guard, no allocator changes, no generational age bitmap, no write barriers, no profiling/RDTSC.
The B2 delta is applied reproducibly by `b2_patch.py`; the build fails if any expected B1 source anchor no longer matches.
## Why this test exists
A delayed crash can be caused by corruption that happened earlier, so the crashing stack does not have to contain the GC companion. B2 specifically reduces risk during Lua state recreation without changing the core GC experiment.
## Installation
Keep both DLLs next to WoW and list both in `dlls.txt`:
```text
weirdperformance.dll
weirdperformance_gc24b2.dll
```
Remove `weirdperformance_gc24b1.dll` while testing B2. Never load B1 and B2 together.
Removing `weirdperformance_gc24b2.dll` returns exactly to the validated 2.4-A baseline.
## First test
1. Launch and enter the world.
2. Play normally for 1015 minutes.
3. Do one `/reload`, then wait and play for several minutes.
4. If clean, do 10 `/reload` total.
5. Then test logout/relog and character changes.
6. Keep all other DLLs/addons/settings unchanged for the A/B comparison.
If an ERROR #132 occurs, keep both `Crash.txt` and `Crash.dmp` and note what happened shortly before the crash.
+46
View File
@@ -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.
+79
View File
@@ -0,0 +1,79 @@
from pathlib import Path
root = Path(__file__).resolve().parent
main_path = root / "main.zig"
build_path = root / "build.zig"
main = main_path.read_text(encoding="utf-8")
build = build_path.read_text(encoding="utf-8")
replacements = [
(
"const BATCH_HEADROOM: u32 = 128 * 1024;\n",
"const BATCH_HEADROOM: u32 = 128 * 1024;\n"
"const NATIVE_GRACE_COLLECTIONS: u32 = 3;\n",
),
(
"var installed = false;\nvar in_gc = false;\nvar closing_lua = false;\n",
"var installed = false;\nvar in_gc = false;\nvar closing_lua = false;\n"
"var active_g: u32 = 0;\n"
"var native_grace_collections: u32 = 0;\n",
),
(
" // Never carry private list state into another Lua global_State.\n"
" if (sweeping and g != saved_g) {\n"
" resetSweepState();\n"
" collect_hook.callOriginal(.{L});\n"
" return;\n"
" }\n\n"
" // If another module changed the birth byte during our split cycle,\n",
" // Never carry private list state into another Lua global_State.\n"
" if (sweeping and g != saved_g) {\n"
" resetSweepState();\n"
" collect_hook.callOriginal(.{L});\n"
" return;\n"
" }\n\n"
" // B2: when a fresh Lua global_State appears (initial world entry,\n"
" // /reload, logout/relog), keep the first three collections fully\n"
" // native. This gives WoW ownership of the fragile state-rebuild\n"
" // window before incremental rootgc sweeping resumes.\n"
" if (active_g != g) {\n"
" active_g = g;\n"
" native_grace_collections = NATIVE_GRACE_COLLECTIONS;\n"
" }\n"
" if (native_grace_collections != 0) {\n"
" native_grace_collections -= 1;\n"
" collect_hook.callOriginal(.{L});\n"
" return;\n"
" }\n\n"
" // If another module changed the birth byte during our split cycle,\n",
),
(
" reconnectSweep();\n in_gc = false;\n\n lua_close_hook.callOriginal(.{L});\n",
" reconnectSweep();\n in_gc = false;\n"
" active_g = 0;\n"
" native_grace_collections = 0;\n\n"
" lua_close_hook.callOriginal(.{L});\n",
),
(
'const version: [*:0]const u8 = "2.4-B1-gc-safe-sweep-abi";\n',
'const version: [*:0]const u8 = "2.4-B2-gc-safe-sweep-reload-guard";\n',
),
]
for old, new in replacements:
count = main.count(old)
if count != 1:
raise SystemExit(f"main.zig patch anchor mismatch: expected 1, found {count}: {old[:80]!r}")
main = main.replace(old, new, 1)
old_name = '.name = "weirdperformance_gc24b1",'
new_name = '.name = "weirdperformance_gc24b2",'
if build.count(old_name) != 1:
raise SystemExit("build.zig output-name anchor mismatch")
build = build.replace(old_name, new_name, 1)
main_path.write_text(main, encoding="utf-8")
build_path.write_text(build, encoding="utf-8")
print("B2 patch applied: new-global_State native grace = 3 GC cycles")
+30
View File
@@ -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_gc24b1",
.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);
}
+16
View File
@@ -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",
},
}
+364
View File
@@ -0,0 +1,364 @@
//! 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.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 BIRTH_MARK_ADDR: usize = 0x6F7B37;
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;
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;
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;
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;
}
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();
collect_hook.callOriginal(.{L});
}
fn collectGarbageDetour(L: u32) callconv(X86_FASTCALL) void {
if (closing_lua) {
collect_hook.callOriginal(.{L});
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;
in_gc = true;
defer in_gc = false;
const g = getGlobalState(L);
if (g == 0) {
collect_hook.callOriginal(.{L});
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)) {
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);
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;
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;
}
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 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.
closing_lua = true;
reconnectSweep();
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 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) {
lua_close_hook.detach();
return;
}
installed = true;
}
const version: [*:0]const u8 = "2.4-B1-gc-safe-sweep-abi";
pub export fn WeirdPerformanceGC24B_GetVersion() callconv(.c) [*:0]const u8 {
return version;
}
pub export fn WeirdPerformanceGC24B_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);
}