Effect shader programs
Every effect draw of an FXR file is shaded by a program from one bundle, separate from the material bundles (shader bundles). This page records what the programs of Sekiro’s effect bundle read and write, as their own reflection data states it, so a renderer knows the interface of each family before it runs one. How the engine fills that interface from an effect’s actions is recorded for the multi-texture draws (below) and in part for the distortion draws; the rest is open.
The bundle
Section titled “The bundle”/shader/gxffxshader.shaderbnd.dcx holds 399 Shader Model 5 programs: 249 pixel, 35 domain, 29 vertex, 24 hull, 17 geometry
and 45 compute programs. Observed · Sekiro · every member of the bundle parsed: container, reflection and signature chunks
The names fall into four families.
| Family | What the names say | Pixel | Vertex | Hull | Domain | Geometry | Compute |
|---|---|---|---|---|---|---|---|
GXFfx (not tessellated) |
texture, bumpTexture, colorOnly, softTracer, distortion*, radialBlur, SoftRadialBlur, LightShaft* |
28 | 10 | - | - | - | - |
GXFfxtessellate |
the same draws with a tessellated surface: Texture, BumpTexture, MultiTexture, Blend*, PointSprite, LineColorOnly, QuadLineColorOnly, LensFlare, Tracer*, DistortionTracer*, with Soft, GlowD, AboveSdw and Specular variants |
85 | 16 | 24 | 35 | 10 | 1 |
*_VFX_* |
StandardParticle, SparkParticle and their SingleTexture, AllLight, Glow, Soft, Overlay and AboveShadow variants |
136 | 3 | - | - | 7 | 28 |
| sorting | BitonicSort*, PrepareSort*, MatrixTranspose |
- | - | - | - | - | 16 |
Observed · Sekiro · the same parse
The draw names match the forms an effect file can draw (a textured billboard, a normal-mapped one, several textures, a point
sprite, a tracer, lines and quad lines, distortion, radial blur, a light shaft, a lens flare). Inferred · by the names
The _VFX_ programs are the pipeline of the particles that live on the GPU: compute programs that emit and update them with
append and consume buffers, sorting programs, a vertex program that reads one record of a structured buffer by vertex number, a
geometry program that expands it to a quad, and the pixel programs. Observed · Sekiro That these are the programs of the
GPU particle action types is inferred from the names. Inferred
The effect draws
Section titled “The effect draws”The vertex program of a plain textured draw takes one vertex per corner of the sprite: a position of three floats, two colours
of four, a texture coordinate of two and three further coordinates of one and two floats (POSITION, COLOR0, COLOR1,
TEXCOORD0, TEXCOORD2, TEXCOORD3, TEXCOORD4), with no vertex number among its inputs, and outputs the clip position and
the values the pixel program reads. Observed · Sekiro The corners of such a sprite are therefore made before the draw,
not expanded by the GPU. Inferred The 29 vertex programs have 12 input layouts. The tessellated draws take the same
vertex layout in the hull, domain and geometry programs (the hull and domain programs come in _quad, _octed and _quincunx
variants) and their vertex programs pass the inputs through unchanged; the multi-texture ones take five colours and eight
coordinates. The bump-mapped draws take a normal, tangent and binormal as well, the distortion and radial-blur draws only a
position and a coordinate, the colour-only draws a position and two colours, and the three _VFX_ vertex programs only a vertex
number. Observed · Sekiro
All 249 pixel programs write two render targets. Observed · Sekiro What they read:
| Resource | Programs |
|---|---|
3-D volumetric fog g_volumetricFog (slot 16) |
226 of the pixel programs |
3-D shadow volume g_shadowVolumeTexture (slot 5) |
200 |
colour texture g_texAlbedo0 (slot 0) |
136, the _VFX_ ones |
colour texture g_diffuseTexture (slot 0) |
77 |
depth g_depthTexture |
123 |
shadow of the area above g_aboveShaodowTexture and the sampler g_ShadowSampler (slot 12) |
94 |
normal maps g_texNormalMap, g_normalTexture |
64, 52 |
motion vectors g_MotionVectorTexture |
32 |
further layers g_multiTexture0 to g_multiTexture2, g_blendTexture, g_specularTexture, g_frameBufferTexture, g_TraceframeBufferTexture |
16 to 4 each |
Observed · Sekiro · the reflection data of all 249 pixel programs
The samplers are a clamping linear one at slot 9 (240 pixel programs), a wrapping linear one at slot 8 (234: g_samLinearWrap
in the _VFX_ ones, g_WrapLinearSampler in the others) and a clamping point one at slot 7 (123). Observed · Sekiro The
irradiance volumes (g_IrradianceVolumeX to W, g_IrradianceVolume2X to W) and the tile-based light lists (g_LightIndices,
g_LightLookupBuffer, g_LightBuffer, g_SpotLightBuffer, structured buffers at slots 59 to 62) are read by the vertex (5 and
6 programs), domain (31 and 30) and geometry (15 and 15) programs, not the pixel programs: the lighting of an effect is done per
vertex. Observed · Sekiro
The values that choose how a draw looks are constants of one buffer shared by the pixel programs, PSCommonBuffer of 960
bytes: a colour, the texture and colour combination words (g_ps_TexBlendType, g_ps_TexBlendType2, g_ps_TexBlendType3,
g_ps_ColorBlendType, g_ps_BlendingMode, g_ps_FogMultMode, g_ps_bPreAlphaMult, g_flipbookBlendMode), the soft-edge
and fog switches and parameters, the tracer parameters (g_vTracerParam), the distortion wave parameters, the radial-blur
and light-shaft parameters, and the lighting switches. Observed · Sekiro The vertex programs share VSCommonBuffer
(720 bytes: view, projection and view-projection matrices, fog range, screen size, near and far distances, camera position,
near and far fade ranges, soft-sprite parameters, the irradiance volume matrices and parameters, the tile light parameters);
the domain programs DSCommonBuffer (736 bytes, the same kinds of values), the hull programs HSCommonBuffer (80 bytes), and
the lit variants PSEnvLightBuffer and VSDSEnvLightBuffer (576 bytes each). Observed · Sekiro
How the multi-texture draws compose their layers
Section titled “How the multi-texture draws compose their layers”A multi-texture billboard (action 604) draws three textures on one quad. The pixel
program samples each layer’s texture at its own coordinates (g_multiTexture0 at TEXCOORD0.xy, g_multiTexture1 at
TEXCOORD0.zw, g_multiTexture2 at TEXCOORD1.xy) and composes the three into one colour before the particle’s colour applies.
Observed · Elden Ring · the disassembly of the pixel program
Each texel’s alpha has TEXCOORD2.z added and is clamped to 0..1; the texel’s colour is then multiplied by its layer’s colour
(the first layer’s red, green and blue from TEXCOORD4, which has no alpha read; the second’s and third’s colour and alpha from
TEXCOORD5 and TEXCOORD6), and with g_ps_bPreAlphaMult above 0 also by the layer’s own opacity.
Observed · Elden Ring · the disassembly of the pixel program
The first layer gives the colour and the opacity. The second and third each change one of the two, chosen by
g_ps_TexBlendType, by an operation chosen by g_ps_TexBlendType2 (the second layer) and g_ps_TexBlendType3 (the third),
weighted by the layer’s own opacity w. With b the value the layer changes and l the layer’s colour (for the colour) or its
brightness, the mean of its red, green and blue clamped to 0..1 (for the opacity):
| Word | Value | Effect |
|---|---|---|
g_ps_TexBlendType |
0 | both layers change the colour |
| 1 | both layers change the opacity | |
| 2 | the second layer changes the opacity, the third the colour | |
g_ps_TexBlendType2, g_ps_TexBlendType3 |
0 | b * (1 + (l - 1) * w) (multiply) |
| 1 | b + l * w (add) |
|
| 2 | b + (overlay(b, l) - b) * w, where overlay(b, l) is 2 b l for b below 0.5 and 1 - 2 (1 - b) (1 - l) otherwise |
|
| any other | the layer changes nothing |
Running Elden Ring’s GXFfxtessellateMultiTexture pixel program (lifted from its module and executed on the CPU) with one
texel per layer, every value 0 to 2 of g_ps_TexBlendType, every value 0 to 3 of the two operation words, both premultiply
settings and eight sets of texels and layer colours gives this rule on all 768 pixels, to within 1e-4, with the particle
colour not applied (g_ps_ColorBlendType 1), unlit, unfogged and at an exposure of 1.
Proven · Elden Ring · that program, the inputs stated The other multi-texture pixel programs (the Soft, GlowD,
2Sampler, Bump and Blend variants) are not run. The program then clamps the opacity to 0..1 and the colour to at least 0.
Observed · Elden Ring · the disassembly of the pixel program
The particle’s colour (COLOR0) applies by g_ps_ColorBlendType: with 0 the composed colour is multiplied by its direction
n = COLOR0.rgb / |COLOR0.rgb|, squared and scaled by |COLOR0.rgb| and g_vColorScale.rgb; with 2 the composed colour
times its opacity is overlaid with n and then squared and scaled the same way; with any other value it does not apply. The
opacity is multiplied by COLOR0.a and g_vColorScale.a with 0 and 2. Observed · Elden Ring · the disassembly, not run
No step after a sample converts its colour (no power or curve applies to a texel), so the texels are used as the texture’s
view gives them. Observed · Elden Ring · the disassembly of GXFfxtessellateMultiTexture and GXFfxtessellateSoftMultiTextureGlowD
After fog (the colour times the fog’s transmittance, plus its in-scatter times g_ps_invPreExposure), the glow variant
GXFfxtessellateSoftMultiTextureGlowD multiplies the colour by g_ps_preExposure unless g_ps_BlendingMode is 4; with
g_ps_BlendingMode 1 or 2 an opacity above 1 multiplies the colour and is then clamped to 1; and with
g_transductionAlphaToColorScale s below 1 it keeps the colour up to 1 and scales what exceeds 1 by s², the opacity by
s. Its second target takes g_psGlowScale.rgb times the colour (with the same pre-exposure) and g_psGlowScale.a times the
opacity. Observed · Elden Ring · the disassembly, not run That second target is the frame’s
glow target. Observed · Elden Ring · one frame’s draws The frame’s pre-exposure is off in every recorded frame
(frame), so an effect’s colour lands in the frame’s own light units and the
scene exposure and tone curve apply to it as to the rest of the frame. Inferred · Elden Ring
In the effect files, the multi-texture billboard’s layer word (unk_ds3_f2_10 in the fxr project’s naming) holds 0, 1 or 2 in
all 13,852 of Elden Ring’s multi-texture billboards (1,375, 2,411 and 10,066), as do the two words after it (unk_ds3_f2_11
and unk_ds3_f2_12), and the fourth (unk_ds3_f2_13) holds 0 or 2 (13,429 and 423). Proven[every multi-texture billboard
of the install’s effect binders]{games=“elden-ring”} The fxr project documents the layer word’s three values as the table
above gives g_ps_TexBlendType’s. Sourced · fxr That the four words are g_ps_TexBlendType, g_ps_TexBlendType2,
g_ps_TexBlendType3 and g_ps_ColorBlendType follows from the documented first, the value sets and the order. Inferred
What the engine takes from the file
Section titled “What the engine takes from the file”A draw does not name its effect, but its textures and vertices do. A multi-texture draw binds the three layers’ textures
(g_multiTexture0 to 2), which the files’ layer1 to layer3 numbers name (sfx/tex/s%05d), and its vertices hold the layers’
colours as bytes and the flipbook’s cell size (the inverse of columns). The files whose multi-texture billboards have those textures
and that column count, and whose layer colours (a constant, or the range of a keyframed curve) contain the vertex’s, are the draw’s
candidates. In Sekiro’s Ashina frame (25 multi-texture draws) and three Elden Ring frames of Stormveil’s gate (21, 20 and 20 draws)
every draw has candidates, from one to several hundred; Elden Ring’s five draws of effect 810341 (2123 to 2126 and 2128 of one frame) and Sekiro’s two of effect 839520 are
tied to a single file. Observed · Sekiro, Elden Ring · four frame captures, the files of the common and area effect binders
Comparing each draw’s constants with its candidates:
| File field | Reaches | Evidence |
|---|---|---|
unk_ds3_f2_10 to _13 |
g_ps_TexBlendType, g_ps_TexBlendType2, g_ps_TexBlendType3, g_ps_ColorBlendType, as the same integers |
the constants equal the words of every candidate in 21 of Sekiro’s 25 draws and 19 of the 61 Elden Ring draw records (three frames), and of at least one candidate in all the others; no draw’s constants differ from all its candidates |
premultiplyAlpha |
g_ps_bPreAlphaMult (1 for true) |
every candidate agrees with the constant in all 25 Sekiro draws (true for texture 11634’s files, false for 11633’s) and in 34 of the 61 Elden Ring records, some candidate in the rest; no draw contradicts |
blendMode |
g_ps_BlendingMode and the pipeline’s blend: 6 gives 1 and source alpha against inverse source alpha, 4 and 7 give 2 and source alpha against one |
Elden Ring: the single file of draws 2123 to 2126 and 2128 (mode 6) has 1, the files of draws 2087 and 2088 (9 and 6 candidates, all mode 4) have 2; Sekiro: the three draws with 2 have layer-2 scale values 0.202 to 0.796 (66 values), which fit the one candidate of mode 7 (scale 0.5 varied by up to 60 %, so 0.2 to 0.8) and not the mode 6 candidates (0.625 varied by up to 60 %, so 0.25 to 1.0), whose draws have 1 and values 0.253 to 0.998 (147) |
rgbMultiplier, alphaMultiplier |
g_vColorScale = (rgbMultiplier × K, the same, the same, alphaMultiplier) |
Sekiro: 1.4 gives 1.58285, 1.0 gives 1.13061, 1.8 gives 2.0351, 1.2 gives 1.35673 and 1.5 gives 1.69591, with the alphas 0.3, 0.2, 1.0, 0.9 and 0.5 as stored; Elden Ring: 20 gives 1.4956 and 10 gives 0.7478, with the alphas 10, 5, 2.5 and 1.25 as stored |
lighting |
g_ps_bEnableLighting: -1 gives 0, -2 gives 1 |
Elden Ring: the 48 records with 0 have only files with -1 and the 13 with 1 only files with -2; Sekiro has ten unlit draws whose only candidates hold -2 (four of them draws of texture 1001’s files), so the field is not the whole rule there |
bloomColor |
g_psGlowScale, as stored |
Elden Ring: in a frame with a Great Glintstone Shard in flight, seven of the nine tracer draws on its tracers’ textures hold (0.5, 0.75, 1, 0.1), the bloomColor of 13 nodes of its flight effect 523022 (all with bloom set), to the bit; the other two hold (1, 0.75, 0.5, 0.1), which 523022 does not store |
depthBlend, bloom, interpolateFrames |
the program: Soft, GlowD and Blend |
Elden Ring: no Soft record has a candidate set that is all depthBlend false; the 13 records of GlowD programs whose candidates agree on bloom have true, and the 13 of the program without GlowD have false; all 18 Blend records have interpolateFrames true in every candidate, while the records of programs without Blend have it false (13), true (15) or both (15) |
Observed · Sekiro, Elden Ring · the table’s scopes: four frame captures, and one of the shard Only the first word takes several values in these frames (0, 1 and 2 in Elden Ring, 1 in Sekiro) and the other three are 0 in every draw, so the order of the four words is shown for the first alone and the other three are shown only at 0. A file that holds 2 in the fourth word is among the candidates of draws whose constant is 0: it is not the file those draws drew. Observed · Sekiro, Elden Ring
The factor K of g_vColorScale is 1 in every draw with g_ps_bEnableLighting 1 (the 34 lit effect draws of Sekiro’s frame, the 6
of one Elden Ring frame) and the same number in every draw without it within a frame, whatever its file: 1.13061 in Sekiro’s 50
unlit draws (ten different scales) and 0.07478 in Elden Ring’s frames (0.07476 to 0.07478; 42 of the 48 unlit draws of one frame are
a whole rgbMultiplier times it, the other 6 are 1.0). Observed · Sekiro, Elden Ring The engine therefore folds a
frame-wide factor into the multiplier of a draw whose pixel program does not light it; where the factor comes from is not read.
Inferred
How the layers’ coordinates are made
Section titled “How the layers’ coordinates are made”The vertex holds each layer’s offset and scale, and the domain program applies them to the corner of the sprite. The corner is the
tessellation domain (x, y) (the octagon variants first trim its corners), taken as c = (½ + x, ½ - y). From the control point’s
TEXCOORD0 to TEXCOORD4, the domain program of GXFfxtessellateSoftMultiTexture computes:
| Output | Value |
|---|---|
layer 1 (TEXCOORD0.xy) |
TEXCOORD1.zw × (c - k) + k + TEXCOORD0.xy, with k = TEXCOORD4.x |
layer 2 (TEXCOORD0.zw) |
(c - ½) × TEXCOORD2.xy + ½ + TEXCOORD0.zw |
layer 3 (TEXCOORD1.xy) |
(c - ½) × TEXCOORD2.zw + ½ + TEXCOORD1.xy |
the next flipbook cell (TEXCOORD1.zw) |
TEXCOORD1.zw × (c - k) + k + TEXCOORD3.xy |
Observed · Sekiro · the disassembly of the domain program, from the domain point to these outputs A layer’s scale is therefore about
the centre of the sprite’s coordinate square, and the flipbook cell applies to the first layer alone (with its next cell, for frame
interpolation): the second and third layers are not cells of the atlas. Observed · Sekiro In every vertex read TEXCOORD4.x
is 0, so k is 0; TEXCOORD1.zw is the inverse of the file’s columns (0.125 for 8), TEXCOORD0.xy is a cell’s corner (multiples
of 0.125) and TEXCOORD3.xy is the next cell’s (the cell index plus one, wrapping to the next row: (0.875, 0.625) then (0, 0.75) in
one Sekiro vertex). Observed · Sekiro, Elden Ring
The scale and the offset arrive per particle. In Elden Ring’s draw 2123 of effect 810341 (14 particles) TEXCOORD2 is exactly
(0.5, 0.5, 0.5, 0.5), the file’s constant layer-2 and layer-3 scale, while TEXCOORD0.zw and TEXCOORD1.xy take 0.08 to 1.07: the
file’s offset is 0.5 varied by up to 100 % (0 to 1), and its speeds are 0.06 and 0.03 for layer 2 and 0.03 and 0.015 for layer 3.
Observed · Elden Ring In Sekiro’s draws of the one mode 7 file TEXCOORD2.xy takes 0.202 to 0.796 over 33 particles, the file’s
scale of 0.5 varied by up to 60 %; those of the mode 6 files take 0.253 to 0.998 over 147, inside their 0.25 to 1.0. Observed · Sekiro
That a particle’s offset is the file’s offset plus its speed times its age is inferred from the excess over 1 (at most 0.07) being
less than a speed times a second or two; it is not read from the code. Inferred
The glow target
Section titled “The glow target”An effect node that blooms is drawn by a GlowD program, which writes a second target beside the colour: its final colour times
g_psGlowScale.rgb and its opacity times g_psGlowScale.a (see the layers).
Observed · Elden Ring · the disassembly of GXFfxtessellateTracerTextureGlowD and GXFfxtessellateSoftMultiTextureGlowD
One blend state applies to both targets: of the frame’s 72 GlowD draws, 45 add (source alpha and one) and 27 blend over
(source alpha and its inverse), on every target alike. Observed[the blend states of every GlowD draw of one
frame]{games=“elden-ring”} An additive draw thus adds its colour times its glow colour times the glow’s alpha to the glow
target, and a draw that does not glow leaves it as it is. Inferred · Elden Ring The target is R11G11B10_FLOAT at the frame’s size, cleared before the effects, and the
glow chain spreads it into the picture. Observed · Elden Ring
g_psGlowScale is the node’s bloom colour (bloom_color, four floats beside the bloom flag in the fxr project’s naming) as
stored, and the flag picks the program: Observed[one frame drawing effect 523022, its draws matched to its nodes by
texture]{games=“elden-ring”}
| Nodes of effect 523022 | Draws |
|---|---|
7 tracers, bloom true, colour (0.5, 0.75, 1, 0.1) |
7 GXFfxtessellateTracerTextureGlowD draws holding (0.5, 0.75, 1, 0.1) |
2 point sprites, bloom true, colour (0.5, 0.75, 1, 0.25) |
2 GXFfxtessellatePointSpriteGlowD draws holding (0.5, 0.75, 1, 0.25) |
1 billboard, bloom false, colour (0.5, 0.75, 1, 0.05) |
the frame’s one GXFfxtessellateSoftMultiTexture draw (no GlowD), its g_psGlowScale still the node’s colour |
The frame’s 72 GlowD draws hold eight distinct values, with alphas from 0.05 to 0.5. Observed · Elden Ring · one frame
How the distortion draws move the picture
Section titled “How the distortion draws move the picture”A distortion sheet (action 607) draws the picture already drawn behind it, sampled at
a moved point. Elden Ring’s three distortion pixel programs (GXFfxdistortion, GXFfxdistortionBump and
GXFfxdistortionRotation, each with a Soft variant) read the frame drawn so far (g_frameBufferTexture, through the clamping
linear sampler) and the depth (g_depthTexture, through the clamping point sampler). Observed[the disassembly of the pixel
programs]{games=“elden-ring”}
The move is a strength times a falloff from the sheet’s centre. With r the distance from the centre in the sheet’s own
coordinates (TEXCOORD0.xy, 0 to 1 across the sheet: r = |(|u - 0.5| * 2, |v - 0.5| * 2)|), the strength is
max(1 - g_clampDistanceInverse * r, 0) * 0.01 * g_waveStrength, in shares of the view: the radius at which it reaches 0 is the
inverse of g_clampDistanceInverse, 1 being the ellipse the sheet inscribes. Observed[the disassembly of the pixel
programs]{games=“elden-ring”} The three programs move the point differently:
| Program | The moved point |
|---|---|
GXFfxdistortion (stir) |
along the direction at the angle -TEXCOORD1.z - g_waveTime * TEXCOORD1.y + g_waveNum * r, by the strength; the vertex program sets TEXCOORD1.y to 1 or -1 and TEXCOORD1.z to 0 or pi together, by one condition |
GXFfxdistortionBump (push) |
by the strength times the normal texture’s xy * 2 - 1 (g_normalTexture, through the wrapping linear sampler), read at the sheet’s coordinates scrolled by g_waveTime times the two scroll distances, g_vNormalMapScrollDistance added to u and g_hNormalMapScrollDistance taken from v, and scaled about the centre by g_normalMapScrollScale |
GXFfxdistortionRotation (twist) |
turned about the sheet’s projected centre (TEXCOORD6) by the strength in radians, in the view’s coordinates (not its pixels, so the turn is not round on a view that is not square) |
Observed · Elden Ring · the disassembly of the pixel programs The point is the pixel’s own place in the view (from
TEXCOORD0.zw over TEXCOORD1.x, the clip position over its w) plus the move, scaled by g_dynamicResolution_UVScale and
clamped to g_resolveRectUV. Where the depth stored at the moved point is nearer than the sheet (TEXCOORD3.z), the program
samples the pixel’s own place instead, so a nearer surface is not pulled in behind the sheet. Observed[the disassembly of the
pixel programs]{games=“elden-ring”}
The sample is multiplied by g_ps_invPreExposure, then by the colour g_color (with g_ps_ColorBlendType 2 it is overlaid
with it instead) and by g_vColorScale; the opacity is g_color.a * g_vColorScale.a * TEXCOORD5, and 0 at or below a
cutoff constant. Observed · Elden Ring · the disassembly of the pixel programs
The distortion draws blend by their opacity: source colour times its alpha plus the target times one minus it, the target’s
alpha kept. The two distortion pixel programs the captures hold (GXFfxdistortionBump and GXFfxdistortionSoftBump) each
have one pipeline in a frame, and all 156 of their draws in two frame captures (82 and 74) carry that blend; the other
distortion programs are not drawn in them. Observed · Elden Ring · two frame captures of a running session
The frame each sheet moves is copied just before it: each distortion draw follows a PointSampleColor1 draw into a target of
its own, and the distortion draws come as one run after the other effect draws of the full-resolution effect pass, with no
other effect draw among them (21, 22 and 20 sheets in three frames of two sessions). Observed[three frames of two capture
sessions]{games=“elden-ring”} So each sheet moves the picture with every effect drawn before it, and the sheets drawn before
it, in it. Inferred · Elden Ring
The constants a sheet is drawn with
Section titled “The constants a sheet is drawn with”In one recorded frame of Stormveil Castle the 20 distortion draws (17 GXFfxdistortionBump, 3 GXFfxdistortionSoftBump, each
a quad of six vertices) carry these pixel constants: Observed · Elden Ring · the constant buffers of one frame capture
| Constant | GXFfxdistortionBump (17 draws) |
GXFfxdistortionSoftBump (3 draws) |
|---|---|---|
g_waveStrength |
10 in every draw | 0.296, 4.308 and 8.348: 10 times an opacity that rounds to the draw’s g_color.a (0.031, 0.431 and 0.835) |
g_clampDistanceInverse |
1 | 1 |
g_waveTime |
1 | 1 |
g_vNormalMapScrollDistance, g_hNormalMapScrollDistance |
different in each draw, 0.05 to 0.95 and 0.19 to 1.32 | 0 and 0 |
g_normalMapScrollScale |
0.5, 0.5 | 1, 1 |
g_color |
white, its alpha rising from 0.19 to 1 over four draws, 1 over the next nine and falling to 0.22 over the last four | white, the alphas above |
g_vColorScale |
1, 1, 1, 1 | 1, 1, 1, 1 |
Every g_color alpha in them is a whole number of 255ths. Observed · Elden Ring · the constant buffers of one frame capture
So the scrolls reach the program as distances already travelled, with g_waveTime left at 1, and each Bump sheet moves the
picture by the same strength while it fades in and out by its opacity alone. Inferred · Elden Ring
The executable writes one record per particle of a distortion sheet: it evaluates action 607’s properties at the particle,
packs color times the particle’s own colour into four bytes, and keeps the 4-component property after color, intensity,
the property after it and radius as the floats they evaluate to, intensity with no factor applied. Observed[the decompiled
per-particle writer of FXSingleAppearanceDistortion]{games=“elden-ring”} The bytes account for the 255ths of g_color. How
the record becomes the draw’s constants is not traced. Inferred · Elden Ring
Of the 1,348 distortion actions of Elden Ring’s effect files (each effect id once), 1,304 have mode 1 (the normal-map push)
and 44 mode 0 (the stir); none has another. 726 store intensity as a constant (most often 30, in 106; then 5, in 75;
1, in 68; 2.5, in 57; and 10, in 51) and 622 as a curve. radius is 1 in all but one (0.85), and normalMapScaleU and V
are both 1 in 985 and both 0.5 in 267.
Observed · Elden Ring · every distortion action the effect reader types in Elden Ring’s effect binders
All 20 sheets of the captured frames (2 in Sekiro, 6 in each of three Elden Ring frames) are drawn with one blend (source alpha
against inverse source alpha, alpha written as source one and destination zero), g_ps_BlendingMode 1 and g_ps_ColorBlendType 0.
Observed · Sekiro, Elden Ring The distortion files whose normal map is the bound texture (6000 in Sekiro, 60012 in Elden Ring)
number 27 and 217 actions in the binders read; 20 and 211 of them have blend mode 2 and 7 and 6 have 0. For 19 of the 20 sheets the pair of
g_waveStrength and g_color.a is reproduced, at one age along the files’ keyframes read as linear, only by mode 2 files; for the
20th (Sekiro, 0.5 and 1.0) by both. Observed · Sekiro, Elden Ring No sheet is tied to a file of another mode, so the captures
show the blend of mode 2 files only. A multi-texture billboard’s mode does change its pipeline’s blend (above); whether a distortion’s
does needs a frame that draws a sheet of a mode 0 file (effect 30050 or 613215 in Sekiro, 523804 in Elden Ring). Unknown
g_waveStrength is the file’s intensity at the particle’s age, not multiplied by the colour’s alpha. In both Sekiro sheets it is 0.5, the
constant intensity of three candidate files, while g_color.a is 0.714 and 1.0. Observed · Sekiro In Elden Ring the sheets
come in threes (each drawn twice) of steps of about 4.03 in strength (1.667, 5.70, 9.733; 2.70, 6.733, 10.0; 2.0, 6.033, 10.0), with the
alphas 0.169, 0.569, 0.973; 0.271, 0.675, 0.616; 0.2, 0.604, 0.965. The third sheets of the last two have strength exactly 10.0 with alpha
0.616 and 0.965, so the strength saturates at 10 while the alpha does not: a product of a constant 10 and the alpha would give 6.2 and
9.65, and the earlier fit of 10 times the alpha (above) was two curves rising together. Observed · Elden Ring The file that drew
them is not identified. Observed · Elden Ring
The two scroll distances are, by their values, the offset plus speed times age of the normal map’s U axis (g_vNormalMapScrollDistance) and V axis (g_hNormalMapScrollDistance), named across: in
both Sekiro sheets the candidates scroll along V alone (speed 0.25, U speed 0), and the distance g_vNormalMapScrollDistance is 0.6185
and 0.6278, inside the 0 to 1 of the random offset, while g_hNormalMapScrollDistance is 0.8765 and 14.79, which only a V speed can reach.
Inferred · Sekiro All Elden Ring candidates have offset 0 and speed 0, matching the 0 and 0 captured, which tests nothing.
g_clampDistanceInverse is 1 and radius 1 in every candidate but one, g_normalMapScrollScale is (1, 1) and normalMapScaleU and V
1 in all, and g_waveTime is 1 and g_waveNum 0 in every sheet: the captured sheets are all of mode 1 (the normal-map push), so
what the stir (mode 0) sets for g_waveTime, g_waveNum and the strength is not captured, nor whether g_clampDistanceInverse is
the inverse of radius. Unknown
Elden Ring’s bundle
Section titled “Elden Ring’s bundle”/shader/gxffxshader.shaderbnd.dcx holds 488 members: 279 pixel, 77 compute, 41 vertex, 39 domain, 27 geometry and 25 hull
programs. Observed · Elden Ring Every member is a container of the DXBC wrapper that holds a Shader Model 6 module (a DXIL
part, a root signature, pipeline-state and signature parts), not Shader Model 5 code, and none has a reflection part.
Observed · Elden Ring · every member’s part list The module itself still names its constant buffers’ variables in its
type annotations: GXFfxtessellateMultiTexture.ppo names every field of its PSCommonBuffer (832 bytes, g_color at 0 to
g_dynamicResolution_UVScale at 816) and of PSEnvLightBuffer, with the same names as Sekiro’s.
Observed · Elden Ring · that program’s module 381 of
the 488 member names occur in Sekiro’s bundle, 107 are new and 18 of Sekiro’s are absent (the LightShaft programs, the
DistortionTracer and SoftTracer variants of the tessellated family and the seven SparkParticle level-of-detail programs).
Observed The new ones are the GPU billboard programs (CS_GpuBillboard, CS_GpuBillboard_Emit, the CS_GpuClearEmitCounter and
CS_GpuCopyEmitCounter families, GS_GpuBillboard), the particle correction programs (CS_VFX_StandardParticleCorrect* in
variants for collision, force, a sway called Yurayura and splitting, CS_VFX_SparkParticleCorrect), the Specular and
GlowD pixel variants of the bump-mapped programs, the Blend*BumpMultiTexture* programs and two Rotation distortion vertex
programs. Observed · by the names The register classes the programs bind agree with Sekiro’s: pixel programs use the constant
buffers at registers 0 (all 279) and 7 (225), shader resources at 0 to 6, 16 (252) and 32 (94), samplers at 7, 8, 9 and 12; the
vertex, domain and geometry programs read the structured buffers at registers 8 to 15 and 59 to 62; the geometry and domain
programs the constant buffers at 0, 1 and 10. Observed · Elden Ring · the resource ranges of each program’s module
Where a billboard’s corners are placed
Section titled “Where a billboard’s corners are placed”The 12 geometry programs of the textured billboards (GXFfxtessellate with Texture, SoftTexture, BumpTexture,
SoftBumpTexture and their AboveSdw forms, and the four MultiTexture ones) each take one point per particle and read
g_CroppingTexture. Each reads the x, y and w lanes of three coordinates of the point: TEXCOORD3 to TEXCOORD5 in
the single-texture programs, TEXCOORD6 to TEXCOORD8 in the multi-texture ones. The bump-mapped programs read the z
lanes as well. Observed · Elden Ring · the input loads of every geometry program in the bundle In
GXFfxtessellateMultiTexture the three coordinates are the rows of a 3×4 matrix. Each corner’s world position is that
matrix applied to (u − ½, v − ½, 0, 1), where (u, v) is a corner of the cropping polygon read from g_CroppingTexture. The
position then goes through g_ds_viewMatrix and g_ds_projectionMatrix of DSCommonBuffer (constant buffer 0), and no
other transform applies. Observed · Elden Ring · that program’s module The program’s vertex program passes all 14
inputs through unchanged and writes no position. Observed · Elden Ring · GXFfxtessellateMultiTexture.vpo So a
billboard’s plane and turn in the world are not made on the GPU: they arrive with each particle in the vertex buffer, with
the quad’s two edges as the matrix’s first two columns and its centre as the last. Inferred[from the loads; the other
eleven programs were checked for the same lanes, not read through] The z lanes of the bump-mapped programs are the
quad’s third axis, for the lighting. Inferred · by the lanes alone
What the engine binds for an effect draw
Section titled “What the engine binds for an effect draw”A session of Sekiro with a capture of the Direct3D 11 calls (five captures of two frames each) recorded 904 draws and 9 dispatches of programs of the bundle, with what each had bound at the call: the constant buffers and structured buffers of every stage, the vertex and index buffers, the input layout, the topology and the blend, depth-stencil and rasterizer states. Observed · Sekiro
Geometry. The draws that shade an effect’s quads are patch lists of one control point per particle (topology 33): 632 draws
of the GXFfxtessellateTexture vertex program with a 112-byte vertex, 242 of the GXFfxtessellateSoftMultiTexture program
with 160 bytes and 8 of GXFfxtessellatePointSprite with 36, each with a hull and a domain program (the _quad, _octed and
_quincunx variants) and a pixel program (Blend, Soft, GlowD, 2Sampler variants). The distortion draws (22,
GXFfxdistortionSoft) are triangle lists of 20-byte vertices (a position of three floats and a texture coordinate of two).
Observed · Sekiro A particle is therefore one vertex, and the quad is built by the tessellation stages, not by the CPU.
Inferred The 112-byte vertex of the plain texture draw holds:
| Bytes | Semantic, format | Value |
|---|---|---|
| 0 | POSITION0, 3 floats |
zero in the vertices read; the particle’s place is in the matrix rows below |
| 12 | COLOR0, 4 bytes (unorm) |
the particle’s colour packed red, green, blue, alpha from the lowest byte, the packed colour that the engine keeps in each billboard particle’s record (the appearance) |
| 16 | COLOR1, 4 floats |
a colour multiplier: 1, 1, 1, 1 in the vertices read, 1.5 in others |
| 32 | TEXCOORD0, 4 floats |
texture coordinates (0.5625, 0.1875, 0.6875, 0.1875 in the first vertex read), probably the sprite’s flipbook cell |
| 48 | TEXCOORD1, 4 unorm 16-bit |
four fractions (0.125, 0.125, 0.96, 0 in the first vertex read) |
| 56 | TEXCOORD2, 2 floats |
two values from 0 to 1 |
| 64, 80, 96 | TEXCOORD3 to TEXCOORD5, 4 floats each |
three rows of a 3 by 4 matrix: the columns of the first three entries are three axes of length 0.315, 0.315 and 1.0, and the last entries are a position (-170.6, -25.2, 172.5 beside a camera at -206.8, -24.9, 177.3) |
Observed · Sekiro · the first 4 vertices of one draw and the same fields of others That the matrix rows are the sprite’s world transform (two scaled axes and the facing axis) follows from those values. Inferred The multi-texture vertex of 160 bytes has a position of four floats, four packed colours, a colour of four floats, three texture coordinates of four floats, one of three, a pair of 16-bit fractions and the same three matrix rows at 112, 128 and 144. Observed
Constants. The domain, hull and pixel programs read the camera from their common buffers: the view matrix (rotation in the
first three columns, translation in the fourth), the projection, the view-projection, the camera position and up vector, the near
distance 0.08, the far distance 10,000 and the screen size 1,920 by 1,080. Observed Of the PSCommonBuffer variables of the 904
draws, g_ps_BlendingMode is 1 in 642 and 2 in 262, g_ps_FogMultMode is 1 in the same 262 and 0 in the rest,
g_ps_TexBlendType 1 in 246, g_ps_bPreAlphaMult 1 in 206, g_ps_bEnableLighting 1 in 374 and g_ps_bEnableFog 1 in 470 (the
integers are stored as integers in the floats’ bytes); g_vColorScale has 128 distinct values, (15, 15, 15, 5) in 197 draws,
(3.75, 3.75, 3.75, 1) in 88 and (1, 1, 1, 1) in 26; g_ps_softSpriteScaleOffset has the scale 1 (547 draws), 0.25 (234), 0.5 (108)
or 0.1 (15) and a zero offset; the wave, radial-blur, light-shaft and tracer variables are zero in every draw (no draw was of
those programs). Observed · Sekiro The light lists at registers 59 to 62 are bound in the vertex, domain and pixel
stages. Observed
State. Every draw tests depth with GREATER_EQUAL (the depth is stored reversed) and writes none, culls nothing and fills
solid. The colour blends source alpha against inverse source alpha (668 draws) or against one (236); the alpha channel is
written with source one and destination zero (550 draws), source zero and destination inverse source alpha (248) or source zero
and destination one (106). The plain draws (574) go to the
1,920 by 1,080 scene target (an 11-bit-float colour format) with eight render targets bound; the soft and multi-texture draws
(330) to a 960 by 540 target of 16-bit floats with two targets bound. Observed · Sekiro · the recorded states of all 904 draws
The nine dispatches are all of CS_VFX_StandardParticleEmit. Observed
The particles on the GPU
Section titled “The particles on the GPU”The GPU particle programs read their parameters from larger buffers: gcbSceneParam (880 bytes: base colour, near and far,
blend parameters, a billboard matrix, view and projection matrices, the previous frame’s matrix for motion blur, trace
parameters), ccbSceneParam (752 bytes) and ccbEmitionSeed (960 bytes: the emitter’s initial values and random ranges for
position, move vector, rotation, scale and colour, its lifetime and its placement) for the emit and update programs, and
ccbForceFiledParam (3,616 bytes) for the force fields. Observed · Sekiro A particle is one element of a structured
buffer, 176 bytes in the standard and 336 bytes in the spark programs. Observed · Sekiro
In a frame at the Ashina gate a compute dispatch writes 176-byte records into a list, each thread taking its slot with an atomic add, and an indirect instanced draw then draws from that list, blended additively into the 11-bit-float scene target. Run again on the frame’s recorded inputs, the dispatch writes the same records in another order. Observed[one captured frame, re-runs of the dispatch]{games=“sekiro”} The record size is the standard particle’s, so this is the list of particles to draw, whose order changes from run to run; added in another order, a pixel of the scene target can round differently. Inferred · Sekiro
For the multi-texture draws the file fields that reach the pixel constants and the vertex are listed above (the four texture and colour
words, premultiplyAlpha, blendMode, the two multipliers with a frame-wide factor, lighting, and the layers’ colours, offsets, scales
and flipbook cells); the second colour COLOR4 (four floats) lies inside the range of the file’s color3 in all 61 Elden Ring records,
and its range of color2 in 34 of them, and the single-texture vertex’s COLOR1 (four floats at byte 16) inside color3’s range
in all 59 Sekiro draws against color2‘s in 40. Observed · Sekiro, Elden Ring The Elden Ring vertex of these draws is 148 bytes
(COLOR0 to COLOR3 bytes at 0, COLOR4 at 16, TEXCOORD0 to TEXCOORD2 at 32, 48 and 64, TEXCOORD3 at 80 as three floats,
TEXCOORD4 at 92 as two 16-bit fractions, TEXCOORD5 at 96 as one float, TEXCOORD6 to TEXCOORD8 at 100, 116 and 132); Sekiro’s is
160 with a POSITION of four floats first. Observed · input layouts of the captured draws
Which program a draw selects from the other flags, the single-texture draws’ fields, the source of the frame-wide factor, the float
at TEXCOORD5 (0.17 to 0.23 in some sheets, 12 to 31 in others) and POSITION.w (21 to 27), the contents of the 176 and 336-byte
particle records, and what the second render target holds are not recorded. Unknown · Sekiro
The per-particle state the billboard appearance keeps before it draws is on Particle appearances in the executable.