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
|
|
@ -22,7 +22,7 @@ retail-faithful gameplay
|
||||||
| F2 — static projection journal | complete | Static and EnvCell-shell projections now append ordered deltas only after the final activation receipt and exact detach receipt. Same-landblock rehydrate reconciles retained/new/omitted identities; stale Far completions are inert. Seven focused lifecycle tests pass inside the 3,708-App / 8,192-solution Release gate. The scene remains unconstructed and non-drawing in production. |
|
| F2 — static projection journal | complete | Static and EnvCell-shell projections now append ordered deltas only after the final activation receipt and exact detach receipt. Same-landblock rehydrate reconciles retained/new/omitted identities; stale Far completions are inert. Seven focused lifecycle tests pass inside the 3,708-App / 8,192-solution Release gate. The scene remains unconstructed and non-drawing in production. |
|
||||||
| F3 — live/equipped projection | complete | Exact EntityReady/resource teardown, visibility, attachment pose/removal, and active-only final-frame seams now drive generation/incarnation-gated live records. Duplicate CreateObject, pending/loaded rebucket, hidden/appearance, reentrancy, session clear, attachment, and GUID-generation replacement pass 11 focused tests. Production still constructs no render scene. |
|
| F3 — live/equipped projection | complete | Exact EntityReady/resource teardown, visibility, attachment pose/removal, and active-only final-frame seams now drive generation/incarnation-gated live records. Duplicate CreateObject, pending/loaded rebucket, hidden/appearance, reentrancy, session clear, attachment, and GUID-generation replacement pass 11 focused tests. Production still constructs no render scene. |
|
||||||
| F4 — dynamic indices | complete | The contained scene now maintains outdoor/static, per-cell static/dynamic, special dynamic-route, translucent, selectable, light-candidate, and dirty indices incrementally. Final-frame live/equipped synchronization remains active-only, and active animated statics are synchronized from the scheduler's active workset rather than a resident-world scan. The production scene remains unconstructed and non-drawing. |
|
| F4 — dynamic indices | complete | The contained scene now maintains outdoor/static, per-cell static/dynamic, special dynamic-route, translucent, selectable, light-candidate, and dirty indices incrementally. Final-frame live/equipped synchronization remains active-only, and active animated statics are synchronized from the scheduler's active workset rather than a resident-world scan. The production scene remains unconstructed and non-drawing. |
|
||||||
| F5 | active — connected gate | Lifecycle automation now constructs the otherwise-absent non-drawing scene, drains ordered static/live deltas after post-network spatial reconciliation, compares the accepted current partition every 30 rendered frames and at every canonical checkpoint, retains only the first field-level mismatch, and publishes scene/journal/index/memory evidence in checkpoint JSON. Focused and complete App suites are green; capped/uncapped nine-stop and dense-Arwic physical gates remain. |
|
| F5 | active — connected redrive | Lifecycle automation now constructs the otherwise-absent non-drawing scene, drains ordered static/live deltas after post-network spatial reconciliation, compares the accepted current partition every 30 rendered frames and at every canonical checkpoint, retains only the first field-level mismatch, and publishes scene/journal/index/memory evidence in checkpoint JSON. The first exact `056bbd4e` capped route passed the harness and graceful-shutdown gates but correctly exposed missing live roots after a same-location Sawato revisit. Root synchronization now recovers ordinary roots from `LiveEntityRuntime`'s canonical active spatial workset while attached children remain callback-authoritative. Immutable selection geometry is fingerprinted once per GfxObj/mesh identity, and retained scene/journal update paths have zero-allocation regression tests. Release is green at 3,733 App tests / 3 skips and 8,217 complete-solution tests / 5 skips; all three connected routes must be redriven before F5 closes. |
|
||||||
| G0–G5 | pending | No production consumer has switched. |
|
| G0–G5 | pending | No production consumer has switched. |
|
||||||
|
|
||||||
The exact pre-F/G runtime rollback anchor remains `e7d9d6fa`. F0 is
|
The exact pre-F/G runtime rollback anchor remains `e7d9d6fa`. F0 is
|
||||||
|
|
@ -404,6 +404,31 @@ switched. F may temporarily cost memory and a small amount of CPU in diagnostic
|
||||||
mode; those costs are reported separately and shadow comparison is disabled by
|
mode; those costs are reported separately and shadow comparison is disabled by
|
||||||
default after its gate.
|
default after its gate.
|
||||||
|
|
||||||
|
#### F5 connected-gate correction record
|
||||||
|
|
||||||
|
The first physical capped nine-stop route on exact commit `056bbd4e` completed
|
||||||
|
the harness and graceful shutdown, but the shadow referee rejected the route at
|
||||||
|
the same-location Sawato revisit. The first mismatch was a missing live root
|
||||||
|
(`sourceChannel=live`, `field=presence`), while journal pending and rejected
|
||||||
|
delta counts remained zero. This proved the problem was source-workset
|
||||||
|
reconciliation rather than a drain failure.
|
||||||
|
|
||||||
|
The derived live journal had been synchronizing only records it already
|
||||||
|
retained. Once a root projection was absent, that loop could not recover it.
|
||||||
|
The correction sources ordinary world roots from the canonical active spatial
|
||||||
|
workset already owned by `LiveEntityRuntime`. Attached children deliberately
|
||||||
|
remain callback-authoritative and are refreshed only while retained, preventing
|
||||||
|
removed equipment from being resurrected.
|
||||||
|
|
||||||
|
The same route also showed that the diagnostic referee was re-hashing immutable
|
||||||
|
selection polygons and vertices every sampled frame. Geometry fingerprints are
|
||||||
|
now cached by GfxObj and mesh identity, and all retained projection collection
|
||||||
|
walks use allocation-free indexed access. Permanent tests cover root recovery,
|
||||||
|
attachment non-resurrection, cached selection geometry, retained transform
|
||||||
|
updates, and active animated-static synchronization. The capped, uncapped, and
|
||||||
|
dense-town routes are all rerun from the corrective commit; no F5 success is
|
||||||
|
claimed from the rejected `056bbd4e` route.
|
||||||
|
|
||||||
## 6. Slice G — Frame product and production cutover
|
## 6. Slice G — Frame product and production cutover
|
||||||
|
|
||||||
### G0 — Borrowed double-buffered frame product
|
### G0 — Borrowed double-buffered frame product
|
||||||
|
|
|
||||||
|
|
@ -211,6 +211,8 @@ internal sealed class CurrentRenderSceneOracle :
|
||||||
new(ReferenceEqualityComparer.Instance);
|
new(ReferenceEqualityComparer.Instance);
|
||||||
private readonly List<CurrentRenderSelectionFingerprint>
|
private readonly List<CurrentRenderSelectionFingerprint>
|
||||||
_selectionParts = [];
|
_selectionParts = [];
|
||||||
|
private readonly Dictionary<uint, SelectionGeometryCacheEntry>
|
||||||
|
_selectionGeometryByGfxObj = [];
|
||||||
private ulong _frameSequence;
|
private ulong _frameSequence;
|
||||||
private int _abortedFrames;
|
private int _abortedFrames;
|
||||||
private int _outdoorStaticCount;
|
private int _outdoorStaticCount;
|
||||||
|
|
@ -637,16 +639,15 @@ internal sealed class CurrentRenderSceneOracle :
|
||||||
}
|
}
|
||||||
ArgumentNullException.ThrowIfNull(mesh);
|
ArgumentNullException.ThrowIfNull(mesh);
|
||||||
|
|
||||||
StableRenderHash128 geometry = StableRenderHash128.Create();
|
if (!_selectionGeometryByGfxObj.TryGetValue(
|
||||||
geometry.Add(mesh.SphereCenter);
|
gfxObjId,
|
||||||
geometry.Add(mesh.SphereRadius);
|
out SelectionGeometryCacheEntry cachedGeometry)
|
||||||
geometry.Add(mesh.Polygons.Count);
|
|| !ReferenceEquals(cachedGeometry.Mesh, mesh))
|
||||||
foreach (RetailSelectionPolygon polygon in mesh.Polygons)
|
|
||||||
{
|
{
|
||||||
geometry.Add(polygon.SingleSided);
|
cachedGeometry = new SelectionGeometryCacheEntry(
|
||||||
geometry.Add(polygon.Vertices.Count);
|
mesh,
|
||||||
foreach (Vector3 vertex in polygon.Vertices)
|
FingerprintSelectionGeometry(mesh));
|
||||||
geometry.Add(vertex);
|
_selectionGeometryByGfxObj[gfxObjId] = cachedGeometry;
|
||||||
}
|
}
|
||||||
|
|
||||||
var fingerprint = new CurrentRenderSelectionFingerprint(
|
var fingerprint = new CurrentRenderSelectionFingerprint(
|
||||||
|
|
@ -656,7 +657,7 @@ internal sealed class CurrentRenderSceneOracle :
|
||||||
PartIndex: partIndex,
|
PartIndex: partIndex,
|
||||||
GfxObjId: gfxObjId,
|
GfxObjId: gfxObjId,
|
||||||
LocalToWorld: localToWorld,
|
LocalToWorld: localToWorld,
|
||||||
Geometry: geometry.Finish());
|
Geometry: cachedGeometry.Fingerprint);
|
||||||
_selectionParts.Add(fingerprint);
|
_selectionParts.Add(fingerprint);
|
||||||
_selectionHash.Add(fingerprint.Sequence);
|
_selectionHash.Add(fingerprint.Sequence);
|
||||||
_selectionHash.Add(fingerprint.ServerGuid);
|
_selectionHash.Add(fingerprint.ServerGuid);
|
||||||
|
|
@ -722,8 +723,11 @@ internal sealed class CurrentRenderSceneOracle :
|
||||||
|
|
||||||
StableRenderHash128 geometry = StableRenderHash128.Create();
|
StableRenderHash128 geometry = StableRenderHash128.Create();
|
||||||
geometry.Add(entity.MeshRefs.Count);
|
geometry.Add(entity.MeshRefs.Count);
|
||||||
foreach (MeshRef mesh in entity.MeshRefs)
|
for (int meshIndex = 0;
|
||||||
|
meshIndex < entity.MeshRefs.Count;
|
||||||
|
meshIndex++)
|
||||||
{
|
{
|
||||||
|
MeshRef mesh = entity.MeshRefs[meshIndex];
|
||||||
geometry.Add(mesh.GfxObjId);
|
geometry.Add(mesh.GfxObjId);
|
||||||
geometry.Add(mesh.PartTransform);
|
geometry.Add(mesh.PartTransform);
|
||||||
}
|
}
|
||||||
|
|
@ -734,8 +738,12 @@ internal sealed class CurrentRenderSceneOracle :
|
||||||
appearance.Add(true);
|
appearance.Add(true);
|
||||||
appearance.Add(palette.BasePaletteId);
|
appearance.Add(palette.BasePaletteId);
|
||||||
appearance.Add(palette.SubPalettes.Count);
|
appearance.Add(palette.SubPalettes.Count);
|
||||||
foreach (PaletteOverride.SubPaletteRange range in palette.SubPalettes)
|
for (int rangeIndex = 0;
|
||||||
|
rangeIndex < palette.SubPalettes.Count;
|
||||||
|
rangeIndex++)
|
||||||
{
|
{
|
||||||
|
PaletteOverride.SubPaletteRange range =
|
||||||
|
palette.SubPalettes[rangeIndex];
|
||||||
appearance.Add(range.SubPaletteId);
|
appearance.Add(range.SubPaletteId);
|
||||||
appearance.Add(range.Offset);
|
appearance.Add(range.Offset);
|
||||||
appearance.Add(range.Length);
|
appearance.Add(range.Length);
|
||||||
|
|
@ -747,8 +755,11 @@ internal sealed class CurrentRenderSceneOracle :
|
||||||
}
|
}
|
||||||
|
|
||||||
appearance.Add(entity.PartOverrides.Count);
|
appearance.Add(entity.PartOverrides.Count);
|
||||||
foreach (PartOverride part in entity.PartOverrides)
|
for (int partIndex = 0;
|
||||||
|
partIndex < entity.PartOverrides.Count;
|
||||||
|
partIndex++)
|
||||||
{
|
{
|
||||||
|
PartOverride part = entity.PartOverrides[partIndex];
|
||||||
appearance.Add(part.PartIndex);
|
appearance.Add(part.PartIndex);
|
||||||
appearance.Add(part.GfxObjId);
|
appearance.Add(part.GfxObjId);
|
||||||
}
|
}
|
||||||
|
|
@ -784,6 +795,31 @@ internal sealed class CurrentRenderSceneOracle :
|
||||||
Appearance: appearance.Finish());
|
Appearance: appearance.Finish());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private static RenderSceneHash128 FingerprintSelectionGeometry(
|
||||||
|
RetailSelectionMesh mesh)
|
||||||
|
{
|
||||||
|
StableRenderHash128 geometry = StableRenderHash128.Create();
|
||||||
|
geometry.Add(mesh.SphereCenter);
|
||||||
|
geometry.Add(mesh.SphereRadius);
|
||||||
|
geometry.Add(mesh.Polygons.Count);
|
||||||
|
for (int polygonIndex = 0;
|
||||||
|
polygonIndex < mesh.Polygons.Count;
|
||||||
|
polygonIndex++)
|
||||||
|
{
|
||||||
|
RetailSelectionPolygon polygon = mesh.Polygons[polygonIndex];
|
||||||
|
geometry.Add(polygon.SingleSided);
|
||||||
|
geometry.Add(polygon.Vertices.Count);
|
||||||
|
for (int vertexIndex = 0;
|
||||||
|
vertexIndex < polygon.Vertices.Count;
|
||||||
|
vertexIndex++)
|
||||||
|
{
|
||||||
|
geometry.Add(polygon.Vertices[vertexIndex]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return geometry.Finish();
|
||||||
|
}
|
||||||
|
|
||||||
private static InteriorEntityPartition.ProjectionClass ProjectionClassOf(
|
private static InteriorEntityPartition.ProjectionClass ProjectionClassOf(
|
||||||
WorldEntity entity) =>
|
WorldEntity entity) =>
|
||||||
entity.ServerGuid != 0
|
entity.ServerGuid != 0
|
||||||
|
|
@ -792,6 +828,10 @@ internal sealed class CurrentRenderSceneOracle :
|
||||||
? InteriorEntityPartition.ProjectionClass.CellStatic
|
? InteriorEntityPartition.ProjectionClass.CellStatic
|
||||||
: InteriorEntityPartition.ProjectionClass.OutdoorStatic;
|
: InteriorEntityPartition.ProjectionClass.OutdoorStatic;
|
||||||
|
|
||||||
|
private readonly record struct SelectionGeometryCacheEntry(
|
||||||
|
RetailSelectionMesh Mesh,
|
||||||
|
RenderSceneHash128 Fingerprint);
|
||||||
|
|
||||||
private static void AddPViewCandidate(
|
private static void AddPViewCandidate(
|
||||||
ref StableRenderHash128 hash,
|
ref StableRenderHash128 hash,
|
||||||
in CurrentRenderPViewCandidateFingerprint candidate)
|
in CurrentRenderPViewCandidateFingerprint candidate)
|
||||||
|
|
|
||||||
|
|
@ -25,6 +25,7 @@ internal sealed class LiveRenderProjectionJournal : ILiveRenderProjectionSink
|
||||||
private readonly LiveEntityRuntime _runtime;
|
private readonly LiveEntityRuntime _runtime;
|
||||||
private readonly RenderProjectionJournal _journal;
|
private readonly RenderProjectionJournal _journal;
|
||||||
private readonly Dictionary<uint, TrackedProjection> _byLocalId = [];
|
private readonly Dictionary<uint, TrackedProjection> _byLocalId = [];
|
||||||
|
private readonly List<LiveEntityRecord> _activeRootScratch = [];
|
||||||
private readonly List<TrackedProjection> _activeScratch = [];
|
private readonly List<TrackedProjection> _activeScratch = [];
|
||||||
|
|
||||||
public LiveRenderProjectionJournal(
|
public LiveRenderProjectionJournal(
|
||||||
|
|
@ -107,10 +108,34 @@ internal sealed class LiveRenderProjectionJournal : ILiveRenderProjectionSink
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public void SynchronizeActiveSources()
|
public void SynchronizeActiveSources()
|
||||||
{
|
{
|
||||||
|
// The canonical live-runtime workset is the source of truth for world
|
||||||
|
// roots. Walking only this journal's already-tracked values would make
|
||||||
|
// a missed/reordered derived callback permanent and prevent shadow
|
||||||
|
// comparison from converging after a same-location revisit.
|
||||||
|
_runtime.CopySpatialRootObjectRecordsTo(_activeRootScratch);
|
||||||
|
for (int i = 0; i < _activeRootScratch.Count; i++)
|
||||||
|
{
|
||||||
|
LiveEntityRecord record = _activeRootScratch[i];
|
||||||
|
if (!record.ResourcesRegistered
|
||||||
|
|| record.WorldEntity is not { } entity
|
||||||
|
|| !_runtime.IsCurrentSpatialRootObject(record))
|
||||||
|
{
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
Upsert(record, entity, record.IsSpatiallyVisible);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Attached children are not members of the ordinary root workset.
|
||||||
|
// Their exact EntityReady/pose/removal callbacks remain authoritative;
|
||||||
|
// synchronization only refreshes children whose attachment projection
|
||||||
|
// is still retained by this derived scene.
|
||||||
_activeScratch.Clear();
|
_activeScratch.Clear();
|
||||||
foreach (TrackedProjection tracked in _byLocalId.Values)
|
foreach (TrackedProjection tracked in _byLocalId.Values)
|
||||||
{
|
{
|
||||||
if (tracked.Record.IsSpatiallyProjected)
|
if (tracked.Record.IsSpatiallyProjected
|
||||||
|
&& tracked.Record.ProjectionKind is
|
||||||
|
LiveEntityProjectionKind.Attached)
|
||||||
_activeScratch.Add(tracked);
|
_activeScratch.Add(tracked);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -137,6 +162,7 @@ internal sealed class LiveRenderProjectionJournal : ILiveRenderProjectionSink
|
||||||
public void ResetTracking()
|
public void ResetTracking()
|
||||||
{
|
{
|
||||||
_byLocalId.Clear();
|
_byLocalId.Clear();
|
||||||
|
_activeRootScratch.Clear();
|
||||||
_activeScratch.Clear();
|
_activeScratch.Clear();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -290,6 +290,45 @@ public sealed class ArchRenderSceneTests
|
||||||
Assert.Equal(current.Bounds, stored.Bounds);
|
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]
|
[Fact]
|
||||||
public void Digest_IsStableAcrossRegistrationOrderAndBufferReuse()
|
public void Digest_IsStableAcrossRegistrationOrderAndBufferReuse()
|
||||||
{
|
{
|
||||||
|
|
|
||||||
|
|
@ -2,6 +2,7 @@ using System.Numerics;
|
||||||
using AcDream.App.Rendering;
|
using AcDream.App.Rendering;
|
||||||
using AcDream.App.Rendering.Scene;
|
using AcDream.App.Rendering.Scene;
|
||||||
using AcDream.App.Rendering.Wb;
|
using AcDream.App.Rendering.Wb;
|
||||||
|
using AcDream.Core.Selection;
|
||||||
using AcDream.Core.World;
|
using AcDream.Core.World;
|
||||||
|
|
||||||
namespace AcDream.App.Tests.Rendering;
|
namespace AcDream.App.Tests.Rendering;
|
||||||
|
|
@ -363,6 +364,48 @@ public sealed class CurrentRenderSceneOracleTests
|
||||||
Assert.Empty(oracle.DispatcherCandidates);
|
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(
|
private static CurrentRenderProjectionFingerprint CaptureSingle(
|
||||||
WorldEntity entity,
|
WorldEntity entity,
|
||||||
uint landblockId,
|
uint landblockId,
|
||||||
|
|
|
||||||
|
|
@ -244,6 +244,48 @@ public sealed class LiveRenderProjectionJournalTests
|
||||||
Assert.Same(record, harness.Runtime.Records.Single());
|
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)
|
private static Harness CreateHarness(bool loadLandblock)
|
||||||
{
|
{
|
||||||
var state = new GpuWorldState();
|
var state = new GpuWorldState();
|
||||||
|
|
|
||||||
|
|
@ -263,6 +263,48 @@ public sealed class StaticRenderProjectionJournalTests
|
||||||
unchanged.ProjectionClass);
|
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(
|
private static GpuLandblockSpatialPublication Publication(
|
||||||
LandblockBuild build,
|
LandblockBuild build,
|
||||||
bool requiresActivation = true) =>
|
bool requiresActivation = true) =>
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue