Compare commits
24 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| a2b8d2a7e0 | |||
| 5dde1f641e | |||
| d6262135a8 | |||
| 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,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
|
||||
@@ -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 10–15 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.
|
||||
@@ -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,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")
|
||||
@@ -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);
|
||||
}
|
||||
@@ -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,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);
|
||||
}
|
||||
Reference in New Issue
Block a user