test(overhaul): integrate reviewed consumed-light checkpoint observation
This commit is contained in:
parent
ad866b7f65
commit
59b0b6e72f
9 changed files with 703 additions and 1 deletions
|
|
@ -0,0 +1,71 @@
|
|||
# #478 checkpoint observation — lead verification
|
||||
|
||||
Contract6c3ffd79e. OpenAI return in `s5-478-light-observation`, seven files,
|
||||
no shader/RHI/selector/runtime changes. This is an observation, not a lighting
|
||||
repair, a retail-parity closure, an FPS result or G4 acceptance.
|
||||
|
||||
## Independent checks
|
||||
|
||||
Lead read the complete source/test delta and the actual consumption chain:
|
||||
WorldRenderFrameBuilder568–570 produces and lends one PointSnapshot;
|
||||
EnvCellRenderer.GetCellLightSet has only SubmitRhi as production caller;
|
||||
SubmitRhi copies that set to binding5 and its packed snapshot to binding4.
|
||||
Both arrays remain retained. RenderFrameResourceController begins the existing
|
||||
EnvCell generation every opened frame. RenderFrameOrchestrator does not run
|
||||
postdiagnostics after failure, and zero-area returns before opening a frame.
|
||||
Source gating rejects zero-area/non-world outcomes; generation filtering
|
||||
rejects prior-frame cell sets. Private entity viewports do not change the
|
||||
EnvCell buffers. No selector, packer, or snapshot rebuild occurs on capture.
|
||||
|
||||
The added fixture compares actual RecordingGpuDevice ring bytes, not a second
|
||||
packing/selection oracle. Two nonuniform cells are submitted in reverse order
|
||||
to the output's sorted cell IDs; their distinct eight-index sets still match
|
||||
their own binding5 slices. Binding4 is compared bit-for-bit, including the
|
||||
one-zero-light dummy upload. Copied source identity, scalar/vector bits,
|
||||
local pose, ambient and checkpoint JSON are covered. These observations prove
|
||||
CPU-submitted inputs, not GPU execution, a particular vertex contribution, or
|
||||
pixel brightness. Copies/allocations occur only at requested checkpoints.
|
||||
|
||||
During implementation the lead required the contract's optional appended DTO
|
||||
field and tightened the canonical composition witness: mere parameter types
|
||||
or any earlier getter call are not an argument-ownership proof. The final IL
|
||||
test pins adjacent DrawDispatcher→EnvCellRenderer→Lighting→FrameProfiler
|
||||
getter calls at the actual constructor invocation.
|
||||
|
||||
Lead actual negative controls, together in one targeted run:
|
||||
|
||||
- Replacing only the composed EnvCellRenderer argument with null fails the
|
||||
canonical-argument test (expected getter index217, actual153).
|
||||
- Removing the current-generation filter fails the no-draw stale-frame
|
||||
assertion (HasCurrentEnvCellInput expectedfalse, actualtrue).
|
||||
|
||||
Both failed as intended, 0 passed/2 failed; exact raw source hashes restored.
|
||||
Then affected Release tests: **60/60 passed, zero skips/failures**. No clients
|
||||
were running before either invocation. Agent's final full Release build:
|
||||
0 warnings/errors; agent focused60/60. Fresh scratch initially had missing
|
||||
assets (NETSDK1004), restored successfully before compilation; not a code bug.
|
||||
|
||||
## Exact return / test seals
|
||||
|
||||
Paths below relative to the scratch worktree:
|
||||
|
||||
| File | SHA256 |
|
||||
| --- | --- |
|
||||
| src/AcDream.App/Composition/FrameRootComposition.cs | DF536DC96FB075792D3E1E14A2022BB19AAB9546FB0E2860A3E4D86A68524071 |
|
||||
| src/AcDream.App/Diagnostics/WorldLifecycleAutomationController.cs | D23C952AC064A54D027A9EC20F10611CA3232F48C63150789AB53E685EA0F0E0 |
|
||||
| src/AcDream.App/Diagnostics/WorldLifecycleResourceSnapshotSource.cs | 413DBF21A466B399B97E90DC87A05CFAC9CA3696708C3871524CA799558DAEF9 |
|
||||
| src/AcDream.App/Rendering/Wb/EnvCellRenderer.LightingObservation.cs | 13761B2248291A28B408BCA6F7F67A1C2CDCA860B62ECDB90914F8FD2DD53FAD |
|
||||
| tests/AcDream.App.Tests/Composition/FrameRootCompositionTests.cs | 6E6A87490A87FCCD377AC659B03237FF90F90841A53D2FD3DCCA029A8241CD2A |
|
||||
| tests/AcDream.App.Tests/Diagnostics/WorldLifecycleAutomationControllerTests.cs | F58D2E13578F78EBBE3F3F03EC22F377465F5FD1CB8C3C395AB862554DA7E205 |
|
||||
| tests/AcDream.App.Tests/Rendering/Wb/EnvCellRendererTests.cs | 35ABB9273F741C699B7623248FF0A74072F498C8CD7A8C2A9B7551978F26FB15 |
|
||||
| artifacts/478-consumed-light/focused-final2/focused-final2.trx | 6E59ABCF93AA56013EA25A6D4D04114118C21EA2D76494ED29BCF9E9A16DEB08 |
|
||||
| artifacts/478-consumed-light/root-mutations/root-mutations.trx | 4E9D5B378D7160567C9A14C45DE940EAA9C5EF770AE8220CDFA853DBD3AEC921 |
|
||||
| artifacts/478-consumed-light/root-restored60/root-restored60.trx | 57E74670066B34721BF30E9349D83FF355C4AA7CCFFA931383B53A4BADAB9761 |
|
||||
|
||||
Wegener's one narrow production/lifetime review PASS, no blockers; no builds
|
||||
or tests by the reviewer. Lead committed the exact seven-file return as
|
||||
`41128d8164821fd186a1e2d36e042e2dea425270` and integrates those blobs unchanged.
|
||||
No new retail deviation is introduced by this diagnostic. Next: fresh
|
||||
integrated Release and exact-binary Town Network High/off/High with all PNGs retained.
|
||||
Remove temporary observation with the eventual evidence-backed #478 repair.
|
||||
FPS/#481/C2 remain deferred. No main merge; G4FAIL.
|
||||
|
|
@ -4917,3 +4917,10 @@ and JSON serialization are the tests; no new selector or per-frame recorder.
|
|||
No lighting fix or FPS work. Register AP-16/AP-35/AP-85 remains unchanged.
|
||||
Lead owns the contract/integration; one OpenAI implementer and one narrow
|
||||
production/lifetime review. The next live result determines the repair scope.
|
||||
|
||||
Seven-file return41128d816 accepted: agent full Release0W0E/60PASS;
|
||||
root both actual wiring/generation mutations fail, exact restore/60PASS;
|
||||
Wegener narrow production/lifetime PASS/no blockers. Lead verification and
|
||||
seals in478-observation-lead-verification.md. No per-frame path/algorithm
|
||||
change or new divergence. Integrate exact blobs, then fresh build/tests and
|
||||
unchanged Town Network run. FPS deferral and G4FAIL remain.
|
||||
|
|
|
|||
|
|
@ -596,6 +596,8 @@ internal sealed class FrameRootCompositionPhase
|
|||
foundation.MeshAdapter,
|
||||
foundation.TextureCache,
|
||||
live.DrawDispatcher,
|
||||
live.EnvCellRenderer,
|
||||
d.Lighting,
|
||||
d.FrameProfiler,
|
||||
content.Dats,
|
||||
foundation.Residency,
|
||||
|
|
|
|||
|
|
@ -2,6 +2,7 @@ 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;
|
||||
|
|
@ -96,7 +97,8 @@ internal sealed record WorldLifecycleResourceSnapshot(
|
|||
ResidencySnapshot Residency,
|
||||
double Fps,
|
||||
double FrameMilliseconds,
|
||||
string? LastFrameProfile);
|
||||
string? LastFrameProfile,
|
||||
EnvCellLightingConsumptionSnapshot? EnvCellLighting = null);
|
||||
|
||||
internal sealed record WorldLifecycleCheckpoint(
|
||||
int Sequence,
|
||||
|
|
|
|||
|
|
@ -7,6 +7,7 @@ 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;
|
||||
|
||||
|
|
@ -39,6 +40,8 @@ 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;
|
||||
|
|
@ -62,6 +65,8 @@ internal sealed class WorldLifecycleResourceSnapshotSource
|
|||
WbMeshAdapter? meshes,
|
||||
TextureCache textures,
|
||||
WbDrawDispatcher? dispatcher,
|
||||
EnvCellRenderer? envCellRenderer,
|
||||
LightManager lighting,
|
||||
FrameProfiler frameProfiler,
|
||||
IDatReaderWriter dats,
|
||||
ResidencyManager residency,
|
||||
|
|
@ -88,6 +93,8 @@ 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));
|
||||
|
|
@ -156,6 +163,10 @@ 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,
|
||||
|
|
@ -203,4 +214,17 @@ internal sealed class WorldLifecycleResourceSnapshotSource
|
|||
FrameMilliseconds: render.FrameMilliseconds,
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,176 @@
|
|||
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 Position,
|
||||
EnvCellLightingVectorBits Forward,
|
||||
EnvCellLightingVectorBits Color,
|
||||
uint Intensity,
|
||||
uint Range,
|
||||
uint ConeAngle,
|
||||
uint DistSq,
|
||||
uint[] LocalPose);
|
||||
|
||||
internal sealed record EnvCellLightingCellSetSnapshot(
|
||||
uint CellId,
|
||||
int[] Indices);
|
||||
|
||||
/// <summary>
|
||||
/// 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 eight-index values copied to binding 5.
|
||||
/// </summary>
|
||||
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
|
||||
{
|
||||
/// <summary>
|
||||
/// 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.
|
||||
/// </summary>
|
||||
internal EnvCellLightingConsumptionSnapshot CaptureLightingConsumption(
|
||||
LightManager lighting,
|
||||
bool currentWorldFrame)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(lighting);
|
||||
|
||||
EnvCellLightingAmbientSnapshot ambient = CaptureAmbient(
|
||||
lighting.CurrentAmbient);
|
||||
if (!currentWorldFrame)
|
||||
return EmptyLightingConsumption(lighting.RegisteredCount, ambient);
|
||||
|
||||
List<KeyValuePair<uint, CachedCellLightSet>> currentCells = [];
|
||||
foreach (KeyValuePair<uint, CachedCellLightSet> 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<LightSource>? 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<uint, CachedCellLightSet> 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,
|
||||
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);
|
||||
}
|
||||
|
|
@ -7,6 +7,7 @@ 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;
|
||||
|
||||
|
|
@ -77,6 +78,52 @@ 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<CompiledCall> 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()
|
||||
{
|
||||
|
|
@ -149,6 +196,19 @@ 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(
|
||||
|
|
@ -167,6 +227,12 @@ 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)
|
||||
|
|
|
|||
|
|
@ -4,6 +4,7 @@ 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;
|
||||
|
|
@ -412,6 +413,60 @@ 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,
|
||||
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, -1, -1, -1, -1, -1, -1, -1]),
|
||||
],
|
||||
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,
|
||||
|
|
@ -530,6 +585,28 @@ 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");
|
||||
|
|
|
|||
|
|
@ -18,16 +18,19 @@
|
|||
// 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;
|
||||
using AcDream.App.Tests.Rendering.Gpu;
|
||||
using AcDream.Content;
|
||||
using AcDream.Core.Lighting;
|
||||
using AcDream.Core.Meshing;
|
||||
using Microsoft.Extensions.Logging.Abstractions;
|
||||
using Xunit;
|
||||
|
|
@ -37,6 +40,201 @@ 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),
|
||||
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,
|
||||
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),
|
||||
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<InstanceData>
|
||||
{
|
||||
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<GpuRecordedStorageBind>(),
|
||||
bind => bind.Binding == GpuBindingModel.StorageGlobalLights);
|
||||
uint[] actualGlobalBits = MemoryMarshal.Cast<byte, uint>(
|
||||
device.RingBytes.Slice(
|
||||
(int)globalBind.OffsetBytes,
|
||||
(int)globalBind.SizeBytes)).ToArray();
|
||||
Assert.Equal(actualGlobalBits, snapshot.PackedGlobalLightBits);
|
||||
|
||||
GpuRecordedStorageBind setBind = Assert.Single(
|
||||
device.Calls.OfType<GpuRecordedStorageBind>(),
|
||||
bind => bind.Binding == GpuBindingModel.StorageInstanceLightSets);
|
||||
int[] actualSets = MemoryMarshal.Cast<byte, int>(
|
||||
device.RingBytes.Slice(
|
||||
(int)setBind.OffsetBytes,
|
||||
(int)setBind.SizeBytes)).ToArray();
|
||||
Assert.Equal(
|
||||
actualSets.AsSpan(0, LightManager.MaxLightsPerObject).ToArray(),
|
||||
Assert.Single(snapshot.CellSets, set => set.CellId == cellB).Indices);
|
||||
Assert.Equal(
|
||||
actualSets.AsSpan(
|
||||
LightManager.MaxLightsPerObject,
|
||||
LightManager.MaxLightsPerObject).ToArray(),
|
||||
Assert.Single(snapshot.CellSets, set => set.CellId == cellA).Indices);
|
||||
Assert.Equal([0, 1, -1, -1, -1, -1, -1, -1],
|
||||
Assert.Single(snapshot.CellSets, set => set.CellId == cellA).Indices);
|
||||
Assert.Equal([0, 2, -1, -1, -1, -1, -1, -1],
|
||||
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(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<GpuRecordedStorageBind>(),
|
||||
bind => bind.Binding == GpuBindingModel.StorageGlobalLights);
|
||||
Assert.Equal(
|
||||
MemoryMarshal.Cast<byte, uint>(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));
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData(CullMode.Landblock)]
|
||||
[InlineData(CullMode.None)]
|
||||
|
|
@ -555,6 +753,85 @@ 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<ushort>.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)
|
||||
{
|
||||
Type rendererType = typeof(EnvCellRenderer);
|
||||
rendererType.GetField(
|
||||
"_commands", BindingFlags.NonPublic | BindingFlags.Instance)!
|
||||
.SetValue(renderer, new[]
|
||||
{
|
||||
new DrawElementsIndirectCommand
|
||||
{
|
||||
Count = 3,
|
||||
InstanceCount = (uint)instanceCount,
|
||||
FirstIndex = 0,
|
||||
BaseVertex = 0,
|
||||
BaseInstance = 0,
|
||||
},
|
||||
});
|
||||
rendererType.GetField(
|
||||
"_modernBatches", BindingFlags.NonPublic | BindingFlags.Instance)!
|
||||
.SetValue(renderer, new ModernBatchData[] { default });
|
||||
var ranges = (List<EnvCellRenderer.MdiDrawRange>)rendererType.GetField(
|
||||
"_mdiDrawRanges", BindingFlags.NonPublic | BindingFlags.Instance)!
|
||||
.GetValue(renderer)!;
|
||||
ranges.Clear();
|
||||
EnvCellRenderer.AppendMdiDrawRange(
|
||||
ranges,
|
||||
groupIndex: 0,
|
||||
firstCommand: 0,
|
||||
commandCount: 1,
|
||||
RetailSetSurfaceMaterialState.Opaque);
|
||||
}
|
||||
|
||||
private static void InvokeSubmitRhi(
|
||||
EnvCellRenderer renderer,
|
||||
List<InstanceData> instances,
|
||||
int instanceCount)
|
||||
{
|
||||
typeof(EnvCellRenderer).GetMethod(
|
||||
"SubmitRhi", BindingFlags.NonPublic | BindingFlags.Instance)!
|
||||
.Invoke(
|
||||
renderer,
|
||||
new object[]
|
||||
{
|
||||
instances,
|
||||
WbRenderPass.Opaque,
|
||||
1,
|
||||
instanceCount,
|
||||
});
|
||||
}
|
||||
|
||||
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
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue