name: 'dx9-ffp-port' description: 'Use when porting a game for RTX Remix or to the fixed-function pipeline, working on renderer.cpp / ffp_state / remix-comp-proxy.ini / draw routing / VS constants / vertex declarations / matrix mapping / skinning, building or deploying a remix-comp-proxy patch, or diagnosing rendering issues in a patched game (white geometry, missing objects, wrong transforms, ImGui F4, diagnostics.log).' user-invocable: true
DX9 FFP Proxy -- Game Porting
Port a DX9 shader-based game to fixed-function pipeline (FFP) for RTX Remix compatibility. Remix requires FFP geometry to inject path-traced lighting and replaceable assets.
NEVER MODIFY TEMPLATE CODE. The following directories are read-only templates:
rtx_remix_tools/dx/remix-comp-proxy/— remix-comp-proxy framework template
To create a game patch, copy the template to patches/<GameName>/ and edit the copy. If the user asks you to edit remix-comp-proxy code, always confirm whether they mean the template or a game-specific copy under patches/. Only modify the template if the user explicitly says to change the template itself.
SKINNING IS OFF BY DEFAULT. Do NOT enable skinning in remix-comp-proxy.ini, modify skinning code, or discuss skinning infrastructure unless the user explicitly asks for character model / bone / skeletal animation support. When requested, read src/comp/modules/skinning.hpp and src/comp/modules/skinning.cpp for the full implementation.
SKINNING APPROACH: FFP indexed vertex blending, NOT CPU matrix math. When skinning is enabled, the correct approach is:
- Keep BLENDINDICES and BLENDWEIGHT elements in the vertex declaration and vertex buffer
- Upload bone matrices via
SetTransform(D3DTS_WORLDMATRIX(n), &boneMatrix[n])for each bone - Enable
D3DRS_INDEXEDVERTEXBLENDENABLE = TRUE - Set
D3DRS_VERTEXBLENDto the appropriate weight count (e.g.D3DVBF_3WEIGHTS) - Let the FFP hardware pipeline do the blending
CPU-side vertex skinning (manually multiplying vertices by bone matrices) is a last resort only. It is extremely expensive, tanks frame rate, and should only be considered when FFP indexed vertex blending is not feasible. Always prefer the hardware path above.
What remix-comp-proxy Does
Each game folder under patches/<GameName>/ is a self-contained remix-comp-proxy project (copied from rtx_remix_tools/dx/remix-comp-proxy/). It is a d3d9.dll proxy that:
- Captures VS constants (View, Projection, World matrices) from
SetVertexShaderConstantFviaffp_state::on_set_vs_const_f - Parses
SetVertexDeclarationviaffp_state::on_set_vertex_declarationto detect BLENDWEIGHT+BLENDINDICES (skinned), POSITIONT (screen-space), NORMAL presence, and per-element byte offsets - Routes
DrawIndexedPrimitiveviarenderer::on_draw_indexed_prim:- No NORMAL -> HUD/UI pass-through
- Skinned + skinning module enabled ->
skinning::draw_skinned_dip() - Rigid 3D (has NORMAL) -> NULLs shaders, applies FFP transforms
- Routes
DrawPrimitiveviarenderer::on_draw_primitive: world-space (has decl, no POSITIONT, not skinned) -> FFP; otherwise pass-through - Applies captured matrices via
ffp_state::apply_transforms->SetTransform - Sets up texture stages and lighting for FFP rendering (stages 1-7 disabled to prevent stale auxiliary textures reaching Remix)
- Loads the real d3d9 chain (RTX Remix
d3d9_remix.dllor system d3d9) via d3d9_proxy
Codebase File Map
| File | Role |
|---|---|
src/comp/modules/renderer.cpp |
Draw routing -- on_draw_indexed_prim() and on_draw_primitive() |
src/comp/modules/renderer.hpp |
drawcall_mod_context for save/restore state around draws |
src/shared/common/ffp_state.cpp |
Core FFP state tracker -- engage/disengage, transforms, texture stages |
src/shared/common/ffp_state.hpp |
FFP state class with all accessors |
src/shared/common/config.hpp |
Config structure parsed from remix-comp-proxy.ini |
src/comp/main.cpp |
DLL entry, d3d9 proxy init, window finder, config loading |
src/comp/comp.cpp |
Module init: registers renderer, diagnostics, skinning, imgui |
src/comp/d3d9_proxy.cpp |
Loads real d3d9 chain, DLL pre/post-load, forwarded exports |
src/comp/modules/d3d9ex.cpp |
IDirect3DDevice9 / IDirect3D9 wrapper + exported Direct3DCreate9 |
src/comp/modules/d3d9ex.hpp |
D3D9 wrapper class declarations |
src/comp/modules/diagnostics.cpp |
50-sec delay, 3-frame diagnostic log to rtx_comp/diagnostics.log |
src/comp/modules/skinning.cpp |
Optional skinning module (vertex expansion + bone upload) |
src/comp/modules/skinning.hpp |
Skinning class declaration |
src/comp/modules/imgui.cpp |
ImGui debug overlay (F4) with FFP tab |
src/comp/game/game.cpp |
Per-game address init (patterns, hooks) |
src/comp/game/game.hpp |
Per-game variables and function typedefs |
remix-comp-proxy.ini (in assets/) |
Runtime config: albedo stage, skinning toggle, diagnostics, DLL chain |
build.bat |
Build script: outputs d3d9.dll proxy. build.bat [release|debug] [--name Name] |
rtx_remix_tools/dx/remix-comp-proxy/ is the TEMPLATE. Each game gets a full copy under patches/<GameName>/ — the entire folder is self-contained and can be distributed as a standalone repo. Edit src/comp/ directly in the game's copy.
Before reading remix-comp-proxy source files, read references/remix-comp-context.md for a skip-list of boilerplate files (~7,000 lines) you should never open, with summaries of what they do. It also lists the ~1,200 lines of files that actually matter for per-game work.
Porting Workflow
Step 1: Static Analysis
Run the analysis scripts to understand the game's D3D9 usage:
# Core discovery
python rtx_remix_tools/dx/scripts/find_d3d_calls.py "<game.exe>"
python rtx_remix_tools/dx/scripts/find_device_calls.py "<game.exe>"
python rtx_remix_tools/dx/scripts/classify_draws.py "<game.exe>"
# Shader constants and vertex formats
python rtx_remix_tools/dx/scripts/find_vs_constants.py "<game.exe>"
python rtx_remix_tools/dx/scripts/find_ps_constants.py "<game.exe>"
python rtx_remix_tools/dx/scripts/decode_vtx_decls.py "<game.exe>" --scan
python rtx_remix_tools/dx/scripts/decode_fvf.py "<game.exe>"
# Skinning analysis (bone palettes, blend weights, suggested INI)
python rtx_remix_tools/dx/scripts/find_skinning.py "<game.exe>"
python rtx_remix_tools/dx/scripts/find_blend_states.py "<game.exe>"
# Render state and texture pipeline
python rtx_remix_tools/dx/scripts/find_render_states.py "<game.exe>"
python rtx_remix_tools/dx/scripts/find_texture_ops.py "<game.exe>"
python rtx_remix_tools/dx/scripts/find_transforms.py "<game.exe>"
python rtx_remix_tools/dx/scripts/find_surface_formats.py "<game.exe>"
Scripts are fast first-pass scanners -- surface candidate addresses only. Always follow up with retools and livetools for deep analysis.
Alternative: DX9 Tracer capture. Deploy the tracer proxy (graphics/directx/dx9/tracer/) to capture a full frame, then analyze with --matrix-flow, --vtx-formats, --shader-map, and --const-provenance to discover the register layout without reverse engineering the binary.
Key things to find:
- How the game obtains its D3D device (Direct3DCreate9 -> CreateDevice)
- Which functions call
SetVertexShaderConstantFand with what register/count patterns - What vertex declaration formats are used (BLENDWEIGHT/BLENDINDICES = skinning)
- Where the main render loop / draw calls live
Step 2: Discover VS Constant Register Layout
This is the most critical step. Determine which VS constant registers hold View, Projection, and World matrices.
Remix REQUIRES separate World, View, and Projection matrices. A concatenated WorldViewProj (WVP) or ViewProj (VP) matrix will NOT work -- Remix needs individual matrices to apply its own camera and per-object transforms. If the game uploads a pre-multiplied WVP, the proxy must intercept the individual W, V, P matrices before the game concatenates them. This is the #1 source of broken Remix ports.
Start with the matrix register finder:
python rtx_remix_tools/dx/scripts/find_matrix_registers.py "<game.exe>"
This cross-references SVSCF call patterns, shader CTAB names, and frequency analysis to suggest a register layout. Always verify its output with runtime data.
Static approach: Decompile call sites:
python -m retools.decompiler <game.exe> <call_site_addr> --types patches/<project>/kb.h
Dynamic approach: Trace SetVertexShaderConstantF live:
python -m livetools trace <call_addr> --count 50 \
--read "[esp+8]:4:uint32; [esp+10]:4:uint32; *[esp+c]:64:float32"
Captures: startRegister, Vector4fCount, and the first 4 vec4 constants of actual float data.
DX9 Tracer approach: Capture a frame and analyze:
python -m graphics.directx.dx9.tracer analyze <JSONL> --const-provenance
python -m graphics.directx.dx9.tracer analyze <JSONL> --matrix-flow
python -m graphics.directx.dx9.tracer analyze <JSONL> --shader-map
How to identify matrices:
- View matrix: changes with camera movement; contains camera orientation
- Projection matrix: contains aspect ratio and FOV; rarely changes
- World matrix: changes per object; contains position/rotation/scale
- Look for 4x4 matrices (16 floats = 4 registers). Row 3 often has
[0, 0, 0, 1]for affine transforms. - Watch for concatenated matrices: If the game only uploads one matrix per draw (e.g. WVP at c0-c3), the individual W/V/P are being multiplied before upload. Trace back to find where the multiplication happens -- you need to capture W, V, P separately before that point.
Step 3: Set Up Per-Game Project
IMPORTANT: rtx_remix_tools/dx/remix-comp-proxy/ is the template. NEVER edit it directly. Each game gets a full copy of the framework.
- Copy the entire
rtx_remix_tools/dx/remix-comp-proxy/folder topatches/<GameName>/(excludingbuild/) - Edit
src/comp/directly in the game's copy — this is the per-game customization layer - Edit register layout defaults in
src/shared/common/ffp_state.hpp(see Register Layout section below) - Edit
src/comp/main.cpp: setWINDOW_CLASS_NAMEto the game's window class - Customize
src/comp/modules/renderer.cppdraw routing if needed (see Decision Trees below) - Customize
src/comp/game/game.cppwith game-specific address init if hooks are needed - Update
kb.hwith discovered function signatures, structs, and globals
The game folder is now fully self-contained and can be distributed as a standalone git repo.
Step 4: Build and Deploy
From the game folder:
cd patches/<GameName>
build.bat release --name <GameName>
The build produces d3d9.dll in patches/<GameName>/build/bin/release/. Deploy:
d3d9.dllto the game directory (the game loads this as its d3d9 proxy)remix-comp-proxy.inito the game directoryd3d9_remix.dllto the game directory if using Remix
Step 5: Diagnose with Log and ImGui
rtx_comp/diagnostics.log: Written to the rtx_comp/ subfolder of the game directory after a configurable delay (default 50 seconds), then logs 3 frames of detailed draw call data:
- VS regs written: which constant registers the game actually fills
- Vertex declarations: what vertex elements each draw uses
- Draw calls: primitive type, vertex count, index count, textures per stage
- Matrices: actual View/Proj/World values being applied
- Raw vertex bytes: hex dump of first vertices for early draw calls
Do not change the logging delay unless the user asks -- it ensures the user gets into the game with real geometry before logging begins.
ImGui overlay (F4): Press F4 to toggle the debug overlay. The FFP tab shows real-time draw call stats, VS constant register write history, and enables a fake camera for testing transforms.
Tell the user when you need them to interact with the game for logging or hooking purposes. They must be in-game with geometry visible for the log to be useful.
Register Layout (ffp_state.hpp)
The VS constant register layout is defined as member defaults in src/shared/common/ffp_state.hpp. Edit these when porting a new game:
// In ffp_state.hpp — private members with game-specific defaults
int vs_reg_view_start_ = 0;
int vs_reg_view_end_ = 4;
int vs_reg_proj_start_ = 4;
int vs_reg_proj_end_ = 8;
int vs_reg_world_start_ = 16;
int vs_reg_world_end_ = 20;
int vs_reg_bone_threshold_ = 20; // only matters when [Skinning] Enabled=1
int vs_regs_per_bone_ = 3; // 3 = 4x3 packed bones (most common), 4 = full 4x4
int vs_bone_min_regs_ = 3; // minimum register count to qualify as bone upload
Each matrix occupies 4 consecutive vec4 registers (= 16 floats). After changing defaults, rebuild with build.bat.
Bone Configuration for Skinning
Before enabling skinning, run find_skinning.py to determine the bone start register (vs_reg_bone_threshold_) and upload pattern. Some games upload all bones in one call; others upload in groups until hitting a max (e.g., groups of 15, max 75). If the game uses grouped uploads, lower vs_bone_min_regs_ so the proxy doesn't reject the smaller batches. If bone uploads overlap with non-bone constants, raise vs_reg_bone_threshold_.
INI Config (remix-comp-proxy.ini)
Runtime settings that don't require recompile:
[FFP]
Enabled=1
AlbedoStage=0
; Albedo texture stage (0-7). Set to whichever stage the game binds the diffuse texture.
[Skinning]
Enabled=0
; Only set to 1 after rigid FFP works correctly.
; Run find_skinning.py to determine bone register layout before enabling.
[Diagnostics]
Enabled=1
DelayMs=50000
LogFrames=3
[Remix]
Enabled=1
DLLName=d3d9_remix.dll
[Chain]
PreLoad=
PostLoad=
; Semicolon-separated DLLs/ASIs to load before/after the d3d9 chain.
; Example: PreLoad=patch.dll;fix.asi
Architecture: What to Edit vs What to Leave Alone
Each game folder under patches/<GameName>/ is a self-contained copy of the full remix-comp-proxy framework. Edit files directly in the game's copy.
Component (in patches/<GameName>/) |
Edit Per-Game? |
|---|---|
ffp_state.hpp register layout defaults |
YES — rebuild after changing |
remix-comp-proxy.ini albedo stage, diagnostics, chain |
YES |
src/comp/main.cpp WINDOW_CLASS_NAME |
YES |
src/comp/modules/renderer.cpp draw routing |
YES -- main draw routing |
src/comp/game/game.cpp address init and hooks |
YES -- per-game hooks |
src/comp/game/structs.hpp game structs |
YES -- per-game data structures |
src/shared/common/ffp_state.cpp engage/disengage/transforms |
MAYBE -- only for unusual FFP needs |
src/shared/common/config.hpp |
MAYBE -- add new INI sections if needed |
src/comp/modules/d3d9ex.cpp |
NO -- forwards all 119 methods |
src/comp/modules/diagnostics.cpp |
NO -- generic frame logger |
src/comp/modules/imgui.cpp |
NO -- debug overlay |
src/shared/ everything else |
NO -- framework code |
DrawIndexedPrimitive Decision Tree
ffp.is_enabled() AND ffp.view_proj_valid()?
+-- NO -> passthrough with shaders
+-- YES
+-- ffp.cur_decl_is_skinned()?
| +-- YES + skinning module -> skinning::draw_skinned_dip()
| +-- YES + no skinning -> passthrough with shaders
+-- !ffp.cur_decl_has_normal()?
| +-- passthrough (HUD/UI)
| GAME-SPECIFIC: remove this filter if world geometry lacks NORMAL
+-- else (rigid 3D mesh)
+-- ffp.engage() + draw + restore
Common per-game changes:
- World geometry omits NORMAL -> remove or change
!ffp.cur_decl_has_normal()filter - Special passes (shadow, reflection) -> filter by shader pointer, render target, or vertex count
- UI drawn with DrawIndexedPrimitive + NORMAL -> add a filter (e.g. check stride or texture)
DrawPrimitive Decision Tree
ffp.is_enabled() AND ffp.view_proj_valid() AND ffp.last_decl()
AND !ffp.cur_decl_has_pos_t() AND !ffp.cur_decl_is_skinned()?
+-- YES -> ffp.engage() (world-space particles / non-indexed geometry)
+-- NO -> passthrough (screen-space UI, POSITIONT, no decl, skinned)
Common Pitfalls
- Concatenated WVP/VP instead of separate matrices: This is the #1 Remix porting mistake. Remix requires separate World, View, and Projection matrices passed via
SetTransform. If the game uploads a pre-multiplied WorldViewProj or ViewProj to a single register range, the proxy gets a combined matrix it can't decompose. Fix: find where the game multiplies WVP (or V*P) and hook that function to capture the individual matrices before concatenation. Usefind_matrix_registers.pyto detect this -- if CTAB shows "WorldViewProj" or only one matrix register is uploaded per draw, you have this problem. - Matrices look wrong: D3D9 FFP
SetTransformexpects row-major.ffp_state::apply_transformstransposes column-major VS constants. If the game stores matrices row-major in VS constants (uncommon), remove the transpose inffp_state::apply_transforms. - Everything is white/black: Albedo texture is on stage 1+, not stage 0. Set
AlbedoStageinremix-comp-proxy.ini, or traceSetTexturecalls to find the correct stage. - Some objects render, others don't: Check whether missing geometry has NORMAL in its vertex decl. Check
ffp.view_proj_valid()is true at draw time. DrawPrimitive routes on decl presence + no POSITIONT + not skinned. - Skinned meshes invisible: Set
[Skinning] Enabled=1inremix-comp-proxy.ini. Check log for skinning errors. Verifybone_start_regandnum_bonesare non-zero in the log. - Bones mixed up between NPCs: Stale WORLDMATRIX slots from a previous object. The game may need a game-specific reset hook at a per-object boundary -- see Skinning Stability below.
- Game crashes on startup: Set
[Remix] Enabled=0inremix-comp-proxy.inito test without Remix. CheckWINDOW_CLASS_NAMEincomp/main.cpp. - Geometry at origin / piled up: World matrix register mapping wrong. Re-examine VS constant writes via
livetools traceor DX9 tracer--const-provenance. - World geometry shifts after skinned draws:
WORLDMATRIX(0)clobbered by bone[0]. The proxy tracksworld_dirty_for re-application. If still broken, check for bone register overlap with world matrix range inffp_state.hpp. - ImGui overlay not appearing: Press F4. Check that
WINDOW_CLASS_NAMEis correct and the window was found (console output). Check for DirectInput hook conflicts.
Skinning Stability: Finding Game-Specific Hook Points
The proxy's generic heuristics handle most games. If bones still leak between objects, the game needs a hook at a per-object boundary function -- one that's called once per skinned object, before its bones are uploaded.
Finding the per-object function:
- Capture 2+ frames with the D3D9 tracer while multiple skinned NPCs are on screen
- Hotpaths:
--hotpaths --resolve-addrs <game.exe>-- look at callers of bone-rangeSetVertexShaderConstantFwrites - Caller histogram:
--callers SetVertexShaderConstantF-- the function that appears N times per frame (N = number of skinned objects) is the per-object boundary - Live confirm:
livetools trace <candidate_addr> --count 50-- with 3 NPCs, expect ~3 hits/frame - Static context:
callgraph.py --up+decompiler.pyon the caller -- confirm it loops over objects