Outline render order, stencil protection, JFA sentinel fix, diagnostics

Render order: 3-way M2 batch partition — game objects + local player
render first (write depth), then outline targets (stencil marks), then
other players/gear/NPCs. Outlines show through other players but are
occluded by world/WMO/game objects/local player.

Stencil protection: set STENCILWRITEMASK=0 after outline target DIPs
to prevent subsequent renders from overwriting stencil marks.

JFA sentinel: changed from (1,1) to (-1,-1) to move it outside UV
space. Did not fix banding but is correct regardless.

Debug: added DEBUG_SHOW_SILHOUETTE comptime flag to bypass JFA and
composite raw silhouette RT. Confirmed silhouette is clean — banding
is in the JFA pipeline, not stale vertex buffers.

Game object + local player tracking in tracker.zig for batch ordering.

Added project README and outline subsystem README documenting render
pipeline, architecture, known issues, and planned features.
This commit is contained in:
MarcelineVQ
2026-02-25 18:32:49 -08:00
parent 661e1562d9
commit fbfe1cb545
6 changed files with 656 additions and 33 deletions
+28 -17
View File
@@ -62,11 +62,14 @@ var reordered_indices: [MAX_REORDER]i32 = undefined;
// __thiscall(this, viewMatrix, batchData, batchIndices, batchCount)
// Native thiscall detour — no thunk needed.
//
// Reorders batch indices so outline targets draw LAST. Non-outline models
// (game objects, other characters, NPCs) render first, filling the depth
// buffer with full scene geometry. When outline targets then render, the
// DIP hook writes stencil marks against this complete depth buffer, so
// outlines are properly occluded by all scene objects (not just terrain/WMOs).
// 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(THISCALL) void {
// One-time: install D3D9 hooks now that the game is actively rendering.
@@ -83,13 +86,18 @@ fn renderDrawDetour(this: u32, view_matrix: u32, batch_data: u32, batch_indices:
const indices: [*]i32 = @ptrFromInt(batch_indices);
// Pass 1: count outline targets
// 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 and tracker.findOutlineEntry(model_ptr) != null) {
outline_count += 1;
if (model_ptr != 0) {
if (tracker.findOutlineEntry(model_ptr) != null) {
outline_count += 1;
} else if (tracker.isGameObjectModel(model_ptr)) {
game_obj_count += 1;
}
}
}
@@ -98,13 +106,13 @@ fn renderDrawDetour(this: u32, view_matrix: u32, batch_data: u32, batch_indices:
return;
}
// Pass 2: partition — non-outline models first, outline targets last.
// Rendering non-outline models first fills the depth buffer with game
// objects, other characters, etc., so outline target stencil marks
// respect full scene occlusion (not just terrain+WMO).
const normal_count = batch_count - outline_count;
var normal_pos: u32 = 0;
var outline_pos: u32 = normal_count;
// 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);
@@ -112,9 +120,12 @@ fn renderDrawDetour(this: u32, view_matrix: u32, batch_data: u32, batch_indices:
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[normal_pos] = batch_idx;
normal_pos += 1;
reordered_indices[rest_pos] = batch_idx;
rest_pos += 1;
}
}