BTL light banks
BTL banks store named point, spot and directional lights, with separate diffuse and specular colours and powers. Sekiro’s 18 banks (735 lights) all use version 16 and 107 of Elden Ring’s 109 banks version 18; both use 232-byte records. The other two Elden Ring banks store version 15 with a 240-byte record (see the open question below). Point and spot lights also appear inside effect files; see FXR effect files. Proven · Sekiro · all 18 Sekiro banks Proven · Elden Ring · 107 version 18 banks, 2 version 15
The whole-install Elden Ring census covers 109 BTL banks, 11,229 lights: 107 version 18 banks hold 11,228 lights and the two version 15 banks one light. A strict reader that accounts for the complete bank length, the 240-byte record included, reads all 109. For the 107 version 18 banks every bank’s version and every light’s name, type, colours, powers, shadow flag, cone, position, rotation and radius agree with SoulsFormatsNEXT, with floats compared as bits; that reader lists versions 1, 2, 5, 6, 16 and 18 only and refuses the two version 15 banks by their version. Proven · Elden Ring · 109 of 109 read; 107 compared
Little-endian. A name block of the stated length follows, then light count times 232 bytes through the file end.
| Offset | Size | Type | Field | Confidence | Notes |
|---|---|---|---|---|---|
| 0x0 | 4 | u32 | unknown | Unknown | Observed 2 |
| 0x4 | 4 | u32 | version | Proven | 16 |
| 0x8 | 4 | u32 | light count | Proven | |
| 0xC | 4 | u32 | name block length | Proven | |
| 0x10 | 4 | u32 = 0 | padding | Proven | |
| 0x14 | 4 | u32 = 232 | light record size | Proven | |
| 0x18 | 36 | u8[36] = 0 | padding | Proven |
Little-endian. A name block of the stated length follows, then light count times 232 bytes through the file end.
| Offset | Size | Type | Field | Confidence | Notes |
|---|---|---|---|---|---|
| 0x0 | 4 | u32 | unknown | Unknown | Observed 2 |
| 0x4 | 4 | u32 | version | Proven | 18 |
| 0x8 | 4 | u32 | light count | Proven | |
| 0xC | 4 | u32 | name block length | Proven | |
| 0x10 | 4 | u32 = 0 | padding | Proven | |
| 0x14 | 4 | u32 = 232 | light record size | Proven | |
| 0x18 | 36 | u8[36] = 0 | padding | Proven |
Fields without established meanings remain opaque; name offsets are relative to the name block.
| Offset | Size | Type | Field | Confidence | Notes |
|---|---|---|---|---|---|
| 0x0 | 16 | u8[16] | unknown | Unknown | Retained as four words |
| 0x10 | 8 | u64 | UTF-16 name offset | Proven | |
| 0x18 | 4 | u32 | light type | Sourced | 0 point; 1 spot; 2 directional |
| 0x1C | 1 | u8 | unknown boolean | Unknown | 0 or 1 |
| 0x1D | 3 | u8[3] | diffuse RGB | Sourced | |
| 0x20 | 4 | f32 | diffuse power | Sourced | |
| 0x24 | 3 | u8[3] | specular RGB | Sourced | |
| 0x27 | 1 | u8 | cast shadows | Sourced | |
| 0x28 | 4 | f32 | specular power | Sourced | |
| 0x2C | 4 | f32 | cone angle | Observed | Spot: full angle in degrees of the vertical axis of the cone; see below |
| 0x30 | 8 | u8[8] | unknown | Unknown | Retained as two words |
| 0x38 | 12 | f32[3] | position | Proven | |
| 0x44 | 12 | f32[3] | rotation | Observed | Radians |
| 0x50 | 8 | u8[8] | unknown | Unknown | Retained as two words |
| 0x58 | 4 | f32 | radius | Sourced | |
| 0x5C | 140 | u8[140] | auxiliary fields | Unknown | Includes zero words at 96; 140; 148; 168; 176; 180 |
Fields without established meanings remain opaque; name offsets are relative to the name block.
| Offset | Size | Type | Field | Confidence | Notes |
|---|---|---|---|---|---|
| 0x0 | 16 | u8[16] | unknown | Unknown | Retained as four words |
| 0x10 | 8 | u64 | UTF-16 name offset | Proven | |
| 0x18 | 4 | u32 | light type | Proven | 0 point; 1 spot; 2 directional |
| 0x1C | 1 | u8 | unknown boolean | Unknown | 0 or 1 |
| 0x1D | 3 | u8[3] | diffuse RGB | Proven | |
| 0x20 | 4 | f32 | diffuse power | Proven | |
| 0x24 | 3 | u8[3] | specular RGB | Proven | |
| 0x27 | 1 | u8 | cast shadows | Proven | |
| 0x28 | 4 | f32 | specular power | Proven | |
| 0x2C | 4 | f32 | cone angle | Proven | Spot: full angle in degrees of the vertical axis of the cone (read for Sekiro, see below) |
| 0x30 | 8 | u8[8] | unknown | Unknown | Retained as two words |
| 0x38 | 12 | f32[3] | position | Proven | |
| 0x44 | 12 | f32[3] | rotation | Proven | Radians (every spot light of the install lies in -pi..pi) |
| 0x50 | 8 | u8[8] | unknown | Unknown | Retained as two words |
| 0x58 | 4 | f32 | radius | Proven | |
| 0x5C | 140 | u8[140] | auxiliary fields | Unknown | Includes zero words at 96; 140; 148; 168; 176; 180 |
Words with community names
Section titled “Words with community names”The community editor names eleven words of the opaque auxiliary block. Over every light of both installs the values fit the names, though no value establishes what the game does with them. Observed The zero words at 96, 140, 148, 168, 176 and 180 are zero in all 735 Sekiro and 11,229 Elden Ring lights. Proven · every light of both archive sets
| Offset | Community name | Sekiro (735 lights) | Elden Ring (11,228 lights, version 18) |
|---|---|---|---|
0x74 |
flicker interval minimum | 0.0 (680) or 0.2 (55) | 0.0 |
0x78 |
flicker interval maximum | 0.0 (646), 0.8 (55) or 1.0 (34) | 0.0 |
0x7C |
flicker brightness multiplier | 1.0 (646) or 0.6 (89) | 1.0 |
0x80 |
event ID (i32) | -1 | -1 |
0x9C |
near clip (not the spot’s near plane) | 1.0 | 1.0 |
0xA3 |
shadow model cull flip (byte) | 1 (729) or 0 (6) | 0 (9,599) or 1 (1,629) |
0xA4 |
sharpness (the diffuse record’s fourth value is 1 divided by it) | 33 values, 0.2 to 30 | 70 values, 0.01 to 80 |
0xB8 |
width (a spot’s full horizontal cone angle in degrees) | 16 values, 0 to 90 (0 on 668) | 28 values, 0 to 100 (0 on 11,006) |
0xBC |
light start cutoff (a spot’s near plane) | 20 values, 0 to 34 (0 on 670) | 23 values, 0 to 60 (0 on 10,997) |
0xC4 |
enable distance | 8 values, 0 to 1000 (0 on 685) | 27 values, 0 to 1000 (40 on 4,928; 0 on 1,611) |
0xD0 |
volume density | 17 values, 0 to 10 (1.0 on 302; 0 on 193) | 16 values, 0 to 10 (0 on 8,008; 1.0 on 2,221) |
The meanings in the parentheses are read back from a running Sekiro (below, “Spot lights” and “How the renderer takes a light”);
the other names are the editor’s and rest on the values fitting them. Observed · Sekiro The word at 0xE0 is the exposure
weight (below), 0 on every Elden Ring light. Proven · Elden Ring · all 109 banks
Ashina Castle’s examined banks 0000, 0002 and 0004 contain point and spot lights with shadows disabled.
Observed · Sekiro
How the renderer takes a light
Section titled “How the renderer takes a light”Sekiro’s deferred lighting stage takes every point light as one 208-byte record and every spot light as one 192-byte record, from two structured buffers of 1,024 records that the engine fills each frame; compute programs sort the lights into screen tiles and a tile-lighting pass shades with them. A per-light variant of the shader family reads one record from a constant buffer instead. Over four frames of Ashina Castle the lighting passes that ran were the organise and tile-lighting programs, with 19 point records in the buffer and none for spot lights. Observed · Sekiro
A point record holds, as floats: the light’s position in view space and 1 / radius^2 (offset 0); the diffuse colour (red,
green, blue) and a fourth value (16); the specular colour and a fourth value (32); the translucency parameters (48) and the
radius (60); the shadowed flag LightType (64; see light records). Observed · Sekiro Against the stored values of the five lights of the map’s banks that
the frames held (matched by position, within 0.01 m, over two frames):
| Record field | Value | Largest relative difference |
|---|---|---|
| diffuse colour | (diffuse byte / 255)^2 * diffuse power per channel |
1.4e-7 |
| specular colour | (specular byte / 255)^2 * specular power per channel (0 for the four lights of specular power 0) |
7e-8 |
| position | the stored position, moved into view space | 2e-5 m |
1 / radius^2 and radius |
the stored radius (offset 0x58) |
2e-8 |
| diffuse fourth value | 1 divided by the float at record offset 0xA4 (0.5 on four lights, 4 on one) |
0 |
| specular fourth value | 0.2989 r + 0.5866 g + 0.1145 b of the record’s specular colour (0 where the power is 0) |
2e-8 |
The colour transfer is therefore the square of the byte over 255, not the sRGB curve: for a byte of 158 the record holds
0.3839 of the power where sRGB would give 0.3416. Observed · Sekiro[5 lights, 2 frames, one area] Over more lights the
specular colour is exactly (specular byte / 255)^2 * specular power: 55 of 55 point lights and 1 of 1 spot light. The engine’s
distance fade scales the diffuse colour only. Observed · Sekiro
In the lighting shaders’ disassembly the radius enters a point light’s contribution only as the window saturate(1 - distance^2 / radius^2), which multiplies a wrapped Lambert term whose wrap depends on distance * (the diffuse fourth value); the colour is the record’s diffuse colour times that. The same expression is in the tile compute program and in the
per-light pixel program. A spot light’s record has a range instead of 1 / radius^2 (the shader squares it), the same window,
and a second factor saturate(1 - x^2 - y^2) on its position in the cone’s projection space, which is zero at the edge of the
base. Observed · Sekiro[disassembly of the programs, read not run]
Tile lists
Section titled “Tile lists”GXLightOrganize16 sorts the lights into screen tiles of 32 pixels: the tile of a pixel is the pixel divided by
FC_LightTile_Width (cbSceneParam +896), with FC_LightTile_NumCellX (+900) tiles to a row, so 60 x 34 = 2,040 tiles at
1920x1080 with room for 130,560 index entries. It writes the index entries at u0, the per-tile lookup at u1, an entry counter
at u2 (zeroed before the dispatch) and 40-byte per-tile records at u3. Its g_ViewMatrix (+64 of its constants) is marked
unused: the light records already hold view-space positions. Observed · Sekiro
One record per tile.
| Offset | Size | Type | Field | Confidence | Notes |
|---|---|---|---|---|---|
| 0x0 | 4 | u32 | first entry | Observed | index of the tile's first entry in g_LightIndices |
| 0x4 | 2 | u16 | point count | Observed | |
| 0x6 | 2 | u16 | spot count | Observed | the tile's spot entries follow its point entries |
| 0x8 | 2 | u16 | points in opaque range | Observed | how many of the point entries have bit 31 set; read only by the tile pass's debug view |
| 0xA | 2 | u16 | spots in opaque range | Observed | the same for the spot entries |
| Offset | Size | Type | Field | Confidence | Notes |
|---|---|---|---|---|---|
| 0x0 | 2 | u16 | light record | Observed | index of the point or spot record |
| 0x2 | 2 | u16 | slices and range bit | Observed | bits 0-14: depth-slice mask, slice k at bit k; bit 15 (bit 31 of the entry): the light reaches the tile's opaque depth range |
The depth slice of a pixel comes from FC_FrustumNear, FC_SliceScale and FC_SliceBias (cbSceneParam +880, +884,
+888). A forward material skips an entry unless its pixel’s slice bit is set, and the tile-lighting pass visits only the
entries with bit 31 set. Observed[disassembly of the forward GXFlver_*_Fwd programs, GXRend_TileBasedLighting and
GXLightOrganize16]{games=“sekiro”} In 7 captured frames with the organise program’s outputs read back, the per-tile counts sum
to the counter (4,106, 5,069 and 6,481 among them), every entry’s light index is below the frame’s light count, and no entry has
every slice bit set; on average the game culls 3.99 of 6, 16.52 of 19 and 28.82 of 32 lights per tile. Observed · Sekiro · 7 frames
The organise program reserves each tile’s run of index entries with an atomic add on the entry counter, so the runs lie in
g_LightIndices in the order the GPU happened to run the tiles, not in tile order. Observed[disassembly of the tile-list
dispatch’s program]{games=“sekiro”} In one frame at the Ashina gate (60 x 34 groups dispatched) the 2,040 lookup records’ first entries
are all different and, sorted, each run ends where the next begins, 5,069 entries in all with no gap; the first tiles of the
top row start at entries 582, 302, 273 and 438. Run again three times on the frame’s recorded inputs, the program puts 2,022
to 2,034 of the 2,040 runs at other entries and writes the same index entries in another order.
Observed · Sekiro · one captured frame, three re-runs A tile’s first entry is therefore not a property of the tile,
and the two buffers differ from frame to frame even where the scene does not; what each tile’s run holds is fixed.
Inferred · Sekiro
only in Elden Ring Elden Ring’s GXLightOrganize8 builds the same lists. At 2560x1440 it is dispatched as 80 x 45 groups and
writes 3,600 lookup records of 12 bytes at u1 (g_lightLookup, a structured view of stride 12 over a 43,264-byte buffer,
so 64 bytes follow the records), the index entries at u0 (g_wLightIndices, 345,600 entries) and its counter at u2
(g_workData). Observed · Elden Ring · one captured frame’s bindings In one frame with 8 point lights and no spot
lights (g_lightCount 8, g_spotLightCount 0), every one of the 3,600 records holds the first entry of its tile’s run in its
first word, the run’s length in its second, and in its third the number of the run’s entries with bit 31 set.
Observed · Elden Ring · one frame’s outputs, all 3,600 tiles With no spot lights in the frame, whether the second
word splits into a point count and a spot count as Sekiro’s does is not shown. Unknown · Elden Ring The runs land in
the order the GPU ran the tiles, as in Sekiro. Running the dispatch again on each frame’s recorded inputs, on the GPU
that captured it, put 3,590 of the 3,600 runs at other entries in one frame and 3,548 in another. Every run held the same
entries in the same order, and the 64 bytes after the records were unchanged.
Observed · Elden Ring · two frames of two sessions, one re-run each
Spot lights
Section titled “Spot lights”A spot light’s record (192 bytes) holds the position and the range (the stored radius), the diffuse and specular colours as a point
light’s, the translucency parameters, and a 4 x 4 matrix of rows that take a view-space position into the cone’s space; the
shader shades with max(0, 1 - x^2 - y^2) of the cone-space coordinates, so there is no inner cone. Observed · Sekiro
Read back from a running Sekiro in Ashina Castle, five spot records of two maps (the spot buffer held one record in one view and
five in another) match their banks’ lights as follows. Observed · Sekiro[5 lights, 2 maps]
| Bank word | Spot record |
|---|---|
| diffuse bytes and power, radius, the word at offset 164 | as for a point light: (byte / 255)^2 * power, range = radius, fourth diffuse value 1 / word (to 2e-7) |
| cone angle (44) | the vertical axis’s full angle in degrees: that row’s scale is cot(angle / 2) (to 1.5e-7) |
| the word at 184 (community name “width”) | the horizontal axis’s full angle in degrees, cot(angle / 2); where it is 0 the cone angle serves both (code read) |
| the word at 188 (community name “light start cutoff”) | the near plane: the depth row’s scale is far / (far - near) (to 1e-7) |
| rotation (68), radians | the beam is Ry(y) * Rx(x) * Rz(z) applied to +Z in the map frame, the cone’s horizontal axis the rotated +X and its vertical axis the rotated +Y (to 4e-8) |
The five lights carry angles of 30 x 30, 90 x 8, 20 x 30, 10 x 50 and 25 x 20 degrees (horizontal x vertical). In the executable’s loader a spot light whose two angles are both 0 is left switched off. Inferred · code read
The exposure term
Section titled “The exposure term”A light object carries a weight, read from the bank word at offset 224, and the packer multiplies the record’s colours by
2^(-weight * X) where X is a value of the renderer’s scene parameters (the engine’s per-frame exposure term) when the weight
is not 0. Inferred · code read, Sekiro 1.06 and Elden Ring 1.17.2 Two Sekiro lights of weight 1 read back with a colour 1.081215
times their (byte / 255)^2 * power (identical on both lights to 1e-7, red, green and blue), a light of weight 0 with 1.0000000;
the weight is 0 on 120 of the 122 lights of the five Ashina banks (1.0 on two) and on all 11,228 lights of version 18 Elden Ring banks.
Observed · Sekiro[3 lights] Proven · Elden Ring[the weight word of every light]
only in Elden Ring The lighting programs (DXIL) declare the same point-light structure and apply the same window; the executable’s
point-light packer is the Sekiro packer’s: the same squaring of the colour (through a helper that squares the three channels and keeps
the fourth), the same 1 / radius^2, 1 / word and exposure term (its scene value one field further on). Observed · Elden Ring[code read] One
light of a bank was read back (m10_00_00_00_0000 light 32: bytes 240, 180, 120, power 0.5, radius 8, fourth word 2): the record’s diffuse
colour is (byte / 255)^2 * power per channel, 0.4429066, 0.2491350 and 0.1107266, to 2e-6. Observed · Elden Ring[1 light]
only in Elden Ring At the Stormveil main gate, in one session four consecutive dispatches of GXLightOrganize8 held 16 point
records and no spot record each, and 20 of the records are five lights of bank m10_00_00_00_0000, matched by position within
0.01 m; in another, four dispatches held 22 point records and no spot record each, and 8 of them are the bank’s lights 32 and 116.
Observed · Elden Ring · two sessions, one area Their fields are Sekiro’s: the diffuse colour (byte / 255)^2 * power; its fourth value 1 divided by the word at +164 (8, 1.5 and 0.5 seen); the
specular colour (specular byte / 255)^2 * specular power, separate from the diffuse (it differs on all 20); its fourth value the
Rec. 601 luma (0.2989, 0.5866, 0.1145) of the specular colour, which no Elden Ring program reads; the translucency parameters’
first two values 1 and 1. LightType is 0 in every record but light 32’s, which casts shadows and holds 1.0; the shadow fields
of an unshadowed light keep stale bytes of the buffer (see light records).
Observed · Elden Ring · two sessions, one area In the program’s DXIL, g_lightCount (cbLightAccParam
+272) bounds the loop over the point records and g_spotLightCount (+280) the loop over the spot records.
Observed · Elden Ring The positions are in the renderer’s frame, whose origin moves (see
coordinate frames). Elden Ring’s spot-light record is known only from the program’s
declaration. Unknown · Elden Ring
The renderer’s unit is the record’s colour^2 * power; stored values still do not establish watts, lumens or lux. The lights of
effect files are in the same unit: an effect light’s
multiplier times its colour alpha is a BTL power, with no factor between them.
Banks for m60_42_33_00 and m60_42_36_00 store version 15; SoulsFormatsNEXT refuses them by their version.
Both declare a 240-byte light record: the first holds no lights (60 bytes), the second one light (332 bytes: the 60-byte
header, a 32-byte name block and 240). Its fields up to offset 232 read as in version 18 (point light, diffuse colour
F0 B4 78, diffuse power 0.15, a plausible position) and the 8 bytes after them are zero. Observed · Elden Ring
Whether the extra 8 bytes ever hold more than zeros is not established.