- 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
217 lines
8.6 KiB
Zig
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();
|
|
}
|