fix(rendering): reconcile shadow roots from live runtime
Recover ordinary live roots from LiveEntityRuntime's canonical active spatial workset instead of relying on already-retained derived records. Keep attached children callback-authoritative, cache immutable selection geometry fingerprints, and pin zero-allocation retained update paths with regression tests. Release gate: 3,733 App tests / 3 skips; 8,217 complete-solution tests / 5 skips. Co-Authored-By: OpenAI Codex <codex@openai.com>
This commit is contained in:
parent
056bbd4efd
commit
91463db551
7 changed files with 272 additions and 15 deletions
|
|
@ -290,6 +290,45 @@ public sealed class ArchRenderSceneTests
|
|||
Assert.Equal(current.Bounds, stored.Bounds);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void TransformUpdateBatch_ReusesRetainedStorage()
|
||||
{
|
||||
const int count = 1_000;
|
||||
RenderSceneGeneration generation = Generation(11);
|
||||
using var scene = new ArchRenderScene(generation);
|
||||
var registrations = new RenderProjectionDelta[count];
|
||||
var updates = new RenderProjectionDelta[count];
|
||||
for (int index = 0; index < count; index++)
|
||||
{
|
||||
RenderProjectionRecord record = Record(
|
||||
(ulong)index + 1,
|
||||
1,
|
||||
RenderProjectionClass.ActiveAnimatedStatic,
|
||||
x: index);
|
||||
registrations[index] = RenderProjectionDelta.Register(
|
||||
generation,
|
||||
(ulong)index + 1,
|
||||
record);
|
||||
updates[index] = RenderProjectionDelta.Update(
|
||||
RenderProjectionDeltaKind.UpdateTransform,
|
||||
generation,
|
||||
(ulong)count + (ulong)index + 1,
|
||||
record with
|
||||
{
|
||||
Transform = new RenderTransform(
|
||||
Matrix4x4.CreateTranslation(index + 1, 0, 0)),
|
||||
});
|
||||
}
|
||||
|
||||
scene.Apply(registrations);
|
||||
long before = GC.GetAllocatedBytesForCurrentThread();
|
||||
|
||||
scene.Apply(updates);
|
||||
|
||||
long allocated = GC.GetAllocatedBytesForCurrentThread() - before;
|
||||
Assert.True(allocated == 0, $"Allocated {allocated:N0} bytes.");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Digest_IsStableAcrossRegistrationOrderAndBufferReuse()
|
||||
{
|
||||
|
|
|
|||
|
|
@ -2,6 +2,7 @@ using System.Numerics;
|
|||
using AcDream.App.Rendering;
|
||||
using AcDream.App.Rendering.Scene;
|
||||
using AcDream.App.Rendering.Wb;
|
||||
using AcDream.Core.Selection;
|
||||
using AcDream.Core.World;
|
||||
|
||||
namespace AcDream.App.Tests.Rendering;
|
||||
|
|
@ -363,6 +364,48 @@ public sealed class CurrentRenderSceneOracleTests
|
|||
Assert.Empty(oracle.DispatcherCandidates);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void SelectionGeometryFingerprint_IsCachedAndFrameStorageIsReused()
|
||||
{
|
||||
const int partCount = 1_000;
|
||||
var oracle = new CurrentRenderSceneOracle();
|
||||
var mesh = new RetailSelectionMesh(
|
||||
Vector3.Zero,
|
||||
2f,
|
||||
[new RetailSelectionPolygon(
|
||||
[
|
||||
new(-1f, -1f, 0f),
|
||||
new(1f, -1f, 0f),
|
||||
new(1f, 1f, 0f),
|
||||
new(-1f, 1f, 0f),
|
||||
],
|
||||
SingleSided: false)]);
|
||||
PublishSelectionFrame();
|
||||
long before = GC.GetAllocatedBytesForCurrentThread();
|
||||
|
||||
PublishSelectionFrame();
|
||||
|
||||
long allocated = GC.GetAllocatedBytesForCurrentThread() - before;
|
||||
Assert.Equal(partCount, oracle.Snapshot.SelectionPartCount);
|
||||
Assert.True(allocated == 0, $"Allocated {allocated:N0} bytes.");
|
||||
|
||||
void PublishSelectionFrame()
|
||||
{
|
||||
oracle.BeginSelectionFrame();
|
||||
for (int index = 0; index < partCount; index++)
|
||||
{
|
||||
oracle.ObserveSelectionPart(
|
||||
serverGuid: (uint)index + 1,
|
||||
localEntityId: (uint)index + 1,
|
||||
partIndex: index,
|
||||
gfxObjId: 0x0100_0001u,
|
||||
Matrix4x4.CreateTranslation(index, 0, 0),
|
||||
mesh);
|
||||
}
|
||||
oracle.CompleteSelectionFrame();
|
||||
}
|
||||
}
|
||||
|
||||
private static CurrentRenderProjectionFingerprint CaptureSingle(
|
||||
WorldEntity entity,
|
||||
uint landblockId,
|
||||
|
|
|
|||
|
|
@ -244,6 +244,48 @@ public sealed class LiveRenderProjectionJournalTests
|
|||
Assert.Same(record, harness.Runtime.Records.Single());
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void SynchronizeActiveSources_RecoversCurrentRootFromCanonicalWorkset()
|
||||
{
|
||||
Harness harness = CreateHarness(loadLandblock: true);
|
||||
LiveEntityRecord record = Materialize(harness, Guid, instance: 8);
|
||||
Assert.True(harness.Projections.OnEntityReady(
|
||||
LiveEntityReadyCandidate.Capture(record)));
|
||||
harness.Journal.DrainTo(harness.Scene);
|
||||
harness.Projections.ResetTracking();
|
||||
|
||||
harness.Projections.SynchronizeActiveSources();
|
||||
|
||||
RenderProjectionDelta recovered =
|
||||
Assert.Single(harness.Journal.Pending.ToArray());
|
||||
Assert.Equal(RenderProjectionDeltaKind.Register, recovered.Kind);
|
||||
Assert.Equal(
|
||||
RenderProjectionClass.LiveDynamicRoot,
|
||||
recovered.Record.ProjectionClass);
|
||||
Assert.Equal(record.LocalEntityId, recovered.Record.Source.LocalEntityId);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void SynchronizeActiveSources_DoesNotResurrectRemovedAttachment()
|
||||
{
|
||||
Harness harness = CreateHarness(loadLandblock: true);
|
||||
LiveEntityRecord record = Materialize(
|
||||
harness,
|
||||
Guid,
|
||||
instance: 9,
|
||||
projectionKind: LiveEntityProjectionKind.Attached);
|
||||
Assert.True(harness.Projections.OnEntityReady(
|
||||
LiveEntityReadyCandidate.Capture(record)));
|
||||
harness.Journal.DrainTo(harness.Scene);
|
||||
harness.Projections.OnProjectionRemoved(record.WorldEntity!.Id);
|
||||
harness.Journal.DrainTo(harness.Scene);
|
||||
|
||||
harness.Projections.SynchronizeActiveSources();
|
||||
|
||||
Assert.Equal(0, harness.Journal.Count);
|
||||
Assert.Equal(0, harness.Projections.ProjectionCount);
|
||||
}
|
||||
|
||||
private static Harness CreateHarness(bool loadLandblock)
|
||||
{
|
||||
var state = new GpuWorldState();
|
||||
|
|
|
|||
|
|
@ -263,6 +263,48 @@ public sealed class StaticRenderProjectionJournalTests
|
|||
unchanged.ProjectionClass);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ActiveAnimatedSynchronization_ReusesRetainedJournalStorage()
|
||||
{
|
||||
const int count = 1_000;
|
||||
RenderSceneGeneration generation = Generation(9);
|
||||
var journal = new RenderProjectionJournal(generation);
|
||||
var statics = new StaticRenderProjectionJournal(journal);
|
||||
var animated = new WorldEntity[count];
|
||||
for (int index = 0; index < animated.Length; index++)
|
||||
animated[index] = Entity((uint)index + 1);
|
||||
LandblockBuild build = Build(
|
||||
LandblockId,
|
||||
animated,
|
||||
includeShell: false);
|
||||
statics.Reconcile(build, Publication(build));
|
||||
using var scene = new ArchRenderScene(generation);
|
||||
journal.DrainTo(scene);
|
||||
statics.SynchronizeActiveAnimatedSources(animated);
|
||||
journal.DrainTo(scene);
|
||||
for (int index = 0; index < animated.Length; index++)
|
||||
{
|
||||
animated[index].Rotation = Quaternion.CreateFromAxisAngle(
|
||||
Vector3.UnitZ,
|
||||
0.25f);
|
||||
}
|
||||
statics.SynchronizeActiveAnimatedSources(animated);
|
||||
journal.DrainTo(scene);
|
||||
for (int index = 0; index < animated.Length; index++)
|
||||
{
|
||||
animated[index].Rotation = Quaternion.CreateFromAxisAngle(
|
||||
Vector3.UnitZ,
|
||||
0.5f);
|
||||
}
|
||||
long before = GC.GetAllocatedBytesForCurrentThread();
|
||||
|
||||
statics.SynchronizeActiveAnimatedSources(animated);
|
||||
|
||||
long allocated = GC.GetAllocatedBytesForCurrentThread() - before;
|
||||
Assert.Equal(count, journal.Count);
|
||||
Assert.True(allocated == 0, $"Allocated {allocated:N0} bytes.");
|
||||
}
|
||||
|
||||
private static GpuLandblockSpatialPublication Publication(
|
||||
LandblockBuild build,
|
||||
bool requiresActivation = true) =>
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue