- DS3 is a deferred renderer on DirectX 11: a six-target G-buffer, one draw per light volume (~210 in the courtyard), a fog composite, forward passes for water and effects, then Silicon Studio's YEBIS 2 post chain and DLAA.
- Its world light is mostly baked: lightmaps with a dominant direction on map pieces, SH probes for characters, cubemap probes in blended boxes for reflections. The sun's real-time shadow holds only characters and objects.
- Visibility is authored (draw groups), then each instance and mesh is culled by its oriented box on the CPU, and repeated models are instanced. LODs switch at twice LodParam's distances.
- Our engine follows all of it. Against capture images from the same cameras, mean levels agree to within a few points: the courtyard's sky 200 against DS3's 199.
1DS3's frame, pass by pass
Five frames of Firelink were captured at 1920 × 1080 with RenderDoc, through Proton and DXVK. DXVK translates DS3's D3D11 into Vulkan, so the capture shows DS3's own passes, targets and draws capture. The shrine's courtyard at maximum settings takes 108 passes, 741 draws, 3.11 M triangles:
At low settings (an AMD laptop iGPU) it's 46 passes and 734 draws. The shadow map, the reflection, the G-buffer, the lights and the tessellated water all remain; most of the post-processing goes. Each draw is labelled with DS3's own shader name, because DXVK names a shader by the first seven hex digits of its DXBC checksum, which our capture tools match against the game's shader files.
Our Vulkan renderer runs the same sequence. Our own addition is a depth pre-pass for nearby opaque map pieces (DS3 has none): it took the spawn view from 17 to 25 fps on a laptop iGPU, and it's marked as ours.
2What gets drawn: groups, boxes, LODs, instances
One of our tools matches every draw of a capture to a FLVER face set (one model's group of triangles) (DS3 uploads a model's whole FLVER data block as its index buffer, so a draw's first index is a face set's byte offset). Then the engine renders the same camera and compares, piece by piece:
| View (capture) | DS3 | Ours, spheres | Ours, boxes | + per-mesh boxes |
|---|---|---|---|---|
| Shrine courtyard (max) | 2.88 M | 3.86 M | 2.92 M | 2.82 M |
| Cemetery path (max) | 5.88 M | 6.10 M | 6.02 M | |
| Cliff view (max) | 5.56 M | 5.57 M | 5.54 M | |
| Start path (low) | 4.30 M | 4.40 M | 4.09 M |
- Oriented-box culling per instance ("Enable OBB PrimCulling", renderer settings
+0x4e28) and per mesh ("Enable Primitive Culling",+0x4e27) exe. Bounding spheres let 0.74 M triangles behind the camera through. - Instancing: the
_Insvertex shaders addSV_InstanceIDto an offset and index 17-row instance blocks (world matrix, SH, light mask, lightmap UV). The pixel shader gets the index inNORMAL.w. We do the same atgl_InstanceIndex: map draws at the spawn went 2,319 → 341, and passing the record by index instead of as varyings saved 1 ms. - LODs: measured in all five captures, DS3 switches at 2 × LodParam's distances (LOD1 at 30 m, not 15), from the camera to the part's box. Where the 2 comes from isn't found. With it, 159 of 163 models pick DS3's LOD.

3Materials
A FLVER material names an MTD (512 of them, in allmaterialbnd) and lists textures, each with a slot, a path and a
UV scale, plus a GX list of parameter blocks. The MTD gives the shader, blend mode, alpha-test cutoff and which UV set each slot reads.
The shaders themselves are in shaderbdle.shaderbdlebnd as DXBC (_Gbuf, _Fwd, _Dpt, _Ins variants),
with a .metaparam mapping GXMD ids to constants. So our shader reader can read DS3's shading rather than guess it exe.
Multi-blend terrain
MultiBlendN_et1_lightmap, the ground and big walls, blends up to four layers of albedo, normal and reflectance by vertex weights.
An edge mask eats into each weight, and the vertex alpha sharpens it:w = sat((1 + a·f1)(b − m(1 − b))), m = (edge + 0.01)·f0.
Before we read it, the ground drew one layer, with UVs 2048× too big.
Two-layer masked (_l_m)
The courtyard floor: a second texture set over the first by a mask, smoothstepped. First read from DXVK's SPIR-V translation in the capture, then checked against the DXBC.
Texture scrolling
Every scroll is added before the texture's scale: uv = (uv + speed·t)·scale, with FC_GlobalTime in seconds. We had it after, so the sky drifted 4× too slowly.
UV sets
A FLVER holds one UV set per distinct UV number its MTD uses, in order. _l_m uses 1-4, so its lightmap is the third set. Taking the second drew stripes across the courtyard floor.
4The G-buffer
What the material _Gbuf shaders write, and who reads it exe capture:
| Target | Format | Holds |
|---|---|---|
| 0 | A2B10G10R10 | World normal × 0.5 + 0.5 |
| 1 | RGBA8 | x gloss (power 213x), y translucency, z the light mask (how much sun reaches here) |
| 2 | RGBA8 | Albedo; w a per-instance value (the glow) |
| 3 | RGBA8 | √F0 (specular colour); w picks the pixel's light set and fog from per-frame tables |
| 4 | B10G11R11F | The baked ambient × (1 − F0): lightmap or SH probe. The light sum starts from it. |
| 5 | RGBA16F | Not written by the material shaders read |
Our engine writes the same six (albedo in sRGB, since DS3's 8-bit linear albedo bands the darks) and has a debug view for each one. All from one camera on the Cemetery path:












5Baked light
Lightmaps: light and its direction
gi_env_m40.tpfbhd holds 1,176 BC7 textures in pairs: _dol_00 (light, alpha = the light mask) and _dol_01
(direction, alpha = sharpness). A model placed once has its own pair. A model placed many times (m000320: 1,678 times) gets a rectangle of
an atlas, with each part's UV scale and offset in the .btab. Each light texture carries a float block, its maximum per channel: the HDR scale.

dol_00: the light, stored as √ (squared in the shader)

dol_01: the dominant direction, in the map's frameL = dol_00.rgb² × FC_TexHdrScaleParam × FC_DominantRgbScale
d = rotate(normalize(dol_01.rgb × 2 − 1), q) q = 28° about the vertical, the same on every m40 piece
s = sat(dol_01.a + Offset) Offset = 1 in m40: fully directional
ambient = (1 − F0)·(L·ColorScale·s·(1 − F0)·sat(n·d) + L·ColorScale·(1 − s))
Read from MultiBlend4_et1_lightmap_Gbuf and the captured constants. Before the capture, our engine had three wrong guesses
that cancelled each other out: × π, offset 0, directions in model space. With DS3's values the courtyard's walls and ground match DS3's to a few levels.
Characters: SH probes and the light mask
m40_0000.btpb holds 9 grids of 144,974 probes 2 m apart: first-order SH (L0 + 3 L1 per colour, i16 with 1024 = 1) and a light mask each.
The capture shows what the CPU uploads per instance: the nearest probe × the SH basis constants Y₀₀ = 0.2821 and Y₁ = 0.4886, with L1 turned by
the collision parts' 28°. Taking the raw coefficients made our characters 3.5× too bright. Only 15 % of m40's ground-level probes see the sun at all.
Reflections: cubemaps in boxes
m40_envmap.tpf: 14 BC6H cubemaps, 128², 8 mips. The 31 EnvironmentMapEffectBox regions are drawn one by one. Each fades its probe in by
the pixel's distance to the box's nearest face (GXEnvMapRegion), with Fresnel F0 + (1 − F0)(1 − n·v)⁵·gloss² and mip
min(6.5(1 − gloss), 6). They give map pieces only reflections; the ambient is the lightmaps'. Before, our engine lit everything with the probe nearest the camera,
so the whole frame dimmed as you crossed between probes.
6Lights: draw params, the sun, lamps and torches
Each area's look is a gparam (param/drawparam/m40_00_0000.gparam): groups of params whose values are tagged by variant id.
MSB parts pick variants. m40's light sets include 0 "MAP main", 30 "grass dark", 40 "MAP darkness" and 300 "distant mountains", and
every collision uses 100 "character main", so characters are lit by that set.
Colours are gamma 2
The light-set builder (0x14014a2d0) makes every colour rgb² × w exe. Set 100's sun (0.78, 0.85, 1.0) becomes (0.61, 0.72, 1.0): bluer than it reads. With a strong blue fill, that's why Firelink looks cold.
The sun is turned 28°
Every gparam light reaches the shaders turned 28° about the vertical. 28° is also the yaw of m40's collision parts and the lightmaps' frame capture. Our first guess put the sun 152° off, so the shrine's doors were sunlit where DS3 has them in shade.
The sun (GXLightAcc_DDirLight1): diffuse (1 − F0)·sat(n·l) × the light mask × the shadow; specular
F(h·v)·pow(n·h, P')·(P' + 2)/8·sat(n·l)/max(n·v, n·l), with the gloss power capped per light, P' = max(P·k/(0.01P + k), 1).
Point lights come from the map's .btl (38 points, 6 spots in m40) and from effects (210 candle flames, 10 torches).
Most of DS3's point draws use GXLightAcc_LegacyDPointLight1: falloff (1 − d/r)² and a Phong specular. Spots add a cone
max(1 − |xy/w|², 0). The .btl's 25° turns out to be the full cone angle. Ours: each light in view is drawn over the screen rectangle its sphere covers,
and 137 lights inside the shrine cost ~1.2 ms.

o004665): its effect's point light (colour² × alpha × 10) falls on the arch.
7Shadows and SSAO
The sun's shadow
No map piece is drawn into the shadow map, in any of the five captures. The map's sun shade is baked (the light mask).
What casts is what the MSB marks ShadowSource: every player and enemy, 81 objects, 11 map pieces. Four cascades split at 2, 8 and 15 m by view depth;
a soft filter (4 blocker taps, then 16 Poisson comparisons); a 12-tap depth-aware blur; a fade from 65 m.
The sun's shadow
The same chain with DS3's constants (shadow 1.4 ms, map 0.8 ms on the laptop). A hash stands in for DS3's dither and rotation textures.
Casters don't receive (the knight went black otherwise) guess. The 157 pieces with ShadowDest off aren't shaded.
SSAO (ScreenSpaceAO2): at half size, 8 pairs of samples turned per pixel, scaled by 10 / depth, faded out from 100 m; then
reprojected with 70 % of last frame's, and bilaterally upscaled. It's a small contact shadow of at most 6 half-size pixels.
Point and spot shadows: none in m40, though DS3 has the machinery. Two spots are marked shadowed and DS3 runs their shadowed shaders, but they
sample a 4096² atlas that's cleared every frame and never drawn into. Most likely no part sets PointLightShadowSource. Our engine draws none either.
8Fog
f = sat(a − a·2^(−(d − start)·falloff)) d: the HORIZONTAL distance from the camera
colour → fogColour·(1 + contrast·(lum − 1)) lum of the unfogged pixel (0.29891, 0.58661, 0.11448)
then a height fog the same way
m40's main fog: a = 0.26, start 15 m, falloff 1/60, colour² (0.17, 0.21, 0.27), contrast 0.35 exe. Before this was read, we used the colour unsquared and a base-e exponential, and the distance was milky: 98 against DS3's 81 at the spawn. It's 80 now.
9The sky
m009000 is three meshes ~9 km out: an opaque horizon band, a translucent copy faded by vertex alpha, and an overhead dome.
The dome blends tiling clouds with a top-down sky disk (the sun's in it) through two masks that scroll at different speeds,
so the clouds open and close as they drift. It's all multiplied by 3 (a GXMD colour power) and fogged.


sky_53_a, tiled 4.1 × 3.6
sky_54_a, planar over the dome
sky_51_m, 3× as fast
sky_52_m, with the cloudsm = (sky_52_m + sky_51_m + 0.01) × 1.5
t = sat((1 + 2.5 × vertexAlpha) × (w − m(1 − w))) w: the vertex's blend weight
colour = lerp(clouds, skyDisk, t) × 3, then fog

The layer blend, scrolls, ×3 and fog, all from the dome's shader and the capture's constants.
10Water
The water shader (GXFlver_WaterBumpTri: _WatDpt.ppo and _FwdSdw.ppo in gxflvershader.shaderbnd) was read from DXVK's SPIR-V in the captures, then checked against its DXBC in the game files exe. It draws in
two alpha-blended passes over the composited scene, neither writing depth:
- Absorption: the absorption colour (dark teal) at alpha
1 − 1/(1 + 6·d), d being the depth in metres of water seen through the pixel: 10 cm is 38 % tinted, 1 m 86 %. (We had 0.9 until the DXBC showed which row the rate comes from.) - Surface, over a copy of the tinted scene: three normal maps (10 m, 2 m and 200 m patterns drifting 5, 1 and 15 cm/s in different directions), Schlick Fresnel with F0 0.1, the planar reflection and the refracted copy both shifted by the wave tilt, sun and point-light specular, fog, then faded into the unshifted copy by depth, so shallow water seen from above shows its bed.
The reflection is a half-size pass that mirrors only what's marked for it: characters, and bands of unlit cards with 128×64 panoramas
(m200000, m200010) standing in for the terrain. The plane is the collision part's ReflectPlaneHeight.


The cards are never seen directly: DS3 draws them only from the mirrored camera. Drawn in the main view instead (with a debug option of ours, everything else hidden),
the stream's set (m200000, 849 vertices) is a band of upright panels standing a few metres back along both banks, where the slopes and trees are. Each panel is
painted with a photo-like panorama of what stands behind it, sky above. Mirrored in the water plane, the band lands where the upside-down banks should be, so the
water seems to reflect the terrain without the terrain being drawn twice data.
The panoramas are pre-rendered: ordinary compressed textures shipped with the map, never redrawn while the game runs, so only the characters and the few objects drawn
on top of them move in the water (how they were made: How it was probably made).



m40_00_reflection_startB_001, 003-008; 002 is unused): 128 × 64 BC1 each, shown 4× here. The M[A]_NoLight material draws them unlit, so the lighting is baked in.Ripples
Where you wade, rings spread through the reflection. They aren't drawn on the screen. Four captures in the stream, one standing still and three walking, showed a pass that appears
only when walking: a 960 × 540 height map, cleared to 128/255 each frame, into which the footstep's three ring particles are added, just before the water draws
capture. An emitter goes there when its BillboardEx fields2[1] is 1: only 133 of DS3's 4,115 billboards, every one a water ring or sheet
data. The surface pass takes the map's slope from three taps, turns it into world space along the water, flattens the waves by
(1 − a)² where the water is raised, and adds the slope × 4: the reflection and refraction then bend round each ring.

DS3's passes, scroll rows and constants (GX07 rearranged into cbMatDynParam rows 17-23).
The ripple map is DS3's, from the walking captures.
Open: our pool was brighter than DS3's (92 against 80, measured before the absorption rate was fixed; not yet measured again), and only replaying the capture on its NVIDIA machine would show what DS3's reflection holds.
The ripples' strength is by eye: the ripple draws' constants weren't read.
11Post-processing: YEBIS 2, DLAA and the glow
DS3's post chain is Silicon Studio's YEBIS 2. Its shaders aren't in the game files: they're 3,534 DXBC blobs inside the exe, all sharing
one generic $Globals cbuffer, so the meaning lives in CPU code exe.
Tone map
From the final pass and its captured constants:x = colour × exposure × vignettey = sat((1 − e)(1 − k1·e)²), e = exp(−k0·x), k0 2.11, k1 0.38
then the glare added as a screen blend, saturation 0.8 about Rec. 709 luminance, gamma 1/2.2, dither, then a 16³ colour LUT.
Faithful
Bloom
A bright pass c/m·(x + √(x² + Rx + 1) − 1), then 5 levels of halving and 15-tap blurs, upsampled, summed, added after the curve. The scales aren't found in the captures. Ours: the same shape, our weights.
Stand-in
Light shafts
gparam LightShaft[Yebis] confirms the sun direction (within 3°). Which passes really are the shafts isn't settled (they may be star/ghost glare). Ours is a guessed radial blur.
Stand-in
Depth of field
mapbase.gparam: far blur from 50 m to 10 km, the circle of confusion from a depth LUT built on the CPU (not found). That's why distant things look soft. Ours: half-size Gaussian mixed in linearly by depth.
Close
DLAA
Not FXAA: DLAA.ppo, Andreev's directionally localised AA, on the gamma image. Short edges blur across, long edges move by an 8-tap mean, and pixels darker than 0.1 are left alone. Ported line for line with the captured constants.
Faithful
Emissive glow
FromSoft's own after the AA: rgb × alpha into a 4-level 13-tap pyramid, added back before the HUD. Which instances glow isn't read.
Not done

12What it costs
Per-pass GPU timestamps at the spawn, headless 1600 × 900 on the original machine's integrated AMD GPU (2026-10-13, before later work):
~40 ms total. The frame is bound by triangles and pixels, not draws: instancing cut map draws by 85 % without changing the GPU time. DS3 draws ~3-3.7 M triangles in 740 draws for these views; we draw a similar count.