Compare commits

..

153 Commits

Author SHA1 Message Date
Dusk-92 3fe1509536 test: build EndScene-only isolation 2026-08-29 18:48:46 +02:00
Dusk-92 2ddbbe9fc7 test: isolate EndScene hook only 2026-08-29 18:48:37 +02:00
Dusk-92 7a959c7112 test: build V37A without Reset hook 2026-08-29 18:45:26 +02:00
Dusk-92 1508f4d8a6 test: isolate D3D9 path without Reset hook 2026-08-29 18:45:14 +02:00
Dusk-92 b812006831 test: build V37A without automatic D3D9 rehook 2026-08-29 18:32:47 +02:00
Dusk-92 163d6e6470 test: disable automatic D3D9 rehook during rendering 2026-08-29 18:32:38 +02:00
Dusk-92 4121cb6457 test: build V37A radii-only isolation 2026-08-29 18:20:50 +02:00
Dusk-92 43c4160b8d V37A test: apply only V37 outline radii 2026-08-29 18:20:40 +02:00
Dusk-92 c3c0e9a525 test: build V36 with process-exit safety 2026-08-29 18:13:00 +02:00
Dusk-92 6541727419 V36 test: add process-exit safety only 2026-08-29 18:12:52 +02:00
github-actions[bot] 4bbee94388 V36: soft Retail-style JFA decode 2026-08-29 09:32:44 +00:00
Dusk-92 a36ce6ea76 Temporary: commit V36 Retail Soft decode 2026-08-29 11:32:34 +02:00
Dusk-92 db472789d3 Remove temporary V36 Retail Soft build workflow 2026-08-29 11:32:13 +02:00
Dusk-92 f95ab007d2 Fix V36 build workflow YAML 2026-08-29 11:31:02 +02:00
Dusk-92 0ea0c4a94c Temporary: build V36 Retail Soft 2026-08-29 11:30:20 +02:00
Dusk-92 09863ad9e3 Remove invalid temporary V36 patch workflow 2026-08-29 11:30:04 +02:00
Dusk-92 c82f2b9e52 Fix temporary V36 patch workflow syntax 2026-08-29 11:29:20 +02:00
Dusk-92 49a8644d13 Temporary: patch V36 soft Retail decode 2026-08-29 11:28:43 +02:00
Dusk-92 c35b95a4c4 Stable V34 AutoFix: automatic target publication 2026-08-29 11:04:44 +02:00
Dusk-92 2165887e6b Stable V34 AutoFix: self-heal lost D3D9 hooks 2026-08-29 11:04:37 +02:00
Dusk-92 63c929ceb6 Stable V34 AutoFix: sync target automatically 2026-08-29 11:04:31 +02:00
Dusk-92 9f58796ca1 Stable V34 AutoFix: register automatic target sync 2026-08-29 11:04:27 +02:00
Dusk-92 f81851bb84 V34: use full-float JFA seed render targets 2026-08-29 10:18:28 +02:00
Dusk-92 35b3d00519 V34: add G32R32F JFA format 2026-08-29 10:18:10 +02:00
Dusk-92 80b1177da9 V33: fix signed seed test in cleanup shader 2026-08-29 10:05:20 +02:00
Dusk-92 c70c5a37ba V33: conservatively clean silhouette pinholes before JFA 2026-08-29 10:04:42 +02:00
Dusk-92 6354255965 Remove temporary V32 transport file 2026-08-29 09:41:43 +02:00
Dusk-92 70e7a2f24f Remove temporary V32 build workflow 2026-08-29 09:41:40 +02:00
github-actions[bot] d024a5ff69 Build V32 test package [skip ci] 2026-08-29 07:40:32 +00:00
Dusk-92 ebdfa57d88 Publish temporary V32 build asset 2026-08-29 09:39:58 +02:00
github-actions[bot] 5bb482e0c2 Build V32 test package [skip ci] 2026-08-29 07:35:09 +00:00
Dusk-92 6c9e951bf5 Expose compiled V32 outline for transport 2026-08-29 09:34:49 +02:00
github-actions[bot] d3f8dcd76f Build V32 test package [skip ci] 2026-08-29 07:30:47 +00:00
Dusk-92 707481d143 Add temporary V32 build workflow 2026-08-29 09:30:18 +02:00
Dusk-92 5f25e2494c V32: hard 3px outline without feather 2026-08-29 09:29:27 +02:00
Dusk-92 eb810db78c Build V31 outline polish 2026-08-28 22:59:41 +02:00
Dusk-92 85507f7e73 V31 report additive glow filtering 2026-08-28 22:59:36 +02:00
Dusk-92 8ce131c614 V31 polish outline edge and filter additive glow 2026-08-28 22:59:32 +02:00
Dusk-92 f2f0b789bb Build DEBUG30 real StateBlock Apply 2026-08-28 22:51:44 +02:00
Dusk-92 bb26e3fd71 DEBUG30 fix StateBlock Apply vtable slot 2026-08-28 22:51:39 +02:00
Dusk-92 7fcd97c5fa Fix DEBUG29 viewport setter 2026-08-28 22:47:43 +02:00
Dusk-92 a0ca341177 Build DEBUG29 isolated state-block replay 2026-08-28 22:46:56 +02:00
Dusk-92 1ea8067930 DEBUG29 report full D3D state restore 2026-08-28 22:46:48 +02:00
Dusk-92 d344f74fbf DEBUG29 fully isolate exact-material replay state 2026-08-28 22:46:41 +02:00
Dusk-92 ec5e5f189e Build DEBUG28 exact-material state-block replay 2026-08-28 22:41:15 +02:00
Dusk-92 d5a3a0e925 DEBUG28 report D3D state-block capture 2026-08-28 22:41:08 +02:00
Dusk-92 ef0e32de53 DEBUG28 replay exact D3D material via state blocks 2026-08-28 22:41:04 +02:00
Dusk-92 e2f3b3092a Add D3D9 state block type 2026-08-28 22:40:57 +02:00
Dusk-92 dd8286559d Build DEBUG27 fixed-function material alpha replay 2026-08-28 22:29:18 +02:00
Dusk-92 9d1673b359 DEBUG27 replay fixed-function material alpha 2026-08-28 22:29:13 +02:00
Dusk-92 3b5246e0ee Add fixed-function texture-stage constants for Outline 2026-08-28 22:29:07 +02:00
Dusk-92 11d219e54f Build DEBUG26 opaque/cutout silhouette mode 2026-08-28 22:22:45 +02:00
Dusk-92 a92d9caec1 DEBUG26 report skipped translucent passes 2026-08-28 22:22:38 +02:00
Dusk-92 6ef67ff11c DEBUG26 ignore translucent M2 carrier passes 2026-08-28 22:22:33 +02:00
Dusk-92 9a51cc6e1a Build DEBUG25 WoW-aware transparency replay 2026-08-28 22:18:11 +02:00
Dusk-92 ca445ef9bd DEBUG25 replay WoW alpha and blend semantics 2026-08-28 22:18:07 +02:00
Dusk-92 8d2db15f93 Add alpha-test and blend constants for Outline replay 2026-08-28 22:18:02 +02:00
Dusk-92 83627ea829 Build DEBUG24 alpha-aware outline 2026-08-28 22:12:17 +02:00
Dusk-92 dab770c018 DEBUG24 respect texture alpha in silhouette replay 2026-08-28 22:12:08 +02:00
Dusk-92 a933f0b639 Build DEBUG23 stable no-stencil outline 2026-08-28 22:05:31 +02:00
Dusk-92 68206a4347 DEBUG23 compact no-stencil diagnostics 2026-08-28 22:05:26 +02:00
Dusk-92 b65e441261 DEBUG23 run real outline without Reset or stencil 2026-08-28 22:05:21 +02:00
Dusk-92 5b95716a82 Fix DEBUG22 compact-output verifier 2026-08-28 21:59:20 +02:00
Dusk-92 774763f848 Remove obsolete DEBUG14 no-reset verifier 2026-08-28 21:58:26 +02:00
Dusk-92 91607bc42f Build DEBUG22 safe D24S8 stencil path 2026-08-28 21:57:18 +02:00
Dusk-92 2558cd9709 DEBUG22 request and report stencil readiness 2026-08-28 21:57:13 +02:00
Dusk-92 a11ff8d82b DEBUG22 restore D24S8 safely and rehook after Reset 2026-08-28 21:57:09 +02:00
Dusk-92 03fd1c7f38 Build DEBUG21 raw silhouette stencil isolation 2026-08-28 21:51:13 +02:00
Dusk-92 67817052b7 DEBUG21 show raw silhouette without stencil gate 2026-08-28 21:51:08 +02:00
Dusk-92 457b15999b Build DEBUG20 JFA decode fix 2026-08-28 21:40:56 +02:00
Dusk-92 4d3bae29fb Fix JFA decode constant-register restriction 2026-08-28 21:40:45 +02:00
Dusk-92 605ca09804 Build DEBUG19 JFA constant-register fix 2026-08-28 21:37:06 +02:00
Dusk-92 220236855f Fix JFA shader constant-register restriction 2026-08-28 21:37:02 +02:00
Dusk-92 fab99d1f96 Fix DEBUG18 compact-output verifier 2026-08-28 21:25:48 +02:00
Dusk-92 68ccdb253f Fix DEBUG18 output verifier 2026-08-28 21:25:00 +02:00
Dusk-92 a6bf31160f Fix DEBUG18 unused late rehook result 2026-08-28 21:24:05 +02:00
Dusk-92 9a3d431d32 Build DEBUG18 D3DX error text diagnostics 2026-08-28 21:23:20 +02:00
Dusk-92 441a97646e DEBUG18 show D3DX assembler message 2026-08-28 21:23:09 +02:00
Dusk-92 e27fcf1eba DEBUG18 capture D3DX shader error text 2026-08-28 21:23:04 +02:00
Dusk-92 66a77224e3 Build DEBUG17 HRESULT diagnostics 2026-08-28 21:05:02 +02:00
Dusk-92 1f1420ea53 DEBUG17 expose D3D9 failure HRESULTs 2026-08-28 21:04:57 +02:00
Dusk-92 0b8b415bab DEBUG17 capture shader and texture HRESULTs 2026-08-28 21:04:52 +02:00
Dusk-92 6392a960a0 Fix DEBUG16 verifier after compact debug output 2026-08-28 20:40:31 +02:00
Dusk-92 94e1189456 Build DEBUG16 graphics-resource diagnostics 2026-08-28 20:39:19 +02:00
Dusk-92 eb07e1d237 DEBUG16 expose shader and RT stages 2026-08-28 20:39:15 +02:00
Dusk-92 52361a9e42 DEBUG16 instrument shader and render-target creation 2026-08-28 20:39:11 +02:00
Dusk-92 9674807ea4 Build DEBUG15 late rehook test 2026-08-28 20:35:28 +02:00
Dusk-92 1ed94e0387 DEBUG15 late rehook from OutlineDebug 2026-08-28 20:35:23 +02:00
Dusk-92 7797221071 DEBUG15 add late D3D9 rehook 2026-08-28 20:35:20 +02:00
Dusk-92 07ec0b9b74 Fix DEBUG14 verifier location 2026-08-28 20:29:36 +02:00
Dusk-92 9c536db53c Build DEBUG14 without forced D3D9 reset 2026-08-28 20:28:30 +02:00
Dusk-92 4acbd4e965 DEBUG14 disable first-frame forced D3D9 reset 2026-08-28 20:28:27 +02:00
Dusk-92 380f62d5ec Update DEBUG13 output verifier 2026-08-28 20:21:50 +02:00
Dusk-92 3751675336 Keep DEBUG13 output within Zig format arg limit 2026-08-28 20:20:57 +02:00
Dusk-92 659fcceb85 Verify live D3D9 hook diagnostics 2026-08-28 20:20:01 +02:00
Dusk-92 2c3b35e788 Show live D3D9 vtable hook ownership 2026-08-28 20:19:57 +02:00
Dusk-92 0eb5cc2520 Expose live D3D9 hook ownership diagnostics 2026-08-28 20:19:53 +02:00
Dusk-92 2b5c53548d Verify post-publication scan instrumentation 2026-08-28 20:15:58 +02:00
Dusk-92 cc7a5dd7f9 Expose post-publication scan diagnostic 2026-08-28 20:15:54 +02:00
Dusk-92 57fa079361 Track scans occurring after Outline publication 2026-08-28 20:15:49 +02:00
Dusk-92 0ac3809942 Verify Outline handoff instrumentation 2026-08-28 20:11:44 +02:00
Dusk-92 41a53ce388 Expose Outline handoff diagnostics 2026-08-28 20:11:40 +02:00
Dusk-92 50c1a0b508 Instrument Outline Lua-to-render handoff 2026-08-28 20:11:36 +02:00
Dusk-92 9f9e56e764 Use atomic Lua-to-render handoff for Outline target 2026-08-28 20:02:45 +02:00
Dusk-92 f1399599d5 Build Lua-thread pinned Outline debug 2026-08-28 19:59:33 +02:00
Dusk-92 4e8791687b Capture and pin Outline objects from Lua thread 2026-08-28 19:59:30 +02:00
Dusk-92 d4df1143ce Use Lua-thread pinned objects for Outline debug rendering 2026-08-28 19:59:25 +02:00
Dusk-92 cdd3724c8e Build live UnitGUID Outline tracking debug 2026-08-28 19:56:32 +02:00
Dusk-92 e0f8592015 Expose UnitGUID object-resolution stages 2026-08-28 19:56:28 +02:00
Dusk-92 8e6d8e292e Use working UnitGUID path for Outline tracking 2026-08-28 19:56:25 +02:00
Dusk-92 8a0c8eec18 Verify one-shot UnitGUID debug probe 2026-08-28 19:52:07 +02:00
Dusk-92 35f90c85fb Add one-shot UnitGUID diagnostics to OutlineDebug 2026-08-28 19:52:04 +02:00
Dusk-92 c575ec8498 Document independent target debug stage 2026-08-28 18:31:18 +02:00
Dusk-92 2aeee3fceb Expose player GUID stage in OutlineDebug 2026-08-28 18:31:15 +02:00
Dusk-92 6ef29ba99f Track target independently from local player resolution 2026-08-28 18:31:12 +02:00
Dusk-92 e5acd33f0c Verify Nampower-style Outline object path 2026-08-28 18:26:19 +02:00
Dusk-92 fb4fb880e5 Use Nampower-style player and target GUID path 2026-08-28 18:26:15 +02:00
Dusk-92 61cf447248 Add locked target GUID address for Outline debug 2026-08-28 18:26:11 +02:00
Dusk-92 1314860bde Let target Outline work without legacy object manager global 2026-08-28 18:22:12 +02:00
Dusk-92 134e5a37f9 Allow GUID resolution when legacy object manager global is zero 2026-08-28 18:21:59 +02:00
Dusk-92 bff19dd95b Show object manager readiness in OutlineDebug 2026-08-28 18:16:19 +02:00
Dusk-92 4eb37f4c23 Bypass broken IS_IN_WORLD gate in Outline debug 2026-08-28 18:16:14 +02:00
Dusk-92 3ee72d70d6 Add object manager readiness check for Outline debug 2026-08-28 18:16:10 +02:00
Dusk-92 b714c38ab7 Expose target acquisition stages in OutlineDebug 2026-08-28 18:06:19 +02:00
Dusk-92 bcf8cefb35 Split Outline target acquisition diagnostics 2026-08-28 18:06:15 +02:00
Dusk-92 1632d98797 Keep V8 debug ABI checks optimizer independent 2026-08-28 17:59:35 +02:00
Dusk-92 c890a6bc69 Relax debug build FrameScript disassembly matcher 2026-08-28 17:58:55 +02:00
Dusk-92 086dd17a31 Trigger V8 debug diagnostics build 2026-08-28 17:57:54 +02:00
Dusk-92 b128c01a7c Build V8 Outline debug diagnostics 2026-08-28 17:57:41 +02:00
Dusk-92 33ec80d742 Register OutlineDebug Lua command 2026-08-28 17:57:19 +02:00
Dusk-92 ad706ccb9a Add in-game OutlineDebug diagnostics 2026-08-28 17:57:09 +02:00
Dusk-92 d7f93b3c00 Add sticky D3D9 Outline diagnostics 2026-08-28 17:56:47 +02:00
Dusk-92 62026da380 Add sticky Outline tracker diagnostics 2026-08-28 17:56:26 +02:00
Dusk-92 45635455b9 Verify V8 UnitGUID ECX ABI 2026-08-28 17:48:24 +02:00
Dusk-92 37757a2347 Fix V8 UnitGUID x86 ABI stack leak 2026-08-28 17:48:06 +02:00
Dusk-92 855ea340cc Fix V8 registerFunction asm build 2026-08-28 17:39:31 +02:00
Dusk-92 6e0c0850e8 Verify V8 FrameScript registration ABI 2026-08-28 17:38:20 +02:00
Dusk-92 17af79dff4 Fix V8 FrameScript registration ABI 2026-08-28 17:37:45 +02:00
Dusk-92 a55f43a5d5 Verify V8 OutlineCommand ECX ABI in machine code 2026-08-28 17:27:43 +02:00
Dusk-92 126f617ca1 Fix V8 OutlineCommand ABI to receive Lua state in ECX 2026-08-28 17:27:24 +02:00
Dusk-92 43839b2b41 Build safe standalone core V8 artifact 2026-08-28 17:09:54 +02:00
Dusk-92 dbdb513e94 Use x86 fastcall for OutlineCommand callback 2026-08-28 17:09:38 +02:00
Dusk-92 4aae16646c Build safe standalone core V7 artifact 2026-08-28 17:05:10 +02:00
Dusk-92 5fe9a92672 Force Outline Lua calls through x86 register asm 2026-08-28 17:04:59 +02:00
Dusk-92 b587992a6c Build safe standalone core V5 artifact 2026-08-28 16:55:06 +02:00
Dusk-92 bbcc70fa0d Fix Outline Lua API calls on safe core V5 2026-08-28 16:54:46 +02:00
Dusk-92 ebccbee66a Build safe standalone core V4 artifact 2026-08-28 16:47:23 +02:00
Dusk-92 68c0ad2b75 Use minimal standalone core for Outline CustomAssets and TransmogFix 2026-08-28 16:46:54 +02:00
Dusk-92 7bf5a11011 Add minimal core mode for safe standalone variants 2026-08-28 16:46:17 +02:00
Dusk-92 947056f1c2 Test full release packaging on main 2026-08-28 15:13:48 +02:00
Dusk-92 c4d133887a Run release build CI on main and support source refs 2026-08-28 15:12:47 +02:00
Dusk-92 f4cbacb7f9 Fix DllMain return type for Zig 0.16 2026-08-28 15:12:18 +02:00
Dusk-92 82dc02e2e7 Add automated DLL release workflow 2026-08-28 14:57:49 +02:00
23 changed files with 2146 additions and 223 deletions
+77
View File
@@ -0,0 +1,77 @@
name: Commit V36 Retail Soft decode
on:
push:
branches:
- test/v36-retail-jfa-soft
permissions:
contents: write
jobs:
commit-v36-soft:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
with:
ref: test/v36-retail-jfa-soft
- name: Apply V36 decode
shell: bash
run: |
set -euo pipefail
python3 <<'PY'
from pathlib import Path
p = Path('src/outline/d3d9_hook.zig')
s = p.read_text()
if 'V36 RETAIL-SOFT' in s:
raise SystemExit(0)
start = s.index('/// V32 POLISH: hard 3px outline, no feather.')
end = s.index('/// Debug: composite silhouette RT directly.')
lines = [
'/// V36 RETAIL-SOFT: keep the proven V34 JFA, soften only final decode.',
'/// c0 = (screen_width, screen_height, core_radius_px, halo_radius_px).',
'/// Uses the same two texture reads as V34: no 17-tap full-screen morphology.',
'const jfa_decode_src =',
' "ps_3_0\\n" ++',
' "def c1, 0.0, 0.22, 0.82, -0.002\\n" ++',
' "def c2, 1.0, 0.95, 0.58, 0.0\\n" ++',
' "dcl_2d s0\\n" ++',
' "dcl_2d s1\\n" ++',
' "dcl_texcoord0 v0\\n" ++',
' "texld r0, v0, s0\\n" ++',
' "sub r1.xy, v0.xy, r0.xy\\n" ++',
' "mul r1.xy, r1.xy, c0.xy\\n" ++',
' "dp2add r1.z, r1, r1, c1.x\\n" ++',
' "mul r3.x, c0.z, c0.z\\n" ++',
' "mul r3.y, c0.w, c0.w\\n" ++',
' "sub r4.x, r1.z, r3.y\\n" ++',
' "cmp r6.w, r4.x, c1.x, c1.y\\n" ++',
' "sub r4.x, r1.z, r3.x\\n" ++',
' "cmp r6.w, r4.x, r6.w, c1.z\\n" ++',
' "texld r5, v0, s1\\n" ++',
' "add r5.x, r5.a, c1.w\\n" ++',
' "cmp r6.w, r5.x, c1.x, r6.w\\n" ++',
' "mov r6.xyz, c2.xyz\\n" ++',
' "mov oC0, r6\\n";',
'',
'',
]
s = s[:start] + '\n'.join(lines) + s[end:]
old = ' // V34: hard 3px edge unchanged; JFA seed precision is now full-float.\n c0 = [4]f32{ fw, fh, 3.0, 0.0 };'
new_cpu = ' // V36: thin 1.5px core plus a subtle 3px halo.\n c0 = [4]f32{ fw, fh, 1.5, 3.0 };'
if old not in s:
raise SystemExit('V34 CPU decode constants not found')
p.write_text(s.replace(old, new_cpu))
PY
- name: Commit source
shell: bash
run: |
set -euo pipefail
if git diff --quiet; then exit 0; fi
git config user.name "github-actions[bot]"
git config user.email "41898282+github-actions[bot]@users.noreply.github.com"
git add src/outline/d3d9_hook.zig
git commit -m "V36: soft Retail-style JFA decode"
git push origin HEAD:test/v36-retail-jfa-soft
+172
View File
@@ -0,0 +1,172 @@
name: Build and publish release
on:
push:
branches:
- main
tags:
- '*'
workflow_dispatch:
inputs:
release_tag:
description: 'GitHub Release tag to publish'
required: true
default: '0.7.3'
source_ref:
description: 'Source branch/tag/commit to compile'
required: true
default: 'main'
permissions:
contents: write
env:
ZIG_VERSION: '0.16.0'
CUSTOM_WEIRDPERFORMANCE_TAG: '0.7.3'
jobs:
release:
name: Build DLLs and publish release
runs-on: ubuntu-latest
steps:
- name: Resolve build metadata
shell: bash
run: |
set -euo pipefail
if [[ "${{ github.event_name }}" == "workflow_dispatch" ]]; then
echo "RELEASE_TAG=${{ inputs.release_tag }}" >> "$GITHUB_ENV"
echo "SOURCE_DESCRIPTION=${{ inputs.source_ref }}" >> "$GITHUB_ENV"
elif [[ "${GITHUB_REF}" == refs/tags/* ]]; then
echo "RELEASE_TAG=${GITHUB_REF_NAME}" >> "$GITHUB_ENV"
echo "SOURCE_DESCRIPTION=${GITHUB_REF_NAME}" >> "$GITHUB_ENV"
else
echo "RELEASE_TAG=dev" >> "$GITHUB_ENV"
echo "SOURCE_DESCRIPTION=${GITHUB_SHA}" >> "$GITHUB_ENV"
fi
- name: Checkout source
uses: actions/checkout@v4
with:
ref: ${{ github.event_name == 'workflow_dispatch' && inputs.source_ref || github.ref }}
fetch-depth: 0
- name: Install Zig
uses: mlugg/setup-zig@v2
with:
version: ${{ env.ZIG_VERSION }}
- name: Build public DLL variants
shell: bash
run: |
set -euo pipefail
zig build all-variants -Doptimize=ReleaseSmall \
-Dweirdperformance=false \
-Doutline=true \
-Dframecrash=true \
-Ddpslog=true
- name: Collect release DLLs
shell: bash
run: |
set -euo pipefail
mkdir -p release
dlls=(
bigcursor.dll
clickthrough.dll
customassets.dll
dpslog.dll
framecrash.dll
healtextfix.dll
interact.dll
logsessions.dll
minimapicons.dll
outline.dll
pngscreenshots.dll
transmogfix.dll
worldmarkers.dll
)
for dll in "${dlls[@]}"; do
src="zig-out/variants/$dll"
if [[ ! -s "$src" ]]; then
echo "Missing expected DLL: $src" >&2
exit 1
fi
cp "$src" "release/$dll"
done
- name: Add custom WeirdPerformance v0.7.3
shell: bash
run: |
set -euo pipefail
api_url="https://codeberg.org/api/v1/repos/Dusk92/WeirdUtils/releases/tags/${CUSTOM_WEIRDPERFORMANCE_TAG}"
release_json="$(curl --retry 3 --retry-all-errors -fsSL "$api_url")"
download_url="$(printf '%s' "$release_json" | python3 -c '
import json, sys
data = json.load(sys.stdin)
for asset in data.get("assets", []):
if asset.get("name", "").lower() == "weirdperformance.dll":
print(asset.get("browser_download_url", ""))
break
')"
if [[ -z "$download_url" ]]; then
echo "Could not find weirdperformance.dll in Codeberg release ${CUSTOM_WEIRDPERFORMANCE_TAG}" >&2
exit 1
fi
curl --retry 3 --retry-all-errors -fsSL "$download_url" -o release/weirdperformance.dll
python3 - <<'PY'
from pathlib import Path
p = Path('release/weirdperformance.dll')
data = p.read_bytes()
if len(data) < 1024 or data[:2] != b'MZ':
raise SystemExit('Downloaded weirdperformance.dll is not a valid-looking Windows DLL')
print(f'Custom WeirdPerformance: {len(data)} bytes, PE header OK')
PY
- name: Create checksums and ZIP
shell: bash
run: |
set -euo pipefail
(
cd release
sha256sum *.dll > SHA256SUMS.txt
)
zip -j "WeirdUtils-${RELEASE_TAG}.zip" release/*.dll release/SHA256SUMS.txt
- name: Publish GitHub Release
if: github.event_name == 'workflow_dispatch' || startsWith(github.ref, 'refs/tags/')
shell: bash
env:
GH_TOKEN: ${{ github.token }}
run: |
set -euo pipefail
cat > release-notes.md <<EOF
WeirdUtils ${RELEASE_TAG}
- Standard module DLLs are compiled from `${SOURCE_DESCRIPTION}` with Zig ${ZIG_VERSION} in ReleaseSmall mode.
- The upstream WeirdPerformance variant is intentionally excluded.
- `weirdperformance.dll` is the custom Dusk-92 v${CUSTOM_WEIRDPERFORMANCE_TAG} build from the Codeberg release.
- `SHA256SUMS.txt` is included for integrity checks.
EOF
if gh release view "$RELEASE_TAG" >/dev/null 2>&1; then
echo "Release $RELEASE_TAG already exists; replacing assets."
else
gh release create "$RELEASE_TAG" \
--title "WeirdUtils ${RELEASE_TAG}" \
--notes-file release-notes.md
fi
gh release upload "$RELEASE_TAG" \
release/*.dll \
release/SHA256SUMS.txt \
"WeirdUtils-${RELEASE_TAG}.zip" \
--clobber
+146
View File
@@ -0,0 +1,146 @@
name: Test safe standalone core V4
on:
push:
branches:
- test/safe-core-v4
workflow_dispatch:
permissions:
contents: read
jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: mlugg/setup-zig@v2
with:
version: 0.16.0
- name: Build safe standalone variants
shell: bash
run: |
set -euo pipefail
zig build all-variants -Doptimize=ReleaseSmall \
-Dweirdperformance=false \
-Doutline=true
- name: Verify minimal core layout
shell: bash
run: |
set -euo pipefail
python3 - <<'PY'
from pathlib import Path
root = Path("zig-out/variants")
addr = {
"lua_protection": 0x42A320,
"register_commands": 0x490250,
"glue_commands": 0x46ABB0,
"engine_init": 0x46A400,
"logout": 0x491180,
"shutdown": 0x490BD0,
"file_fallback": 0x648620,
"open_file": 0x6477C0,
"file_size": 0x6487F0,
"read_file": 0x648460,
"cleanup_file": 0x648730,
"model_load": 0x71D4E0,
"check_file": 0x654DD0,
}
def has(data, value):
return value.to_bytes(4, "little") in data
outline = (root / "outline.dll").read_bytes()
custom = (root / "customassets.dll").read_bytes()
transmog = (root / "transmogfix.dll").read_bytes()
# Outline: only its Lua registration + engine init core hooks are allowed.
required_outline = ("register_commands", "engine_init")
forbidden_outline = tuple(k for k in addr if k not in required_outline)
for k in required_outline:
if not has(outline, addr[k]):
raise SystemExit(f"outline.dll missing required {k}")
bad = [k for k in forbidden_outline if has(outline, addr[k])]
if bad:
raise SystemExit(f"outline.dll still contains unwanted core hooks: {bad}")
print("outline.dll: minimal core verified")
# CustomAssets: only CheckFileExistence is allowed from the generic core.
if not has(custom, addr["check_file"]):
raise SystemExit("customassets.dll missing CheckFileExistence")
bad = [k for k,v in addr.items() if k != "check_file" and has(custom, v)]
if bad:
raise SystemExit(f"customassets.dll still contains unwanted core hooks: {bad}")
print("customassets.dll: minimal core verified")
# TransmogFix: none of the generic core hook target addresses should remain.
bad = [k for k,v in addr.items() if has(transmog, v)]
if bad:
raise SystemExit(f"transmogfix.dll still contains unwanted core hooks: {bad}")
print("transmogfix.dll: minimal core verified")
# Compatibility repair must exist in customassets and outline.
for name, data in (("outline.dll", outline), ("customassets.dll", custom)):
for gate in (0x654B5C, 0x654B6A):
if not has(data, gate):
raise SystemExit(f"{name} missing MPQ gate restore address 0x{gate:X}")
for seq in (bytes([0x74,0x25]), bytes([0x75,0x17])):
if seq not in data:
raise SystemExit(f"{name} missing original MPQ branch bytes {seq.hex()}")
print("legacy MPQ gate repair verified")
PY
- name: Stage package
shell: bash
run: |
set -euo pipefail
mkdir -p package/DLL
mkdir -p package/Interface/AddOns/WeirdUtils_Outline
cp zig-out/variants/outline.dll package/DLL/
cp zig-out/variants/customassets.dll package/DLL/
cp zig-out/variants/transmogfix.dll package/DLL/
cp src/outline/addon/Outline.lua package/Interface/AddOns/WeirdUtils_Outline/
cp src/outline/addon/Bindings.xml package/Interface/AddOns/WeirdUtils_Outline/
cp src/outline/addon/WeirdUtils_Outline.toc package/Interface/AddOns/WeirdUtils_Outline/
cat > package/README_TEST.txt <<'EOF'
WeirdUtils Safe Standalone Core Test V4
This build fixes the second crash path observed in WoW's Lua engine.
outline.dll
- Keeps only the Player_LoadScriptFunctions hook required to register OutlineCommand.
- Keeps only the GameEngine_MainInitialize hook required to initialise the renderer.
- Does not register the shared WeirdUtils version table.
- Does not install generic MPQ/file hooks.
- Uses the external WeirdUtils_Outline addon included here.
customassets.dll
- Keeps only CheckFileExistence from the shared file core.
- No shared Lua registration hooks.
- No shared engine/logout/shutdown hooks.
- Repairs legacy File_FindInArchive NOPs if present.
transmogfix.dll
- Installs only TransmogFix's own module hooks.
- No shared Lua/file/engine/logout/shutdown hooks.
weirdperformance.dll
- Not built here.
- The final ZIP will use the user's original binary unchanged.
EOF
sha256sum package/DLL/*.dll > package/SHA256SUMS.txt
- uses: actions/upload-artifact@v4
with:
name: safe-core-v4
path: package/
if-no-files-found: error
+161
View File
@@ -0,0 +1,161 @@
name: Test safe standalone core V5
on:
push:
branches:
- test/safe-core-v5
workflow_dispatch:
permissions:
contents: read
jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: mlugg/setup-zig@v2
with:
version: 0.16.0
- name: Build safe standalone variants
shell: bash
run: |
set -euo pipefail
zig build all-variants -Doptimize=ReleaseSmall \
-Dweirdperformance=false \
-Doutline=true
- name: Verify minimal core layout
shell: bash
run: |
set -euo pipefail
python3 - <<'PY'
from pathlib import Path
root = Path("zig-out/variants")
addr = {
"lua_protection": 0x42A320,
"register_commands": 0x490250,
"glue_commands": 0x46ABB0,
"engine_init": 0x46A400,
"logout": 0x491180,
"shutdown": 0x490BD0,
"file_fallback": 0x648620,
"open_file": 0x6477C0,
"file_size": 0x6487F0,
"read_file": 0x648460,
"cleanup_file": 0x648730,
"model_load": 0x71D4E0,
"check_file": 0x654DD0,
}
def has(data, value):
return value.to_bytes(4, "little") in data
outline = (root / "outline.dll").read_bytes()
custom = (root / "customassets.dll").read_bytes()
transmog = (root / "transmogfix.dll").read_bytes()
# Outline: only its Lua registration + engine init core hooks are allowed.
required_outline = ("register_commands", "engine_init")
forbidden_outline = tuple(k for k in addr if k not in required_outline)
for k in required_outline:
if not has(outline, addr[k]):
raise SystemExit(f"outline.dll missing required {k}")
bad = [k for k in forbidden_outline if has(outline, addr[k])]
if bad:
raise SystemExit(f"outline.dll still contains unwanted core hooks: {bad}")
print("outline.dll: minimal core verified")
# CustomAssets: only CheckFileExistence is allowed from the generic core.
if not has(custom, addr["check_file"]):
raise SystemExit("customassets.dll missing CheckFileExistence")
bad = [k for k,v in addr.items() if k != "check_file" and has(custom, v)]
if bad:
raise SystemExit(f"customassets.dll still contains unwanted core hooks: {bad}")
print("customassets.dll: minimal core verified")
# TransmogFix: none of the generic core hook target addresses should remain.
bad = [k for k,v in addr.items() if has(transmog, v)]
if bad:
raise SystemExit(f"transmogfix.dll still contains unwanted core hooks: {bad}")
print("transmogfix.dll: minimal core verified")
# Compatibility repair must exist in customassets and outline.
for name, data in (("outline.dll", outline), ("customassets.dll", custom)):
for gate in (0x654B5C, 0x654B6A):
if not has(data, gate):
raise SystemExit(f"{name} missing MPQ gate restore address 0x{gate:X}")
for seq in (bytes([0x74,0x25]), bytes([0x75,0x17])):
if seq not in data:
raise SystemExit(f"{name} missing original MPQ branch bytes {seq.hex()}")
print("legacy MPQ gate repair verified")
PY
- name: Verify Outline Lua wrapper usage
shell: bash
run: |
set -euo pipefail
python3 - <<'PY'
from pathlib import Path
src = Path("src/outline/outline.zig").read_text()
if 'hook.call(fn (usize)' in src or '0x6F39F0' in src:
raise SystemExit("Outline still contains manual Lua API hook.call usage")
for required in ("lua.gettop(L)", "lua.isstring(L, 1)", "lua.tostring(L, 1)", "lua.pushboolean(L"):
if required not in src:
raise SystemExit(f"Missing expected Lua wrapper usage: {required}")
print("outline.dll source: direct lua.zig wrappers verified")
PY
- name: Stage package
shell: bash
run: |
set -euo pipefail
mkdir -p package/DLL
mkdir -p package/Interface/AddOns/WeirdUtils_Outline
cp zig-out/variants/outline.dll package/DLL/
cp zig-out/variants/customassets.dll package/DLL/
cp zig-out/variants/transmogfix.dll package/DLL/
cp src/outline/addon/Outline.lua package/Interface/AddOns/WeirdUtils_Outline/
cp src/outline/addon/Bindings.xml package/Interface/AddOns/WeirdUtils_Outline/
cp src/outline/addon/WeirdUtils_Outline.toc package/Interface/AddOns/WeirdUtils_Outline/
cat > package/README_TEST.txt <<'EOF'
WeirdUtils Safe Standalone Core Test V5
This build also fixes OutlineCommand's Lua API calls after a crash at lua_pushboolean.
outline.dll
- Keeps only the Player_LoadScriptFunctions hook required to register OutlineCommand.
- Keeps only the GameEngine_MainInitialize hook required to initialise the renderer.
- Does not register the shared WeirdUtils version table.
- Does not install generic MPQ/file hooks.
- Uses the external WeirdUtils_Outline addon included here.
customassets.dll
- Keeps only CheckFileExistence from the shared file core.
- No shared Lua registration hooks.
- No shared engine/logout/shutdown hooks.
- Repairs legacy File_FindInArchive NOPs if present.
transmogfix.dll
- Installs only TransmogFix's own module hooks.
- No shared Lua/file/engine/logout/shutdown hooks.
weirdperformance.dll
- Not built here.
- The final ZIP will use the user's original binary unchanged.
EOF
sha256sum package/DLL/*.dll > package/SHA256SUMS.txt
- uses: actions/upload-artifact@v4
with:
name: safe-core-v5
path: package/
if-no-files-found: error
+167
View File
@@ -0,0 +1,167 @@
name: Test safe standalone core V7
on:
push:
branches:
- test/safe-core-v7
workflow_dispatch:
permissions:
contents: read
jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: mlugg/setup-zig@v2
with:
version: 0.16.0
- name: Build safe standalone variants
shell: bash
run: |
set -euo pipefail
zig build all-variants -Doptimize=ReleaseSmall \
-Dweirdperformance=false \
-Doutline=true
- name: Verify minimal core layout
shell: bash
run: |
set -euo pipefail
python3 - <<'PY'
from pathlib import Path
root = Path("zig-out/variants")
addr = {
"lua_protection": 0x42A320,
"register_commands": 0x490250,
"glue_commands": 0x46ABB0,
"engine_init": 0x46A400,
"logout": 0x491180,
"shutdown": 0x490BD0,
"file_fallback": 0x648620,
"open_file": 0x6477C0,
"file_size": 0x6487F0,
"read_file": 0x648460,
"cleanup_file": 0x648730,
"model_load": 0x71D4E0,
"check_file": 0x654DD0,
}
def has(data, value):
return value.to_bytes(4, "little") in data
outline = (root / "outline.dll").read_bytes()
custom = (root / "customassets.dll").read_bytes()
transmog = (root / "transmogfix.dll").read_bytes()
# Outline: only its Lua registration + engine init core hooks are allowed.
required_outline = ("register_commands", "engine_init")
forbidden_outline = tuple(k for k in addr if k not in required_outline)
for k in required_outline:
if not has(outline, addr[k]):
raise SystemExit(f"outline.dll missing required {k}")
bad = [k for k in forbidden_outline if has(outline, addr[k])]
if bad:
raise SystemExit(f"outline.dll still contains unwanted core hooks: {bad}")
print("outline.dll: minimal core verified")
# CustomAssets: only CheckFileExistence is allowed from the generic core.
if not has(custom, addr["check_file"]):
raise SystemExit("customassets.dll missing CheckFileExistence")
bad = [k for k,v in addr.items() if k != "check_file" and has(custom, v)]
if bad:
raise SystemExit(f"customassets.dll still contains unwanted core hooks: {bad}")
print("customassets.dll: minimal core verified")
# TransmogFix: none of the generic core hook target addresses should remain.
bad = [k for k,v in addr.items() if has(transmog, v)]
if bad:
raise SystemExit(f"transmogfix.dll still contains unwanted core hooks: {bad}")
print("transmogfix.dll: minimal core verified")
# Compatibility repair must exist in customassets and outline.
for name, data in (("outline.dll", outline), ("customassets.dll", custom)):
for gate in (0x654B5C, 0x654B6A):
if not has(data, gate):
raise SystemExit(f"{name} missing MPQ gate restore address 0x{gate:X}")
for seq in (bytes([0x74,0x25]), bytes([0x75,0x17])):
if seq not in data:
raise SystemExit(f"{name} missing original MPQ branch bytes {seq.hex()}")
print("legacy MPQ gate repair verified")
PY
- name: Verify Outline native Lua asm wrappers
shell: bash
run: |
set -euo pipefail
python3 - <<'PY'
from pathlib import Path
src = Path("src/outline/outline.zig").read_text()
for required in (
'luaGetTopNative',
'luaIsStringNative',
'luaToStringNative',
'luaPushBooleanNative',
'"{ecx}"',
'"{edx}"',
'0x6F39F0',
):
if required not in src:
raise SystemExit(f"Missing native register wrapper piece: {required}")
print("outline.dll source: native x86 register wrappers present")
PY
- name: Stage package
shell: bash
run: |
set -euo pipefail
mkdir -p package/DLL
mkdir -p package/Interface/AddOns/WeirdUtils_Outline
cp zig-out/variants/outline.dll package/DLL/
cp zig-out/variants/customassets.dll package/DLL/
cp zig-out/variants/transmogfix.dll package/DLL/
cp src/outline/addon/Outline.lua package/Interface/AddOns/WeirdUtils_Outline/
cp src/outline/addon/Bindings.xml package/Interface/AddOns/WeirdUtils_Outline/
cp src/outline/addon/WeirdUtils_Outline.toc package/Interface/AddOns/WeirdUtils_Outline/
cat > package/README_TEST.txt <<'EOF'
WeirdUtils Safe Standalone Core Test V7
This build forces OutlineCommand's WoW Lua calls through x86 register assembly wrappers.
outline.dll
- Keeps only the Player_LoadScriptFunctions hook required to register OutlineCommand.
- Keeps only the GameEngine_MainInitialize hook required to initialise the renderer.
- Does not register the shared WeirdUtils version table.
- Does not install generic MPQ/file hooks.
- Uses the external WeirdUtils_Outline addon included here.
customassets.dll
- Keeps only CheckFileExistence from the shared file core.
- No shared Lua registration hooks.
- No shared engine/logout/shutdown hooks.
- Repairs legacy File_FindInArchive NOPs if present.
transmogfix.dll
- Installs only TransmogFix's own module hooks.
- No shared Lua/file/engine/logout/shutdown hooks.
weirdperformance.dll
- Not built here.
- The final ZIP will use the user's original binary unchanged.
EOF
sha256sum package/DLL/*.dll > package/SHA256SUMS.txt
- uses: actions/upload-artifact@v4
with:
name: safe-core-v7
path: package/
if-no-files-found: error
+249
View File
@@ -0,0 +1,249 @@
name: Test safe standalone core V8 Debug
on:
push:
branches:
- test/safe-core-v8-debug
workflow_dispatch:
# V8 debug diagnostics build
permissions:
contents: read
jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: mlugg/setup-zig@v2
with:
version: 0.16.0
- name: Build safe standalone variants
shell: bash
run: |
set -euo pipefail
zig build all-variants -Doptimize=ReleaseSmall \
-Dweirdperformance=false \
-Doutline=true
- name: Verify minimal core layout
shell: bash
run: |
set -euo pipefail
python3 - <<'PY'
from pathlib import Path
root = Path("zig-out/variants")
addr = {
"lua_protection": 0x42A320,
"register_commands": 0x490250,
"glue_commands": 0x46ABB0,
"engine_init": 0x46A400,
"logout": 0x491180,
"shutdown": 0x490BD0,
"file_fallback": 0x648620,
"open_file": 0x6477C0,
"file_size": 0x6487F0,
"read_file": 0x648460,
"cleanup_file": 0x648730,
"model_load": 0x71D4E0,
"check_file": 0x654DD0,
}
def has(data, value):
return value.to_bytes(4, "little") in data
outline = (root / "outline.dll").read_bytes()
custom = (root / "customassets.dll").read_bytes()
transmog = (root / "transmogfix.dll").read_bytes()
# Outline: only its Lua registration + engine init core hooks are allowed.
required_outline = ("register_commands", "engine_init")
forbidden_outline = tuple(k for k in addr if k not in required_outline)
for k in required_outline:
if not has(outline, addr[k]):
raise SystemExit(f"outline.dll missing required {k}")
bad = [k for k in forbidden_outline if has(outline, addr[k])]
if bad:
raise SystemExit(f"outline.dll still contains unwanted core hooks: {bad}")
print("outline.dll: minimal core verified")
# CustomAssets: only CheckFileExistence is allowed from the generic core.
if not has(custom, addr["check_file"]):
raise SystemExit("customassets.dll missing CheckFileExistence")
bad = [k for k,v in addr.items() if k != "check_file" and has(custom, v)]
if bad:
raise SystemExit(f"customassets.dll still contains unwanted core hooks: {bad}")
print("customassets.dll: minimal core verified")
# TransmogFix: none of the generic core hook target addresses should remain.
bad = [k for k,v in addr.items() if has(transmog, v)]
if bad:
raise SystemExit(f"transmogfix.dll still contains unwanted core hooks: {bad}")
print("transmogfix.dll: minimal core verified")
# Compatibility repair must exist in customassets and outline.
for name, data in (("outline.dll", outline), ("customassets.dll", custom)):
for gate in (0x654B5C, 0x654B6A):
if not has(data, gate):
raise SystemExit(f"{name} missing MPQ gate restore address 0x{gate:X}")
for seq in (bytes([0x74,0x25]), bytes([0x75,0x17])):
if seq not in data:
raise SystemExit(f"{name} missing original MPQ branch bytes {seq.hex()}")
print("legacy MPQ gate repair verified")
PY
- name: Verify Outline registration + Lua ABI
shell: bash
run: |
set -euo pipefail
python3 - <<'PY'
from pathlib import Path
main = Path("src/main.zig").read_text()
outline = Path("src/outline/outline.zig").read_text()
d3d9 = Path("src/outline/d3d9_hook.zig").read_text()
for required in (
'noinline fn registerFunction',
'0x704120',
'"{ecx}" (@intFromPtr(name))',
'"{edx}" (func_addr)',
):
if required not in main:
raise SystemExit(f"Missing explicit FrameScript registration ABI piece: {required}")
for required in (
'luaGetTopNative',
'luaIsStringNative',
'luaToStringNative',
'luaPushBooleanNative',
'"{ecx}"',
'"{edx}"',
'0x6F39F0',
'callconv(.{ .x86_thiscall = .{} })',
'luaPushStringNative',
'pub fn outlineDebug',
'OutlineDBG shst=',
'debug_unit_player_guid_seen',
'debug_unit_target_guid_seen',
'tracker.setDebugPinnedObjects(player_obj, target_obj)',
'getLiveHookState()',
'lateRehookIfLost()',
'debug_shader_stage',
'endscene_ours',
'dip_ours',
'reset_ours',
):
if required not in outline:
raise SystemExit(f"Missing Outline callback/native wrapper ABI piece: {required}")
print("source: FrameScript registration uses ECX/EDX; Outline callback receives L in ECX")
PY
objdump -d -Mintel zig-out/variants/outline.dll > /tmp/outline.disasm
python3 - <<'PY'
import re
from pathlib import Path
lines = Path("/tmp/outline.disasm").read_text(errors="replace").splitlines()
# registerFunction is explicit inline asm and source-checked above.
# Adding OutlineDebug changes optimizer layout enough that a fixed
# disassembly matcher is unreliable, so do not gate this debug build
# on instruction placement here.
# OutlineCommand must capture the Lua state from ECX. Search function
# prologues for the characteristic saved-ESI form and reject the old
# [ebp+8] capture used by the cdecl experiment.
good_callback = False
for i, line in enumerate(lines):
if re.search(r"push\s+ebp\b", line, re.I):
block = "\n".join(lines[i:i + 12])
if re.search(r"mov\s+esi,ecx\b", block, re.I):
good_callback = True
break
if not good_callback:
raise SystemExit("BAD ABI: no callback body captures Lua state from ECX")
print("outline.dll machine code: registration and callback ABIs verified")
PY
- name: Verify direct player and target GUID path
shell: bash
run: |
set -euo pipefail
python3 - <<'PY'
from pathlib import Path
wow = Path("src/wow.zig").read_text()
offsets = Path("src/offsets.zig").read_text()
for required in (
'const guid = getPlayerGUID();',
'return readGUID(o.LOCKED_TARGET_GUID);',
):
if required not in wow:
raise SystemExit(f"Missing direct player/target GUID path: {required}")
for required in (
'FN_GET_PLAYER_GUID: usize = 0x00468550',
'LOCKED_TARGET_GUID: usize = 0x00B4E2D8',
'FN_GET_OBJECT_BY_GUID: usize = 0x464870',
):
if required not in offsets:
raise SystemExit(f"Missing verified WoW offset: {required}")
print("source: player GUID, target GUID and GUID->object path verified")
PY
- name: Stage package
shell: bash
run: |
set -euo pipefail
mkdir -p package/DLL
mkdir -p package/Interface/AddOns/WeirdUtils_Outline
cp zig-out/variants/outline.dll package/DLL/
cp zig-out/variants/customassets.dll package/DLL/
cp zig-out/variants/transmogfix.dll package/DLL/
cp src/outline/addon/Outline.lua package/Interface/AddOns/WeirdUtils_Outline/
cp src/outline/addon/Bindings.xml package/Interface/AddOns/WeirdUtils_Outline/
cp src/outline/addon/WeirdUtils_Outline.toc package/Interface/AddOns/WeirdUtils_Outline/
cat > package/README_TEST.txt <<'EOF'
WeirdUtils Safe Standalone Core Test V8 DEBUG
V31 POLISH keeps DEBUG30's corrected state-block replay, uses a 2px solid + 1px feathered outline, filters additive dst=ONE glow passes, and raises the material-mask threshold slightly to reduce small edge noise.
outline.dll
- Adds OutlineDebug() sticky in-game diagnostics for the render pipeline.
- Keeps only the Player_LoadScriptFunctions hook required to register OutlineCommand/OutlineDebug.
- Keeps only the GameEngine_MainInitialize hook required to initialise the renderer.
- Does not register the shared WeirdUtils version table.
- Does not install generic MPQ/file hooks.
- Uses the external WeirdUtils_Outline addon included here.
customassets.dll
- Keeps only CheckFileExistence from the shared file core.
- No shared Lua registration hooks.
- No shared engine/logout/shutdown hooks.
- Repairs legacy File_FindInArchive NOPs if present.
transmogfix.dll
- Installs only TransmogFix's own module hooks.
- No shared Lua/file/engine/logout/shutdown hooks.
weirdperformance.dll
- Not built here.
- The final ZIP will use the user's original binary unchanged.
EOF
sha256sum package/DLL/*.dll > package/SHA256SUMS.txt
- uses: actions/upload-artifact@v4
with:
name: safe-core-v8-debug
path: package/
if-no-files-found: error
@@ -0,0 +1,55 @@
name: V36 ExitFix Baseline
on:
push:
branches:
- test/v36-exitfix-baseline
workflow_dispatch:
permissions:
contents: read
jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: mlugg/setup-zig@v2
with:
version: '0.16.0'
- name: Build
shell: bash
run: |
set -euo pipefail
zig build all-variants -Doptimize=ReleaseSmall \
-Dweirdperformance=false \
-Doutline=true \
-Dcustomassets=true \
-Dtransmogfix=true
- name: Package
shell: bash
run: |
set -euo pipefail
rm -rf package dist
mkdir -p package/DLL package/Interface/AddOns/WeirdUtils_Outline dist
cp zig-out/variants/outline.dll package/DLL/outline.dll
cp src/outline/addon/Bindings.xml package/Interface/AddOns/WeirdUtils_Outline/
cp src/outline/addon/Outline.lua package/Interface/AddOns/WeirdUtils_Outline/
cp src/outline/addon/WeirdUtils_Outline.toc package/Interface/AddOns/WeirdUtils_Outline/
printf '%s\n' \
'V36 runtime + process-exit safety only.' \
'Base runtime commit: 4bbee94388f98843b738e275fd9482576be80654.' \
'Compared with V34, only src/outline/d3d9_hook.zig changes runtime behavior.' \
'main branch is untouched.' \
'WeirdPerformance is disabled in this standalone build and not modified.' \
> package/README_TEST.txt
(cd package && sha256sum DLL/outline.dll > SHA256SUMS.txt && zip -r ../dist/WeirdUtils_V36_EXITFIX_BASELINE.zip DLL Interface README_TEST.txt SHA256SUMS.txt)
- uses: actions/upload-artifact@v4
with:
name: WeirdUtils-V36-EXITFIX-BASELINE
path: dist/WeirdUtils_V36_EXITFIX_BASELINE.zip
if-no-files-found: error
@@ -0,0 +1,55 @@
name: V37A EndScene Only
on:
push:
branches:
- test/v37a-endscene-only
workflow_dispatch:
permissions:
contents: read
jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: mlugg/setup-zig@v2
with:
version: '0.16.0'
- name: Build
shell: bash
run: |
set -euo pipefail
zig build all-variants -Doptimize=ReleaseSmall \
-Dweirdperformance=false \
-Doutline=true \
-Dcustomassets=true \
-Dtransmogfix=true
- name: Package
shell: bash
run: |
set -euo pipefail
rm -rf package dist
mkdir -p package/DLL package/Interface/AddOns/WeirdUtils_Outline dist
cp zig-out/variants/outline.dll package/DLL/outline.dll
cp src/outline/addon/Bindings.xml package/Interface/AddOns/WeirdUtils_Outline/
cp src/outline/addon/Outline.lua package/Interface/AddOns/WeirdUtils_Outline/
cp src/outline/addon/WeirdUtils_Outline.toc package/Interface/AddOns/WeirdUtils_Outline/
printf '%s\n' \
'V37A EndScene-only isolation test.' \
'Base: V37A no-Reset-hook + ExitFix.' \
'Only IDirect3DDevice9::EndScene is hooked.' \
'DrawIndexedPrimitive and Reset are untouched.' \
'Visible outline is intentionally absent in this diagnostic build.' \
'main branch untouched; WeirdPerformance not modified.' \
> package/README_TEST.txt
(cd package && sha256sum DLL/outline.dll > SHA256SUMS.txt && zip -r ../dist/WeirdUtils_V37A_ENDSCENE_ONLY.zip DLL Interface README_TEST.txt SHA256SUMS.txt)
- uses: actions/upload-artifact@v4
with:
name: WeirdUtils-V37A-ENDSCENE-ONLY
path: dist/WeirdUtils_V37A_ENDSCENE_ONLY.zip
if-no-files-found: error
@@ -0,0 +1,54 @@
name: V37A No AutoRehook
on:
push:
branches:
- test/v37a-no-autorehook
workflow_dispatch:
permissions:
contents: read
jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: mlugg/setup-zig@v2
with:
version: '0.16.0'
- name: Build
shell: bash
run: |
set -euo pipefail
zig build all-variants -Doptimize=ReleaseSmall \
-Dweirdperformance=false \
-Doutline=true \
-Dcustomassets=true \
-Dtransmogfix=true
- name: Package
shell: bash
run: |
set -euo pipefail
rm -rf package dist
mkdir -p package/DLL package/Interface/AddOns/WeirdUtils_Outline dist
cp zig-out/variants/outline.dll package/DLL/outline.dll
cp src/outline/addon/Bindings.xml package/Interface/AddOns/WeirdUtils_Outline/
cp src/outline/addon/Outline.lua package/Interface/AddOns/WeirdUtils_Outline/
cp src/outline/addon/WeirdUtils_Outline.toc package/Interface/AddOns/WeirdUtils_Outline/
printf '%s\n' \
'V37A no-auto-rehook isolation test.' \
'Base: V37A radii-only + ExitFix.' \
'Only runtime change: automatic lateRehookIfLost() call removed from RenderDraw.' \
'Initial deferred D3D9 hook installation remains unchanged.' \
'main branch untouched; WeirdPerformance not modified.' \
> package/README_TEST.txt
(cd package && sha256sum DLL/outline.dll > SHA256SUMS.txt && zip -r ../dist/WeirdUtils_V37A_NO_AUTOREHOOK.zip DLL Interface README_TEST.txt SHA256SUMS.txt)
- uses: actions/upload-artifact@v4
with:
name: WeirdUtils-V37A-NO-AUTOREHOOK
path: dist/WeirdUtils_V37A_NO_AUTOREHOOK.zip
if-no-files-found: error
@@ -0,0 +1,55 @@
name: V37A No Reset Hook
on:
push:
branches:
- test/v37a-no-reset-hook
workflow_dispatch:
permissions:
contents: read
jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: mlugg/setup-zig@v2
with:
version: '0.16.0'
- name: Build
shell: bash
run: |
set -euo pipefail
zig build all-variants -Doptimize=ReleaseSmall \
-Dweirdperformance=false \
-Doutline=true \
-Dcustomassets=true \
-Dtransmogfix=true
- name: Package
shell: bash
run: |
set -euo pipefail
rm -rf package dist
mkdir -p package/DLL package/Interface/AddOns/WeirdUtils_Outline dist
cp zig-out/variants/outline.dll package/DLL/outline.dll
cp src/outline/addon/Bindings.xml package/Interface/AddOns/WeirdUtils_Outline/
cp src/outline/addon/Outline.lua package/Interface/AddOns/WeirdUtils_Outline/
cp src/outline/addon/WeirdUtils_Outline.toc package/Interface/AddOns/WeirdUtils_Outline/
printf '%s\n' \
'V37A no-Reset-hook isolation test.' \
'Base: V37A no-auto-rehook + ExitFix.' \
'EndScene and DrawIndexedPrimitive hooks remain active.' \
'Reset hook is NOT installed.' \
'Visible outline rendering should still work.' \
'main branch untouched; WeirdPerformance not modified.' \
> package/README_TEST.txt
(cd package && sha256sum DLL/outline.dll > SHA256SUMS.txt && zip -r ../dist/WeirdUtils_V37A_NO_RESET_HOOK.zip DLL Interface README_TEST.txt SHA256SUMS.txt)
- uses: actions/upload-artifact@v4
with:
name: WeirdUtils-V37A-NO-RESET-HOOK
path: dist/WeirdUtils_V37A_NO_RESET_HOOK.zip
if-no-files-found: error
@@ -0,0 +1,51 @@
name: V37A Radii Only
on:
push:
branches:
- test/v37a-radii-only
workflow_dispatch:
permissions:
contents: read
jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: mlugg/setup-zig@v2
with:
version: '0.16.0'
- name: Build
shell: bash
run: |
set -euo pipefail
zig build all-variants -Doptimize=ReleaseSmall \
-Dweirdperformance=false \
-Doutline=true \
-Dcustomassets=true \
-Dtransmogfix=true
- name: Package
shell: bash
run: |
set -euo pipefail
rm -rf package dist
mkdir -p package/DLL package/Interface/AddOns/WeirdUtils_Outline dist
cp zig-out/variants/outline.dll package/DLL/outline.dll
cp src/outline/addon/Bindings.xml package/Interface/AddOns/WeirdUtils_Outline/
cp src/outline/addon/Outline.lua package/Interface/AddOns/WeirdUtils_Outline/
cp src/outline/addon/WeirdUtils_Outline.toc package/Interface/AddOns/WeirdUtils_Outline/
printf '%s\n' \
'V37A isolation test.' \
'Base: V36 + ExitFix (stable in user test).' \
'Only runtime change: radii 1.5/3.0 -> 1.35/3.20.' \
'V36 decode shader remains unchanged.' \
'main branch untouched; WeirdPerformance not modified.' \
> package/README_TEST.txt
(cd package && sha256sum DLL/outline.dll > SHA256SUMS.txt && zip -r ../dist/WeirdUtils_V37A_RADII_ONLY.zip DLL Interface README_TEST.txt SHA256SUMS.txt)
- uses: actions/upload-artifact@v4
with:
name: WeirdUtils-V37A-RADII-ONLY
path: dist/WeirdUtils_V37A_RADII_ONLY.zip
if-no-files-found: error
+8
View File
@@ -66,6 +66,7 @@ pub fn build(b: *std.Build) void {
const build_options = b.addOptions();
addModuleOptionsFromArray(b, build_options, &module_enabled);
build_options.addOption(bool, "safe_variant_core", false);
// transform_capture: records game-x87 transformMatrix4x4 state to disk for
// offline bench parity. Mutually exclusive with bone_sse64 (capture needs
// the real game output; bone_sse64 replaces it).
@@ -324,6 +325,7 @@ pub fn build(b: *std.Build) void {
if (comptime std.mem.eql(u8, m.name, "weirdperformance")) noperf_enabled[i] = false;
}
addModuleOptionsFromArray(b, noperf_opts, &noperf_enabled);
noperf_opts.addOption(bool, "safe_variant_core", false);
const noperf_lib = b.addLibrary(.{
.name = "weirdutils_noperf",
@@ -357,6 +359,12 @@ pub fn build(b: *std.Build) void {
inline for (module_list) |m| {
opts.addOption(bool, "enable_" ++ m.name, std.mem.eql(u8, m.name, variant_mod.name));
}
const safe_variant_core = comptime (
std.mem.eql(u8, variant_mod.name, "outline") or
std.mem.eql(u8, variant_mod.name, "customassets") or
std.mem.eql(u8, variant_mod.name, "transmogfix")
);
opts.addOption(bool, "safe_variant_core", safe_variant_core);
addFileListOptions(b, opts);
const names: []const []const u8 = comptime blk: {
var n: [module_list.len][]const u8 = undefined;
@@ -0,0 +1 @@
768442df41b7ec363c9827c860f139add2dfa5cd718915c3b55adfe93acb65cf dist/WeirdUtils_safe_core_V8_POLISH32_ORIGINAL_WeirdPerformance.zip
+115 -6
View File
@@ -52,6 +52,27 @@ const luagc_mod = if (build_opts.luagc) @import("luagc/luagc.zig") else struct {
const module_active = @import("module_active.zig");
const WINAPI = std.builtin.CallingConvention.winapi;
const safe_variant_core = @import("build_options").safe_variant_core;
// Standalone compatibility helper.
// Older WeirdUtils cores can replace these two File_FindInArchive branches
// with NOP NOP. Safe standalone variants restore the original bytes only when
// that exact legacy patch is present.
fn restoreLegacyFileFindArchiveGates() void {
const gate1: usize = 0x654B5C;
const gate2: usize = 0x654B6A;
const original1 = [2]u8{ 0x74, 0x25 };
const original2 = [2]u8{ 0x75, 0x17 };
if (hook.readMem(u8, gate1) == 0x90 and hook.readMem(u8, gate1 + 1) == 0x90) {
hook.writeProtected(gate1, &original1);
log.print("compat: restored File_FindInArchive gate 1\n");
}
if (hook.readMem(u8, gate2) == 0x90 and hook.readMem(u8, gate2 + 1) == 0x90) {
hook.writeProtected(gate2, &original2);
log.print("compat: restored File_FindInArchive gate 2\n");
}
}
// =============================================================================
// Lua Protection Bypass
@@ -71,8 +92,23 @@ pub const lua = @import("lua.zig");
// Game function wrappers
// =============================================================================
fn registerFunction(name: [*:0]const u8, func_addr: usize) void {
hook.call(fn ([*:0]const u8, usize) callconv(hook.cc.fastcall) void, 0x704120, .{ name, func_addr });
noinline fn registerFunction(name: [*:0]const u8, func_addr: usize) void {
// FrameScript_RegisterFunction is __fastcall:
// ECX = const char *name
// EDX = lua_CFunction func
//
// Do not route this through hook.call here. With the current Zig/zhook
// combination that path was emitted as two stack pushes, so the engine
// consumed stale ECX/EDX values left by the previous hook in the chain.
// Force the verified WoW 1.12.1 ABI explicitly.
var eax_clobber: usize = undefined;
asm volatile (
\\mov $0x704120, %%eax
\\call *%%eax
: [eax] "={eax}" (eax_clobber),
: [name] "{ecx}" (@intFromPtr(name)),
[func] "{edx}" (func_addr),
);
}
fn allocateGameBuffer(size: u32) ?[*]u8 {
@@ -93,6 +129,8 @@ fn registerLuaFunctions() void {
}
if (build_opts.outline) {
registerFunction("OutlineCommand", @intFromPtr(&outline.outlineCommand));
registerFunction("OutlineSyncTarget", @intFromPtr(&outline.outlineSyncTarget));
registerFunction("OutlineDebug", @intFromPtr(&outline.outlineDebug));
}
if (build_opts.logsessions) {
registerFunction("GetCombatLogPath", @intFromPtr(&logsessions.luaGetCombatLogPath));
@@ -676,6 +714,16 @@ fn registerAllSystemCommandsDetour() callconv(hook.cc.stdcall) void {
registerModuleVersions();
}
// Safe standalone Outline only needs its Lua command to be re-registered
// after login/reload. It deliberately does not touch the shared WeirdUtils
// version table, avoiding nested Lua-table mutations across multiple DLLs.
fn registerOutlineCommandsDetour() callconv(hook.cc.stdcall) void {
register_commands_hook.callOriginal(.{});
if (build_opts.outline) {
registerLuaFunctions();
}
}
/// Hook for Glue_LoadScriptFunctions (0x46ABB0).
/// Fires at the login/glue screen — registers version table so addons can query early.
fn glueLoadScriptFunctionsDetour() callconv(hook.cc.stdcall) void {
@@ -692,6 +740,10 @@ var engine_init_hook: hook.Detour(fn () callconv(hook.cc.stdcall) void) = .{};
fn engineInitDetour() callconv(hook.cc.stdcall) void {
engine_init_hook.callOriginal(.{});
if (safe_variant_core and build_opts.outline) {
restoreLegacyFileFindArchiveGates();
}
if (build_opts.screenshot) {
screenshot.installHook();
}
@@ -811,6 +863,41 @@ fn install() void {
logging.init();
log = logging.Logger.open("weirdutils", .console);
if (safe_variant_core) {
log.print("Installing minimal standalone core\n");
// customassets needs only CheckFileExistence for loose Data files.
// Do not install embedded-file hooks and do not NOP File_FindInArchive.
if (build_opts.customassets) {
_ = cfe_hook.attach(0x654DD0, &checkFileExistenceDetour);
}
// Outline needs one Lua registration hook so /outline survives /reload,
// plus the engine-init hook for its deferred renderer initialization.
if (build_opts.outline) {
_ = register_commands_hook.attach(0x490250, &registerOutlineCommandsDetour);
_ = engine_init_hook.attach(0x46a400, &engineInitDetour);
}
// Install only the actual module hooks.
inline for (modules) |m| {
if (m.install) |inst| inst();
if (m.name) |name| {
if (m.is_active) |active_fn| module_active.register(name, active_fn);
}
}
// The user's original WeirdPerformance loads before these variants.
// Repair only its legacy MPQ gate NOPs; leave WeirdPerformance itself untouched.
if (build_opts.customassets or build_opts.outline) {
restoreLegacyFileFindArchiveGates();
}
// No embedded addon registration in minimal variants.
// Outline's addon is supplied normally under Interface\\AddOns.
return;
}
// Core hooks chain safely across multiple DLLs via zhook's E9-detect path:
// each DLL's trampoline JMPs to the previous DLL's detour, forming a LIFO
// call chain. Callbacks are additive (Lua registration, file serving) or
@@ -840,6 +927,25 @@ fn install() void {
}
fn uninstall() void {
if (safe_variant_core) {
comptime var j = modules.len;
inline while (j > 0) {
j -= 1;
if (modules[j].remove) |rm| rm();
}
if (build_opts.outline) {
engine_init_hook.detach();
register_commands_hook.detach();
}
if (build_opts.customassets) {
cfe_hook.detach();
}
logging.deinit();
return;
}
// Remove modules in reverse order
comptime var i = modules.len;
inline while (i > 0) {
@@ -942,12 +1048,15 @@ comptime {
pub export fn DllMain(
_: ?*anyopaque,
reason: u32,
_: ?*anyopaque,
) callconv(WINAPI) i32 {
reserved: ?*anyopaque,
) callconv(WINAPI) std.os.windows.BOOL {
switch (reason) {
1 => install(),
0 => uninstall(),
0 => {
// Process-exit safety only; runtime Outline behavior is unchanged.
if (reserved == null) uninstall();
},
else => {},
}
return 1;
return @enumFromInt(1);
}
+2
View File
@@ -166,6 +166,8 @@ pub const FN_UNIT_REACTION: usize = 0x6061E0;
/// __fastcall(), no params, returns EAX(low):EDX(high).
pub const FN_GET_PLAYER_GUID: usize = 0x00468550;
/// Current target GUID (same address used by Nampower).
pub const LOCKED_TARGET_GUID: usize = 0x00B4E2D8;
/// __fastcall(obj_ECX) → bool. GO interactability check.
pub const FN_CALL_SPELL_CAST_HANDLER: usize = 0x5F8800;
+17
View File
@@ -8,11 +8,28 @@ BINDING_HEADER_OUTLINE = "Outline"
local frame = CreateFrame("Frame")
frame:RegisterEvent("ADDON_LOADED")
frame:RegisterEvent("PLAYER_LOGIN")
frame:RegisterEvent("PLAYER_TARGET_CHANGED")
local syncElapsed = 0
frame:SetScript("OnEvent", function()
if event == "PLAYER_LOGIN" then
OutlineSyncTarget()
local on = OutlineCommand()
DEFAULT_CHAT_FRAME:AddMessage("|cff00ff00Outline|r v" .. OUTLINE_VERSION .. " loaded (" .. (on and "enabled" or "disabled") .. ")")
elseif event == "PLAYER_TARGET_CHANGED" then
OutlineSyncTarget()
syncElapsed = 0
end
end)
-- AutoFix safety net: republish the current target from the safe Lua/main
-- thread at low frequency so a missed/early target event cannot disable Outline.
frame:SetScript("OnUpdate", function()
syncElapsed = syncElapsed + arg1
if syncElapsed >= 0.10 then
syncElapsed = 0
OutlineSyncTarget()
end
end)
+496 -141
View File
@@ -57,14 +57,108 @@ var orig_reset: usize = 0;
var d3d9_vtable: ?[*]usize = null;
var hooks_installed: bool = false;
// Sticky diagnostics for the in-game OutlineDebug() command.
pub var debug_endscene_seen: bool = false;
pub var debug_dip_seen: bool = false;
pub var debug_outline_dip_seen: bool = false;
pub var debug_cached_draw_seen: bool = false;
pub var debug_translucent_skipped_seen: bool = false;
pub var debug_state_block_seen: bool = false;
pub var debug_outer_state_restore_seen: bool = false;
pub var debug_additive_skipped_seen: bool = false;
pub var debug_shaders_ready_seen: bool = false;
pub var debug_resources_ready_seen: bool = false;
pub var debug_pipeline_entered_seen: bool = false;
pub var debug_pipeline_ready_seen: bool = false;
pub var debug_shader_stage: u32 = 0;
pub var debug_resource_stage: u32 = 0;
pub var debug_shader_assemble_hr: i32 = 0;
pub var debug_shader_create_hr: i32 = 0;
pub var debug_texture_create_hr: i32 = 0;
pub var debug_shader_error_text: [160]u8 = [_]u8{0} ** 160;
pub fn hooksInstalled() bool {
return hooks_installed;
}
pub const LiveHookState = struct {
vtable_found: bool = false,
same_vtable: bool = false,
endscene_ours: bool = false,
dip_ours: bool = false,
reset_ours: bool = false,
endscene_ptr: usize = 0,
dip_ptr: usize = 0,
reset_ptr: usize = 0,
};
/// Read the game's current D3D9 vtable and verify whether our entries are
/// still installed. This is intentionally queried on demand from OutlineDebug.
pub fn getLiveHookState() LiveHookState {
const cur = getD3D9VTable() orelse return .{};
var out: LiveHookState = .{ .vtable_found = true };
out.same_vtable = if (d3d9_vtable) |saved| @intFromPtr(saved) == @intFromPtr(cur) else false;
out.endscene_ptr = cur[types.VT.EndScene];
out.dip_ptr = cur[types.VT.DrawIndexedPrimitive];
out.reset_ptr = cur[types.VT.Reset];
out.endscene_ours = out.endscene_ptr == @intFromPtr(&hkEndScene);
out.dip_ours = out.dip_ptr == @intFromPtr(&hkDIP);
out.reset_ours = out.reset_ptr == @intFromPtr(&hkReset);
return out;
}
pub var debug_late_rehook_attempted: bool = false;
pub var debug_late_rehook_succeeded: bool = false;
/// DEBUG15: re-apply the D3D9 hooks on demand after the client is fully loaded.
/// If the current entries are no longer ours, chain whatever is there now as
/// the new originals, then patch the three entries again.
pub fn lateRehookIfLost() bool {
debug_late_rehook_attempted = true;
const cur = getD3D9VTable() orelse return false;
d3d9_vtable = cur;
const ours_end = @intFromPtr(&hkEndScene);
const ours_dip = @intFromPtr(&hkDIP);
const ours_reset = @intFromPtr(&hkReset);
if (cur[types.VT.EndScene] != ours_end) {
if (!patchVtableEntry(cur, types.VT.EndScene, ours_end, &orig_endscene)) return false;
}
if (cur[types.VT.DrawIndexedPrimitive] != ours_dip) {
if (!patchVtableEntry(cur, types.VT.DrawIndexedPrimitive, ours_dip, &orig_dip)) return false;
}
if (cur[types.VT.Reset] != ours_reset) {
if (!patchVtableEntry(cur, types.VT.Reset, ours_reset, &orig_reset)) return false;
}
hooks_installed = true;
const live = getLiveHookState();
debug_late_rehook_succeeded = live.endscene_ours and live.dip_ours and live.reset_ours;
return debug_late_rehook_succeeded;
}
/// True until the first EndScene verifies (and if needed, forces) D24S8 format.
var need_force_reset: bool = true;
var need_force_reset: bool = false;
pub var debug_stencil_ready: bool = false;
pub var debug_stencil_format: u32 = 0;
pub var debug_stencil_reset_hr: i32 = 0;
pub fn requestStencilCheck() void {
// DEBUG23: intentionally disabled. Forcing IDirect3DDevice9::Reset on this
// client can hang the render thread while audio/game logic keeps running.
need_force_reset = false;
}
// =============================================================================
// Shader resources
// =============================================================================
var outline_ps: ?*anyopaque = null; // flat-color PS (silhouettes)
var outline_ps: ?*anyopaque = null; // flat-color PS (solid silhouettes)
var outline_alpha_ps: ?*anyopaque = null; // texture-alpha-aware silhouette PS
var outline_rgb_ps: ?*anyopaque = null; // additive/modulated texture coverage PS
var material_mask_ps: ?*anyopaque = null; // exact-material scratch -> binary mask
var jfa_init_ps: ?*anyopaque = null; // JFA seed init PS
var jfa_prop_ps: ?*anyopaque = null; // JFA propagation PS
var jfa_decode_ps: ?*anyopaque = null; // JFA decode + composite PS
@@ -91,7 +185,9 @@ const D3DXAssembleShaderFn = *const fn (
// Render target resources
// =============================================================================
var rt_silhouette_tex: ?*anyopaque = null; // A8R8G8B8 silhouette texture
var rt_material_tex: ?*anyopaque = null; // A8R8G8B8 exact-material scratch
var rt_material_surf: ?*anyopaque = null;
var rt_silhouette_tex: ?*anyopaque = null; // A8R8G8B8 normalized silhouette mask
var rt_silhouette_surf: ?*anyopaque = null;
var rt_jfa_a_tex: ?*anyopaque = null; // G16R16F JFA ping texture
var rt_jfa_a_surf: ?*anyopaque = null;
@@ -132,6 +228,18 @@ const CachedDraw = struct {
ib: ?*anyopaque = null,
vertex_decl: ?*anyopaque = null,
vertex_shader: ?*anyopaque = null,
tex: [4]?*anyopaque = .{ null, null, null, null },
pixel_shader: ?*anyopaque = null,
state_block: ?*anyopaque = null,
alpha_op: [4]u32 = .{ 1, 1, 1, 1 },
alpha_arg1: [4]u32 = .{ 0, 0, 0, 0 },
alpha_arg2: [4]u32 = .{ 0, 0, 0, 0 },
alpha_test_enable: u32 = 0,
alpha_ref: u32 = 0,
alpha_func: u32 = types.D3DCMP_ALWAYS,
alpha_blend_enable: u32 = 0,
src_blend: u32 = types.D3DBLEND_ONE,
dst_blend: u32 = types.D3DBLEND_ZERO,
// Per-model outline info
color: u32 = 0,
category: types.ModelCategory = .none,
@@ -207,6 +315,12 @@ fn deviceGetViewport(dev: *anyopaque, vp_out: *types.D3DVIEWPORT9) void {
_ = f(dev, vp_out);
}
fn deviceSetViewport(dev: *anyopaque, vp_in: *const types.D3DVIEWPORT9) void {
const f: *const fn (*anyopaque, *const types.D3DVIEWPORT9) callconv(hook.cc.stdcall) i32 =
@ptrFromInt(vt(dev)[types.VT.SetViewport]);
_ = f(dev, vp_in);
}
fn deviceSetRenderTarget(dev: *anyopaque, idx: u32, surf: *anyopaque) void {
const f: *const fn (*anyopaque, u32, *anyopaque) callconv(hook.cc.stdcall) i32 =
@ptrFromInt(vt(dev)[types.VT.SetRenderTarget]);
@@ -227,6 +341,38 @@ fn deviceSetTexture(dev: *anyopaque, stage: u32, tex: ?*anyopaque) void {
_ = @call(.never_tail, f, .{ dev, stage, tex });
}
fn deviceGetTSS(dev: *anyopaque, stage: u32, state_type: u32) u32 {
var val: u32 = 0;
const f: *const fn (*anyopaque, u32, u32, *u32) callconv(hook.cc.stdcall) i32 =
@ptrFromInt(vt(dev)[types.VT.GetTextureStageState]);
_ = f(dev, stage, state_type, &val);
return val;
}
fn deviceSetTSS(dev: *anyopaque, stage: u32, state_type: u32, value: u32) void {
const f: *const fn (*anyopaque, u32, u32, u32) callconv(hook.cc.stdcall) i32 =
@ptrFromInt(vt(dev)[types.VT.SetTextureStageState]);
_ = f(dev, stage, state_type, value);
}
fn deviceCreateStateBlock(dev: *anyopaque) ?*anyopaque {
var out: ?*anyopaque = null;
const f: *const fn (*anyopaque, u32, *?*anyopaque) callconv(hook.cc.stdcall) i32 =
@ptrFromInt(vt(dev)[types.VT.CreateStateBlock]);
if (f(dev, types.D3DSBT_ALL, &out) < 0) return null;
return out;
}
fn stateBlockApply(sb: *anyopaque) bool {
// IDirect3DStateBlock9 vtable:
// 0 QI, 1 AddRef, 2 Release, 3 GetDevice, 4 Capture, 5 Apply.
// DEBUG28/29 accidentally called Capture here, so no captured state was
// ever restored. Use the real Apply slot.
const f: *const fn (*anyopaque) callconv(hook.cc.stdcall) i32 =
@ptrFromInt(vt(sb)[5]);
return f(sb) >= 0;
}
fn deviceSetFVF(dev: *anyopaque, fvf: u32) void {
const f: *const fn (*anyopaque, u32) callconv(hook.cc.stdcall) i32 =
@ptrFromInt(vt(dev)[types.VT.SetFVF]);
@@ -302,7 +448,9 @@ fn deviceDrawPrimitiveUP(dev: *anyopaque, prim_type: u32, prim_count: u32, data:
fn deviceCreateTexture(dev: *anyopaque, w: u32, h: u32, levels: u32, usage: u32, fmt: u32, pool: u32, out: *?*anyopaque) i32 {
const f: *const fn (*anyopaque, u32, u32, u32, u32, u32, u32, *?*anyopaque, u32) callconv(hook.cc.stdcall) i32 =
@ptrFromInt(vt(dev)[types.VT.CreateTexture]);
return f(dev, w, h, levels, usage, fmt, pool, out, 0);
const hr = f(dev, w, h, levels, usage, fmt, pool, out, 0);
if (hr < 0) debug_texture_create_hr = hr;
return hr;
}
/// Get surface level 0 from a texture. Returns AddRef'd surface or null.
@@ -348,53 +496,73 @@ fn ensureResources(device: *anyopaque) void {
var vp: types.D3DVIEWPORT9 = .{};
deviceGetViewport(device, &vp);
if (vp.Width == 0 or vp.Height == 0) return;
debug_resource_stage = 1; // viewport valid
// Check if resources already match current dimensions
if (vp.Width == resource_width and vp.Height == resource_height and
rt_silhouette_tex != null) return;
rt_material_tex != null and rt_silhouette_tex != null) return;
// Release old and create new
releaseResources();
resource_width = vp.Width;
resource_height = vp.Height;
// Silhouette RT (A8R8G8B8)
// Exact-material scratch RT (A8R8G8B8)
if (deviceCreateTexture(device, vp.Width, vp.Height, 1, types.D3DUSAGE_RENDERTARGET, types.D3DFMT_A8R8G8B8, types.D3DPOOL_DEFAULT, &rt_material_tex) < 0) {
releaseResources();
return;
}
rt_material_surf = textureGetSurfaceLevel(rt_material_tex.?);
if (rt_material_surf == null) {
releaseResources();
return;
}
// Normalized silhouette RT (A8R8G8B8)
if (deviceCreateTexture(device, vp.Width, vp.Height, 1, types.D3DUSAGE_RENDERTARGET, types.D3DFMT_A8R8G8B8, types.D3DPOOL_DEFAULT, &rt_silhouette_tex) < 0) {
releaseResources();
return;
}
debug_resource_stage = 2; // silhouette texture ready
rt_silhouette_surf = textureGetSurfaceLevel(rt_silhouette_tex.?);
if (rt_silhouette_surf == null) {
releaseResources();
return;
}
debug_resource_stage = 3; // silhouette surface ready
// JFA A RT (G16R16F)
if (deviceCreateTexture(device, vp.Width, vp.Height, 1, types.D3DUSAGE_RENDERTARGET, types.D3DFMT_G16R16F, types.D3DPOOL_DEFAULT, &rt_jfa_a_tex) < 0) {
// V34: JFA A RT (G32R32F) - full-float seed UVs avoid screen-position quantization.
if (deviceCreateTexture(device, vp.Width, vp.Height, 1, types.D3DUSAGE_RENDERTARGET, types.D3DFMT_G32R32F, types.D3DPOOL_DEFAULT, &rt_jfa_a_tex) < 0) {
releaseResources();
return;
}
debug_resource_stage = 4; // JFA A texture ready
rt_jfa_a_surf = textureGetSurfaceLevel(rt_jfa_a_tex.?);
if (rt_jfa_a_surf == null) {
releaseResources();
return;
}
debug_resource_stage = 5; // JFA A surface ready
// JFA B RT (G16R16F)
if (deviceCreateTexture(device, vp.Width, vp.Height, 1, types.D3DUSAGE_RENDERTARGET, types.D3DFMT_G16R16F, types.D3DPOOL_DEFAULT, &rt_jfa_b_tex) < 0) {
// V34: JFA B RT (G32R32F) - full-float seed UVs avoid screen-position quantization.
if (deviceCreateTexture(device, vp.Width, vp.Height, 1, types.D3DUSAGE_RENDERTARGET, types.D3DFMT_G32R32F, types.D3DPOOL_DEFAULT, &rt_jfa_b_tex) < 0) {
releaseResources();
return;
}
debug_resource_stage = 6; // JFA B texture ready
rt_jfa_b_surf = textureGetSurfaceLevel(rt_jfa_b_tex.?);
if (rt_jfa_b_surf == null) {
releaseResources();
return;
}
debug_resource_stage = 7; // all RT surfaces ready
debug_resources_ready_seen = true;
}
fn releaseResources() void {
inline for (.{
&rt_silhouette_surf, &rt_jfa_a_surf, &rt_jfa_b_surf,
&rt_material_surf, &rt_silhouette_surf, &rt_jfa_a_surf, &rt_jfa_b_surf,
}) |surf_ptr| {
if (surf_ptr.*) |s| {
comRelease(s);
@@ -402,7 +570,7 @@ fn releaseResources() void {
}
}
inline for (.{
&rt_silhouette_tex, &rt_jfa_a_tex, &rt_jfa_b_tex,
&rt_material_tex, &rt_silhouette_tex, &rt_jfa_a_tex, &rt_jfa_b_tex,
}) |tex_ptr| {
if (tex_ptr.*) |t| {
comRelease(t);
@@ -420,6 +588,46 @@ fn releaseResources() void {
/// Flat colour pixel shader - outputs PS constant c0.
const ps_flat_src = "ps_3_0\nmov oC0, c0\n";
/// Texture-alpha-aware silhouette shader.
/// c0 = outline colour/encoded width, c1.x = -coverage threshold.
const ps_alpha_src =
"ps_3_0\n" ++
"dcl_2d s0\n" ++
"dcl_texcoord0 v0\n" ++
"texld r0, v0, s0\n" ++
"add r1, r0.aaaa, c1.xxxx\n" ++
"texkill r1\n" ++
"mov oC0, c0\n";
/// Coverage shader for additive/modulated M2 layers where transparency can be
/// encoded as black RGB rather than useful texture alpha.
const ps_rgb_src =
"ps_3_0\n" ++
"dcl_2d s0\n" ++
"dcl_texcoord0 v0\n" ++
"texld r0, v0, s0\n" ++
"max r1.x, r0.r, r0.g\n" ++
"max r1.x, r1.x, r0.b\n" ++
"add r1, r1.xxxx, c1.xxxx\n" ++
"texkill r1\n" ++
"mov oC0, c0\n";
/// Normalize the exact-material scratch into the uniform outline mask.
/// c0 = outline colour (alpha forced to 1), c1.x = -coverage threshold.
/// Coverage accepts either alpha or RGB, so opaque dark materials and additive
/// effects both survive while untouched transparent pixels remain discarded.
const material_mask_src =
"ps_3_0\n" ++
"dcl_2d s0\n" ++
"dcl_texcoord0 v0\n" ++
"texld r0, v0, s0\n" ++
"max r1.x, r0.r, r0.g\n" ++
"max r1.x, r1.x, r0.b\n" ++
"max r1.x, r1.x, r0.a\n" ++
"add r1, r1.xxxx, c1.xxxx\n" ++
"texkill r1\n" ++
"mov oC0, c0\n";
/// JFA init: sample silhouette, output own UV as seed or sentinel (-1,-1).
/// Sentinel must be outside [0,1] UV space so it never wins distance comparisons.
const jfa_init_src =
@@ -447,13 +655,17 @@ const jfa_prop_src =
"def c9, 1.0, 1.0, 0.0, 0.0\n" ++
"dcl_2d s0\n" ++
"dcl_texcoord0 v0\n" ++
// D3DX on this client only allows one c# register per arithmetic
// instruction. Copy c0.xy (step size) to a temp once, then combine
// that temp with c2..c9 in the neighbor MADs.
"mov r7.xy, c0.xy\n" ++
// Self sample - initialize best seed and distance
"texld r0, v0, s0\n" ++
"sub r2.xy, v0.xy, r0.xy\n" ++
"dp2add r9.x, r2, r2, c1.x\n" ++ // best dist²
"mov r8.xy, r0.xy\n" ++ // best seed UV
// Neighbor (-1,-1) via c2
"mad r4.xy, c2.xy, c0.xy, v0.xy\n" ++
"mad r4.xy, c2.xy, r7.xy, v0.xy\n" ++
"texld r5, r4, s0\n" ++
"sub r2.xy, v0.xy, r5.xy\n" ++
"dp2add r2.z, r2, r2, c1.x\n" ++
@@ -461,7 +673,7 @@ const jfa_prop_src =
"cmp r8.xy, r3.x, r8.xy, r5.xy\n" ++
"cmp r9.x, r3.x, r9.x, r2.z\n" ++
// Neighbor (-1, 0) via c3
"mad r4.xy, c3.xy, c0.xy, v0.xy\n" ++
"mad r4.xy, c3.xy, r7.xy, v0.xy\n" ++
"texld r5, r4, s0\n" ++
"sub r2.xy, v0.xy, r5.xy\n" ++
"dp2add r2.z, r2, r2, c1.x\n" ++
@@ -469,7 +681,7 @@ const jfa_prop_src =
"cmp r8.xy, r3.x, r8.xy, r5.xy\n" ++
"cmp r9.x, r3.x, r9.x, r2.z\n" ++
// Neighbor (-1, 1) via c4
"mad r4.xy, c4.xy, c0.xy, v0.xy\n" ++
"mad r4.xy, c4.xy, r7.xy, v0.xy\n" ++
"texld r5, r4, s0\n" ++
"sub r2.xy, v0.xy, r5.xy\n" ++
"dp2add r2.z, r2, r2, c1.x\n" ++
@@ -477,7 +689,7 @@ const jfa_prop_src =
"cmp r8.xy, r3.x, r8.xy, r5.xy\n" ++
"cmp r9.x, r3.x, r9.x, r2.z\n" ++
// Neighbor (0, -1) via c5
"mad r4.xy, c5.xy, c0.xy, v0.xy\n" ++
"mad r4.xy, c5.xy, r7.xy, v0.xy\n" ++
"texld r5, r4, s0\n" ++
"sub r2.xy, v0.xy, r5.xy\n" ++
"dp2add r2.z, r2, r2, c1.x\n" ++
@@ -485,7 +697,7 @@ const jfa_prop_src =
"cmp r8.xy, r3.x, r8.xy, r5.xy\n" ++
"cmp r9.x, r3.x, r9.x, r2.z\n" ++
// Neighbor (0, 1) via c6
"mad r4.xy, c6.xy, c0.xy, v0.xy\n" ++
"mad r4.xy, c6.xy, r7.xy, v0.xy\n" ++
"texld r5, r4, s0\n" ++
"sub r2.xy, v0.xy, r5.xy\n" ++
"dp2add r2.z, r2, r2, c1.x\n" ++
@@ -493,7 +705,7 @@ const jfa_prop_src =
"cmp r8.xy, r3.x, r8.xy, r5.xy\n" ++
"cmp r9.x, r3.x, r9.x, r2.z\n" ++
// Neighbor (1, -1) via c7
"mad r4.xy, c7.xy, c0.xy, v0.xy\n" ++
"mad r4.xy, c7.xy, r7.xy, v0.xy\n" ++
"texld r5, r4, s0\n" ++
"sub r2.xy, v0.xy, r5.xy\n" ++
"dp2add r2.z, r2, r2, c1.x\n" ++
@@ -501,7 +713,7 @@ const jfa_prop_src =
"cmp r8.xy, r3.x, r8.xy, r5.xy\n" ++
"cmp r9.x, r3.x, r9.x, r2.z\n" ++
// Neighbor (1, 0) via c8
"mad r4.xy, c8.xy, c0.xy, v0.xy\n" ++
"mad r4.xy, c8.xy, r7.xy, v0.xy\n" ++
"texld r5, r4, s0\n" ++
"sub r2.xy, v0.xy, r5.xy\n" ++
"dp2add r2.z, r2, r2, c1.x\n" ++
@@ -509,7 +721,7 @@ const jfa_prop_src =
"cmp r8.xy, r3.x, r8.xy, r5.xy\n" ++
"cmp r9.x, r3.x, r9.x, r2.z\n" ++
// Neighbor (1, 1) via c9
"mad r4.xy, c9.xy, c0.xy, v0.xy\n" ++
"mad r4.xy, c9.xy, r7.xy, v0.xy\n" ++
"texld r5, r4, s0\n" ++
"sub r2.xy, v0.xy, r5.xy\n" ++
"dp2add r2.z, r2, r2, c1.x\n" ++
@@ -520,34 +732,31 @@ const jfa_prop_src =
"mov oC0.xy, r8.xy\n" ++
"mov oC0.zw, c1.xx\n";
/// JFA decode + composite: compute distance to nearest seed, threshold, output outline.
/// c0 = (screen_width, screen_height, 4.0, 0.0) set by CPU.
/// V36 RETAIL-SOFT: keep the proven V34 JFA, soften only final decode.
/// c0 = (screen_width, screen_height, core_radius_px, halo_radius_px).
/// Uses the same two texture reads as V34: no 17-tap full-screen morphology.
const jfa_decode_src =
"ps_3_0\n" ++
"def c1, 0.0, 1.0, -0.002, 0.0\n" ++
"dcl_2d s0\n" ++ // JFA result (nearest seed UV)
"dcl_2d s1\n" ++ // silhouette (colour + width-encoded alpha)
"def c1, 0.0, 0.22, 0.82, -0.002\n" ++
"def c2, 1.0, 0.95, 0.58, 0.0\n" ++
"dcl_2d s0\n" ++
"dcl_2d s1\n" ++
"dcl_texcoord0 v0\n" ++
// Read nearest seed UV
"texld r0, v0, s0\n" ++
// Pixel-space squared distance
"sub r1.xy, v0.xy, r0.xy\n" ++
"mul r1.xy, r1.xy, c0.xy\n" ++ // (du*W, dv*H)
"dp2add r1.z, r1, r1, c0.w\n" ++ // dist² in pixels
// Read seed's silhouette colour + width
"texld r2, r0, s1\n" ++
"mul r3.x, r2.a, c0.z\n" ++ // outline_width = alpha * 4.0
"mul r3.x, r3.x, r3.x\n" ++ // width²
// Threshold: inside outline if dist² < width²
"sub r3.y, r1.z, r3.x\n" ++ // dist² - width²
"cmp r4.w, r3.y, c0.w, c1.y\n" ++ // >= 0 → 0 (outside), < 0 → 1 (inside)
// Exclude silhouette interior (don't draw outline ON the model)
"texld r5, v0, s1\n" ++ // silhouette at current pixel
"add r5.x, r5.a, c1.z\n" ++ // alpha - 0.002
"cmp r4.w, r5.x, c0.w, r4.w\n" ++ // if inside silhouette → 0
// Output
"mov r4.xyz, r2.xyz\n" ++ // outline colour from seed
"mov oC0, r4\n";
"mul r1.xy, r1.xy, c0.xy\n" ++
"dp2add r1.z, r1, r1, c1.x\n" ++
"mul r3.x, c0.z, c0.z\n" ++
"mul r3.y, c0.w, c0.w\n" ++
"sub r4.x, r1.z, r3.y\n" ++
"cmp r6.w, r4.x, c1.x, c1.y\n" ++
"sub r4.x, r1.z, r3.x\n" ++
"cmp r6.w, r4.x, r6.w, c1.z\n" ++
"texld r5, v0, s1\n" ++
"add r5.x, r5.a, c1.w\n" ++
"cmp r6.w, r5.x, c1.x, r6.w\n" ++
"mov r6.xyz, c2.xyz\n" ++
"mov oC0, r6\n";
/// Debug: composite silhouette RT directly. Forces alpha to 1.0 where silhouette
/// has any content (alpha >= 0.002), 0.0 elsewhere. Bypasses JFA entirely.
@@ -567,33 +776,55 @@ const debug_sil_src =
fn ensureShaders(device: *anyopaque) void {
shaders_attempted = true;
debug_shader_stage = 1; // entered
const d3dx = LoadLibraryA("d3dx9_43.dll") orelse
LoadLibraryA("d3dx9_42.dll") orelse
LoadLibraryA("d3dx9_41.dll") orelse return;
debug_shader_stage = 2; // D3DX loaded
const assemble_ptr = GetProcAddress(d3dx, "D3DXAssembleShader") orelse return;
debug_shader_stage = 3; // assembler found
const assemble: D3DXAssembleShaderFn = @ptrCast(assemble_ptr);
// --- Flat-colour PS (for silhouettes) ---
// --- Flat-colour PS (for solid silhouettes) ---
outline_ps = assemblePS(device, assemble, ps_flat_src, ps_flat_src.len) orelse return;
// --- Alpha-aware silhouette PS (for textured cutout/translucent planes) ---
outline_alpha_ps = assemblePS(device, assemble, ps_alpha_src, ps_alpha_src.len) orelse {
releaseShaders();
return;
};
outline_rgb_ps = assemblePS(device, assemble, ps_rgb_src, ps_rgb_src.len) orelse {
releaseShaders();
return;
};
material_mask_ps = assemblePS(device, assemble, material_mask_src, material_mask_src.len) orelse {
releaseShaders();
return;
};
debug_shader_stage = 4; // silhouette/material-mask PS variants ready
// --- JFA Init PS ---
jfa_init_ps = assemblePS(device, assemble, jfa_init_src, jfa_init_src.len) orelse {
releaseShaders();
return;
};
debug_shader_stage = 5; // JFA init ready
// --- JFA Propagation PS ---
jfa_prop_ps = assemblePS(device, assemble, jfa_prop_src, jfa_prop_src.len) orelse {
releaseShaders();
return;
};
debug_shader_stage = 6; // JFA propagation ready
// --- JFA Decode + Composite PS ---
jfa_decode_ps = assemblePS(device, assemble, jfa_decode_src, jfa_decode_src.len) orelse {
releaseShaders();
return;
};
debug_shader_stage = 7; // JFA decode ready
// --- Debug silhouette composite PS (only when diagnostic enabled) ---
if (DEBUG_SHOW_SILHOUETTE) {
@@ -602,14 +833,42 @@ fn ensureShaders(device: *anyopaque) void {
return;
};
}
debug_shader_stage = 8; // complete
debug_shaders_ready_seen = true;
}
/// Assemble a pixel shader from source text, create device PS object.
fn captureD3DXError(buf: *anyopaque) void {
@memset(&debug_shader_error_text, 0);
const get_ptr: *const fn (*anyopaque) callconv(hook.cc.stdcall) usize =
@ptrFromInt(vt(buf)[3]);
const get_size: *const fn (*anyopaque) callconv(hook.cc.stdcall) usize =
@ptrFromInt(vt(buf)[4]);
const ptr_val = get_ptr(buf);
const size = get_size(buf);
if (ptr_val == 0 or size == 0) return;
const src_ptr: [*]const u8 = @ptrFromInt(ptr_val);
const n = @min(size, debug_shader_error_text.len - 1);
@memcpy(debug_shader_error_text[0..n], src_ptr[0..n]);
// Make sure the chat string ends cleanly even if the D3DX buffer does not.
debug_shader_error_text[n] = 0;
}
fn assemblePS(device: *anyopaque, assemble: D3DXAssembleShaderFn, src: [*]const u8, len: usize) ?*anyopaque {
var code: ?*anyopaque = null;
var err_buf: ?*anyopaque = null;
if (assemble(src, @intCast(len), null, null, 0, &code, &err_buf) < 0 or code == null) {
if (err_buf) |e| comRelease(e);
const assemble_hr = assemble(src, @intCast(len), null, null, 0, &code, &err_buf);
if (assemble_hr < 0 or code == null) {
debug_shader_assemble_hr = assemble_hr;
if (err_buf) |e| {
captureD3DXError(e);
comRelease(e);
}
return null;
}
defer comRelease(code.?);
@@ -622,12 +881,16 @@ fn assemblePS(device: *anyopaque, assemble: D3DXAssembleShaderFn, src: [*]const
var ps_out: ?*anyopaque = null;
const create: *const fn (*anyopaque, *anyopaque, *?*anyopaque) callconv(hook.cc.stdcall) i32 =
@ptrFromInt(vt(device)[types.VT.CreatePixelShader]);
if (create(device, buf_ptr, &ps_out) < 0) return null;
const create_hr = create(device, buf_ptr, &ps_out);
if (create_hr < 0) {
debug_shader_create_hr = create_hr;
return null;
}
return ps_out;
}
fn releaseShaders() void {
inline for (.{ &outline_ps, &jfa_init_ps, &jfa_prop_ps, &jfa_decode_ps, &debug_sil_ps }) |ps| {
inline for (.{ &outline_ps, &outline_alpha_ps, &outline_rgb_ps, &material_mask_ps, &jfa_init_ps, &jfa_prop_ps, &jfa_decode_ps, &debug_sil_ps }) |ps| {
if (ps.*) |p| {
comRelease(p);
ps.* = null;
@@ -657,11 +920,10 @@ fn buildFullscreenQuad(w: u32, h: u32) [4]QuadVertex {
// =============================================================================
fn hkEndScene(device: *anyopaque) callconv(hook.cc.stdcall) i32 {
// One-time: check if depth/stencil surface has stencil bits.
if (need_force_reset) {
need_force_reset = false;
forceD24S8IfNeeded(device);
}
debug_endscene_seen = true;
// DEBUG23: no forced D3D9 Reset. The client can hang the render thread
// during Reset, so Outline runs without a stencil dependency.
// Per-frame: scan objects for outline tracking
tracker.scanObjects();
@@ -732,11 +994,14 @@ fn hkDIP(
start_idx: u32,
prim_count: u32,
) callconv(hook.cc.stdcall) i32 {
debug_dip_seen = true;
const OrigDIP = *const fn (*anyopaque, u32, i32, u32, u32, u32, u32) callconv(hook.cc.stdcall) i32;
const origFn: OrigDIP = @ptrFromInt(orig_dip);
// ---- Cache outline draws for EndScene replay ----
if (model_hook.rendering_outline) {
debug_outline_dip_seen = true;
const model_ptr = model_hook.current_model;
const color = tracker.getModelColor(model_ptr) orelse
return origFn(device, prim_type, base_vtx, min_vtx, num_verts, start_idx, prim_count);
@@ -774,52 +1039,33 @@ fn hkDIP(
// GetVertexDeclaration AddRef's
draw.vertex_shader = deviceGetPtr(device, types.VT.GetVertexShader);
// GetVertexShader AddRef's
for (0..4) |stage| {
draw.tex[stage] = deviceGetTexture(device, @intCast(stage));
draw.alpha_op[stage] = deviceGetTSS(device, @intCast(stage), types.D3DTSS.ALPHAOP);
draw.alpha_arg1[stage] = deviceGetTSS(device, @intCast(stage), types.D3DTSS.ALPHAARG1);
draw.alpha_arg2[stage] = deviceGetTSS(device, @intCast(stage), types.D3DTSS.ALPHAARG2);
}
draw.pixel_shader = deviceGetPtr(device, types.VT.GetPixelShader);
draw.state_block = deviceCreateStateBlock(device);
if (draw.state_block != null) debug_state_block_seen = true;
draw.alpha_test_enable = deviceGetRS(device, types.D3DRS.ALPHATESTENABLE);
draw.alpha_ref = deviceGetRS(device, types.D3DRS.ALPHAREF);
draw.alpha_func = deviceGetRS(device, types.D3DRS.ALPHAFUNC);
draw.alpha_blend_enable = deviceGetRS(device, types.D3DRS.ALPHABLENDENABLE);
draw.src_blend = deviceGetRS(device, types.D3DRS.SRCBLEND);
draw.dst_blend = deviceGetRS(device, types.D3DRS.DESTBLEND);
// Capture VS constants (bone matrices, world/view/proj transforms)
deviceGetVSConstF(device, 0, &draw.vs_consts, MAX_VS_CONST_REGS);
cached_draw_count = idx + 1;
frame_has_outlines = true;
debug_cached_draw_seen = true;
}
// Mark visible pixels in stencil for this outline target.
// At this point (outline targets draw last due to batch reordering),
// the game's DS has terrain+WMO+all non-outline M2 model depth.
// Pixels that pass the depth test get stencil=1; pixels behind any
// scene geometry fail and keep stencil=0.
// EndScene uses these marks to gate silhouette rendering.
const s_enable = deviceGetRS(device, types.D3DRS.STENCILENABLE);
const s_func = deviceGetRS(device, types.D3DRS.STENCILFUNC);
const s_ref = deviceGetRS(device, types.D3DRS.STENCILREF);
// (STENCILWRITEMASK not saved - intentionally set to 0 on restore)
const s_pass = deviceGetRS(device, types.D3DRS.STENCILPASS);
const s_fail = deviceGetRS(device, types.D3DRS.STENCILFAIL);
const s_zfail = deviceGetRS(device, types.D3DRS.STENCILZFAIL);
deviceSetRS(device, types.D3DRS.STENCILENABLE, 1);
deviceSetRS(device, types.D3DRS.STENCILFUNC, types.D3DCMP_ALWAYS);
deviceSetRS(device, types.D3DRS.STENCILREF, 1);
deviceSetRS(device, types.D3DRS.STENCILWRITEMASK, 0xFF);
deviceSetRS(device, types.D3DRS.STENCILPASS, types.D3DSTENCILOP_REPLACE);
deviceSetRS(device, types.D3DRS.STENCILFAIL, types.D3DSTENCILOP_KEEP);
deviceSetRS(device, types.D3DRS.STENCILZFAIL, types.D3DSTENCILOP_KEEP);
const result = origFn(device, prim_type, base_vtx, min_vtx, num_verts, start_idx, prim_count);
// Restore stencil state to match WoW's GxDevice cache, but lock
// stencil writes to protect our marks from subsequent draws (other
// players' gear, NPCs, etc. that render after outline targets).
deviceSetRS(device, types.D3DRS.STENCILENABLE, s_enable);
deviceSetRS(device, types.D3DRS.STENCILFUNC, s_func);
deviceSetRS(device, types.D3DRS.STENCILREF, s_ref);
deviceSetRS(device, types.D3DRS.STENCILPASS, s_pass);
deviceSetRS(device, types.D3DRS.STENCILFAIL, s_fail);
deviceSetRS(device, types.D3DRS.STENCILZFAIL, s_zfail);
// Write mask 0 instead of restoring original - prevents any
// subsequent DIP from overwriting our stencil=1 marks.
// Restored properly in EndScene before the JFA pipeline.
deviceSetRS(device, types.D3DRS.STENCILWRITEMASK, 0);
return result;
// DEBUG23: no stencil writes. Preserve WoW's D3D state and draw once.
// The cached geometry is replayed later into the silhouette RT.
return origFn(device, prim_type, base_vtx, min_vtx, num_verts, start_idx, prim_count);
}
// ---- Normal path ----
@@ -846,6 +1092,20 @@ fn clearCachedDraws() void {
comRelease(obj);
draw.vertex_shader = null;
}
if (draw.pixel_shader) |obj| {
comRelease(obj);
draw.pixel_shader = null;
}
if (draw.state_block) |obj| {
comRelease(obj);
draw.state_block = null;
}
for (0..4) |stage| {
if (draw.tex[stage]) |obj| {
comRelease(obj);
draw.tex[stage] = null;
}
}
}
cached_draw_count = 0;
}
@@ -855,11 +1115,25 @@ fn clearCachedDraws() void {
// =============================================================================
fn runJfaPipeline(device: *anyopaque) void {
debug_pipeline_entered_seen = true;
// DEBUG29: capture the complete WoW D3D state before any replay/JFA work.
// The manual save/restore below remains as fallback, but this state block
// restores states that are easy to miss (extra samplers/TSS, scissor,
// shaders/constants, streams, declarations, etc.).
const outer_state = deviceCreateStateBlock(device);
defer if (outer_state) |sb| {
if (stateBlockApply(sb)) debug_outer_state_restore_seen = true;
comRelease(sb);
};
// Verify all resources and shaders
if (rt_silhouette_tex == null or rt_jfa_a_surf == null or rt_jfa_b_surf == null) return;
if (rt_material_tex == null or rt_material_surf == null or rt_silhouette_tex == null or rt_jfa_a_surf == null or rt_jfa_b_surf == null) return;
if (!shaders_attempted) ensureShaders(device);
if (jfa_init_ps == null or jfa_prop_ps == null or jfa_decode_ps == null) return;
if (outline_ps == null or rt_silhouette_surf == null) return;
if (outline_ps == null or material_mask_ps == null or rt_silhouette_surf == null) return;
debug_pipeline_ready_seen = true;
var vp: types.D3DVIEWPORT9 = .{};
deviceGetViewport(device, &vp);
@@ -897,8 +1171,29 @@ fn runJfaPipeline(device: *anyopaque) void {
const saved_dstblend = deviceGetRS(device, types.D3DRS.DESTBLEND);
const saved_cull = deviceGetRS(device, types.D3DRS.CULLMODE);
const saved_atest = deviceGetRS(device, types.D3DRS.ALPHATESTENABLE);
const saved_aref = deviceGetRS(device, types.D3DRS.ALPHAREF);
const saved_afunc = deviceGetRS(device, types.D3DRS.ALPHAFUNC);
const saved_tfactor = deviceGetRS(device, types.D3DRS.TEXTUREFACTOR);
const saved_cwrite = deviceGetRS(device, types.D3DRS.COLORWRITEENABLE);
const SavedTSS = struct {
colorop: u32,
colorarg1: u32,
alphaop: u32,
alphaarg1: u32,
alphaarg2: u32,
};
var saved_tss: [4]SavedTSS = undefined;
for (0..4) |stage| {
saved_tss[stage] = .{
.colorop = deviceGetTSS(device, @intCast(stage), types.D3DTSS.COLOROP),
.colorarg1 = deviceGetTSS(device, @intCast(stage), types.D3DTSS.COLORARG1),
.alphaop = deviceGetTSS(device, @intCast(stage), types.D3DTSS.ALPHAOP),
.alphaarg1 = deviceGetTSS(device, @intCast(stage), types.D3DTSS.ALPHAARG1),
.alphaarg2 = deviceGetTSS(device, @intCast(stage), types.D3DTSS.ALPHAARG2),
};
}
// Stencil states (Phase 1 reads stencil marks written by DIP hook)
const saved_stencil_enable = deviceGetRS(device, types.D3DRS.STENCILENABLE);
const saved_stencil_func = deviceGetRS(device, types.D3DRS.STENCILFUNC);
@@ -936,57 +1231,83 @@ fn runJfaPipeline(device: *anyopaque) void {
if (cached_draw_count > 0) {
const origFn: *const fn (*anyopaque, u32, i32, u32, u32, u32, u32) callconv(hook.cc.stdcall) i32 =
@ptrFromInt(orig_dip);
const quad = buildFullscreenQuad(vp.Width, vp.Height);
// Accumulate normalized per-draw coverage into the silhouette mask.
deviceSetRenderTarget(device, 0, rt_silhouette_surf.?);
clearRenderTarget(device, 0x00000000);
// Keep game's DS bound - it has stencil marks from DIP hook where
// outline targets passed the terrain depth test (stencil=1 = visible).
// Don't write depth or stencil during replay.
deviceSetRS(device, types.D3DRS.ZWRITEENABLE, 0);
deviceSetRS(device, types.D3DRS.ZENABLE, types.D3DZB_FALSE);
deviceSetRS(device, types.D3DRS.STENCILWRITEMASK, 0);
deviceSetPtr(device, types.VT.SetPixelShader, outline_ps.?);
deviceSetRS(device, types.D3DRS.ALPHABLENDENABLE, 0);
deviceSetRS(device, types.D3DRS.COLORWRITEENABLE, 0x0F);
for (0..cached_draw_count) |i| {
const draw = &cached_draws[i];
// V31 POLISH: don't let additive/emissive passes (spell glows,
// bloom-like model layers) expand the selection silhouette.
// Normal alpha-blended materials remain included.
if (draw.alpha_blend_enable != 0 and draw.dst_blend == types.D3DBLEND_ONE) {
debug_additive_skipped_seen = true;
continue;
}
// 1) Replay this draw with WoW's exact captured D3D state into a
// transparent scratch RT. A D3DSBT_ALL state block restores pixel
// shader, PS constants, textures, samplers, texture stages, blend,
// alpha-test, vertex state and stream bindings.
if (draw.state_block) |sb| {
_ = stateBlockApply(sb);
}
// Never rely on D3DSBT_ALL to restore geometry bindings correctly
// across drivers/wrappers. Rebind the cached draw explicitly.
deviceSetStreamSource(device, 0, draw.vb, draw.vb_offset, draw.vb_stride);
deviceSetIndices(device, draw.ib);
deviceSetPtrOrNull(device, types.VT.SetVertexDeclaration, draw.vertex_decl);
deviceSetPtrOrNull(device, types.VT.SetVertexShader, draw.vertex_shader);
deviceSetVSConstF(device, 0, &draw.vs_consts, MAX_VS_CONST_REGS);
var color_f4 = argbToFloat4(draw.color);
color_f4[3] = tracker.getOutlinePixels(draw.category) / 4.0;
deviceSetPSConstF(device, 0, &color_f4);
// Per-category stencil logic:
// - dead_player: no stencil test (visible through walls for corpse finding)
// - target/raid_marked: stencil test gates on terrain visibility
if (draw.category == .dead_player) {
deviceSetRS(device, types.D3DRS.STENCILENABLE, 0);
} else {
deviceSetRS(device, types.D3DRS.STENCILENABLE, 1);
deviceSetRS(device, types.D3DRS.STENCILFUNC, types.D3DCMP_EQUAL);
deviceSetRS(device, types.D3DRS.STENCILREF, 1);
deviceSetRS(device, types.D3DRS.STENCILMASK, 0xFF);
deviceSetRS(device, types.D3DRS.STENCILPASS, types.D3DSTENCILOP_KEEP);
}
deviceSetRenderTarget(device, 0, rt_material_surf.?);
deviceSetViewport(device, &vp);
clearRenderTarget(device, 0x00000000);
deviceSetRS(device, types.D3DRS.ZENABLE, types.D3DZB_FALSE);
deviceSetRS(device, types.D3DRS.ZWRITEENABLE, 0);
deviceSetRS(device, types.D3DRS.STENCILENABLE, 0);
deviceSetRS(device, types.D3DRS.COLORWRITEENABLE, 0x0F);
_ = origFn(device, draw.prim_type, draw.base_vtx, draw.min_vtx, draw.num_verts, draw.start_idx, draw.prim_count);
// 2) Convert only pixels actually produced by that exact material
// into the uniform outline mask, preserving the draw/category colour.
deviceSetRenderTarget(device, 0, rt_silhouette_surf.?);
deviceSetViewport(device, &vp);
deviceSetPtrOrNull(device, types.VT.SetDepthStencilSurface, null);
deviceSetPtrOrNull(device, types.VT.SetVertexShader, null);
deviceSetFVF(device, types.D3DFVF_XYZRHW | types.D3DFVF_TEX1);
deviceSetRS(device, types.D3DRS.ZENABLE, types.D3DZB_FALSE);
deviceSetRS(device, types.D3DRS.ZWRITEENABLE, 0);
deviceSetRS(device, types.D3DRS.STENCILENABLE, 0);
deviceSetRS(device, types.D3DRS.ALPHATESTENABLE, 0);
deviceSetRS(device, types.D3DRS.CULLMODE, types.D3DCULL_NONE);
deviceSetRS(device, types.D3DRS.COLORWRITEENABLE, 0x0F);
deviceSetRS(device, types.D3DRS.ALPHABLENDENABLE, 1);
deviceSetRS(device, types.D3DRS.SRCBLEND, types.D3DBLEND_SRCALPHA);
deviceSetRS(device, types.D3DRS.DESTBLEND, types.D3DBLEND_INVSRCALPHA);
deviceSetSamplerState(device, 0, types.D3DSAMP.ADDRESSU, types.D3DTADDRESS_CLAMP);
deviceSetSamplerState(device, 0, types.D3DSAMP.ADDRESSV, types.D3DTADDRESS_CLAMP);
deviceSetSamplerState(device, 0, types.D3DSAMP.MAGFILTER, types.D3DTEXF_POINT);
deviceSetSamplerState(device, 0, types.D3DSAMP.MINFILTER, types.D3DTEXF_POINT);
deviceSetSamplerState(device, 0, types.D3DSAMP.MIPFILTER, types.D3DTEXF_NONE);
deviceSetTexture(device, 0, rt_material_tex);
deviceSetPtr(device, types.VT.SetPixelShader, material_mask_ps.?);
var mask_color = argbToFloat4(draw.color);
mask_color[3] = 1.0;
deviceSetPSConstF(device, 0, &mask_color);
// Trim very faint scratch pixels that otherwise become tiny hooks/noise.
const threshold: [4]f32 = .{ -0.03, 0.0, 0.0, 0.0 };
deviceSetPSConstF(device, 1, &threshold);
deviceDrawPrimitiveUP(device, types.D3DPT_TRIANGLESTRIP, 2, @ptrCast(&quad), @sizeOf(QuadVertex));
}
clearCachedDraws();
// Clear stencil marks to avoid affecting next frame's rendering
deviceSetRS(device, types.D3DRS.STENCILENABLE, 0);
const clearFn: *const fn (*anyopaque, u32, ?*anyopaque, u32, u32, f32, u32) callconv(hook.cc.stdcall) i32 =
@ptrFromInt(vt(device)[types.VT.Clear]);
_ = clearFn(device, 0, null, types.D3DCLEAR_STENCIL, 0, 1.0, 0);
}
// =====================================================================
@@ -1051,7 +1372,7 @@ fn runJfaPipeline(device: *anyopaque) void {
const fw = @as(f32, @floatFromInt(@max(vp.Width, 1)));
const fh = @as(f32, @floatFromInt(@max(vp.Height, 1)));
// Pass 1: JFA Init (silhouette → JFA_A)
// Pass 1: JFA Init (silhouette → JFA_A).
deviceSetRenderTarget(device, 0, rt_jfa_a_surf.?);
deviceSetTexture(device, 0, rt_silhouette_tex);
deviceSetPtr(device, types.VT.SetPixelShader, jfa_init_ps.?);
@@ -1093,7 +1414,8 @@ fn runJfaPipeline(device: *anyopaque) void {
if (saved_rt0) |rt| deviceSetRenderTarget(device, 0, rt);
deviceSetTexture(device, 0, rt_jfa_a_tex);
deviceSetTexture(device, 1, rt_silhouette_tex);
c0 = [4]f32{ fw, fh, 4.0, 0.0 };
// V37A isolation test: V37 radii only, V36 decode shader unchanged.
c0 = [4]f32{ fw, fh, 1.35, 3.20 };
deviceSetPSConstF(device, 0, &c0);
deviceSetPtr(device, types.VT.SetPixelShader, jfa_decode_ps.?);
deviceSetRS(device, types.D3DRS.ALPHABLENDENABLE, 1);
@@ -1115,7 +1437,17 @@ fn runJfaPipeline(device: *anyopaque) void {
deviceSetRS(device, types.D3DRS.DESTBLEND, saved_dstblend);
deviceSetRS(device, types.D3DRS.CULLMODE, saved_cull);
deviceSetRS(device, types.D3DRS.ALPHATESTENABLE, saved_atest);
deviceSetRS(device, types.D3DRS.ALPHAREF, saved_aref);
deviceSetRS(device, types.D3DRS.ALPHAFUNC, saved_afunc);
deviceSetRS(device, types.D3DRS.TEXTUREFACTOR, saved_tfactor);
deviceSetRS(device, types.D3DRS.COLORWRITEENABLE, saved_cwrite);
for (0..4) |stage| {
deviceSetTSS(device, @intCast(stage), types.D3DTSS.COLOROP, saved_tss[stage].colorop);
deviceSetTSS(device, @intCast(stage), types.D3DTSS.COLORARG1, saved_tss[stage].colorarg1);
deviceSetTSS(device, @intCast(stage), types.D3DTSS.ALPHAOP, saved_tss[stage].alphaop);
deviceSetTSS(device, @intCast(stage), types.D3DTSS.ALPHAARG1, saved_tss[stage].alphaarg1);
deviceSetTSS(device, @intCast(stage), types.D3DTSS.ALPHAARG2, saved_tss[stage].alphaarg2);
}
// Stencil states
deviceSetRS(device, types.D3DRS.STENCILENABLE, saved_stencil_enable);
@@ -1174,42 +1506,65 @@ fn hasStencilBits(fmt: u32) bool {
fmt == types.D3DFMT_D24X4S4 or fmt == types.D3DFMT_D15S1;
}
fn forceD24S8IfNeeded(device: *anyopaque) void {
fn queryStencilFormat(device: *anyopaque) u32 {
var pDS: ?*anyopaque = null;
const getDS: *const fn (*anyopaque, *?*anyopaque) callconv(hook.cc.stdcall) i32 =
@ptrFromInt(vt(device)[types.VT.GetDepthStencilSurface]);
if (getDS(device, &pDS) < 0) return;
const ds = pDS orelse return;
defer comRelease(ds);
if (getDS(device, &pDS) < 0) return 0;
const ds = pDS orelse return 0;
var desc: types.D3DSURFACE_DESC = .{};
const getDesc: *const fn (*anyopaque, *types.D3DSURFACE_DESC) callconv(hook.cc.stdcall) i32 =
@ptrFromInt(vt(ds)[12]);
if (getDesc(ds, &desc) < 0) return;
const hr = getDesc(ds, &desc);
comRelease(ds);
if (hr < 0) return 0;
return desc.Format;
}
if (hasStencilBits(desc.Format)) return;
fn forceD24S8IfNeeded(device: *anyopaque) bool {
const current_fmt = queryStencilFormat(device);
debug_stencil_format = current_fmt;
if (hasStencilBits(current_fmt)) {
debug_stencil_ready = true;
debug_stencil_reset_hr = 0;
return false;
}
var pSwap: ?*anyopaque = null;
const getSC: *const fn (*anyopaque, u32, *?*anyopaque) callconv(hook.cc.stdcall) i32 =
@ptrFromInt(vt(device)[types.VT.GetSwapChain]);
if (getSC(device, 0, &pSwap) < 0) return;
const swap = pSwap orelse return;
defer comRelease(swap);
if (getSC(device, 0, &pSwap) < 0) return false;
const swap = pSwap orelse return false;
var pp: types.D3DPRESENT_PARAMETERS = .{};
const getPP: *const fn (*anyopaque, *types.D3DPRESENT_PARAMETERS) callconv(hook.cc.stdcall) i32 =
@ptrFromInt(vt(swap)[9]);
if (getPP(swap, &pp) < 0) return;
const pp_hr = getPP(swap, &pp);
// IMPORTANT: do not hold a swap-chain COM reference across Reset.
comRelease(swap);
if (pp_hr < 0) return false;
pp.AutoDepthStencilFormat = types.D3DFMT_D24S8;
pp.EnableAutoDepthStencil = 1;
clearCachedDraws();
releaseShaders();
releaseResources();
const resetFn: *const fn (*anyopaque, *types.D3DPRESENT_PARAMETERS) callconv(hook.cc.stdcall) i32 =
@ptrFromInt(vt(device)[types.VT.Reset]);
_ = resetFn(device, &pp);
const reset_hr = resetFn(device, &pp);
debug_stencil_reset_hr = reset_hr;
if (reset_hr < 0) {
debug_stencil_ready = false;
return false;
}
const new_fmt = queryStencilFormat(device);
debug_stencil_format = new_fmt;
debug_stencil_ready = hasStencilBits(new_fmt);
return true;
}
// =============================================================================
@@ -1272,9 +1627,9 @@ pub fn installHooks() bool {
const vtable_ptr = getD3D9VTable() orelse return false;
d3d9_vtable = vtable_ptr;
// Isolation test: hook EndScene only.
// DrawIndexedPrimitive and Reset remain untouched.
if (!patchVtableEntry(vtable_ptr, types.VT.EndScene, @intFromPtr(&hkEndScene), &orig_endscene)) return false;
if (!patchVtableEntry(vtable_ptr, types.VT.DrawIndexedPrimitive, @intFromPtr(&hkDIP), &orig_dip)) return false;
if (!patchVtableEntry(vtable_ptr, types.VT.Reset, @intFromPtr(&hkReset), &orig_reset)) return false;
hooks_installed = true;
return true;
+15
View File
@@ -42,6 +42,19 @@ var draw_batch_hook: hook.Detour(DrawBatchFn) = .{};
/// a dummy D3D9 device during engine init corrupts the proxy's state.
var d3d9_deferred_pending: bool = true;
/// AutoFix: periodically verify that the game's live D3D9 vtable still points
/// at our hooks. OutlineDebug() used to do this manually; keeping it here makes
/// the renderer self-healing if another proxy/mod rewrites the vtable later.
var d3d9_health_tick: u8 = 29;
fn autoRepairD3D9Hooks() void {
d3d9_health_tick +%= 1;
if (d3d9_health_tick < 30) return;
d3d9_health_tick = 0;
_ = d3d9_hook.lateRehookIfLost();
}
// =============================================================================
// Volatile flags shared with d3d9_hook (read by DIP hook)
// =============================================================================
@@ -81,6 +94,8 @@ fn renderDrawDetour(this: u32, view_matrix: u32, batch_data: u32, batch_indices:
api.initD3D9Deferred();
}
// Isolation test: keep the initial deferred D3D9 hook install, but do not
// repeatedly verify/rewrite the live vtable while rendering.
// 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 });
+129 -18
View File
@@ -4,11 +4,12 @@
//! and a Lua C callback for `/wu outline` commands.
const std = @import("std");
const hook = @import("zhook");
const lua = @import("../lua.zig");
const logging = @import("../logging.zig");
const tracker = @import("tracker.zig");
const model_hook = @import("model_hook.zig");
const d3d9_hook = @import("d3d9_hook.zig");
const wow = @import("../wow.zig");
const WINAPI = std.builtin.CallingConvention.winapi;
const mod_mutex = @import("../mutex.zig");
@@ -18,6 +19,63 @@ var log: logging.Logger = .{};
var g_mutex: ?*anyopaque = null;
var g_is_hook_owner: bool = false;
var g_model_hooks_installed: bool = false;
noinline fn luaGetTopNative(L: lua.State) i32 {
var result: i32 = undefined;
asm volatile (
\\mov $0x6F3070, %%eax
\\call *%%eax
: [ret] "={eax}" (result),
: [state] "{ecx}" (@intFromPtr(L)),
);
return result;
}
noinline fn luaIsStringNative(L: lua.State, index: i32) bool {
var result: u32 = undefined;
asm volatile (
\\mov $0x6F3510, %%eax
\\call *%%eax
: [ret] "={eax}" (result),
: [state] "{ecx}" (@intFromPtr(L)),
[index] "{edx}" (index),
);
return result != 0;
}
noinline fn luaToStringNative(L: lua.State, index: i32) ?[*:0]const u8 {
var result: usize = undefined;
asm volatile (
\\mov $0x6F3690, %%eax
\\call *%%eax
: [ret] "={eax}" (result),
: [state] "{ecx}" (@intFromPtr(L)),
[index] "{edx}" (index),
);
return if (result == 0) null else @ptrFromInt(result);
}
noinline fn luaPushBooleanNative(L: lua.State, value: i32) void {
asm volatile (
\\mov $0x6F39F0, %%eax
\\call *%%eax
:
: [state] "{ecx}" (@intFromPtr(L)),
[value] "{edx}" (value),
);
}
noinline fn luaPushStringNative(L: lua.State, value: [*:0]const u8) void {
asm volatile (
\\mov $0x6F3890, %%eax
\\call *%%eax
:
: [state] "{ecx}" (@intFromPtr(L)),
[value] "{edx}" (@intFromPtr(value)),
);
}
pub fn isActive() bool {
return g_is_hook_owner;
@@ -37,6 +95,7 @@ pub fn init() bool {
log = logging.Logger.open(module_name, .console);
tracker.initLogger();
if (!model_hook.installHooks()) return false;
g_model_hooks_installed = true;
return true;
}
@@ -55,6 +114,7 @@ pub fn cleanup() void {
log.close();
mod_mutex.release(&g_mutex);
}
g_model_hooks_installed = false;
g_is_hook_owner = false;
}
@@ -74,28 +134,79 @@ pub fn isEnabled() bool {
/// OutlineCommand() → returns (enabled: bool)
/// OutlineCommand("on") → enable outlines
/// OutlineCommand("off") → disable outlines
pub fn outlineCommand(L: *anyopaque) callconv(.c) u32 {
const nargs = hook.call(fn (usize) callconv(hook.cc.fastcall) i32, 0x6F3070, .{@intFromPtr(L)}); // lua_gettop
pub fn outlineCommand(L: lua.State) callconv(.{ .x86_thiscall = .{} }) u32 {
const nargs = luaGetTopNative(L);
if (nargs >= 1) {
// Check if first arg is a string
const is_str = hook.call(fn (usize, u32) callconv(hook.cc.fastcall) u32, 0x6F3510, .{ @intFromPtr(L), 1 }); // lua_isstring
if (is_str != 0) {
const str_ptr = hook.call(fn (usize, u32) callconv(hook.cc.fastcall) u32, 0x6F3690, .{ @intFromPtr(L), 1 }); // lua_tostring
if (str_ptr != 0) {
const s: [*:0]const u8 = @ptrFromInt(str_ptr);
const span = @import("std").mem.span(s);
if (eql(span, "on") or eql(span, "enable")) {
setEnabled(true);
} else if (eql(span, "off") or eql(span, "disable")) {
setEnabled(false);
}
if (nargs >= 1 and luaIsStringNative(L, 1)) {
if (luaToStringNative(L, 1)) |s| {
const span = std.mem.span(s);
if (eql(span, "on") or eql(span, "enable")) {
setEnabled(true);
} else if (eql(span, "off") or eql(span, "disable")) {
setEnabled(false);
}
}
}
// Push current state as boolean
hook.call(fn (usize, u32) callconv(hook.cc.fastcall) void, 0x6F39F0, .{ @intFromPtr(L), @as(u32, if (isEnabled()) 1 else 0) }); // lua_pushboolean
luaPushBooleanNative(L, if (isEnabled()) 1 else 0);
return 1;
}
fn syncTargetFromLuaContext() void {
// AutoFix: perform the same safe target publication that OutlineDebug()
// used, but from an automatic Lua/main-thread callback.
const player_guid = wow.getPlayerGUID();
const target_guid = wow.getTargetGUID();
const player_obj = if (player_guid != 0) wow.getObjectByGUID(player_guid) else 0;
const target_obj = if (target_guid != 0) wow.getObjectByGUID(target_guid) else 0;
tracker.setDebugPinnedObjects(player_obj, target_obj);
if (player_guid != 0) tracker.debug_unit_player_guid_seen = true;
if (target_guid != 0) tracker.debug_unit_target_guid_seen = true;
}
/// Automatic target publication used by the embedded addon.
/// No user macro/command is required.
pub fn outlineSyncTarget(_: lua.State) callconv(.{ .x86_thiscall = .{} }) u32 {
syncTargetFromLuaContext();
return 0;
}
/// Return a compact sticky diagnostic line for in-game testing.
/// Usage:
/// /run DEFAULT_CHAT_FRAME:AddMessage(OutlineDebug())
pub fn outlineDebug(L: lua.State) callconv(.{ .x86_thiscall = .{} }) u32 {
// Debug keeps the exact same automatic publication path.
syncTargetFromLuaContext();
_ = d3d9_hook.lateRehookIfLost();
const live = d3d9_hook.getLiveHookState();
var buf: [336]u8 = undefined;
const msg = std.fmt.bufPrintZ(
&buf,
"OutlineDBG shst={d} hooks={d}{d}{d} tgt={d} mdl={d} sb={d} rs={d} fx={d} odip={d} cache={d} pipe={d}/{d}",
.{
d3d9_hook.debug_shader_stage,
@intFromBool(live.endscene_ours),
@intFromBool(live.dip_ours),
@intFromBool(live.reset_ours),
@intFromBool(tracker.debug_target_seen),
@intFromBool(tracker.debug_target_model_seen),
@intFromBool(d3d9_hook.debug_state_block_seen),
@intFromBool(d3d9_hook.debug_outer_state_restore_seen),
@intFromBool(d3d9_hook.debug_additive_skipped_seen),
@intFromBool(d3d9_hook.debug_outline_dip_seen),
@intFromBool(d3d9_hook.debug_cached_draw_seen),
@intFromBool(d3d9_hook.debug_pipeline_entered_seen),
@intFromBool(d3d9_hook.debug_pipeline_ready_seen),
},
) catch {
luaPushStringNative(L, "OutlineDBG format error");
return 1;
};
luaPushStringNative(L, msg.ptr);
return 1;
}
+75 -52
View File
@@ -182,7 +182,10 @@ fn addOutlineEntry(model_ptr: u32, cat: types.ModelCategory, mark: u8) void {
// Diagnostic: count classified models by category
switch (cat) {
.target => diag.classify_target += 1,
.target => {
diag.classify_target += 1;
debug_target_model_seen = true;
},
.raid_marked => diag.classify_raid_mark += 1,
.dead_player => diag.classify_dead_player += 1,
.none => {},
@@ -197,6 +200,13 @@ fn addOutlineEntry(model_ptr: u32, cat: types.ModelCategory, mark: u8) void {
/// Stores object pointers directly so classifyModel can match model
/// back-pointers without any pointer dereferencing.
pub fn scanObjects() void {
debug_scan_called_seen = true;
const gen = @atomicRmw(u32, &debug_scan_generation, .Add, 1, .seq_cst) + 1;
const published_at = @atomicLoad(u32, &debug_publish_scan_generation, .seq_cst);
if (published_at != 0 and gen > published_at) {
debug_scan_after_publish_seen = true;
}
// Clear per-frame sets
frame_outline_count = 0;
tracked_obj_count = 0;
@@ -204,62 +214,24 @@ pub fn scanObjects() void {
game_obj_model_count = 0;
resetDiag();
if (!wow.isInGame()) return;
const local_player = wow.getLocalPlayer();
if (local_player == 0) return;
// Local player renders before outline targets (occludes outlines) unless
// the local player IS an outline target (partition logic checks outline first).
if (game_obj_ptr_count < MAX_TRACKED_OBJS) {
// DEBUG10: EndScene must not call WoW object/game functions. On this client
// those functions only behave correctly from the Lua/main-thread context.
// Consume atomically published object pointers captured by OutlineDebug().
const local_player = @atomicLoad(u32, &debug_pinned_player_obj, .seq_cst);
if (local_player != 0) debug_pin_player_consumed_seen = true;
if (local_player != 0 and game_obj_ptr_count < MAX_TRACKED_OBJS) {
game_obj_ptrs[game_obj_ptr_count] = local_player;
game_obj_ptr_count += 1;
}
// Cache raid target GUIDs
wow.cacheRaidTargets();
// Resolve target to object pointer (highest priority - added first)
const target_guid = wow.getTargetGUID();
if (target_guid != 0) {
const target_obj = wow.getObjectByGUID(target_guid);
if (target_obj != 0) {
addTrackedObj(target_obj, .target, 0);
}
const target_obj = @atomicLoad(u32, &debug_pinned_target_obj, .seq_cst);
if (target_obj != 0) {
debug_pin_target_consumed_seen = true;
addTrackedObj(target_obj, .target, 0);
}
// Iterate all visible objects
var obj = wow.objectFirst();
while (obj != 0) : (obj = wow.objectNext(obj)) {
const obj_type = wow.getObjectType(obj);
const guid = wow.getObjectGUID(obj);
if (guid == 0) continue;
switch (obj_type) {
.player => {
if (wow.isUnitDead(obj) and wow.isUnitFriendly(obj, local_player)) {
addTrackedObj(obj, .dead_player, 0);
}
const mark = wow.getRaidMarkForGUID(guid);
if (mark != 0) addTrackedObj(obj, .raid_marked, mark);
},
.unit => {
const mark = wow.getRaidMarkForGUID(guid);
if (mark != 0) addTrackedObj(obj, .raid_marked, mark);
},
.corpse => {
if (!wow.isSkeletonCorpse(obj)) {
addTrackedObj(obj, .dead_player, 0);
}
},
.game_object => {
if (game_obj_ptr_count < MAX_TRACKED_OBJS) {
game_obj_ptrs[game_obj_ptr_count] = obj;
game_obj_ptr_count += 1;
}
},
else => {},
}
}
// Secondary categories (raid marks/dead players/game objects) are disabled
// in this diagnostic build until the target-only path is proven stable.
}
fn addTrackedObj(obj_ptr: u32, cat: types.ModelCategory, mark: u8) void {
@@ -283,7 +255,10 @@ fn addTrackedObj(obj_ptr: u32, cat: types.ModelCategory, mark: u8) void {
// Diagnostic: count tracked objects by category
switch (cat) {
.target => diag.scan_targets += 1,
.target => {
diag.scan_targets += 1;
debug_target_seen = true;
},
.raid_marked => diag.scan_raid_marks += 1,
.dead_player => diag.scan_dead_players += 1,
.none => {},
@@ -308,6 +283,54 @@ pub const Diag = struct {
};
pub var diag: Diag = .{};
// Sticky diagnostics for the debug build. They turn true once the corresponding
// stage has been observed and stay true for the whole process lifetime.
pub var debug_in_world_seen: bool = false;
pub var debug_object_manager_seen: bool = false;
pub var debug_player_guid_seen: bool = false;
pub var debug_local_player_seen: bool = false;
pub var debug_unit_player_guid_seen: bool = false;
pub var debug_unit_player_object_seen: bool = false;
pub var debug_target_guid_seen: bool = false;
pub var debug_target_object_seen: bool = false;
pub var debug_unit_target_guid_seen: bool = false;
pub var debug_unit_target_object_seen: bool = false;
pub var debug_object_scan_seen: bool = false;
pub var debug_scan_called_seen: bool = false;
pub var debug_pin_player_published_seen: bool = false;
pub var debug_pin_target_published_seen: bool = false;
pub var debug_pin_player_consumed_seen: bool = false;
pub var debug_pin_target_consumed_seen: bool = false;
var debug_scan_generation: u32 = 0;
var debug_publish_scan_generation: u32 = 0;
pub var debug_scan_after_publish_seen: bool = false;
// DEBUG9: objects captured explicitly from OutlineDebug() while executing in
// WoW's Lua/main-thread context. This lets us test the render/classification
// path without calling game object lookup functions from D3D9 EndScene.
var debug_pinned_player_obj: u32 = 0;
var debug_pinned_target_obj: u32 = 0;
pub fn setDebugPinnedObjects(player_obj: u32, target_obj: u32) void {
const gen = @atomicLoad(u32, &debug_scan_generation, .seq_cst);
@atomicStore(u32, &debug_publish_scan_generation, gen, .seq_cst);
@atomicStore(u32, &debug_pinned_player_obj, player_obj, .seq_cst);
@atomicStore(u32, &debug_pinned_target_obj, target_obj, .seq_cst);
if (player_obj != 0) debug_pin_player_published_seen = true;
if (target_obj != 0) debug_pin_target_published_seen = true;
if (player_obj != 0) {
debug_unit_player_object_seen = true;
debug_local_player_seen = true;
}
if (target_obj != 0) {
debug_unit_target_object_seen = true;
debug_target_object_seen = true;
}
}
pub var debug_target_seen: bool = false;
pub var debug_target_model_seen: bool = false;
var log: logging.Logger = .{};
pub fn initLogger() void {
+17
View File
@@ -84,6 +84,8 @@ pub const D3DRS = struct {
pub const DESTBLEND: u32 = 20;
pub const CULLMODE: u32 = 22;
pub const ZFUNC: u32 = 23;
pub const ALPHAREF: u32 = 24;
pub const ALPHAFUNC: u32 = 25;
pub const ALPHABLENDENABLE: u32 = 27;
pub const STENCILENABLE: u32 = 52;
pub const STENCILFAIL: u32 = 53;
@@ -105,6 +107,8 @@ pub const D3DRS = struct {
pub const D3DCMP_ALWAYS: u32 = 8;
pub const D3DCMP_EQUAL: u32 = 3;
pub const D3DCMP_LESSEQUAL: u32 = 4;
pub const D3DCMP_GREATER: u32 = 5;
pub const D3DCMP_GREATEREQUAL: u32 = 7;
pub const D3DSTENCILOP_KEEP: u32 = 1;
pub const D3DSTENCILOP_REPLACE: u32 = 3;
@@ -123,8 +127,12 @@ pub const D3DCLEAR_STENCIL: u32 = 4;
// D3D9 blend modes
// =============================================================================
pub const D3DBLEND_ZERO: u32 = 1;
pub const D3DBLEND_ONE: u32 = 2;
pub const D3DBLEND_SRCALPHA: u32 = 5;
pub const D3DBLEND_INVSRCALPHA: u32 = 6;
pub const D3DBLEND_DESTCOLOR: u32 = 9;
pub const D3DBLEND_INVDESTCOLOR: u32 = 10;
// =============================================================================
// D3D9 texture stage state IDs
@@ -133,11 +141,18 @@ pub const D3DBLEND_INVSRCALPHA: u32 = 6;
pub const D3DTSS = struct {
pub const COLOROP: u32 = 1;
pub const COLORARG1: u32 = 2;
pub const COLORARG2: u32 = 3;
pub const ALPHAOP: u32 = 4;
pub const ALPHAARG1: u32 = 5;
pub const ALPHAARG2: u32 = 6;
};
pub const D3DTOP_DISABLE: u32 = 1;
pub const D3DTOP_SELECTARG1: u32 = 2;
pub const D3DTA_DIFFUSE: u32 = 0;
pub const D3DTA_CURRENT: u32 = 1;
pub const D3DTA_TEXTURE: u32 = 2;
pub const D3DTA_TFACTOR: u32 = 3;
// =============================================================================
@@ -167,6 +182,7 @@ pub const D3DTEXF_POINT: u32 = 1;
pub const D3DFMT_A8R8G8B8: u32 = 21;
pub const D3DFMT_G16R16F: u32 = 112;
pub const D3DFMT_G32R32F: u32 = 115;
// =============================================================================
// D3D9 depth/stencil formats
@@ -216,6 +232,7 @@ pub const VT = struct {
pub const GetRenderState: usize = 58;
pub const GetTexture: usize = 64;
pub const SetTexture: usize = 65;
pub const GetTextureStageState: usize = 66;
pub const SetTextureStageState: usize = 67;
pub const GetSamplerState: usize = 68;
pub const SetSamplerState: usize = 69;
+29 -6
View File
@@ -57,6 +57,12 @@ pub fn isInGame() bool {
return hook.readMem(u32, o.IS_IN_WORLD) != 0;
}
/// The object manager is a more useful runtime readiness signal for the
/// standalone Outline path than IS_IN_WORLD on modified 1.12.1 clients.
pub fn hasObjectManager() bool {
return hook.readMem(u32, o.OBJECT_MANAGER_PTR) != 0;
}
pub fn objectFirst() u32 {
const obj_mgr = hook.readMem(u32, o.OBJECT_MANAGER_PTR);
if (obj_mgr == 0) return 0;
@@ -203,17 +209,32 @@ pub fn getMapId() u32 {
/// UnitGUID("player") / UnitGUID("target") → 64-bit GUID.
pub fn unitGUID(unit_id: [*:0]const u8) u64 {
return hook.call(fn ([*:0]const u8) callconv(hook.cc.fastcall) u64, o.FN_UNIT_GUID, .{unit_id});
// UnitGUID is __fastcall with its single argument in ECX.
// hook.call + hook.cc.fastcall is currently miscompiled by Zig 0.16 on x86
// as a stack argument. Because the callee consumes no stack argument, that
// leaked 4 bytes per call; Outline calls UnitGUID("player"/"target") every
// frame, so ESP drifted until execution returned into stack data.
//
// With a single register argument, x86_thiscall is ABI-compatible here:
// ECX=unit_id, no stack args, u64 returned in EDX:EAX.
const f_native: *const fn ([*:0]const u8) callconv(.{ .x86_thiscall = .{} }) u64 =
@ptrFromInt(o.FN_UNIT_GUID);
return @call(.never_tail, f_native, .{unit_id});
}
/// Resolve a GUID → object pointer via the object manager hash table.
pub fn getObjectByGUID(guid: u64) u32 {
if (guid == 0) return 0;
if (hook.readMem(u32, o.OBJECT_MANAGER_PTR) == 0) return 0;
const lo: u32 = @truncate(guid);
const hi: u32 = @truncate(guid >> 32);
// Do not gate this call on OBJECT_MANAGER_PTR. Nampower uses WoW's
// GetObjectPtr at 0x464870 directly, and on this modified client the
// legacy OBJECT_MANAGER_PTR global remains 0 even while the game is live.
// The stdcall(u64) ABI is equivalent on x86 to pushing lo + hi as two u32s.
const result = hook.call(fn (u32, u32) callconv(hook.cc.stdcall) u32, o.FN_GET_OBJECT_BY_GUID, .{ lo, hi });
// Guard: hash table can return stale/invalid pointers for destroyed objects
// Guard: hash table can return stale/invalid pointers for destroyed objects.
if (result != 0 and !isValidPtr(result)) return 0;
return result;
}
@@ -221,7 +242,6 @@ pub fn getObjectByGUID(guid: u64) u32 {
/// Split-GUID variant for callers that already have lo/hi parts.
pub fn getObjectByGUIDSplit(guid_lo: u32, guid_hi: u32) u32 {
if (guid_lo == 0 and guid_hi == 0) return 0;
if (hook.readMem(u32, o.OBJECT_MANAGER_PTR) == 0) return 0;
return hook.call(fn (u32, u32) callconv(hook.cc.stdcall) u32, o.FN_GET_OBJECT_BY_GUID, .{ guid_lo, guid_hi });
}
@@ -231,8 +251,10 @@ pub fn getPlayerGUID() u64 {
}
/// Get the local player's object pointer.
/// Use ClntObjMgrGetActivePlayer (0x468550) instead of UnitGUID("player").
/// This matches the path used by Nampower and does not depend on legacy globals.
pub fn getLocalPlayer() u32 {
const guid = unitGUID("player");
const guid = getPlayerGUID();
if (guid == 0) return 0;
return getObjectByGUID(guid);
}
@@ -317,8 +339,9 @@ pub fn getNameByObject(obj: u32) [*:0]const u8 {
}
/// Get the current target's GUID.
/// Read the locked target GUID directly (0xB4E2D8), as Nampower does.
pub fn getTargetGUID() u64 {
return unitGUID("target");
return readGUID(o.LOCKED_TARGET_GUID);
}
/// Check if a unit is friendly to the local player.