From c770e053aac1d5238a3299c27429feafe643e52e Mon Sep 17 00:00:00 2001 From: MarcelineVQ Date: Thu, 12 Mar 2026 23:38:36 -0700 Subject: [PATCH] Add addonperf stub module for TBC+ addon profiling API MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Registers GetAddOnMemoryUsage, UpdateAddOnMemoryUsage, GetAddOnCPUUsage, UpdateAddOnCPUUsage, ResetAddOnCPUUsage, and GetScriptCPUUsage as global Lua functions. All stubs for now — implementation will hook lua_Alloc and FrameScript dispatch for per-addon tracking. --- build.zig | 1 + src/addonperf/addonperf.zig | 100 ++++++++++++++++++++++++++++++++++++ src/main.zig | 28 ++++++++++ 3 files changed, 129 insertions(+) create mode 100644 src/addonperf/addonperf.zig diff --git a/build.zig b/build.zig index a6f1a7f..bca6765 100644 --- a/build.zig +++ b/build.zig @@ -29,6 +29,7 @@ const module_list = [_]ModuleDesc{ .{ .name = "clickthrough", .desc = "Enable GO click-through (enlarge GO model bounds)" }, .{ .name = "dpslog", .desc = "Enable structured combat log events for addons", .default = false }, .{ .name = "transform44", .desc = "Enable transformMatrix4x4 hook", .default = false }, + .{ .name = "addonperf", .desc = "Enable addon memory/CPU profiling API" }, }; pub fn build(b: *std.Build) void { diff --git a/src/addonperf/addonperf.zig b/src/addonperf/addonperf.zig new file mode 100644 index 0000000..cf26c85 --- /dev/null +++ b/src/addonperf/addonperf.zig @@ -0,0 +1,100 @@ +//! addonperf — Expose addon memory and CPU usage to Lua. +//! +//! Vanilla WoW (1.12.1) lacks the TBC+ addon profiling API: +//! GetAddOnMemoryUsage(index|name) -> kB +//! UpdateAddOnMemoryUsage() +//! GetAddOnCPUUsage(index|name) -> ms +//! UpdateAddOnCPUUsage() +//! ResetAddOnCPUUsage(index|name) +//! GetScriptCPUUsage() -> enabled +//! +//! This module hooks the Lua engine to track per-addon memory and CPU, +//! then registers these functions so addons like !Swatter / Blizzard_DebugTools +//! can query performance data. + +const std = @import("std"); +const hook = @import("zhook"); +const logging = @import("../logging.zig"); +const mod_mutex = @import("../mutex.zig"); + +pub const module_name: [*:0]const u8 = "addonperf"; + +var g_mutex: ?*anyopaque = null; +var g_is_hook_owner: bool = false; +var log: logging.Logger = .{}; + +pub fn isActive() bool { + return g_is_hook_owner; +} + +// ============================================================================= +// Lua API stubs +// ============================================================================= + +/// GetAddOnMemoryUsage(index|"name") -> kB (float) +/// Returns memory used by the specified addon in kilobytes. +pub fn luaGetAddOnMemoryUsage(L: *anyopaque) callconv(.c) u32 { + // TODO: track per-addon Lua memory via lua_gc / allocator hooks + _ = L; + return 0; // pushes nothing — returns 0 results +} + +/// UpdateAddOnMemoryUsage() +/// Triggers a memory usage snapshot for all addons. +pub fn luaUpdateAddOnMemoryUsage(L: *anyopaque) callconv(.c) u32 { + _ = L; + return 0; +} + +/// GetAddOnCPUUsage(index|"name") -> totalMs, recentMs +/// Returns total and recent CPU time for the specified addon. +pub fn luaGetAddOnCPUUsage(L: *anyopaque) callconv(.c) u32 { + // TODO: hook FrameScript dispatch to measure per-addon handler time + _ = L; + return 0; +} + +/// UpdateAddOnCPUUsage() +/// Triggers a CPU usage snapshot for all addons. +pub fn luaUpdateAddOnCPUUsage(L: *anyopaque) callconv(.c) u32 { + _ = L; + return 0; +} + +/// ResetAddOnCPUUsage(index|"name") +/// Resets CPU counters for the specified addon (or all if no arg). +pub fn luaResetAddOnCPUUsage(L: *anyopaque) callconv(.c) u32 { + _ = L; + return 0; +} + +/// GetScriptCPUUsage() -> enabled (1/nil) +/// Returns whether CPU profiling is active. +pub fn luaGetScriptCPUUsage(L: *anyopaque) callconv(.c) u32 { + const L_ptr = @intFromPtr(L); + // Always enabled when this module is loaded + hook.call(fn (usize, i32) callconv(hook.cc.fastcall) void, 0x6F36B0, .{ L_ptr, @as(i32, 1) }); // lua_pushboolean + return 1; +} + +// ============================================================================= +// Install / remove +// ============================================================================= + +pub fn installHooks() void { + const result = mod_mutex.acquire(module_name); + g_mutex = result.handle; + g_is_hook_owner = result.is_owner; + if (!g_is_hook_owner) return; + + log = logging.Logger.open(module_name, .both); + log.print("addonperf: stub module loaded\n"); +} + +pub fn removeHooks() void { + if (g_is_hook_owner) { + log.close(); + mod_mutex.release(&g_mutex); + } + g_is_hook_owner = false; +} diff --git a/src/main.zig b/src/main.zig index e558791..7d6c04a 100644 --- a/src/main.zig +++ b/src/main.zig @@ -19,6 +19,7 @@ const build_opts = struct { const clickthrough = @import("build_options").enable_clickthrough; const dpslog = @import("build_options").enable_dpslog; const transform44 = @import("build_options").enable_transform44; + const addonperf = @import("build_options").enable_addonperf; }; // Conditional module imports @@ -36,9 +37,21 @@ const bigcursor = if (build_opts.bigcursor) @import("bigcursor/bigcursor.zig") e const clickthrough = if (build_opts.clickthrough) @import("clickthrough/clickthrough.zig") else struct {}; const dpslog = if (build_opts.dpslog) @import("dpslog/dpslog.zig") else struct {}; const transform44 = if (build_opts.transform44) @import("transform44/transform44.zig") else struct {}; +const addonperf = if (build_opts.addonperf) @import("addonperf/addonperf.zig") else struct {}; const WINAPI = std.builtin.CallingConvention.winapi; +const SYSTEMTIME = extern struct { + wYear: u16, + wMonth: u16, + wDayOfWeek: u16, + wDay: u16, + wHour: u16, + wMinute: u16, + wSecond: u16, + wMilliseconds: u16, +}; + // ============================================================================= // Lua Protection Bypass // ============================================================================= @@ -99,6 +112,14 @@ fn registerLuaFunctions() void { if (build_opts.transform44) { registerFunction("SetWeatherOverride", @intFromPtr(&transform44.luaSetWeatherOverride)); } + if (build_opts.addonperf) { + registerFunction("GetAddOnMemoryUsage", @intFromPtr(&addonperf.luaGetAddOnMemoryUsage)); + registerFunction("UpdateAddOnMemoryUsage", @intFromPtr(&addonperf.luaUpdateAddOnMemoryUsage)); + registerFunction("GetAddOnCPUUsage", @intFromPtr(&addonperf.luaGetAddOnCPUUsage)); + registerFunction("UpdateAddOnCPUUsage", @intFromPtr(&addonperf.luaUpdateAddOnCPUUsage)); + registerFunction("ResetAddOnCPUUsage", @intFromPtr(&addonperf.luaResetAddOnCPUUsage)); + registerFunction("GetScriptCPUUsage", @intFromPtr(&addonperf.luaGetScriptCPUUsage)); + } if (build_opts.worldmarkers and markers.isActive()) { // User-facing functions stay global registerFunction("WorldMarker", @intFromPtr(&markers.luaWorldMarker)); @@ -190,6 +211,7 @@ const LoadModelFn = fn (u32, u32, u32) callconv(hook.cc.thiscall) u32; var model_load_hook: hook.Detour(LoadModelFn) = .{}; // Windows API imports for async handling +extern "kernel32" fn GetLocalTime(lpSystemTime: *SYSTEMTIME) callconv(WINAPI) void; extern "kernel32" fn EnterCriticalSection(lpCriticalSection: *anyopaque) callconv(WINAPI) void; extern "kernel32" fn LeaveCriticalSection(lpCriticalSection: *anyopaque) callconv(WINAPI) void; extern "kernel32" fn SetEvent(hEvent: *anyopaque) callconv(WINAPI) i32; @@ -227,6 +249,11 @@ fn openFileDetour( flags: u32, handle_out: *u32, ) callconv(hook.cc.stdcall) u32 { + var st: SYSTEMTIME = undefined; + GetLocalTime(&st); + log.to(.file).fmt("{d}/{d} {d:0>2}:{d:0>2}:{d:0>2}.{d:0>3} {s}\n", .{ + st.wMonth, st.wDay, st.wHour, st.wMinute, st.wSecond, st.wMilliseconds, std.mem.span(path), + }); if (findEmbeddedFile(path)) |entry| { // Allocate and zero-fill 0x60-byte file context const ctx = allocateGameBuffer(0x60) orelse { @@ -619,6 +646,7 @@ const modules = [_]ModuleHooks{ if (build_opts.clickthrough) .{ .name = clickthrough.module_name, .install = clickthrough.installHooks, .remove = clickthrough.removeHooks, .is_active = clickthrough.isActive } else .{}, if (build_opts.dpslog) .{ .name = dpslog.module_name, .install = dpslog.installHooks, .remove = dpslog.removeHooks, .is_active = dpslog.isActive } else .{}, if (build_opts.transform44) .{ .name = transform44.module_name, .install = transform44.installHooks, .remove = transform44.removeHooks, .is_active = transform44.isActive } else .{}, + if (build_opts.addonperf) .{ .name = addonperf.module_name, .install = addonperf.installHooks, .remove = addonperf.removeHooks, .is_active = addonperf.isActive } else .{}, if (build_opts.worldmarkers) .{ .name = markers.module_name, .install = markers.installHooks, .remove = markers.removeHooks, .is_active = markers.isActive } else .{}, if (build_opts.interact) .{ .name = interact.module_name, .install = interact.installHooks, .remove = interact.removeHooks, .is_active = interact.isActive } else .{}, if (build_opts.outline) .{ .name = outline.module_name, .remove = outline.cleanup, .is_active = outline.isActive } else .{},