diff --git a/docs/plans/2026-09-01-campaign-overhaul-world-solidity.md b/docs/plans/2026-09-01-campaign-overhaul-world-solidity.md
index e41c4f722..27d6ec800 100644
--- a/docs/plans/2026-09-01-campaign-overhaul-world-solidity.md
+++ b/docs/plans/2026-09-01-campaign-overhaul-world-solidity.md
@@ -814,6 +814,13 @@ room fixtures now reach every drawn cell (7+40product, unchanged ambient.2).
of the temporary observer is now active, followed by final observer-free
re-gate. FPS deferred; owner G4 still FAIL/unpassed.
+**#478 observer cleanup:** reviewed scratch `ed2c8b12c` integrates seven
+paths with unchanged retained blobs; temporary observer and only its tests
+removed. Full scratch Release0W0E, App180/Core69/native2 PASS, lead independent
+106/106. Permanent RHI47/ordinary8/native witnesses remain. Evidence:
+`478-observer-removal-verification.md`. Fresh integrated Release and unchanged
+Town/control re-gate next; FPS deferred, no owner acceptance or main merge.
+
## 14. Risk register (condensed)
- **R1 second scene graph.** Graph records own only topology/membership facts;
diff --git a/docs/research/2026-09-01-overhaul/478-observer-removal-contract.md b/docs/research/2026-09-01-overhaul/478-observer-removal-contract.md
index f0d308cb8..e8c5f24a2 100644
--- a/docs/research/2026-09-01-overhaul/478-observer-removal-contract.md
+++ b/docs/research/2026-09-01-overhaul/478-observer-removal-contract.md
@@ -1,6 +1,8 @@
# #478 temporary lighting observer removal
-ACTIVE after candidatea5debaca2: unchanged Town High/off/High and0125/8C04
+IMPLEMENTED and lead-reviewed at scratch ed2c8b12c; integration's fresh Release
+and final graphical repeat are pending. See `478-observer-removal-verification.md`.
+Activated after candidatea5debaca2: unchanged Town High/off/High and0125/8C04
control PNGs inspected, eight provisional lighting/reveal PASS frames,
`478-repaired-lighting-graphical.md`. Both own clients exited0/gracefully.
This is not owner acceptance. A failed final visual check returns to
diff --git a/docs/research/2026-09-01-overhaul/478-observer-removal-verification.md b/docs/research/2026-09-01-overhaul/478-observer-removal-verification.md
new file mode 100644
index 000000000..44e844b3f
--- /dev/null
+++ b/docs/research/2026-09-01-overhaul/478-observer-removal-verification.md
@@ -0,0 +1,51 @@
+# #478 observer removal — lead verification
+
+The eight candidate PNGs at `a5debaca2` passed the narrow lighting/reveal
+smoke provisionally (`478-repaired-lighting-graphical.md`). Under
+`478-observer-removal-contract.md`, OpenAI James removed only the temporary
+observer and its corresponding tests. Lead committed scratch
+`ed2c8b12c1ecd31dbc0069419db8104ed0e2bd49`, base `1772c1a5a`, and verified
+the six retained integration blobs against that commit; the seventh path is
+the deleted `EnvCellRenderer.LightingObservation.cs`.
+
+## Scope review
+
+Exactly seven paths, 1 insertion / 581 deletions. No lighting selection,
+ambient, shader, generated module, package, geometry or FPS changes. Four
+composition/DTO/test files restore the exact pre-observer (`59b0b6e72^`) Git
+blobs; snapshot-source restoration differs only by one blank line. Remaining
+EnvCell test changes remove only observer tests and unused helpers/imports.
+The permanent two-cell 47-index RHI upload and next-generation overwrite test,
+its required helpers, ordinary 8-index upload and both compiled shader pixel
+witnesses remain. Observer symbol search over `src` and `tests` returns none.
+Unrelated lifecycle and current-render-scene checkpoint evidence remains.
+
+Lead checked the complete diff and all six raw source hashes against the
+frozen implementer return. No new retail algorithm or deviation is introduced;
+the preceding independent retail and production/ABI reviews still cover the
+unchanged repair. No additional retail re-port or review round was needed.
+The removed temporary source remains recoverable from Git history.
+
+## Verification before integration
+
+Artifacts under scratch `s5-478-observer-cleanup/artifacts/478-observer-cleanup/`:
+
+- `release-build-r2/console.log`: full solution Release, 0 warnings/errors.
+ First build caught a removed Wb import also needed by `GpuMemoryTracker`;
+ that import was restored before the final build and freeze.
+- `app-focused/app-focused.trx`: 180/180 affected/permanent tests.
+- `core-lighting/core-lighting.trx`: 69/69, including installed Town DAT.
+- `shader-pixels/shader-pixels.trx`: both committed-module witnesses pass.
+- Lead independent `root-removal/root-removal.trx`: 106/106 affected
+ lifecycle/composition/EnvCell/ordinary-upload tests plus both native pixels.
+
+Lead inspected actual build output and all TRX counters. Both client names
+were absent before the independent execution. These are focused correctness
+tests, not the full solution test lane or an FPS benchmark.
+
+## Final graphical gate
+
+Pending fresh Release on the integrated observer-free commit and the unchanged
+Town High/off/High plus 0125/8C04/Holtburg control routes. Candidate consumed-
+light measurements remain evidence only for `a5debaca2`, not for this binary.
+Owner acceptance remains owed; G4 FAIL/unpassed, no main merge. FPS deferred.
diff --git a/docs/research/2026-09-01-overhaul/s5-consumers-material-closeout-packet.md b/docs/research/2026-09-01-overhaul/s5-consumers-material-closeout-packet.md
index fa15c056e..ff2012d6f 100644
--- a/docs/research/2026-09-01-overhaul/s5-consumers-material-closeout-packet.md
+++ b/docs/research/2026-09-01-overhaul/s5-consumers-material-closeout-packet.md
@@ -4982,3 +4982,11 @@ requested but its live module check missed the already-exited process; no
stronger claim. `478-repaired-lighting-graphical.md` seals paths/hashes/limits.
Observer removal contract activated; final observer-free re-gate still owed.
No FPS work, no owner acceptance, G4FAIL/no main merge.
+
+Observer cleanup reviewed and integrated from scratch `ed2c8b12c`: exactly
+seven paths, temporary capture/DTO/wiring and observer-only tests removed.
+Permanent RHI47/ordinary8/native-pixel coverage retained; six retained blobs
+match. Full scratch Release0W0E, App180/Core69/native2 PASS; lead independent
+affected/native106/106. `478-observer-removal-verification.md` records scope
+and actual reports. No new deviation, retail algorithm or FPS edit. Fresh
+integrated Release and unchanged final graphical repeats remain next.
diff --git a/src/AcDream.App/Composition/FrameRootComposition.cs b/src/AcDream.App/Composition/FrameRootComposition.cs
index dfdd4ff3f..1bb087282 100644
--- a/src/AcDream.App/Composition/FrameRootComposition.cs
+++ b/src/AcDream.App/Composition/FrameRootComposition.cs
@@ -596,8 +596,6 @@ internal sealed class FrameRootCompositionPhase
foundation.MeshAdapter,
foundation.TextureCache,
live.DrawDispatcher,
- live.EnvCellRenderer,
- d.Lighting,
d.FrameProfiler,
content.Dats,
foundation.Residency,
diff --git a/src/AcDream.App/Diagnostics/WorldLifecycleAutomationController.cs b/src/AcDream.App/Diagnostics/WorldLifecycleAutomationController.cs
index f9aba34ef..2034fbb4b 100644
--- a/src/AcDream.App/Diagnostics/WorldLifecycleAutomationController.cs
+++ b/src/AcDream.App/Diagnostics/WorldLifecycleAutomationController.cs
@@ -2,7 +2,6 @@ using System.Text.Json;
using AcDream.App.Rendering;
using AcDream.App.Rendering.Residency;
using AcDream.App.Rendering.Scene;
-using AcDream.App.Rendering.Wb;
using AcDream.App.Streaming;
using AcDream.App.UI.Testing;
using AcDream.Core.Physics;
@@ -97,8 +96,7 @@ internal sealed record WorldLifecycleResourceSnapshot(
ResidencySnapshot Residency,
double Fps,
double FrameMilliseconds,
- string? LastFrameProfile,
- EnvCellLightingConsumptionSnapshot? EnvCellLighting = null);
+ string? LastFrameProfile);
internal sealed record WorldLifecycleCheckpoint(
int Sequence,
diff --git a/src/AcDream.App/Diagnostics/WorldLifecycleResourceSnapshotSource.cs b/src/AcDream.App/Diagnostics/WorldLifecycleResourceSnapshotSource.cs
index ec0bf46fa..e0640c1a2 100644
--- a/src/AcDream.App/Diagnostics/WorldLifecycleResourceSnapshotSource.cs
+++ b/src/AcDream.App/Diagnostics/WorldLifecycleResourceSnapshotSource.cs
@@ -7,7 +7,6 @@ using AcDream.App.Streaming;
using AcDream.App.World;
using AcDream.Content;
using AcDream.Core.Physics;
-using AcDream.Core.Lighting;
using AcDream.Core.Vfx;
using DatReaderWriter.Lib.IO;
@@ -40,8 +39,6 @@ internal sealed class WorldLifecycleResourceSnapshotSource
private readonly WbMeshAdapter? _meshes;
private readonly TextureCache _textures;
private readonly WbDrawDispatcher? _dispatcher;
- private readonly EnvCellRenderer? _envCellRenderer;
- private readonly LightManager _lighting;
private readonly FrameProfiler _frameProfiler;
private readonly IDatReaderWriter _dats;
private readonly ResidencyManager _residency;
@@ -65,8 +62,6 @@ internal sealed class WorldLifecycleResourceSnapshotSource
WbMeshAdapter? meshes,
TextureCache textures,
WbDrawDispatcher? dispatcher,
- EnvCellRenderer? envCellRenderer,
- LightManager lighting,
FrameProfiler frameProfiler,
IDatReaderWriter dats,
ResidencyManager residency,
@@ -93,8 +88,6 @@ internal sealed class WorldLifecycleResourceSnapshotSource
_meshes = meshes;
_textures = textures ?? throw new ArgumentNullException(nameof(textures));
_dispatcher = dispatcher;
- _envCellRenderer = envCellRenderer;
- _lighting = lighting ?? throw new ArgumentNullException(nameof(lighting));
_frameProfiler = frameProfiler
?? throw new ArgumentNullException(nameof(frameProfiler));
_dats = dats ?? throw new ArgumentNullException(nameof(dats));
@@ -163,10 +156,6 @@ internal sealed class WorldLifecycleResourceSnapshotSource
ParticleOwners: _particleSink.TrackedOwnerCount,
EffectOwners: _effects.ReadyOwnerCount,
LightOwners: _lights.TrackedOwnerCount,
- EnvCellLighting: CaptureEnvCellLighting(
- outcome,
- _envCellRenderer,
- _lighting),
ScriptOwners: _scripts.ActiveOwnerCount,
ActiveScripts: _scripts.ActiveScriptCount,
MeshRenderData: mesh.RenderData,
@@ -215,16 +204,4 @@ internal sealed class WorldLifecycleResourceSnapshotSource
LastFrameProfile: _frameProfiler.LastReport);
}
- internal static EnvCellLightingConsumptionSnapshot? CaptureEnvCellLighting(
- RenderFrameOutcome outcome,
- EnvCellRenderer? renderer,
- LightManager lighting)
- {
- ArgumentNullException.ThrowIfNull(lighting);
- return renderer?.CaptureLightingConsumption(
- lighting,
- currentWorldFrame:
- !outcome.SkippedZeroArea
- && outcome.World.NormalWorldDrawn);
- }
}
diff --git a/src/AcDream.App/Rendering/Wb/EnvCellRenderer.LightingObservation.cs b/src/AcDream.App/Rendering/Wb/EnvCellRenderer.LightingObservation.cs
deleted file mode 100644
index 00d1bc90b..000000000
--- a/src/AcDream.App/Rendering/Wb/EnvCellRenderer.LightingObservation.cs
+++ /dev/null
@@ -1,178 +0,0 @@
-using AcDream.Core.Lighting;
-
-namespace AcDream.App.Rendering.Wb;
-
-internal sealed record EnvCellLightingVectorBits(
- uint X,
- uint Y,
- uint Z);
-
-internal sealed record EnvCellLightingAmbientSnapshot(
- EnvCellLightingVectorBits AmbientColor,
- EnvCellLightingVectorBits SunColor,
- EnvCellLightingVectorBits SunDirection);
-
-internal sealed record EnvCellLightingSourceSnapshot(
- int Index,
- uint OwnerId,
- uint CellId,
- int Kind,
- bool IsDynamic,
- bool IsLit,
- bool TracksOwnerPose,
- EnvCellLightingVectorBits RankingOrigin,
- EnvCellLightingVectorBits Position,
- EnvCellLightingVectorBits Forward,
- EnvCellLightingVectorBits Color,
- uint Intensity,
- uint Range,
- uint ConeAngle,
- uint DistSq,
- uint[] LocalPose);
-
-internal sealed record EnvCellLightingCellSetSnapshot(
- uint CellId,
- int[] Indices);
-
-///
-/// Checkpoint-only copy of the point-light inputs the EnvCell RHI submission
-/// already consumed. PackedGlobalLightBits is the authoritative binding-4
-/// payload; Sources records its identity/provenance and CellSets records the
-/// exact cached 47-index values copied to binding 5.
-///
-internal sealed record EnvCellLightingConsumptionSnapshot(
- bool HasCurrentEnvCellInput,
- int FrameGeneration,
- int FrameSlot,
- int RegisteredLightCount,
- int LogicalGlobalLightCount,
- int UploadedGlobalLightCount,
- uint[] PackedGlobalLightBits,
- EnvCellLightingSourceSnapshot[] Sources,
- EnvCellLightingCellSetSnapshot[] CellSets,
- EnvCellLightingAmbientSnapshot Ambient);
-
-public sealed partial class EnvCellRenderer
-{
- ///
- /// Copies the retained inputs from this frame's completed EnvCell submission
- /// for an explicitly requested lifecycle checkpoint. This method never
- /// rebuilds a light snapshot or invokes either cell selector.
- ///
- internal EnvCellLightingConsumptionSnapshot CaptureLightingConsumption(
- LightManager lighting,
- bool currentWorldFrame)
- {
- ArgumentNullException.ThrowIfNull(lighting);
-
- EnvCellLightingAmbientSnapshot ambient = CaptureAmbient(
- lighting.CurrentAmbient);
- if (!currentWorldFrame)
- return EmptyLightingConsumption(lighting.RegisteredCount, ambient);
-
- List> currentCells = [];
- foreach (KeyValuePair entry in _cellLightSetCache)
- {
- if (entry.Value.FrameGeneration == _lightFrameGeneration)
- currentCells.Add(entry);
- }
-
- currentCells.Sort(static (left, right) => left.Key.CompareTo(right.Key));
- if (currentCells.Count == 0)
- return EmptyLightingConsumption(lighting.RegisteredCount, ambient);
-
- IReadOnlyList? pointSnapshot = _pointSnapshot;
- int logicalCount = pointSnapshot?.Count ?? 0;
- int uploadedCount = Math.Max(logicalCount, 1);
- int packedFloatCount = checked(
- uploadedCount * GlobalLightPacker.FloatsPerLight);
- var packedBits = new uint[packedFloatCount];
- for (int index = 0; index < packedFloatCount; index++)
- packedBits[index] = Bits(_globalLightData[index]);
-
- var sources = new EnvCellLightingSourceSnapshot[logicalCount];
- for (int index = 0; index < logicalCount; index++)
- sources[index] = CaptureSource(index, pointSnapshot![index]);
-
- var cellSets = new EnvCellLightingCellSetSnapshot[currentCells.Count];
- for (int index = 0; index < currentCells.Count; index++)
- {
- KeyValuePair entry = currentCells[index];
- cellSets[index] = new EnvCellLightingCellSetSnapshot(
- entry.Key,
- (int[])entry.Value.Indices.Clone());
- }
-
- return new EnvCellLightingConsumptionSnapshot(
- HasCurrentEnvCellInput: true,
- FrameGeneration: _lightFrameGeneration,
- FrameSlot: _dynamicFrameSlot,
- RegisteredLightCount: lighting.RegisteredCount,
- LogicalGlobalLightCount: logicalCount,
- UploadedGlobalLightCount: uploadedCount,
- PackedGlobalLightBits: packedBits,
- Sources: sources,
- CellSets: cellSets,
- Ambient: ambient);
- }
-
- private EnvCellLightingConsumptionSnapshot EmptyLightingConsumption(
- int registeredLightCount,
- EnvCellLightingAmbientSnapshot ambient) =>
- new(
- HasCurrentEnvCellInput: false,
- FrameGeneration: _lightFrameGeneration,
- FrameSlot: _dynamicFrameSlot,
- RegisteredLightCount: registeredLightCount,
- LogicalGlobalLightCount: 0,
- UploadedGlobalLightCount: 0,
- PackedGlobalLightBits: [],
- Sources: [],
- CellSets: [],
- Ambient: ambient);
-
- private static EnvCellLightingSourceSnapshot CaptureSource(
- int index,
- LightSource source) =>
- new(
- Index: index,
- OwnerId: source.OwnerId,
- CellId: source.CellId,
- Kind: (int)source.Kind,
- IsDynamic: source.IsDynamic,
- IsLit: source.IsLit,
- TracksOwnerPose: source.TracksOwnerPose,
- RankingOrigin: CaptureVector(source.RankingOrigin),
- Position: CaptureVector(source.WorldPosition),
- Forward: CaptureVector(source.WorldForward),
- Color: CaptureVector(source.ColorLinear),
- Intensity: Bits(source.Intensity),
- Range: Bits(source.Range),
- ConeAngle: Bits(source.ConeAngle),
- DistSq: Bits(source.DistSq),
- LocalPose:
- [
- Bits(source.LocalPose.M11), Bits(source.LocalPose.M12),
- Bits(source.LocalPose.M13), Bits(source.LocalPose.M14),
- Bits(source.LocalPose.M21), Bits(source.LocalPose.M22),
- Bits(source.LocalPose.M23), Bits(source.LocalPose.M24),
- Bits(source.LocalPose.M31), Bits(source.LocalPose.M32),
- Bits(source.LocalPose.M33), Bits(source.LocalPose.M34),
- Bits(source.LocalPose.M41), Bits(source.LocalPose.M42),
- Bits(source.LocalPose.M43), Bits(source.LocalPose.M44),
- ]);
-
- private static EnvCellLightingAmbientSnapshot CaptureAmbient(
- CellAmbientState ambient) =>
- new(
- CaptureVector(ambient.AmbientColor),
- CaptureVector(ambient.SunColor),
- CaptureVector(ambient.SunDirection));
-
- private static EnvCellLightingVectorBits CaptureVector(
- System.Numerics.Vector3 value) =>
- new(Bits(value.X), Bits(value.Y), Bits(value.Z));
-
- private static uint Bits(float value) =>
- BitConverter.SingleToUInt32Bits(value);
-}
diff --git a/tests/AcDream.App.Tests/Composition/FrameRootCompositionTests.cs b/tests/AcDream.App.Tests/Composition/FrameRootCompositionTests.cs
index 0ebe0959b..d70ff2347 100644
--- a/tests/AcDream.App.Tests/Composition/FrameRootCompositionTests.cs
+++ b/tests/AcDream.App.Tests/Composition/FrameRootCompositionTests.cs
@@ -7,7 +7,6 @@ using AcDream.App.Rendering.Wb;
using AcDream.App.Tests.Architecture;
using AcDream.App.Update;
using AcDream.App.World;
-using AcDream.Core.Lighting;
namespace AcDream.App.Tests.Composition;
@@ -78,52 +77,6 @@ public sealed class FrameRootCompositionTests
Assert.True(secondTransfer > firstTransfer);
}
- [Fact]
- public void ProductionLightingCheckpointReceivesCanonicalRendererAndManager()
- {
- MethodInfo compose = typeof(FrameRootCompositionPhase).GetMethod(
- "ComposeCore",
- BindingFlags.Instance | BindingFlags.NonPublic)
- ?? throw new MissingMethodException(
- typeof(FrameRootCompositionPhase).FullName,
- "ComposeCore");
- IReadOnlyList calls = CompiledCallGraph.Read(compose);
-
- int constructor = CallIndex(
- calls,
- typeof(WorldLifecycleResourceSnapshotSource),
- ".ctor");
- int renderer = calls
- .Select((call, index) => (call, index))
- .Last(pair => pair.index < constructor
- && pair.call.Target.DeclaringType == typeof(LivePresentationResult)
- && pair.call.Target.Name == "get_EnvCellRenderer")
- .index;
- int lighting = calls
- .Select((call, index) => (call, index))
- .Last(pair => pair.index < constructor
- && pair.call.Target.DeclaringType == typeof(FrameRootDependencies)
- && pair.call.Target.Name == "get_Lighting")
- .index;
- int dispatcher = calls
- .Select((call, index) => (call, index))
- .Last(pair => pair.index < constructor
- && pair.call.Target.DeclaringType == typeof(LivePresentationResult)
- && pair.call.Target.Name == "get_DrawDispatcher")
- .index;
- int profiler = calls
- .Select((call, index) => (call, index))
- .Last(pair => pair.index < constructor
- && pair.call.Target.DeclaringType == typeof(FrameRootDependencies)
- && pair.call.Target.Name == "get_FrameProfiler")
- .index;
-
- Assert.Equal(dispatcher + 1, renderer);
- Assert.Equal(renderer + 1, lighting);
- Assert.Equal(lighting + 1, profiler);
- Assert.True(profiler < constructor);
- }
-
[Fact]
public void ProductionWiresTheOptionalScreenshotAdapterIntoTheOuterRenderOwner()
{
@@ -196,19 +149,6 @@ public sealed class FrameRootCompositionTests
BindingFlags.Instance | BindingFlags.NonPublic),
field => field.FieldType == typeof(GameWindow));
- FieldInfo[] snapshotFields = typeof(WorldLifecycleResourceSnapshotSource)
- .GetFields(BindingFlags.Instance | BindingFlags.NonPublic);
- Assert.Contains(snapshotFields, field => field.FieldType == typeof(EnvCellRenderer));
- Assert.Contains(snapshotFields, field => field.FieldType == typeof(LightManager));
- ParameterInfo[] snapshotParameters = typeof(WorldLifecycleResourceSnapshotSource)
- .GetConstructors().Single().GetParameters();
- Assert.Contains(snapshotParameters,
- parameter => parameter.ParameterType == typeof(EnvCellRenderer)
- && !parameter.HasDefaultValue);
- Assert.Contains(snapshotParameters,
- parameter => parameter.ParameterType == typeof(LightManager)
- && !parameter.HasDefaultValue);
-
MethodInfo capture = typeof(WorldLifecycleResourceSnapshotSource)
.GetMethod(nameof(WorldLifecycleResourceSnapshotSource.Capture))!;
Assert.Equal(
@@ -227,12 +167,6 @@ public sealed class FrameRootCompositionTests
calls,
call => call.Target.DeclaringType == typeof(FrameProfiler)
&& call.Target.Name == "get_LastReport");
- Assert.Contains(
- calls,
- call => call.Target.DeclaringType
- == typeof(WorldLifecycleResourceSnapshotSource)
- && call.Target.Name == nameof(
- WorldLifecycleResourceSnapshotSource.CaptureEnvCellLighting));
Assert.Contains(
calls,
call => call.Target.DeclaringType == typeof(WorldRenderFrameOutcome)
diff --git a/tests/AcDream.App.Tests/Diagnostics/WorldLifecycleAutomationControllerTests.cs b/tests/AcDream.App.Tests/Diagnostics/WorldLifecycleAutomationControllerTests.cs
index 7e41a9d19..5c6fbbec2 100644
--- a/tests/AcDream.App.Tests/Diagnostics/WorldLifecycleAutomationControllerTests.cs
+++ b/tests/AcDream.App.Tests/Diagnostics/WorldLifecycleAutomationControllerTests.cs
@@ -4,7 +4,6 @@ using AcDream.App.Rendering;
using AcDream.App.Rendering.Packs;
using AcDream.App.Rendering.Residency;
using AcDream.App.Rendering.Scene;
-using AcDream.App.Rendering.Wb;
using AcDream.App.Streaming;
using AcDream.App.UI.Testing;
using AcDream.Runtime;
@@ -413,62 +412,6 @@ public sealed class WorldLifecycleAutomationControllerTests
{
LoadedLandblocks = 1,
WorldEntities = 42,
- EnvCellLighting = new EnvCellLightingConsumptionSnapshot(
- HasCurrentEnvCellInput: true,
- FrameGeneration: 31,
- FrameSlot: 2,
- RegisteredLightCount: 214,
- LogicalGlobalLightCount: 1,
- UploadedGlobalLightCount: 1,
- PackedGlobalLightBits:
- [
- 0x3F800000u, 0x40000000u, 0x40400000u, 0x3F800000u,
- 0u, 0u, 0x3F800000u, 0x41200000u,
- 0x3DCCCCCDu, 0x3E4CCCCDu, 0x3E99999Au, 0x40000000u,
- 0u, 0x3F800000u, 0u, 0u,
- ],
- Sources:
- [
- new EnvCellLightingSourceSnapshot(
- Index: 0,
- OwnerId: 0x5000000Au,
- CellId: 0x00070143u,
- Kind: 1,
- IsDynamic: true,
- IsLit: true,
- TracksOwnerPose: true,
- RankingOrigin: new EnvCellLightingVectorBits(
- 0x3F000000u, 0x3F800000u, 0x40000000u),
- Position: new EnvCellLightingVectorBits(
- 0x3F800000u, 0x40000000u, 0x40400000u),
- Forward: new EnvCellLightingVectorBits(
- 0u, 0u, 0x3F800000u),
- Color: new EnvCellLightingVectorBits(
- 0x3DCCCCCDu, 0x3E4CCCCDu, 0x3E99999Au),
- Intensity: 0x40000000u,
- Range: 0x41200000u,
- ConeAngle: 0u,
- DistSq: 0x42F60000u,
- LocalPose:
- [
- 0x3F800000u, 0u, 0u, 0u,
- 0u, 0x3F800000u, 0u, 0u,
- 0u, 0u, 0x3F800000u, 0u,
- 0u, 0u, 0u, 0x3F800000u,
- ]),
- ],
- CellSets:
- [
- new EnvCellLightingCellSetSnapshot(
- 0x00070143u,
- [0, .. Enumerable.Repeat(-1, 46)]),
- ],
- Ambient: new EnvCellLightingAmbientSnapshot(
- new EnvCellLightingVectorBits(
- 0x3E4CCCCDu, 0x3E4CCCCDu, 0x3E4CCCCDu),
- new EnvCellLightingVectorBits(0u, 0u, 0u),
- new EnvCellLightingVectorBits(
- 0xBF800000u, 0x3F000000u, 0x3E800000u))),
RenderSceneOracle = new CurrentRenderSceneOracleSnapshot(
Enabled: true,
CompletedFrameSequence: 19,
@@ -587,28 +530,6 @@ public sealed class WorldLifecycleAutomationControllerTests
Assert.True(json.RootElement.GetProperty("render").GetProperty("presentation")
.GetProperty("screenshotCaptured").GetBoolean());
Assert.Equal(42, json.RootElement.GetProperty("resources").GetProperty("worldEntities").GetInt32());
- JsonElement envCellLighting = json.RootElement
- .GetProperty("resources")
- .GetProperty("envCellLighting");
- Assert.True(envCellLighting
- .GetProperty("hasCurrentEnvCellInput").GetBoolean());
- Assert.Equal(31, envCellLighting.GetProperty("frameGeneration").GetInt32());
- Assert.Equal(214, envCellLighting
- .GetProperty("registeredLightCount").GetInt32());
- Assert.Equal(0x3F800000u, envCellLighting
- .GetProperty("sources")[0]
- .GetProperty("position")
- .GetProperty("x").GetUInt32());
- Assert.Equal(0x00070143u, envCellLighting
- .GetProperty("cellSets")[0]
- .GetProperty("cellId").GetUInt32());
- Assert.Equal(-1, envCellLighting
- .GetProperty("cellSets")[0]
- .GetProperty("indices")[7].GetInt32());
- Assert.Equal(0xBF800000u, envCellLighting
- .GetProperty("ambient")
- .GetProperty("sunDirection")
- .GetProperty("x").GetUInt32());
JsonElement renderSceneOracle = json.RootElement
.GetProperty("resources")
.GetProperty("renderSceneOracle");
diff --git a/tests/AcDream.App.Tests/Rendering/Wb/EnvCellRendererTests.cs b/tests/AcDream.App.Tests/Rendering/Wb/EnvCellRendererTests.cs
index 50905a7e8..aedcddcb9 100644
--- a/tests/AcDream.App.Tests/Rendering/Wb/EnvCellRendererTests.cs
+++ b/tests/AcDream.App.Tests/Rendering/Wb/EnvCellRendererTests.cs
@@ -18,13 +18,11 @@
// over the same RecordingGpuDevice, plus a no-op IPreparedAssetSource.
using System.Collections.Generic;
-using System.Collections.Immutable;
using System.Numerics;
using System.Reflection;
using System.Runtime.InteropServices;
using System.Threading;
using AcDream.App.Rendering;
-using AcDream.App.Diagnostics;
using AcDream.App.Rendering.Gpu;
using AcDream.App.Rendering.Gpu.Vk;
using AcDream.App.Rendering.Wb;
@@ -40,206 +38,6 @@ namespace AcDream.App.Tests.Rendering.Wb;
public class EnvCellRendererTests
{
- [Fact]
- public void LightingCheckpoint_ReportsActualBoundInputsAndRejectsStaleFrames()
- {
- const uint landblockId = 0x8C04FFFFu;
- const uint cellA = 0x8C040101u;
- const uint cellB = 0x8C040102u;
-
- using var device = new RecordingGpuDevice();
- using var meshManager = CreateMeshManager(device);
- var frameLifetime = new GpuDeviceFrameLifetime(device);
- var scope = new VulkanWorldPassScope(sampleCount: 1);
- using var renderer = new EnvCellRenderer(
- device,
- frameLifetime,
- scope,
- meshManager,
- new WbFrustum());
-
- renderer.CommitLandblock(new EnvCellLandblockBuild(
- landblockId,
- [],
- [
- LightingShell(cellA, 1, new Vector3(-1f), new Vector3(1f)),
- LightingShell(cellB, 2, new Vector3(99f, -1f, -1f),
- new Vector3(101f, 1f, 1f)),
- ]));
-
- var lighting = new LightManager
- {
- CurrentAmbient = new CellAmbientState(
- new Vector3(0.2f, 0.3f, 0.4f),
- new Vector3(0.5f, 0.6f, 0.7f),
- new Vector3(-0.1f, 0.8f, -0.9f)),
- };
- var dynamicLight = new LightSource
- {
- Kind = LightKind.Spot,
- WorldPosition = new Vector3(50f, 60f, 70f),
- RankingOrigin = new Vector3(5f, 6f, 7f),
- WorldForward = new Vector3(0.25f, -0.5f, 0.75f),
- ColorLinear = new Vector3(0.11f, 0.22f, 0.33f),
- Intensity = 4.5f,
- Range = 1.25f,
- ConeAngle = 0.625f,
- OwnerId = 0x50000001u,
- CellId = cellA,
- IsDynamic = true,
- IsLit = true,
- TracksOwnerPose = true,
- DistSq = 123.5f,
- LocalPose = new Matrix4x4(
- 1f, 2f, 3f, 4f,
- 5f, 6f, 7f, 8f,
- 9f, 10f, 11f, 12f,
- 13f, 14f, 15f, 16f),
- };
- var staticA = new LightSource
- {
- Kind = LightKind.Point,
- WorldPosition = Vector3.Zero,
- RankingOrigin = Vector3.Zero,
- WorldForward = Vector3.UnitX,
- ColorLinear = new Vector3(0.9f, 0.1f, 0.2f),
- Intensity = 2f,
- Range = 4f,
- OwnerId = 0x50000002u,
- CellId = cellA,
- IsLit = true,
- };
- var staticB = new LightSource
- {
- Kind = LightKind.Point,
- WorldPosition = new Vector3(100f, 0f, 0f),
- RankingOrigin = new Vector3(100f, 0f, 0f),
- WorldForward = Vector3.UnitY,
- ColorLinear = new Vector3(0.2f, 0.8f, 0.4f),
- Intensity = 3f,
- Range = 4f,
- OwnerId = 0x50000003u,
- CellId = cellB,
- IsLit = true,
- };
- lighting.Register(dynamicLight);
- lighting.Register(staticA);
- lighting.Register(staticB);
- lighting.BuildPointLightSnapshot(Vector3.Zero);
- renderer.SetPointSnapshot(lighting.PointSnapshot);
-
- frameLifetime.BeginFrame();
- IGpuFrame frame = frameLifetime.CurrentFrame!;
- using IGpuPassEncoder pass = frame.BeginPass(
- GpuPassDescription.BackbufferClear(
- "envcell-lighting-observation",
- Vector4.Zero,
- sampleCount: 1));
- using IDisposable publication = scope.Publish(pass);
- renderer.BeginFrame(frame.SlotIndex);
- SeedLightingSubmission(renderer, instanceCount: 2);
-
- // Deliberately submit in the opposite order from deterministic JSON's
- // outer cell-id ordering. Each exported set must still match the exact
- // binding-5 block associated with its submitted cell.
- var instances = new List
- {
- new() { Transform = Matrix4x4.Identity, CellId = cellB },
- new() { Transform = Matrix4x4.Identity, CellId = cellA },
- };
- device.Clear();
- InvokeSubmitRhi(renderer, instances, instanceCount: 2);
-
- EnvCellLightingConsumptionSnapshot snapshot =
- renderer.CaptureLightingConsumption(lighting, currentWorldFrame: true);
- Assert.True(snapshot.HasCurrentEnvCellInput);
- Assert.Equal(frame.SlotIndex, snapshot.FrameSlot);
- Assert.Equal(3, snapshot.RegisteredLightCount);
- Assert.Equal(3, snapshot.LogicalGlobalLightCount);
- Assert.Equal(3, snapshot.UploadedGlobalLightCount);
- Assert.Equal([cellA, cellB], snapshot.CellSets.Select(set => set.CellId));
-
- GpuRecordedStorageBind globalBind = Assert.Single(
- device.Calls.OfType(),
- bind => bind.Binding == GpuBindingModel.StorageGlobalLights);
- uint[] actualGlobalBits = MemoryMarshal.Cast(
- device.RingBytes.Slice(
- (int)globalBind.OffsetBytes,
- (int)globalBind.SizeBytes)).ToArray();
- Assert.Equal(actualGlobalBits, snapshot.PackedGlobalLightBits);
-
- GpuRecordedStorageBind setBind = Assert.Single(
- device.Calls.OfType(),
- bind => bind.Binding == GpuBindingModel.StorageInstanceLightSets);
- int[] actualSets = MemoryMarshal.Cast(
- device.RingBytes.Slice(
- (int)setBind.OffsetBytes,
- (int)setBind.SizeBytes)).ToArray();
- Assert.Equal(
- actualSets.AsSpan(0, LightManager.MaxLightsPerEnvCell).ToArray(),
- Assert.Single(snapshot.CellSets, set => set.CellId == cellB).Indices);
- Assert.Equal(
- actualSets.AsSpan(
- LightManager.MaxLightsPerEnvCell,
- LightManager.MaxLightsPerEnvCell).ToArray(),
- Assert.Single(snapshot.CellSets, set => set.CellId == cellA).Indices);
- int[] expectedSet = [0, 1, 2, .. Enumerable.Repeat(-1, 44)];
- Assert.Equal(expectedSet,
- Assert.Single(snapshot.CellSets, set => set.CellId == cellA).Indices);
- Assert.Equal(expectedSet,
- Assert.Single(snapshot.CellSets, set => set.CellId == cellB).Indices);
-
- EnvCellLightingSourceSnapshot source = snapshot.Sources[0];
- Assert.Equal(0, source.Index);
- Assert.Equal(dynamicLight.OwnerId, source.OwnerId);
- Assert.Equal(dynamicLight.CellId, source.CellId);
- Assert.Equal((int)LightKind.Spot, source.Kind);
- Assert.True(source.IsDynamic);
- Assert.True(source.IsLit);
- Assert.True(source.TracksOwnerPose);
- Assert.Equal(BitConverter.SingleToUInt32Bits(5f), source.RankingOrigin.X);
- Assert.Equal(BitConverter.SingleToUInt32Bits(50f), source.Position.X);
- Assert.Equal(BitConverter.SingleToUInt32Bits(-0.5f), source.Forward.Y);
- Assert.Equal(BitConverter.SingleToUInt32Bits(0.33f), source.Color.Z);
- Assert.Equal(BitConverter.SingleToUInt32Bits(16f), source.LocalPose[15]);
-
- RenderFrameOutcome nonWorld = new(
- new WorldRenderFrameOutcome(0, 0, NormalWorldDrawn: false),
- default);
- AssertNoCurrentLighting(WorldLifecycleResourceSnapshotSource
- .CaptureEnvCellLighting(nonWorld, renderer, lighting)!);
- AssertNoCurrentLighting(WorldLifecycleResourceSnapshotSource
- .CaptureEnvCellLighting(RenderFrameOutcome.ZeroArea, renderer, lighting)!);
-
- // A real zero-light submission still binds one all-zero dummy light.
- var emptyLighting = new LightManager();
- renderer.BeginFrame(frame.SlotIndex);
- renderer.SetPointSnapshot(emptyLighting.PointSnapshot);
- device.Clear();
- InvokeSubmitRhi(renderer, [instances[0]], instanceCount: 1);
- EnvCellLightingConsumptionSnapshot empty =
- renderer.CaptureLightingConsumption(emptyLighting, currentWorldFrame: true);
- Assert.True(empty.HasCurrentEnvCellInput);
- Assert.Equal(0, empty.LogicalGlobalLightCount);
- Assert.Equal(1, empty.UploadedGlobalLightCount);
- Assert.Equal(GlobalLightPacker.FloatsPerLight, empty.PackedGlobalLightBits.Length);
- Assert.All(empty.PackedGlobalLightBits, bits => Assert.Equal(0u, bits));
- GpuRecordedStorageBind emptyGlobalBind = Assert.Single(
- device.Calls.OfType(),
- bind => bind.Binding == GpuBindingModel.StorageGlobalLights);
- Assert.Equal(
- MemoryMarshal.Cast(device.RingBytes.Slice(
- (int)emptyGlobalBind.OffsetBytes,
- (int)emptyGlobalBind.SizeBytes)).ToArray(),
- empty.PackedGlobalLightBits);
-
- // BeginFrame advances the existing cache generation. Without a draw,
- // the following checkpoint cannot reuse the preceding frame's cell set.
- renderer.BeginFrame(frame.SlotIndex);
- AssertNoCurrentLighting(
- renderer.CaptureLightingConsumption(emptyLighting, currentWorldFrame: true));
- }
-
[Fact]
public void SubmitRhi_BindsTwoCompleteFortySevenIndexCellSetsInCurrentGeneration()
{
@@ -850,23 +648,6 @@ public class EnvCellRendererTests
Assert.Empty(b);
}
- private static EnvCellShellPlacement LightingShell(
- uint cellId,
- ulong geometryId,
- Vector3 minimum,
- Vector3 maximum) =>
- new(
- CellId: cellId,
- GeometryId: geometryId,
- EnvironmentId: 0x0D000001u,
- CellStructure: 1,
- Surfaces: ImmutableArray.Empty,
- WorldPosition: Vector3.Zero,
- Rotation: Quaternion.Identity,
- Transform: Matrix4x4.Identity,
- LocalBounds: new WbBoundingBox(minimum, maximum),
- WorldBounds: new WbBoundingBox(minimum, maximum));
-
private static void SeedLightingSubmission(
EnvCellRenderer renderer,
int instanceCount)
@@ -918,17 +699,6 @@ public class EnvCellRendererTests
});
}
- private static void AssertNoCurrentLighting(
- EnvCellLightingConsumptionSnapshot snapshot)
- {
- Assert.False(snapshot.HasCurrentEnvCellInput);
- Assert.Equal(0, snapshot.LogicalGlobalLightCount);
- Assert.Equal(0, snapshot.UploadedGlobalLightCount);
- Assert.Empty(snapshot.PackedGlobalLightBits);
- Assert.Empty(snapshot.Sources);
- Assert.Empty(snapshot.CellSets);
- }
-
// -----------------------------------------------------------------------
// Prepare gate (2026-07-24) — pure camera-tolerance half.
// The tolerance must swallow the ~36 µm eye rest jitter (RetailPViewRenderer