From d6fb788c9699b623c3cca0750be948c1f9bae5d1 Mon Sep 17 00:00:00 2001 From: Erik Date: Sat, 13 Jun 2026 19:43:27 +0200 Subject: [PATCH] =?UTF-8?q?diag:=20ACDREAM=5FPROBE=5FLIGHT=20=E2=80=94=20l?= =?UTF-8?q?og=20dungeon=20ambient/sun/active-light=20state=20(#133=20A7)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Opus 4.8 (1M context) --- src/AcDream.App/Rendering/GameWindow.cs | 18 +++++ .../Rendering/RenderingDiagnostics.cs | 72 +++++++++++++++++++ 2 files changed, 90 insertions(+) diff --git a/src/AcDream.App/Rendering/GameWindow.cs b/src/AcDream.App/Rendering/GameWindow.cs index 16956ac4..24472020 100644 --- a/src/AcDream.App/Rendering/GameWindow.cs +++ b/src/AcDream.App/Rendering/GameWindow.cs @@ -7625,6 +7625,24 @@ public sealed class GameWindow : IDisposable _sceneLightingUbo?.Upload(ubo); + // #133 A7 (2026-06-13): objective dungeon-lighting probe. One + // rate-limited [light] line — insideCell / ambient / sun / + // registered-point-lights / active-slot-count / player cell — so + // the dungeon-dim question is self-verifiable from launch.log + // without a screenshot. RegisteredCount is point/spot lights only + // (the sun lives in LightManager.Sun, never in the _all list); + // ubo.CellAmbient.W is the shader active-slot count, which counts + // the (zeroed) sun slot indoors. Inert unless ACDREAM_PROBE_LIGHT=1. + AcDream.Core.Rendering.RenderingDiagnostics.EmitLight( + insideCell: playerInsideCell, + ambientR: Lighting.CurrentAmbient.AmbientColor.X, + ambientG: Lighting.CurrentAmbient.AmbientColor.Y, + ambientB: Lighting.CurrentAmbient.AmbientColor.Z, + sunIntensity: Lighting.Sun?.Intensity ?? 0f, + registeredLights: Lighting.RegisteredCount, + activeLights: (int)ubo.CellAmbient.W, + playerCellId: playerRoot?.CellId ?? 0u); + // Never cull the landblock the player is currently on. uint? playerLb = null; if (_playerMode && _playerController is not null) diff --git a/src/AcDream.Core/Rendering/RenderingDiagnostics.cs b/src/AcDream.Core/Rendering/RenderingDiagnostics.cs index ba081f71..a070fe54 100644 --- a/src/AcDream.Core/Rendering/RenderingDiagnostics.cs +++ b/src/AcDream.Core/Rendering/RenderingDiagnostics.cs @@ -243,6 +243,34 @@ public static class RenderingDiagnostics public static bool ProbePhantomEnabled { get; set; } = Environment.GetEnvironmentVariable("ACDREAM_PROBE_PHANTOM") == "1"; + /// + /// #133 A7 (2026-06-13) dungeon-lighting objective probe. When true, + /// the per-frame scene-lighting build emits ONE [light] line + /// roughly every second (wall-clock rate-limited like WB-DIAG) via + /// : + /// + /// [light] insideCell=<bool> ambient=(r,g,b) sun=<intensity> + /// registeredLights=<N> activeLights=<uCellAmbient.w> playerCell=0x<id> + /// + /// This is the self-verification signal for the dungeon-dim question: + /// + /// insideCell=true ambient=(0.20,0.20,0.20) sun=0 + /// confirms the indoor branch fired (retail flat ambient, sun killed). + /// registeredLights is the count of dat-baked + /// point/spot lights (Setup.Lights) registered with the + /// LightManager — if this is 0 in a dungeon, the cell's static + /// objects carry no baked torches (so the only illumination IS the + /// 0.2 ambient → dim). + /// activeLights is uCellAmbient.w — the + /// shader's active-slot count, which INCLUDES the (zeroed) sun slot + /// indoors. So activeLights=1 registeredLights=0 = "only the dead + /// sun slot, no torches in range". + /// + /// Output-only, inert when off. Initial state from ACDREAM_PROBE_LIGHT=1. + /// + public static bool ProbeLightEnabled { get; set; } = + Environment.GetEnvironmentVariable("ACDREAM_PROBE_LIGHT") == "1"; + // Cell-change gate for EmitVis. The probe fires once per distinct root cell // so launch.log stays readable under motion (the per-frame call is a no-op // when the root is unchanged). Sentinel 0 = "no root yet" — the first real @@ -336,6 +364,50 @@ public static class RenderingDiagnostics /// internal static void ResetVisibilityProbeForTests() => _lastVisRootCellId = 0; + // Wall-clock rate-limit gate for EmitLight. Ticks (100 ns) is plenty — + // we only need ~1 Hz and avoid a Stopwatch allocation/field. Sentinel 0 + // = "never emitted" so the first call always fires. + private static long _lastLightEmitTicks; + private const long LightEmitIntervalTicks = 10_000_000; // 1 s in 100-ns ticks + + /// + /// #133 A7 — emit ONE rate-limited [light] line describing the + /// current scene-lighting state. Cheap no-op when + /// is false; otherwise fires at most + /// once per second. Pull the values from the spot where + /// GameWindow.UpdateSunFromSky set Lighting.CurrentAmbient + /// / Lighting.Sun and where SceneLightingUbo.Build computed + /// the active-slot count. + /// + /// The playerInsideCell value driving the indoor branch. + /// Cell ambient red (xyz of uCellAmbient). + /// Cell ambient green. + /// Cell ambient blue. + /// The sun LightSource.Intensity (0 indoors). + /// Total point/spot lights registered with the LightManager. + /// uCellAmbient.w — shader active-slot count (includes the zeroed sun slot indoors). + /// The player's current cell id (0 if unresolved → outside). + public static void EmitLight(bool insideCell, + float ambientR, float ambientG, float ambientB, + float sunIntensity, + int registeredLights, + int activeLights, + uint playerCellId) + { + if (!ProbeLightEnabled) return; + + long now = DateTime.UtcNow.Ticks; + if (_lastLightEmitTicks != 0 && (now - _lastLightEmitTicks) < LightEmitIntervalTicks) + return; + _lastLightEmitTicks = now; + + var ci = System.Globalization.CultureInfo.InvariantCulture; + Console.WriteLine(string.Format(ci, + "[light] insideCell={0} ambient=({1:0.###},{2:0.###},{3:0.###}) sun={4:0.###} registeredLights={5} activeLights={6} playerCell=0x{7:X8}", + insideCell, ambientR, ambientG, ambientB, sunIntensity, + registeredLights, activeLights, playerCellId)); + } + private static bool _probeEnvCellEnabled = Environment.GetEnvironmentVariable("ACDREAM_PROBE_ENVCELL") == "1";