feat(rendering): journal live scene projections

Mirror exact live-root and equipped-child lifetimes behind the non-drawing render-scene boundary. Ready, visibility, final-pose, rebucket, removal, and resource teardown edges retain canonical LiveEntityRuntime identity while active-only synchronization avoids resident-static scans.

Co-authored-by: Erik Nilsson <erikn@users.noreply.github.com>
This commit is contained in:
Erik 2026-07-24 22:05:42 +02:00
parent 5d19c56d15
commit 58e7c2eb99
10 changed files with 949 additions and 148 deletions

View file

@ -1,6 +1,7 @@
using System.Numerics;
using AcDream.App.Input;
using AcDream.App.Rendering;
using AcDream.App.Rendering.Scene;
using AcDream.App.Streaming;
using AcDream.App.World;
using AcDream.Core.Items;
@ -1496,6 +1497,63 @@ public sealed class LiveEntityHydrationControllerTests
Assert.False(expected.IsSpatiallyProjected);
}
[Theory]
[InlineData(true)]
[InlineData(false)]
public void ReadyPublisher_PublishesRenderProjectionAfterExistingReadyStages(
bool projectionAccepted)
{
using var fixture = new Fixture(originKnown: true);
fixture.Controller.OnCreate(Spawn(Generation: 1, PositionSequence: 1));
LiveEntityRecord record = fixture.Record;
var operations = new List<string>();
var projection = new RecordingLiveProjectionSink(
_ =>
{
operations.Add("projection");
return projectionAccepted;
});
var publisher = new LiveEntityReadyPublisher(
fixture.Runtime,
_ => { operations.Add("effects-owner"); return true; },
_ => { operations.Add("state"); return true; },
_ => { operations.Add("effects-replay"); return true; },
projection);
bool result = publisher.Publish(
LiveEntityReadyCandidate.Capture(record));
Assert.Equal(projectionAccepted, result);
Assert.Equal(
["effects-owner", "state", "effects-replay", "projection"],
operations);
}
[Fact]
public void ReadyPublisher_RevalidatesAfterReentrantRenderProjectionCallback()
{
using var fixture = new Fixture(originKnown: true);
fixture.Controller.OnCreate(Spawn(Generation: 1, PositionSequence: 1));
LiveEntityReadyCandidate candidate =
LiveEntityReadyCandidate.Capture(fixture.Record);
var projection = new RecordingLiveProjectionSink(
_ =>
{
fixture.Runtime.RegisterLiveEntity(
Spawn(Generation: 1, PositionSequence: 2));
return true;
});
var publisher = new LiveEntityReadyPublisher(
fixture.Runtime,
static _ => true,
static _ => true,
static _ => true,
projection);
Assert.False(publisher.Publish(candidate));
Assert.Equal(2UL, fixture.Record.CreateIntegrationVersion);
}
[Fact]
public void EquippedChildReadyCandidate_RejectsVersionAdvancedByPoseCallback()
{
@ -1937,6 +1995,36 @@ public sealed class LiveEntityHydrationControllerTests
}
}
private sealed class RecordingLiveProjectionSink(
Func<LiveEntityReadyCandidate, bool> ready)
: ILiveRenderProjectionSink
{
public bool OnEntityReady(LiveEntityReadyCandidate candidate) =>
ready(candidate);
public void OnProjectionVisibilityChanged(
LiveEntityRecord record,
bool visible)
{
}
public void OnProjectionPoseReady(uint serverGuid)
{
}
public void OnProjectionRemoved(uint localEntityId)
{
}
public void OnResourceUnregister(WorldEntity entity)
{
}
public void SynchronizeActiveSources()
{
}
}
private sealed class RecordingOrigin(bool isKnown) : ILiveEntityWorldOriginCoordinator
{
public bool IsKnownValue { get; set; } = isKnown;