Files
WeirdUtils/src/outline/model_hook.zig
T
MarcelineVQ 79d9521953 Refactor addon system: derive module list from build.zig, prune inactive prefixes at runtime
- Eliminate hardcoded module_names in addons.zig — now derived from
  build.zig via all_module_names build option
- Add module_active.zig runtime registry: main.zig registers isActive
  pointers during install(), addons.zig queries them without importing
  each module directly
- Prune embedded file prefixes at startup: after all modules claim
  mutexes, build a runtime active_prefixes table excluding modules we
  don't own. findEmbeddedFile searches only active entries — no
  per-lookup isActive check on the hot path
- Tag both addon and asset prefixes with module_name so pruning applies
  to all embedded files for a module
- Rename src/markers/ → src/worldmarkers/, src/outline/api.zig →
  src/outline/outline.zig to follow {name}/{name}.zig convention
2026-03-13 11:40:49 -07:00

217 lines
8.6 KiB
Zig

//! WoW model rendering pipeline hooks.
//!
//! Hooks three WoW functions to integrate outline rendering:
//! - CM2SceneRenderDraw - reorders batches so outline targets render last.
//! - CM2Model_ManageRenderListNode - classifies models on render-list add.
//! - CM2Scene_DrawBatchProjected - flags the DIP hook for outline rendering.
//!
//! Calling conventions:
//! - RenderDraw & ManageRender use callconv(.x86_thiscall) - direct native detours.
//! - DrawBatchProj uses a callconv(.naked) entry point because Zig 0.15 has a
//! codegen bug with callconv(.x86_fastcall) that generates wrong ret instructions
//! for functions with ≤2 register params. The naked wrapper bridges to a cdecl
//! implementation function.
const std = @import("std");
const hook = @import("zhook");
const api = @import("outline.zig");
const o = @import("offsets.zig");
const types = @import("types.zig");
const tracker = @import("tracker.zig");
const d3d9_hook = @import("d3d9_hook.zig");
const wow = @import("../wow.zig");
// =============================================================================
// Calling convention constants
// =============================================================================
// =============================================================================
// Hook state
// =============================================================================
const RenderDrawFn = fn (u32, u32, u32, u32, u32) callconv(hook.cc.thiscall) void;
const ManageRenderFn = fn (u32, u32) callconv(hook.cc.thiscall) void;
const DrawBatchFn = fn (u32) callconv(hook.cc.thiscall) void;
var render_draw_hook: hook.Detour(RenderDrawFn) = .{};
var manage_render_hook: hook.Detour(ManageRenderFn) = .{};
var draw_batch_hook: hook.Detour(DrawBatchFn) = .{};
/// D3D9 hooks are deferred until the first model hook fires, because creating
/// a dummy D3D9 device during engine init corrupts the proxy's state.
var d3d9_deferred_pending: bool = true;
// =============================================================================
// Volatile flags shared with d3d9_hook (read by DIP hook)
// =============================================================================
/// True while DrawBatchProj is rendering an outline-target model.
pub var rendering_outline: bool = false;
/// Model pointer currently being rendered (for colour lookup in DIP).
pub var current_model: u32 = 0;
// =============================================================================
// Batch reordering
// =============================================================================
const MAX_REORDER = 1024;
var reordered_indices: [MAX_REORDER]i32 = undefined;
// =============================================================================
// CM2SceneRenderDraw hook
// =============================================================================
// __thiscall(this, viewMatrix, batchData, batchIndices, batchCount)
// Native thiscall detour - no thunk needed.
//
// Reorders batch indices into 3 groups:
// 1. Game objects/doodads - render first, write depth so outlines respect them
// 2. Outline targets - render second, DIP hook writes stencil against depth
// 3. Other players, gear, NPCs - render last, draw over targets normally
//
// This gives outlines that are occluded by world/WMO/game objects but show
// through other players and gear (since those aren't in depth when stencil
// is written). The outline composites on top of everything in EndScene.
fn renderDrawDetour(this: u32, view_matrix: u32, batch_data: u32, batch_indices: u32, batch_count: u32) callconv(hook.cc.thiscall) void {
// One-time: install D3D9 hooks now that the game is actively rendering.
if (d3d9_deferred_pending) {
d3d9_deferred_pending = false;
api.initD3D9Deferred();
}
// Skip reordering if nothing to outline or too many batches
if (!tracker.enabled or !tracker.hasTargets() or batch_count == 0 or batch_count > MAX_REORDER) {
render_draw_hook.callOriginal(.{ this, view_matrix, batch_data, batch_indices, batch_count });
return;
}
const indices: [*]i32 = @ptrFromInt(batch_indices);
// Pass 1: count outline targets and game objects for partitioning
var outline_count: u32 = 0;
var game_obj_count: u32 = 0;
for (0..batch_count) |i| {
const idx_u: u32 = @bitCast(indices[i]);
const model_ptr = hook.readMem(u32, batch_data +% idx_u *% 0x40 +% 4);
if (model_ptr != 0) {
if (tracker.findOutlineEntry(model_ptr) != null) {
outline_count += 1;
} else if (tracker.isGameObjectModel(model_ptr)) {
game_obj_count += 1;
}
}
}
if (outline_count == 0) {
render_draw_hook.callOriginal(.{ this, view_matrix, batch_data, batch_indices, batch_count });
return;
}
// Pass 2: 3-way partition:
// [0 .. game_obj_count) → game objects (write depth first)
// [game_obj_count .. +outline) → outline targets (stencil against depth)
// [remainder ..] → other players, gear, NPCs
var go_pos: u32 = 0;
var outline_pos: u32 = game_obj_count;
var rest_pos: u32 = game_obj_count + outline_count;
for (0..batch_count) |i| {
const batch_idx = indices[i];
const idx_u: u32 = @bitCast(batch_idx);
const model_ptr = hook.readMem(u32, batch_data +% idx_u *% 0x40 +% 4);
if (model_ptr != 0 and tracker.findOutlineEntry(model_ptr) != null) {
reordered_indices[outline_pos] = batch_idx;
outline_pos += 1;
} else if (model_ptr != 0 and tracker.isGameObjectModel(model_ptr)) {
reordered_indices[go_pos] = batch_idx;
go_pos += 1;
} else {
reordered_indices[rest_pos] = batch_idx;
rest_pos += 1;
}
}
// Write reordered indices back to game's array in-place
for (0..batch_count) |i| {
indices[i] = reordered_indices[i];
}
render_draw_hook.callOriginal(.{ this, view_matrix, batch_data, batch_indices, batch_count });
}
// =============================================================================
// CM2Model_ManageRenderListNode hook
// =============================================================================
// __thiscall(model_ECX, addToList_stack)
// Native thiscall detour - no thunk needed.
fn manageRenderDetour(model: u32, add_to_list: u32) callconv(hook.cc.thiscall) void {
// Classify the model when it's being ADDED to the render list
if (add_to_list == 1 and model != 0 and tracker.enabled and tracker.hasTargets()) {
tracker.classifyModel(model);
}
manage_render_hook.callOriginal(.{ model, add_to_list });
}
// =============================================================================
// CM2Scene_DrawBatchProjected hook
// =============================================================================
// __fastcall(renderContext_ECX)
//
// Uses a naked entry point because Zig 0.15's x86_fastcall codegen generates
// wrong ret instructions for functions with ≤2 register params. The naked
// function bridges fastcall → cdecl and calls the implementation function.
fn drawBatchProjDetour(ctx: u32) callconv(hook.cc.thiscall) void {
// Fast path: no tracking enabled or nothing tracked → just call original
if (!tracker.enabled or !tracker.hasTargets()) {
draw_batch_hook.callOriginal(.{ctx});
return;
}
const model_ptr = if (wow.isValidPtr(ctx +% @as(u32, @intCast(o.RENDER_CONTEXT_MODEL_OFFSET))))
hook.readMem(u32, ctx + o.RENDER_CONTEXT_MODEL_OFFSET)
else
0;
const entry = if (model_ptr != 0) tracker.findOutlineEntry(model_ptr) else null;
if (entry != null) {
// This batch is an outline target - signal the DIP hook
rendering_outline = true;
current_model = model_ptr;
draw_batch_hook.callOriginal(.{ctx});
rendering_outline = false;
current_model = 0;
} else {
draw_batch_hook.callOriginal(.{ctx});
}
}
// =============================================================================
// Install / Remove
// =============================================================================
pub fn installHooks() bool {
if (render_draw_hook.attach(o.FN_CM2SCENE_RENDER_DRAW, &renderDrawDetour) != .ok)
return false;
if (manage_render_hook.attach(o.FN_CM2MODEL_MANAGE_RENDER_LIST, &manageRenderDetour) != .ok)
return false;
if (draw_batch_hook.attach(o.FN_DRAW_BATCH_PROJ, &drawBatchProjDetour) != .ok)
return false;
return true;
}
pub fn removeHooks() void {
draw_batch_hook.detach();
manage_render_hook.detach();
render_draw_hook.detach();
}