fix(lighting): #142 — per-instance sun gate for windowed-building interiors
Standing inside (or looking into) a windowed building like the Agent of Arcanum, interior objects (furniture, NPCs, the player) were lit by the directional sun because acdream's sun gate was per-FRAME (keyed on the player being in a sealed cell), not per-DRAW as retail does it. Retail's PView::DrawCells (0x005a4840) runs two stages per frame: outdoor stage → useSunlightSet(1) (0x005a485a): sun ON interior stage → useSunlightSet(0) (0x005a49f3): sun OFF DrawMeshInternal (0x0059f398) then calls minimize_object_lighting only when useSunlight==0, so indoor objects ALWAYS skip the sun regardless of whether the player's cell is windowed or sealed. Fix: add a per-instance uint SSBO (binding=6 instanceIndoor[]) whose value is IndoorObjectReceivesTorches(ParentCellId) — the same predicate AP-43 already uses for the torch gate. In mesh_modern.vert, nest the sun loop inside an additional `if (instanceIndoor[instanceIndex] == 0u)` check inside the existing `if (uLightingMode == 0)` block. Indoor objects get torches (unchanged) but now skip the sun; outdoor objects keep the sun and still get no torches. The ambient regime (UpdateSunFromSky: 0.2 sealed / sky otherwise) is untouched — it was already correct. Mechanically: _currentEntityIndoor set once per entity in ComputeEntityLightSet; appended to InstanceGroup.IndoorFlags in AppendCurrentLightSet; grown/packed/uploaded in the same cursor loop as _clipSlotData and _lightSetData; deleted in Dispose. Mode-1 draws (EnvCellRenderer) never read binding=6 — the sun loop is inside the uLightingMode==0 uniform-control-flow branch. AP-43 divergence register updated: the sun half is now per-draw (no longer a residual). Residual narrowed to the unaudited ebp_2 test in CellManager::ChangePosition (no observed impact). Tests: WbDrawDispatcherIndoorFlagTests pins IndoorObjectReceivesTorches for the spec §5 representative ids: 0xA9B40172 (Agent of Arcanum EnvCell) → 1; 0xA9B40031 (land sub-cell) → 0; 0xA9B4FFFF (landblock) → 0; null (outdoor shell) → 0; plus the boundary cases 0x0100/0x00FF. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
cd024377a0
commit
ef5049fd58
4 changed files with 148 additions and 10 deletions
|
|
@ -0,0 +1,82 @@
|
|||
using AcDream.App.Rendering.Wb;
|
||||
using Xunit;
|
||||
|
||||
namespace AcDream.App.Tests.Rendering.Wb;
|
||||
|
||||
/// <summary>
|
||||
/// #142 — pins the per-instance "indoor" flag predicate used by both the torch
|
||||
/// gate (AP-43) and the new sun gate (binding=6 <c>instanceIndoor[]</c>).
|
||||
/// The SSBO flag written by <see cref="WbDrawDispatcher.AppendCurrentLightSet"/>
|
||||
/// equals <c>IndoorObjectReceivesTorches(entity.ParentCellId) ? 1u : 0u</c>.
|
||||
/// This test pins that the shared predicate returns the correct value for the
|
||||
/// representative cell ids used in the spec acceptance gate (§5).
|
||||
///
|
||||
/// <para>
|
||||
/// Retail decomp anchor: <c>useSunlightSet</c> 0x0054d450 (sun only, never
|
||||
/// ambient); <c>PView::DrawCells</c> 0x005a4840 — outdoor stage runs
|
||||
/// <c>useSunlightSet(1)</c> (0x005a485a), interior stage runs
|
||||
/// <c>useSunlightSet(0)</c> (0x005a49f3). Indoor objects skip the sun, not
|
||||
/// because the player is inside, but because the OBJECT is in an interior cell.
|
||||
/// </para>
|
||||
/// </summary>
|
||||
public sealed class WbDrawDispatcherIndoorFlagTests
|
||||
{
|
||||
// ── Spec §5 representative ids ─────────────────────────────────────────
|
||||
|
||||
[Fact]
|
||||
public void EnvCell_AgenOfArcanum_IndoorFlag1()
|
||||
{
|
||||
// 0xA9B40172: low word 0x0172 >= 0x0100 → indoor (EnvCell)
|
||||
// This is the exact cell from the in-game probe (agent-arcanum-probe.log
|
||||
// 2026-06-20) that triggered #142 — the Agent of Arcanum interior.
|
||||
uint envCell = 0xA9B40172u;
|
||||
Assert.True(WbDrawDispatcher.IndoorObjectReceivesTorches(envCell),
|
||||
"EnvCell 0xA9B40172 must be indoor (flag=1): objects here skip the sun.");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void LandSubCell_OutdoorFlag0()
|
||||
{
|
||||
// 0xA9B40031: low word 0x0031 < 0x0100 → outdoor land sub-cell
|
||||
uint landSubCell = 0xA9B40031u;
|
||||
Assert.False(WbDrawDispatcher.IndoorObjectReceivesTorches(landSubCell),
|
||||
"Land sub-cell 0xA9B40031 must be outdoor (flag=0): objects here get the sun.");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void LandblockId_OutdoorFlag0()
|
||||
{
|
||||
// 0xA9B4FFFF: low word 0xFFFF — a landblock pseudo-id, not an indoor cell
|
||||
uint landblockId = 0xA9B4FFFFu;
|
||||
Assert.False(WbDrawDispatcher.IndoorObjectReceivesTorches(landblockId),
|
||||
"Landblock id 0xA9B4FFFF must be outdoor (flag=0): the 0xFFFF low-word is excluded.");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void NullParentCellId_OutdoorFlag0()
|
||||
{
|
||||
// Outdoor scenery / building exterior shells have no ParentCellId → outdoor
|
||||
Assert.False(WbDrawDispatcher.IndoorObjectReceivesTorches(null),
|
||||
"Null ParentCellId must be outdoor (flag=0): building shells get the sun, not torches.");
|
||||
}
|
||||
|
||||
// ── Boundary: first valid EnvCell low word ──────────────────────────────
|
||||
|
||||
[Fact]
|
||||
public void CellId_LowWord0x0100_IsIndoor()
|
||||
{
|
||||
// Low word exactly 0x0100 is the first indoor EnvCell id — just on the boundary
|
||||
uint firstEnvCell = 0xA9B40100u;
|
||||
Assert.True(WbDrawDispatcher.IndoorObjectReceivesTorches(firstEnvCell),
|
||||
"Low word 0x0100 is the lowest valid EnvCell id (inclusive boundary → indoor).");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void CellId_LowWord0x00FF_IsOutdoor()
|
||||
{
|
||||
// Low word 0x00FF is one below the EnvCell range — still outdoor
|
||||
uint lastLandSubCell = 0xA9B400FFu;
|
||||
Assert.False(WbDrawDispatcher.IndoorObjectReceivesTorches(lastLandSubCell),
|
||||
"Low word 0x00FF is just below the EnvCell range (exclusive boundary → outdoor).");
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue