Skip to content

BTL light banks

FromSoftware engineLighting and skyApplies toSekiroElden Ring

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

BTL header (Sekiro)60 bytesSekiro93% known

Little-endian. A name block of the stated length follows, then light count times 232 bytes through the file end.

ProvenObservedSourcedInferredUnknownNot described
OffsetSizeTypeFieldConfidenceNotes
0x04u32unknownUnknownObserved 2
0x44u32versionProven16
0x84u32light countProven
0xC4u32name block lengthProven
0x104u32 = 0paddingProven
0x144u32 = 232light record sizeProven
0x1836u8[36] = 0paddingProven
BTL header (Elden Ring)60 bytesElden Ring93% known

Little-endian. A name block of the stated length follows, then light count times 232 bytes through the file end.

ProvenObservedSourcedInferredUnknownNot described
OffsetSizeTypeFieldConfidenceNotes
0x04u32unknownUnknownObserved 2
0x44u32versionProven18
0x84u32light countProven
0xC4u32name block lengthProven
0x104u32 = 0paddingProven
0x144u32 = 232light record sizeProven
0x1836u8[36] = 0paddingProven
BTL light (Sekiro)232 bytesSekiro16% known

Fields without established meanings remain opaque; name offsets are relative to the name block.

ProvenObservedSourcedInferredUnknownNot described
OffsetSizeTypeFieldConfidenceNotes
0x016u8[16]unknownUnknownRetained as four words
0x108u64UTF-16 name offsetProven
0x184u32light typeSourced0 point; 1 spot; 2 directional
0x1C1u8unknown booleanUnknown0 or 1
0x1D3u8[3]diffuse RGBSourced
0x204f32diffuse powerSourced
0x243u8[3]specular RGBSourced
0x271u8cast shadowsSourced
0x284f32specular powerSourced
0x2C4f32cone angleObservedSpot: full angle in degrees of the vertical axis of the cone; see below
0x308u8[8]unknownUnknownRetained as two words
0x3812f32[3]positionProven
0x4412f32[3]rotationObservedRadians
0x508u8[8]unknownUnknownRetained as two words
0x584f32radiusSourced
0x5C140u8[140]auxiliary fieldsUnknownIncludes zero words at 96; 140; 148; 168; 176; 180
BTL light (Elden Ring)232 bytesElden Ring25% known

Fields without established meanings remain opaque; name offsets are relative to the name block.

ProvenObservedSourcedInferredUnknownNot described
OffsetSizeTypeFieldConfidenceNotes
0x016u8[16]unknownUnknownRetained as four words
0x108u64UTF-16 name offsetProven
0x184u32light typeProven0 point; 1 spot; 2 directional
0x1C1u8unknown booleanUnknown0 or 1
0x1D3u8[3]diffuse RGBProven
0x204f32diffuse powerProven
0x243u8[3]specular RGBProven
0x271u8cast shadowsProven
0x284f32specular powerProven
0x2C4f32cone angleProvenSpot: full angle in degrees of the vertical axis of the cone (read for Sekiro, see below)
0x308u8[8]unknownUnknownRetained as two words
0x3812f32[3]positionProven
0x4412f32[3]rotationProvenRadians (every spot light of the install lies in -pi..pi)
0x508u8[8]unknownUnknownRetained as two words
0x584f32radiusProven
0x5C140u8[140]auxiliary fieldsUnknownIncludes zero words at 96; 140; 148; 168; 176; 180

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

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]

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

Tile lookup record (g_LightLookupBuffer, t60)12 bytesSekiro100% known

One record per tile.

ProvenObservedSourcedInferredUnknownNot described
OffsetSizeTypeFieldConfidenceNotes
0x04u32first entryObservedindex of the tile's first entry in g_LightIndices
0x42u16point countObserved
0x62u16spot countObservedthe tile's spot entries follow its point entries
0x82u16points in opaque rangeObservedhow many of the point entries have bit 31 set; read only by the tile pass's debug view
0xA2u16spots in opaque rangeObservedthe same for the spot entries
Tile index entry (g_LightIndices, t59)4 bytesSekiro100% known
ProvenObservedSourcedInferredUnknownNot described
OffsetSizeTypeFieldConfidenceNotes
0x02u16light recordObservedindex of the point or spot record
0x22u16slices and range bitObservedbits 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

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

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

How do bank powers map to physical light units?

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.

What is the version-15 BTL layout?

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.