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";