docs(lighting): A7 Fix D investigation RESOLVED + implementation spec (#140)

Resolve the Fix D contradiction with decomp (workflow wf_f660eb88 + adversarial
verify) + 4 live cdb captures. The D3D-FF model was the WRONG oracle: retail has
TWO light systems — STATIC torches BAKE into wall vertices (calc_point_light,
triple-clamped: range gate + per-channel min(scale*color,color) + per-vertex
[0,1] from black), DYNAMIC lights go D3D hardware. The captured intensity=100 is
the purple PORTAL (magenta, dynamic), not a wall torch. Ground truth: 38 static
warm torches (orange (1,0.588,0.314)/cream, intensity=100, falloff 3-5) + 2 dynamic.

acdream over-brightness = two confirmed bugs: D-1 mesh_modern.vert folds
ambient+sun+torches into one UNCLAMPED accumulator (single frag clamp) -> warm
blowout; D-2 EnvCellRenderer never binds SSBO 4/5 so the cell shell reads a leaked
light set. Spec: D-1 in-shader clamp-split (clamp the torch sum on its own before
ambient/sun); D-2 bind the shell's own per-cell light set (mirror WbDrawDispatcher);
LightBake.cs is the C# conformance oracle. Adds the 4 reusable cdb capture scripts.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
Erik 2026-06-18 17:08:27 +02:00
parent f384d036a3
commit c407104ab9
7 changed files with 419 additions and 46 deletions

View file

@ -0,0 +1,15 @@
$$ A7 Fix D — GOLDEN: dump the nearest static lights (the meeting-hall wall torches)
$$ + the ambient/sun that acdream folds into its accumulator. Breakpoint-free, instant.
$$ Render::world_lights @ 0x008672a0; sorted_static_lights[] (RenderLight*) @ +0x3498
$$ (verified: num_static_lights@+0x104=38, num_dynamic_lights@+0x3588=2).
$$ Stand near the meeting-hall torches so the nearest sorted lights ARE them.
.logopen C:\Users\erikn\source\repos\acdream\.claude\worktrees\thirsty-goldberg-51bb9b\a7-fixd-golden-probe.log
.sympath C:\Users\erikn\source\repos\acdream\refs
.symopt+ 0x40
.reload /f acclient.exe
.echo === ambient_color / sunlight_color / sunlight (what acdream folds into the accumulator) ===
dt -r1 acclient!Render::world_lights ambient_color sunlight_color sunlight num_static_lights num_dynamic_lights
.echo === nearest 10 sorted static lights (RenderLight.d3dLightIndex + info: type/intensity/falloff/color) ===
.for (r $t0=0; @$t0 < 10; r $t0=@$t0+1) { r $t1 = poi(acclient!Render::world_lights + 0x3498 + @$t0*4); .printf "--- sorted_static[%d] RenderLight=%p ---\n", @$t0, @$t1; dt -r2 acclient!RenderLight @$t1 d3dLightIndex distancesq info }
.echo === END ===
qd

View file

@ -0,0 +1,17 @@
$$ A7 Fix D — GOLDEN v2: explicit LIGHTINFO/RGBColor dump of the nearest static
$$ lights. info @ RenderLight+0x70 (LIGHTINFO); within info: color@+0x50, intensity@+0x5C,
$$ falloff@+0x60 -> absolute color@RL+0xC0, intensity@RL+0xCC, falloff@RL+0xD0.
$$ Characterizes the 38-light static set (warm town torches?) + golden for the fix.
$$ Breakpoint-free, instant, uses current scene.
.logopen C:\Users\erikn\source\repos\acdream\.claude\worktrees\thirsty-goldberg-51bb9b\a7-fixd-golden2-probe.log
.sympath C:\Users\erikn\source\repos\acdream\refs
.symopt+ 0x40
.reload /f acclient.exe
.echo === ambient_color (r,g,b) ===
dt acclient!RGBColor acclient!Render::world_lights+0x0
.echo === sunlight_color (r,g,b) ===
dt acclient!RGBColor acclient!Render::world_lights+0xc
.echo === nearest 8 sorted static lights: type/intensity/falloff + color(r,g,b) + distsq ===
.for (r $t0=0; @$t0 < 8; r $t0=@$t0+1) { r $t1 = poi(acclient!Render::world_lights + 0x3498 + @$t0*4); .printf "--- sorted_static[%d] RL=%p d3dIdx=%d ---\n", @$t0, @$t1, dwo(@$t1+0x68); dt acclient!LIGHTINFO @$t1+0x70 type intensity falloff; .echo color(r,g,b):; dt acclient!RGBColor @$t1+0xc0; .echo distancesq:; dd @$t1+0xd8 L1 }
.echo === END ===
qd

View file

@ -0,0 +1,36 @@
$$
$$ A7 Fix D (#140) v2 — fills the two gaps v1 left:
$$ (1) light COLORS (v1's dt did not expand RGBColor); expanded here as a
$$ typed RGBColor dump + raw dd hex backup (reinterpret IEEE-754 if dt fails).
$$ (2) the STATIC wall torches (the lights that actually BAKE the walls) — these
$$ only re-register on a visible-cell-set change, so the player must MOVE
$$ (walk IN and OUT of the meeting hall, circle past the torches) to trigger
$$ Render::add_static_light.
$$
$$ v1 already proved: intensity=100/falloff=6 light is DYNAMIC (add_dynamic_light,
$$ d3dIdx=2) = the portal/effect on the hardware path, NOT a baked wall torch.
$$ viewer light = intensity 2.25 / falloff 10 (dynamic, d3dIdx=1).
$$
$$ add_static_light / add_dynamic_light(LIGHTINFO* info, cellID, Frame* offset):
$$ LIGHTINFO* = poi(@esp+4). color@+0x50 (r/g/b floats), origin@+0x38, intensity@+0x5C, falloff@+0x60.
$$
$$ Dynamic logging is limited to the first 8 hits (we already characterised them);
$$ ALL static hits log. qd when 12 static torches captured OR 1500 total hits (safety).
.logopen C:\Users\erikn\source\repos\acdream\.claude\worktrees\thirsty-goldberg-51bb9b\a7-fixd-lights-v2.log
.sympath C:\Users\erikn\source\repos\acdream\refs
.symopt+ 0x40
.reload /f acclient.exe
r $t0 = 0
r $t2 = 0
r $t3 = 0
$$ STATIC wall torches (baked path) — MOVE to trigger. Color (typed + hex) + origin.
bp acclient!Render::add_static_light "r $t0=@$t0+1; r $t2=@$t2+1; .printf /D \"[STATIC torch] hit#%d\\n\", @$t2; dt acclient!LIGHTINFO poi(@esp+4) type intensity falloff cone_angle; .echo color_typed:; dt acclient!RGBColor poi(@esp+4)+0x50; .echo color_hex(r,g,b):; dd poi(@esp+4)+0x50 L3; .echo origin_hex(x,y,z):; dd poi(@esp+4)+0x38 L3; .if (@$t2 >= 12) { qd } .elsif (@$t0 >= 1500) { qd } .else { gc }"
$$ DYNAMIC lights (portal/viewer) — log first 8 with color, then silent gc.
bp acclient!Render::add_dynamic_light "r $t0=@$t0+1; r $t3=@$t3+1; .if (@$t3 <= 8) { .printf /D \"[DYNAMIC light] hit#%d\\n\", @$t3; dt acclient!LIGHTINFO poi(@esp+4) type intensity falloff; .echo color_typed:; dt acclient!RGBColor poi(@esp+4)+0x50; .echo color_hex(r,g,b):; dd poi(@esp+4)+0x50 L3 }; .if (@$t0 >= 1500) { qd } .else { gc }"
.printf "v2 armed: STATIC=wall torches (MOVE in/out of hall to trigger), DYNAMIC=portal/viewer; colors expanded. qd at 12 statics or 1500 total.\\n"
g

View file

@ -0,0 +1,50 @@
$$
$$ A7 Fix D (#140) — wall-torch vs portal light OWNERSHIP + the actual LIGHTINFO
$$ values that feed the EnvCell wall bake. 2026-06-18.
$$
$$ Decomp already settled the render path (workflow wf_f660eb88):
$$ STATIC lights -> CPU per-vertex bake (SetStaticLightingVertexColors ->
$$ calc_point_light), DOUBLE-clamped (per-light min(scale*color,color) +
$$ per-vertex [0,1]) -> walls stay DIM even at intensity=100.
$$ DYNAMIC lights -> D3D hardware FF (minimize_envcell_lighting).
$$ Render::insert_light copies intensity VERBATIM to BOTH paths, so the only
$$ open empirical question is: which light carries intensity=100, and what do
$$ the actual wall-torch LIGHTINFOs look like (intensity/falloff/color)?
$$
$$ CLASSIFICATION via config_hardware_light's d3dLightIndex (arg1 @ [esp+4]):
$$ add_dynamic_light base index = 1 -> dynamic idx in [1..10] (viewer light / teleport PORTAL)
$$ add_static_light base index = 11 -> static idx in [11..70] (WALL TORCHES, baked)
$$
$$ config_hardware_light(d3dIndex, _D3DLIGHT9* out, ulong cellID, LIGHTINFO* info):
$$ d3dIndex = dwo(@esp+4) ; LIGHTINFO* = poi(@esp+0x10) (PROVEN last session)
$$ add_static_light / add_dynamic_light(LIGHTINFO* info, cellID, Frame* offset):
$$ LIGHTINFO* = poi(@esp+4)
$$ `dt acclient!LIGHTINFO <ptr> type intensity falloff color` resolves the
$$ float fields symbolically (PDB types) -> readable values, no hex reinterp.
$$
$$ USAGE: with retail in-world standing in/near the Holtburg meeting hall by a
$$ wall torch, WALK around the hall (and past the teleport portal if present)
$$ for ~15 s so static torch sets re-register. Auto-detaches (qd) after 600
$$ total hits, leaving retail running.
.logopen C:\Users\erikn\source\repos\acdream\.claude\worktrees\thirsty-goldberg-51bb9b\a7-fixd-lights-capture.log
.sympath C:\Users\erikn\source\repos\acdream\refs
.symopt+ 0x40
.reload /f acclient.exe
r $t0 = 0
r $t1 = 0
r $t2 = 0
r $t3 = 0
$$ BP1: config_hardware_light — EVERY light (static+dynamic); d3dIdx classifies.
bp acclient!PrimD3DRender::config_hardware_light "r $t0=@$t0+1; r $t1=@$t1+1; .printf /D \"[CHL] hit#%d d3dIdx=%d (1-10=DYNAMIC portal/viewer, 11+=STATIC torch)\\n\", @$t1, dwo(@esp+4); dt acclient!LIGHTINFO dwo(@esp+0x10) type intensity falloff color; .if (@$t0 >= 600) { qd } .else { gc }"
$$ BP2: add_static_light — every hit is a WALL TORCH (baked path).
bp acclient!Render::add_static_light "r $t0=@$t0+1; r $t2=@$t2+1; .printf /D \"[STATIC torch] hit#%d\\n\", @$t2; dt acclient!LIGHTINFO dwo(@esp+4) type intensity falloff color; .if (@$t0 >= 600) { qd } .else { gc }"
$$ BP3: add_dynamic_light — viewer light + teleport PORTAL (hardware path).
bp acclient!Render::add_dynamic_light "r $t0=@$t0+1; r $t3=@$t3+1; .printf /D \"[DYNAMIC light] hit#%d\\n\", @$t3; dt acclient!LIGHTINFO dwo(@esp+4) type intensity falloff color; .if (@$t0 >= 600) { qd } .else { gc }"
.printf "a7-fixd-lights armed: BP1 CHL (classify via d3dIdx), BP2 STATIC=torch, BP3 DYNAMIC=portal/viewer. qd after 600 total hits.\\n"
g

View file

@ -0,0 +1,18 @@
$$ A7 Fix D — instant (breakpoint-free) read of how many STATIC lights the
$$ current scene bakes with. Confirms whether the meeting hall has static torches
$$ (-> D-1 summed-torches matters) or near-zero (-> D-2 leaked-SSBO is the cause).
$$ Stand where the meeting-hall walls are visible. No movement / no breakpoints.
.logopen C:\Users\erikn\source\repos\acdream\.claude\worktrees\thirsty-goldberg-51bb9b\a7-fixd-numstatic-probe.log
.sympath C:\Users\erikn\source\repos\acdream\refs
.symopt+ 0x40
.reload /f acclient.exe
.echo === x acclient!*world_lights* ===
x acclient!*world_lights*
.echo === x acclient!Render::world_lights ===
x acclient!Render::world_lights
.echo === dt typed (num_static_lights / num_dynamic_lights / ambient_color) ===
dt acclient!Render::world_lights num_static_lights num_dynamic_lights ambient_color sunlight_color
.echo === dt LightParms at symbol (fallback by explicit type) ===
dt acclient!LightParms acclient!Render::world_lights num_static_lights num_dynamic_lights
.echo === END ===
qd