Add module control API, bigcursor with fractional scaling, shared mutex

Module control API:
- Three cdecl exports: WeirdUtils_IsModuleActive, WeirdUtils_DisableModule, WeirdUtils_DisableAll
- C header include/weirdutils_api.h for runtime DLL discovery
- All modules gain pub module_name, isActive(), shared mutex via src/mutex.zig
- build.zig refactored to module_list array of ModuleDesc

Bigcursor:
- D3D9 vtable hooks on SetCursorProperties/ShowCursor
- Scale2x/Scale3x pixel-art upscalers in pure Zig (replaces 12K-line hqx C)
- Bilinear resampler for fractional scales (1.0-4.0, default 1.2)
- Win32 cursor via CreateIconIndirect bypasses D3D9 32x32 limit
- FNV-1a hash cache for ~16 cursor bitmaps
- Lua API: SetCursorScale(n) / GetCursorScale()
- CVar cursorScale for Config.wtf persistence (tenths)

Other:
- Add dpslog module stub
- Docs and README updates
This commit is contained in:
MarcelineVQ
2026-03-06 13:27:35 -08:00
parent 0783f3b0dd
commit 2aaa089c76
44 changed files with 1904 additions and 1022 deletions
+6 -6
View File
@@ -1,5 +1,5 @@
//! Minimal PNG encoder with deflate compression.
//! No large struct literals or lookup tables — entire module adds ~2KB to .rdata.
//! No large struct literals or lookup tables - entire module adds ~2KB to .rdata.
//! Supports store blocks (level 0) and fixed-Huffman encoding (levels 1-9).
const std = @import("std");
@@ -72,7 +72,7 @@ pub fn encode(
}
// =============================================================================
// Stream wrapper — tracks CRC and Adler inline
// Stream wrapper - tracks CRC and Adler inline
// =============================================================================
fn Stream(comptime Ctx: type) type {
@@ -140,7 +140,7 @@ fn Stream(comptime Ctx: type) type {
}
// =============================================================================
// Store blocks (level 0) — no compression, byte-aligned
// Store blocks (level 0) - no compression, byte-aligned
// =============================================================================
fn writeIdatStore(s: anytype, pixels: [*]const u8, w: u32, h: u32) void {
@@ -204,7 +204,7 @@ fn writeIdatStore(s: anytype, pixels: [*]const u8, w: u32, h: u32) void {
}
// =============================================================================
// Fixed Huffman + LZ77 — RFC 1951 §3.2.6 fixed codes with back-references
// Fixed Huffman + LZ77 - RFC 1951 §3.2.6 fixed codes with back-references
//
// Sub filter makes adjacent pixel differences small (often zero in flat areas).
// LZ77 with hash-table matching finds repeated byte sequences within a 32KB
@@ -339,7 +339,7 @@ fn writeIdatFixedHuffman(s: anytype, pixels: [*]const u8, w: u32, h: u32, level:
};
defer std.heap.page_allocator.free(out_buf);
// Hash chains for LZ77 matching — chain depth = 2 * level (stb approach)
// Hash chains for LZ77 matching - chain depth = 2 * level (stb approach)
const ZHASH: u32 = 16384;
const chain_depth: u32 = @as(u32, level) * 2;
const chain_mem = std.heap.page_allocator.alloc(u32, ZHASH * chain_depth) catch {
@@ -425,7 +425,7 @@ fn writeIdatFixedHuffman(s: anytype, pixels: [*]const u8, w: u32, h: u32, level:
const prev2 = chain_mem[base2 + j];
if ((i + 1) -% prev2 <= WINDOW) {
if (countMatch(filtered[0..raw_size], prev2, i + 1) > best_len) {
best_dist = 0; // cancel — next position is better
best_dist = 0; // cancel - next position is better
break;
}
}
+22 -35
View File
@@ -61,7 +61,7 @@ var tga_hook: hook.Detour(TgaWriteFn) = .{};
var screenshot_dir: [260]u8 = undefined;
var screenshot_dir_len: usize = 0;
var screenshot_counter: u8 = 0;
var last_screenshot_time: u64 = 0; // packed YMDHMS — resets counter on new second
var last_screenshot_time: u64 = 0; // packed YMDHMS - resets counter on new second
// =============================================================================
// Ring buffer queue (max 8 pending screenshots)
@@ -83,9 +83,17 @@ var queue_tail: usize = 0;
var queue_count: usize = 0;
var mutex: std.atomic.Mutex = .unlocked;
var worker_running: bool = false;
const mod_mutex = @import("../mutex.zig");
pub const module_name: [*:0]const u8 = "screenshot";
var g_mutex: ?HANDLE = null;
var g_is_hook_owner: bool = false;
pub fn isActive() bool {
return g_is_hook_owner;
}
fn enqueue(shot: PendingScreenshot) bool {
if (queue_count >= MAX_PENDING) return false;
queue[queue_tail] = shot;
@@ -103,7 +111,7 @@ fn dequeue() ?PendingScreenshot {
}
// =============================================================================
// Directory extraction — capture path prefix from game's first TGA filename
// Directory extraction - capture path prefix from game's first TGA filename
// =============================================================================
fn extractDir(filename_ptr: u32) void {
@@ -120,7 +128,7 @@ fn extractDir(filename_ptr: u32) void {
}
// =============================================================================
// Call original CTgaFile::Write — __thiscall(self_ECX, filename_stack) ret 4
// Call original CTgaFile::Write - __thiscall(self_ECX, filename_stack) ret 4
// =============================================================================
fn callOriginal(self: u32, filename: u32) i32 {
@@ -133,7 +141,6 @@ fn callOriginal(self: u32, filename: u32) i32 {
// =============================================================================
fn tgaWriteDetour(self: u32, filename: u32) callconv(tc) i32 {
if (!enabled) return callOriginal(self, filename);
// Validate TGA header fields
@@ -182,7 +189,7 @@ fn tgaWriteDetour(self: u32, filename: u32) callconv(tc) i32 {
}
// =============================================================================
// Worker thread — dequeues shots, converts BGR→RGB, writes PNG
// Worker thread - dequeues shots, converts BGR→RGB, writes PNG
// =============================================================================
fn workerThread() void {
@@ -220,7 +227,7 @@ fn processScreenshot(shot: PendingScreenshot) void {
var st: SYSTEMTIME = undefined;
GetLocalTime(&st);
// Pack timestamp into a single comparable value — reset counter on new second
// Pack timestamp into a single comparable value - reset counter on new second
const now: u64 = @as(u64, st.wYear) << 32 | @as(u64, st.wMonth) << 24 |
@as(u64, st.wDay) << 16 | @as(u64, st.wHour) << 10 |
@as(u64, st.wMinute) << 4 | @as(u64, st.wSecond);
@@ -288,8 +295,7 @@ fn luaPushNumber(L_ptr: usize, n: f64) void {
[lo] "r" (raw[0]),
[hi] "r" (raw[1]),
[func] "r" (@as(u32, 0x6F3810)),
: .{ .eax = true, .ecx = true, .edx = true, .memory = true, .cc = true }
);
: .{ .eax = true, .ecx = true, .edx = true, .memory = true, .cc = true });
}
// =============================================================================
@@ -303,7 +309,7 @@ fn luaPushNumber(L_ptr: usize, n: f64) void {
pub fn screenshotCommand(L: *anyopaque) callconv(.c) u32 {
const L_ptr = @intFromPtr(L);
// lua_gettop(L) — __fastcall(L_ECX), EDX unused
// lua_gettop(L) - __fastcall(L_ECX), EDX unused
const nargs = hook.fastcall(i32, 0x6F3070, L_ptr, @as(u32, 0));
if (nargs == 0) {
@@ -314,7 +320,7 @@ pub fn screenshotCommand(L: *anyopaque) callconv(.c) u32 {
return 2;
}
// lua_tostring(L, 1) — __fastcall(L_ECX, index_EDX)
// lua_tostring(L, 1) - __fastcall(L_ECX, index_EDX)
const raw_str = hook.fastcall(usize, 0x6F3690, L_ptr, @as(i32, 1));
if (raw_str != 0) {
const str: [*:0]const u8 = @ptrFromInt(raw_str);
@@ -326,7 +332,7 @@ pub fn screenshotCommand(L: *anyopaque) callconv(.c) u32 {
enabled = false;
} else if (std.mem.eql(u8, arg, "quality")) {
if (nargs >= 2) {
// lua_tonumber(L, 2) — __fastcall(L_ECX, index_EDX), returns f64 in ST(0)
// lua_tonumber(L, 2) - __fastcall(L_ECX, index_EDX), returns f64 in ST(0)
const level = hook.fastcall(f64, 0x6F3620, L_ptr, @as(i32, 2));
compression_level = std.math.clamp(@as(i32, @intFromFloat(level)), 0, 9);
}
@@ -343,22 +349,10 @@ pub fn screenshotCommand(L: *anyopaque) callconv(.c) u32 {
pub fn installHook() void {
con.print("[screenshot] Module loaded\n");
// Multi-DLL safety: only one instance per process should hook
var mutex_name_buf: [64]u8 = undefined;
const mutex_name = std.fmt.bufPrint(&mutex_name_buf, "Local\\WeirdUtils_ScreenshotHook_{d}", .{GetCurrentProcessId()}) catch return;
mutex_name_buf[mutex_name.len] = 0;
g_mutex = CreateMutexA(null, 1, @ptrCast(mutex_name_buf[0..mutex_name.len :0]));
if (g_mutex == null) return;
if (GetLastError() == ERROR_ALREADY_EXISTS) {
_ = CloseHandle(g_mutex.?);
g_mutex = null;
g_is_hook_owner = false;
con.print("[screenshot] Another DLL owns hooks (mutex taken), skipping\n");
return;
}
g_is_hook_owner = true;
const result = mod_mutex.acquire(module_name);
g_mutex = result.handle;
g_is_hook_owner = result.is_owner;
if (!g_is_hook_owner) return;
// CTgaFile::Write at 0x5a4810
// __thiscall(self, filename) ret 4
@@ -374,14 +368,7 @@ pub fn installHook() void {
pub fn removeHook() void {
if (g_is_hook_owner) {
tga_hook.detach();
}
if (g_is_hook_owner) {
if (g_mutex) |m| {
_ = ReleaseMutex(m);
_ = CloseHandle(m);
g_mutex = null;
}
mod_mutex.release(&g_mutex);
}
g_is_hook_owner = false;
}