The DS3 Codex
Chapter IV

The renderer

How DS3 draws a frame, read from its compiled shaders and from RenderDoc captures of the running game, and how our raw-Vulkan renderer copies it, pass for pass and constant for constant.

The light of Firelink is cold, and that is no accident: every lamp's colour is squared, every sun turned twenty-eight degrees, and the clouds are three times brighter than they were painted.Codex · The Lamp of the World

In short
  • 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:

1
Sun shadow map · 4096² D16, 28 draws: characters and objects only, four cascades in a 2×2 atlas
2
Planar reflection · 960 × 540 RGBA16F, 9 draws: characters and stand-in cards, for the water
3
G-buffer · six colour targets + D32S8 depth, 224 draws carrying 2.96 M triangles; 35 instanced draws carry 573 instances
4
Screen space · half and quarter depth, SSAO with history, the sun's shadow per pixel, its blur, the "shadow texture"
5
Lights · probe boxes ×5, deferred AO, the directional light, then 213 point-light draws, one per light volume, into diffuse and specular sums
6
Composite · albedo × diffuse + specular + emissive, then fog; then a depth-only pass with 21 occlusion queries (not map culling)
7
Forward · blended geometry, 32 tessellated draws (the water), effects
8
YEBIS 2 post · ~60 small passes: bloom pyramid down to 37 × 31, depth of field at 1176 × 441, tone map, 3D LUT
9
DLAA, glow, HUD · FromSoft's own anti-aliasing and emissive glow, then the menus

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.

Close

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)DS3Ours, spheresOurs, boxes+ per-mesh boxes
Shrine courtyard (max)2.88 M3.86 M2.92 M2.82 M
Cemetery path (max)5.88 M6.10 M6.02 M
Cliff view (max)5.56 M5.57 M5.54 M
Start path (low)4.30 M4.40 M4.09 M
Wireframe view
Our engine's wireframe view over the scene, from the same camera as the G-buffer views below.

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:

TargetFormatHolds
0A2B10G10R10World normal × 0.5 + 0.5
1RGBA8x gloss (power 213x), y translucency, z the light mask (how much sun reaches here)
2RGBA8Albedo; w a per-instance value (the glow)
3RGBA8√F0 (specular colour); w picks the pixel's light set and fog from per-frame tables
4B10G11R11FThe baked ambient × (1 − F0): lightmap or SH probe. The light sum starts from it.
5RGBA16FNot 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.

L = 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))
Faithful

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.

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.

m = (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 sky over the Cemetery of Ash
The sky in our engine. At the capture's camera the courtyard's sky measures 200 against DS3's 199 (it was 142 while the blend was guessed).
Faithful

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:

  1. 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.)
  2. 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).

The seven panoramas
The panoramas (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.

Ripples round the knight's feet
Ripples round the knight's feet in our engine: the rings exist only in the ripple map.
Close

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 × vignette
y = 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

Firelink in the distance
Distance: fog (squared colour, horizontal distance), the far depth of field from 50 m, bloom and the tone curve together.

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):

pre-pass 6.9
G-buffer 17.9
lights 3.8
shadows+SSAO 5.5
rest ~6

~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.