weirdperformance: consolidate luastr and luavm as sub-modules
Drop the top-level luastr and luavm module flags and wire both through weirdperformance, matching the existing luaalloc/luagc sub-module pattern. Also removes luavm's A/B rdtsc instrumentation now that the newlstr hash pre-check is production-only. - build.zig: remove luastr/luavm from module_list - DLL_README.md: add Lua Runtime bullet under Performance, swap em/en-dashes for ASCII - src/luavm/: delete (files moved into src/weirdperformance/ which was already the tracked location in HEAD)
This commit is contained in:
+7
-6
@@ -234,7 +234,7 @@ This value is saved to the `cursorScale` CVar in tenths: `/script SetCVar("curso
|
||||
|
||||
Lua API for addon developers:
|
||||
|
||||
- `SetCursorScale(n)` -- set scale factor (1.0–4.0), takes effect on next cursor change
|
||||
- `SetCursorScale(n)` -- set scale factor (1.0-4.0), takes effect on next cursor change
|
||||
- `GetCursorScale()` -- returns current scale factor
|
||||
|
||||
**DLL:** `bigcursor.dll`
|
||||
@@ -245,12 +245,13 @@ Lua API for addon developers:
|
||||
|
||||
Engine-level optimizations that reduce CPU time on math, rendering helpers, file lookups, and data decompression.
|
||||
|
||||
- **SIMD Math** — replaces 20+ internal math functions with SSE/AVX equivalents covering skeletal animation, particle rendering, frustum culling, collision detection, text glyph caching, and float-to-integer conversion
|
||||
- **Data Decompression** — swaps the game's 2004-era zlib with a modern library (2.2x faster). Loading screen times reduced by at least 13%
|
||||
- **MPQ File Cache** — caches archive file lookups so repeat file opens skip the archive chain walk. Saving 50-160ms every 15 seconds during heavy gameplay
|
||||
- **Timer Calibration** — recalibrates the OS performance counter for accurate animation timing. Ported from [VanillaFixes](https://github.com/hannesmann/vanillafixes)
|
||||
- **SIMD Math** -- replaces 20+ internal math functions with SSE/AVX equivalents covering skeletal animation, particle rendering, frustum culling, collision detection, text glyph caching, and float-to-integer conversion
|
||||
- **Data Decompression** -- swaps the game's 2004-era zlib with a modern library (2.2x faster). Loading screen times reduced by at least 13%
|
||||
- **MPQ File Cache** -- caches archive file lookups so repeat file opens skip the archive chain walk. Saving 50-160ms every 15 seconds during heavy gameplay
|
||||
- **Timer Calibration** -- recalibrates the OS performance counter for accurate animation timing. Ported from [VanillaFixes](https://github.com/hannesmann/vanillafixes)
|
||||
- **Lua Runtime** -- custom slab allocator (O(1) free/realloc), incremental/generational GC (turns the ~5s stop-the-world freeze into ~9ms chunks), faster string interning (~40% on `luaS_newlstr`), and a literal prefilter on `string.find`/`gfind`/`gsub` that kills the O(n²) backtracking addon combat log parsers inflict on every chat message
|
||||
|
||||
Most noticeable in cities, raids, and during zone transitions.
|
||||
Most noticeable in cities, raids, during zone transitions, and in addon-heavy setups.
|
||||
|
||||
**DLL:** `weirdperformance.dll`
|
||||
|
||||
|
||||
@@ -35,9 +35,9 @@ const module_list = [_]ModuleDesc{
|
||||
.{ .name = "addonperf", .version = "1.0", .desc = "Enable addon memory/CPU profiling API", .default = false },
|
||||
.{ .name = "ssemaths", .version = "1.0", .desc = "Enable UnitXP x87 math polyfill replacements (SSE)", .default = false },
|
||||
.{ .name = "silicon", .version = "1.0", .desc = "Enable SSE2 math replacements (ported from libSiliconPatch)", .default = false },
|
||||
.{ .name = "weirdperformance", .version = "1.1.1", .desc = "Enable production performance optimizations (SSE, inflate, filecache, timer)", .default = true },
|
||||
.{ .name = "luavm", .version = "0.1", .desc = "Enable Lua VM hotspot optimizations (hash, string intern, opcode SSE)", .default = false },
|
||||
.{ .name = "weirdperformance", .version = "1.1.1", .desc = "Enable production performance optimizations (SSE, inflate, filecache, timer, luastr, luavm)", .default = true },
|
||||
.{ .name = "superweirdo", .version = "0.1", .desc = "Enable GO loot sparkle on interactable objects", .default = false },
|
||||
.{ .name = "luagc", .version = "0.1", .desc = "Enable incremental Lua GC (replaces stop-the-world mark+sweep)", .default = false },
|
||||
};
|
||||
|
||||
pub fn build(b: *std.Build) void {
|
||||
|
||||
@@ -1,67 +0,0 @@
|
||||
# Lua 5.0 GC Write Barrier in lua_vm_execute
|
||||
|
||||
## Location
|
||||
|
||||
`lua_vm_execute` (0x6F8720), the main VM interpreter loop.
|
||||
The barrier appears after every TValue copy (~20 sites in the function).
|
||||
|
||||
## Globals
|
||||
|
||||
- `0xCEEAC0` -- GC barrier value (current white marker / gc object pointer)
|
||||
- `0xCEEAC4` -- GC barrier flag (non-zero = barrier is active)
|
||||
|
||||
## Pattern (from disassembly)
|
||||
|
||||
After copying a 16-byte TValue (type_tag, gc_ptr, value_lo, value_hi):
|
||||
|
||||
```asm
|
||||
mov eax, [dst + 4] ; eax = copied gc_ptr field
|
||||
test eax, eax
|
||||
jz skip ; NULL gc_ptr -> no barrier needed
|
||||
cmp dword ptr [0xCEEAC4], 0
|
||||
jz skip ; barrier disabled -> skip
|
||||
mov [0xCEEAC0], eax ; mark: write gc_ptr into barrier global
|
||||
skip:
|
||||
```
|
||||
|
||||
## What it does
|
||||
|
||||
The gc_ptr field at TValue+0x04 holds a pointer to a GC-managed object
|
||||
(string, table, closure, userdata) or NULL for non-collectable types
|
||||
(number, boolean, nil, lightuserdata).
|
||||
|
||||
When a TValue is copied (MOVE, GETGLOBAL, GETTABLE, LOADK, etc.), the
|
||||
barrier checks:
|
||||
1. Is the copied value a GC object? (gc_ptr != NULL)
|
||||
2. Is the write barrier active? (flag at 0xCEEAC4 != 0)
|
||||
3. If both true, write the gc_ptr to the barrier global at 0xCEEAC0
|
||||
|
||||
This is Lua 5.0's incremental GC write barrier. It tracks which GC objects
|
||||
have been moved/copied so the collector knows which objects are reachable
|
||||
from newly-written locations. The barrier global accumulates the "last written"
|
||||
gc object -- the actual GC uses this to avoid rescanning the full root set.
|
||||
|
||||
## Opcodes that trigger the barrier
|
||||
|
||||
Every opcode that writes a TValue to a register or table slot:
|
||||
- MOVE (op 0)
|
||||
- LOADK (op 1) -- loads constant, has gc_ptr for string constants
|
||||
- GETUPVAL (op 4)
|
||||
- GETGLOBAL (op 5)
|
||||
- GETTABLE (op 6)
|
||||
- SETGLOBAL (op 7) -- barrier on the table side
|
||||
- SETTABLE (op 9)
|
||||
- NEWTABLE (op 10)
|
||||
- SELF (op 11)
|
||||
- CONCAT (op 21)
|
||||
- CLOSURE (op 30)
|
||||
- FORLOOP (op 23) -- number only, but barrier still present
|
||||
|
||||
## Relevance to luagc module
|
||||
|
||||
Our luagc module (in weirdperformance) hooks `lua_gc_step` (0x6FAE00).
|
||||
Understanding the barrier globals is useful for:
|
||||
- Knowing when/how often the barrier fires during heavy addon activity
|
||||
- Potentially batching barrier writes if we ever replace the GC step
|
||||
- The flag at 0xCEEAC4 could be used to temporarily disable the barrier
|
||||
during bulk operations (dangerous -- must re-enable before GC runs)
|
||||
@@ -1,198 +0,0 @@
|
||||
//! luavm -- Lua VM hotspot optimizations.
|
||||
//!
|
||||
//! luaS_newlstr (0x6F9D00, 1.35% CPU): hash pre-check before memcmp.
|
||||
//! A/B: alternating calls, per-call rdtsc, periodic dump via OnWorldUpdate.
|
||||
|
||||
const hook = @import("zhook");
|
||||
const logging = @import("../logging.zig");
|
||||
const mod_mutex = @import("../mutex.zig");
|
||||
|
||||
pub const module_name: [*:0]const u8 = "luavm";
|
||||
|
||||
var g_is_hook_owner: bool = false;
|
||||
var log: logging.Logger = .{};
|
||||
|
||||
pub fn isActive() bool {
|
||||
return g_is_hook_owner;
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// A/B instrumentation
|
||||
// =============================================================================
|
||||
|
||||
inline fn rdtsc() u64 {
|
||||
var lo: u32 = undefined;
|
||||
var hi: u32 = undefined;
|
||||
asm volatile ("rdtsc"
|
||||
: [lo] "={eax}" (lo),
|
||||
[hi] "={edx}" (hi),
|
||||
);
|
||||
return @as(u64, hi) << 32 | lo;
|
||||
}
|
||||
|
||||
const AB_DUMP_INTERVAL: u64 = 10000;
|
||||
|
||||
const ABStats = struct {
|
||||
cycles: u64 = 0,
|
||||
calls: u64 = 0,
|
||||
};
|
||||
|
||||
var custom_ab: ABStats = .{};
|
||||
var baseline_ab: ABStats = .{};
|
||||
|
||||
// =============================================================================
|
||||
// luaS_newlstr (0x6F9D00)
|
||||
// __fastcall(ECX=lua_State*, EDX=str_ptr, stack=len) -> TString*
|
||||
// RET 0x4
|
||||
// =============================================================================
|
||||
|
||||
const NewLStrFn = fn (u32, u32, u32) callconv(hook.cc.fastcall) u32;
|
||||
var newlstr_hook: hook.Detour(NewLStrFn) = .{};
|
||||
|
||||
var call_ctr: u32 = 0;
|
||||
|
||||
fn luaCreateStringObject(state: u32, str_ptr: u32, len: u32, hash_val: u32) u32 {
|
||||
return hook.call(
|
||||
fn (u32, u32, u32, u32) callconv(hook.cc.fastcall) u32,
|
||||
0x6F9D90,
|
||||
.{ state, str_ptr, len, hash_val },
|
||||
);
|
||||
}
|
||||
|
||||
fn newlstrDetour(state: u32, str_ptr: u32, len: u32) callconv(hook.cc.fastcall) u32 {
|
||||
asm volatile ("" ::: .{ .esi = true, .edi = true, .ebx = true });
|
||||
|
||||
const use_custom = (call_ctr & 1) == 0;
|
||||
call_ctr +%= 1;
|
||||
const t0 = rdtsc();
|
||||
|
||||
const result = if (use_custom)
|
||||
newlstrImpl(state, str_ptr, len)
|
||||
else
|
||||
newlstr_hook.callOriginal(.{ state, str_ptr, len });
|
||||
|
||||
const elapsed = rdtsc() - t0;
|
||||
if (use_custom) {
|
||||
custom_ab.cycles +|= elapsed;
|
||||
custom_ab.calls +|= 1;
|
||||
} else {
|
||||
baseline_ab.cycles +|= elapsed;
|
||||
baseline_ab.calls +|= 1;
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
fn newlstrImpl(state: u32, str_ptr: u32, len: u32) u32 {
|
||||
const str: [*]const u8 = @ptrFromInt(str_ptr);
|
||||
var h: u32 = len;
|
||||
const step: u32 = (len >> 5) + 1;
|
||||
var l1: u32 = len;
|
||||
while (l1 >= step) {
|
||||
const c: u32 = str[l1 - 1];
|
||||
h = h ^ (c +% (h << 5) +% (h >> 2));
|
||||
l1 -= step;
|
||||
}
|
||||
|
||||
const global_state: u32 = hook.readMem(u32, state + 0x10);
|
||||
const strt_hash: u32 = hook.readMem(u32, global_state + 0x04);
|
||||
const strt_size: u32 = hook.readMem(u32, global_state + 0x0C);
|
||||
|
||||
const bucket: u32 = h & (strt_size - 1);
|
||||
var ts: u32 = hook.readMem(u32, strt_hash + bucket * 4);
|
||||
|
||||
while (ts != 0) {
|
||||
const ts_len: u32 = hook.readMem(u32, ts + 0x0C);
|
||||
if (ts_len == len) {
|
||||
const ts_hash: u32 = hook.readMem(u32, ts + 0x08);
|
||||
if (ts_hash == h) {
|
||||
if (len == 0 or strEqual(str_ptr, ts + 0x10, len)) {
|
||||
return ts;
|
||||
}
|
||||
}
|
||||
}
|
||||
ts = hook.readMem(u32, ts);
|
||||
}
|
||||
|
||||
return luaCreateStringObject(state, str_ptr, len, h);
|
||||
}
|
||||
|
||||
fn strEqual(a_ptr: u32, b_ptr: u32, len: u32) bool {
|
||||
const a: [*]const u8 = @ptrFromInt(a_ptr);
|
||||
const b: [*]const u8 = @ptrFromInt(b_ptr);
|
||||
|
||||
var i: u32 = 0;
|
||||
while (i + 4 <= len) : (i += 4) {
|
||||
const va = @as(*align(1) const u32, @ptrCast(a + i)).*;
|
||||
const vb = @as(*align(1) const u32, @ptrCast(b + i)).*;
|
||||
if (va != vb) return false;
|
||||
}
|
||||
while (i < len) : (i += 1) {
|
||||
if (a[i] != b[i]) return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// OnWorldUpdate (0x482EA0) -- periodic stats dump
|
||||
// =============================================================================
|
||||
|
||||
const WorldUpdateFn = fn (u32) callconv(hook.cc.fastcall) void;
|
||||
var world_update_hook: hook.Detour(WorldUpdateFn) = .{};
|
||||
|
||||
var frame_count: u64 = 0;
|
||||
|
||||
fn worldUpdateDetour(fc: u32) callconv(hook.cc.fastcall) void {
|
||||
frame_count +%= 1;
|
||||
|
||||
if (frame_count % AB_DUMP_INTERVAL == 0 and frame_count > 0) {
|
||||
dumpStats();
|
||||
}
|
||||
|
||||
world_update_hook.callOriginal(.{fc});
|
||||
}
|
||||
|
||||
fn dumpStats() void {
|
||||
const ca = custom_ab.calls;
|
||||
const ba = baseline_ab.calls;
|
||||
const c_avg: u64 = if (ca > 0) custom_ab.cycles / ca else 0;
|
||||
const b_avg: u64 = if (ba > 0) baseline_ab.cycles / ba else 0;
|
||||
|
||||
log.fmt("[luavm] {d}f newlstr: c={d} b={d} cyc/call ({d}k calls)\n", .{
|
||||
frame_count, c_avg, b_avg, ca / 1000,
|
||||
});
|
||||
|
||||
custom_ab = .{};
|
||||
baseline_ab = .{};
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// Install / Remove
|
||||
// =============================================================================
|
||||
|
||||
pub fn installHooks() void {
|
||||
const result = mod_mutex.acquire(module_name);
|
||||
g_is_hook_owner = result.is_owner;
|
||||
if (!g_is_hook_owner) return;
|
||||
|
||||
log = logging.Logger.open(module_name, .both);
|
||||
|
||||
if (newlstr_hook.attach(0x6F9D00, &newlstrDetour) == .ok) {
|
||||
log.print(" newlstr: A/B hash pre-check vs original\n");
|
||||
}
|
||||
|
||||
if (world_update_hook.attach(0x482EA0, &worldUpdateDetour) == .ok) {
|
||||
log.print(" OnWorldUpdate: periodic stats dump\n");
|
||||
}
|
||||
|
||||
log.print("luavm: active\n");
|
||||
}
|
||||
|
||||
pub fn removeHooks() void {
|
||||
if (g_is_hook_owner) {
|
||||
dumpStats();
|
||||
world_update_hook.detach();
|
||||
newlstr_hook.detach();
|
||||
log.close();
|
||||
}
|
||||
g_is_hook_owner = false;
|
||||
}
|
||||
Reference in New Issue
Block a user