Watch Dogs 1 / Legion — Shader Compiler Pipeline & Shader Registry
Sources: Ubisoft leak (
leak/ubisoft/), Disrupt-Shader-Compiler RE, WDLobj_editor/analysis (2026-09-12, updated 2026-09-28). Documents how Ubisoft's ShaderCompiler2 actually producesshadersobj.dat, theFastInitDatashader registry, and the compiled shader format — the ground truth for recompiling/modding Disrupt engine shaders.
Correction chain: what a shipped shader file starts with
Container-format claims changed three times. The history, so it stays auditable:
- Early claim (original doc): Ubisoft shaders have a custom container, an extra
header before the
DXBCmagic. - 2026-09-12: "Extra header claim WRONG, both samples start with plain
DXBCat offset 0." The sample was wrong: one file was our own DXC compiler output (pixel_05439780.pso), the other a leakobj_editorfile (pixel_4271c18a7feae380.pso). Neither came from a retailshadersobjarchive. - 2026-09-27 (current, retail-verified): shipped
.pso/.vso/.csobegin with a short.headerstub, then theDXBCmagic (observed at offset 4 or 12). A 300-file random sample of retail unpackedshadersobjcontained zero files withDXBCat offset 0.
The stub is reproducible: per-file sibling <name>.pso.header stubs ship with the
reconstruction, and the build script prepends them. Without the prefix the engine
will not load the shader. The .crc file (2 bytes) is a separate verifier, not
part of the stub. Open question: the leak's WDL obj_editor files do start with
plain DXBC at offset 0, so whether WDL retail archives carry the stub is untested.
The compiler pipeline (ShaderCompiler2)
Ubisoft's toolchain (from the leak bin/, decompiled ubi_shader_compiler2/):
ShaderGenerator2 --GenerateAllVariations()
-> ComputeShaderID(name, defines[]) -> TShaderID<uint64>
-> CShaderCache -> CPlatformShaderCompiler
-> CBackEndD3D11 / D3D12 -> ByteCodeCompiler (d3dcompiler_47.dll)
-> obj/pixel_<64bitid>.pso
- TShaderID = uint64: HIGH bits = shader family, LOW bits = define-variation.
- ComputeShaderID mangled signature (recovered from ShaderGenerator2 import table):
?ComputeShaderID@@YA?AU?$TShaderID@_K@@AEAVCShaderHandlerManager@@AEAV?$ndStringBase@DUndStringAllocator@@@@AEBV?$ndVector@USDefi...=ComputeShaderID(CShaderHandlerManager&, ndStringBase<char,ndStringAllocator>&, ndVector<SDefine>&) -> TShaderID<u64>. Imported fromShaderCompilerUtils_r64.dll. - Build flow (PC) (
comp_shadersids.bat):ShaderGenerator2 operation=Generate shaderidsfile=shaderids.txt→prepareplatformdata64.exe -shaders→ collectobj/*.pso→FileArchiver archive=shadersobj.dat. - ShaderCompilerUtils_r64.dll is obfuscated: export dir base=1, nfuncs=526,
nnames=0 (ordinal-only), and
AddressOfFunctions RVA = 0x20e(bogus/corrupted). Cannot resolve ComputeShaderID's code address via the PE export dir. The base string hash is FNV-1 64 (0xcbf29ce484222325offset +0x100000001b3prime,NomadDefaultHashFunctor<ndStringBase>).
Compiled shader format (.pso/.vso/.cso/.gso)
.headerstub + DXBC: files begin with a short stub,DXBCfollows at offset 4 or 12 (never offset 0 in the 300-file retail sample, 2026-09-27; see the correction chain above). 64-bit ID in WDL filename:pixel_<16hex>.pso, 32-bit in WD1:pixel_<8hex>.pso.- Bucket rule: shaders are grouped into
obj/h00..h7fwherebucket = shaderID_low_byte & 0x7F(verified 100% on 2388 WDL shaders). Bit 7 of the low byte = free flag (shader-type or variant bit, ~50% set). .depfile (dependencies_<id>.dep): XML<ShaderFileChecksums>listing every#included source file + a uint64 content Checksum.- The Checksum = plain FNV-1 64 of the raw (CRLF) file bytes (VERIFIED:
fnv164(DepthShadow.inc.fx raw)=0xb8d6781ddfedf051= 13318965017701118033 exactly matches the .dep). NOT FNV-1a, NOT masked, NOT a path hash. .crcfile: 2-byte shader verifier, separate from the.headerstub. The 4-byteengine/shaders/shaders.crcis a different file: the engine reads it from two mounts and compares the values, so it is a cross-mount consistency check, not a content hash of the shader set. Retail and the working community mod TFOWC2 ship byte-identical values, so a recompile does not need to regenerate it..headerstub (WD1 DSC): first u32 = content version (observed 0-9), then version-dependent fields; reconstruction stubs measure 4, 8, 12, 16, 20, 24, or 40 bytes (4 and 12 dominate), while the 300-file retail sample only showedDXBCat offset 4 or 12 (open question: whether the larger stub sizes ship in retail). NOT the shader ID (u32[1] ≠ filename id). These are prepended to compiled output — see below.
FastInitData_editor.bin — the REQUIRED shader registry
leak/ubisoft/data_win64/engine/shaders/FastInitData_editor.bin (3.5MB, nbCF v3) is
the AUTHORITATIVE shader registry — required for shader modding.
[nbCF v3 header] RootId 0x67974467
[family base-ID block] 143 u64s (file 0x30..0x4A8): 0x03,0x05,0x06,0x09,0x0B..0xE6
each 0xNN_00000000000000 = a shader FAMILY base ID
[shader member block] 444k u64s (from 0x4A8): 0x54_0000..., 0x58_...01...
each family's member shader IDs (low bits = defines + type flag)
- shader top byte (
sid>>56) = family ID (240 distinct among 2385 shipped WDL shaders). - To MOD a shader: its ID must fall in the family's range AND be registered in
FastInitData's membership, or the engine won't enumerate/load it. A mod coordinates:
recompiled
.psoinshadersobj.dat+ shader IDs + FastInitData membership.
The Disrupt-Shader-Compiler project (WD1 recompile)
Disrupt-Shader-Compiler/ (git repo) reconstructs the WD1 shader DB:
Shader_Compile_Command_Sorted.txt(39,131 lines): 39,128 retail fxc command lines, one per permutation (the exact set Ubisoft compiled; 23357 ps_5_0 pixel, 15649 vs_5_0 vertex, 101 cs_5_0, 21 gs_5_0), plus 3 research-only RT test lines (rt_probes.fx,rt_probes_radiance.fx,rt_shadow_ao.fx).CompileShaders.py— original (Windows fxc). Includes aprepend_header()step that prepends the shipped.headerstub onto each compiled output — the engine-compat bridge.compile_shaders_linux.py— the Linux port (DXC); must ALSO prepend headers. Which compiler backend actually produces output the game accepts is settled in the recompilation section below.- The
.pso.headerstubs inobj/= exactly the shipped set (39,128 stubs, 1:1 with commands).
Recompilation: what actually works (verified 2026-09-28)
- All 39,131 command lines compile (100%) using the shipped fxc command lines
against Microsoft
d3dcompilerDLLs through a D3DCompile wrapper. The command list plus d3dcompiler is sufficient; no Ubisoft tool is needed to compile. - DXC (modern) fails: it promotes
ps_5_0/vs_5_0to shader model 6 / DXIL regardless of flags, raises semantic errors on the reconstruction's struct I/O, and the D3D11 runtime rejects the output. Only fxc-era d3dcompiler produces valid retail-style DXBC. - Semantic strictness is uniform across era compilers (d3dcompiler_43 / _46 /
_47): a bare struct member in an entry-function parameter is rejected with
error X3502: 'MainPS': input parameter 'x' missing semantics. Non-standard but explicit semantics compile:float2 uv : uv;works, and the compiler preserves the name verbatim instead of renaming it to TEXCOORD. - Version fingerprints embedded in the DXBC (readable with
strings): retail =HLSL Shader Compiler 9.29.952.3111(fxc 2010), working community mod TFOWC2 = 10.1 (d3dcompiler_47). The game accepts both. Modern DXC output is rejected.
Semantic system (the reconstruction gap, dated)
The DSC source tree is a reconstruction. As found (2026-09-12), many struct
members and bare entry returns lacked semantics, and era compilers reject both
cases: E5004: missing a semantic for returns, X3502: 'MainPS': input parameter 'x' missing semantics for bare struct members (see the strictness notes above).
Examples from that state:
bbox.fx:23returnedfloat4(1,1,1,1)with no return semantic (the file in the tree now carries: SV_Target).- Shipped bbox shader ground truth: PS ISGN=
SV_Position, OSGN=SV_Target.
As of 2026-09-28 the tree compiles 100% of command lines against d3dcompiler, so the missing semantics have been filled in. Compiling is solved; matching retail's signatures is the remaining problem (next sections).
The semantic layer uses custom semantic names + macros:
GlobalSemantics.inc.fx:ATTR0=position, ATTR1=normal, ATTR2=color0, ATTR3=color1, ATTR4=blendweight, ATTR5=blendindices, ATTR6-15=texcoord0-9.CustomSemantics.inc.fx:CS_Position=ATTR0, etc.Profile.inc.fx(PC):SEMANTIC_VAR(var) var : var,SEMANTIC_OUTPUT(semantic) : semantic,VS_TARGET=vs_3_0,PS_TARGET=ps_3_0, cbuffer gating for SHADERMODEL>=40.- These custom names map to real DXBC semantics at compile time via the shader
signature tables (
SMeshVertex,SVertexToPixel).
SEMANTIC_VAR and what shipped builds actually used
Profile.inc.fx defines the macro twice:
#ifdef INTERPOLATOR_PACKING
#define SEMANTIC_VAR(var) var
#define SEMANTIC_OUTPUT(semantic)
#else
#define SEMANTIC_VAR(var) var : var
#define SEMANTIC_OUTPUT(semantic) : semantic
#endif
INTERPOLATOR_PACKINGappears only as that#ifdef. Zero occurrences across all 39,131 compile command lines, so no build ever defined it: shipped compiles usedvar : var(variable-name semantics).- That matches the working community mod TFOWC2, which ships variable-name
semantics (
uv,color,animColorfrom theSEMANTIC_VAR(var) → var : varexpansion) and works in-game. It does not match retail's TEXCOORD numbering. - The leak's source is not byte-identical to retail's shipped source. The leak's
SVertexToPixeldeclares a barefloat2 pixelCoords, which no era compiler accepts, and itsMainPSdeclares a: VPOSparameter that retail's shipped ISGN lacks for the same permutation.
Implication: compiling is solved, signature matching is not. Derive the
engine-bound sides (VS input, PS output) from the shipped .pso DXBC ISGN/OSGN,
which is ground truth per permutation, and keep the internal sides consistent
between VS and PS. Which side must do what is the subject of the next section.
The signature contract (ISGN/OSGN): what actually breaks a recompile
DXBC carries two signatures per shader: an input signature (ISGN) and an output signature (OSGN). Diffing retail shaders, the working community mod TFOWC2, and our broken recompiles produced this contract:
- Vertex-shader INPUT and pixel-shader OUTPUT are engine-bound. They must match retail byte-for-byte because C++ code maps them to vertex declarations and render targets. Measured: TFOWC2 matches retail on 15643/15649 vso inputs and 23355/23357 pso outputs.
- Vertex-shader OUTPUT ↔ pixel-shader INPUT are internal. They only need to
agree with each other. TFOWC2 ships variable-name semantics on this side
(
uv,color,animColor) and works fine in-game. - Retail's numbering is per-permutation active-member order: the first active
struct member gets TEXCOORD0, the second TEXCOORD1, and so on, produced by
retail's 2010 compiler (
HLSL Shader Compiler 9.29.952.3111). No static source annotation can reproduce it, because the active member set changes with each permutation's#defines. - A static per-file TEXCOORD renumbering looks correct but breaks linkage. Wrong indices on the engine-bound sides → VS/PS linkage mismatch → black screen with audio. Verified by elimination (see triage below).
- The engine pairs VS with PS via
fastinitdata.bin, not via source file. Same-source pairing heuristics mislabel even retail (about 19k naive "mismatches" that are subset-safe in practice), so same-source pair counts are not a broken/working signal.
Black-screen triage (2026-09-28)
Order that isolated the fault:
- Full recompile pack → black screen.
- Added missing retail-only file classes → still black.
- Retail vertex shaders + recompiled pixel shaders → still black (pixel-side signatures implicated).
- Retail content through the identical pack pipeline → works, exonerating the
.fat/.nfo/.datformat. - Working mod's shader set inspected → the signature contract above.
Current status: the identified fix is a signature-matching recompile (variable-name semantics plus the d3dcompiler_47 recipe, mirroring TFOWC2). It is not yet verified in-game.
WDL vs WD1 shader differences
| Aspect | WD1 | WDL (Legion) |
|---|---|---|
| Filename | pixel_<8hex>.pso | pixel_<16hex>.pso (full 64-bit TShaderID) |
| Bucket | obj/h00-h7f | same id_low_byte & 0x7F |
| Source | shaders.dat (full source shipped) | leak data/engine/shaders/ (942 fx) |
| Registry | FastInitData (nbCF) | same |
| File prefix | .header stub (4/12 bytes) before DXBC, retail-verified | sampled leak obj_editor files start with plain DXBC; retail WDL untested (open) |
| .dep checksum | — | FNV-1 64 of raw CRLF bytes |
Key tooling locations
- Leak compiler binaries:
leak/ubisoft/bin/(ShaderCompilerUtils_r64.dll,ShaderGenerator2_r64.dllintoolframework/plugins/,ToolLauncher_r64.exe,PreparePlatformData64.exe,ShaderCompiler2.exe) - Leak d3dcompiler:
leak/ubisoft/bin/FXC/PC/d3dcompiler_47.dll(4.4MB, exact Ubi build) - WDL source:
leak/ubisoft/data/engine/shaders/(942 fx) - WDL compiled:
leak/ubisoft/data_win64/engine/shaders/obj_editor/(2388 shaders) - WDL FastInitData:
leak/ubisoft/data_win64/engine/shaders/FastInitData_editor.bin - WD1 reconstruction:
Disrupt-Shader-Compiler/(git repo) - WDL shader dictionary:
WDL/wdl_shader_dictionary.md - Unpacked WD1 shaders: retail
shadersobjunpack (~23,359.psounderengine/shaders/obj/)
Open items
- Verify the signature-matching recompile in-game (variable-name semantics + d3dcompiler_47 recipe, mirroring TFOWC2). Identified fix, not yet tested.
- Build the signature-derivation tool: map each shader ID → shipped .pso DXBC ISGN/OSGN → auto-inject into the reconstructed source. Must reproduce retail numbering on the engine-bound sides (VS input, PS output).
- Decode the FastInitData shader-member low-56-bit encoding (define-variation + bit-7 type flag) to generate valid member IDs.
- Map the 143 FastInitData family base-IDs → family NAMES (correlate top-byte
with .dep meta/
.fx in canonical order). - ComputeShaderID full formula remains uncracked (obfuscated exports); the shipped .pso + FastInitData registry sidestep it.