feat(rendering): journal static scene projections

Append exact static and EnvCell-shell projection deltas only at committed spatial activation and detach receipts. Same-landblock rehydrate now reconciles retained, new, and omitted presentation identities without constructing or drawing the shadow scene in production.

Co-authored-by: Erik Nilsson <erikn@users.noreply.github.com>
This commit is contained in:
Erik 2026-07-24 21:57:33 +02:00
parent dbd8318417
commit 5d19c56d15
10 changed files with 997 additions and 10 deletions

View file

@ -1,4 +1,5 @@
using AcDream.App.Rendering.Wb;
using AcDream.App.Rendering.Scene;
using AcDream.Core.Terrain;
using AcDream.Core.World;
@ -73,6 +74,7 @@ public sealed class LandblockPresentationPipeline
private readonly GpuWorldState _state;
private readonly Action<uint>? _onLandblockLoaded;
private readonly Action<EnvCellLandblockBuild>? _ensureEnvCellMeshes;
private readonly IRenderStaticProjectionJournalSink? _staticProjectionSink;
private readonly LandblockRetirementCoordinator _retirements;
private readonly Dictionary<LandblockStreamResult, PublicationTransaction> _publications =
new(ReferenceEqualityComparer.Instance);
@ -84,7 +86,8 @@ public sealed class LandblockPresentationPipeline
Action<EnvCellLandblockBuild>? ensureEnvCellMeshes = null,
LandblockRetirementCoordinator? retirementCoordinator = null,
Action<uint>? removeTerrain = null,
Action<uint>? demoteNearLayer = null)
Action<uint>? demoteNearLayer = null,
IRenderStaticProjectionJournalSink? staticProjectionSink = null)
{
ArgumentNullException.ThrowIfNull(publishBeforeSpatialCommit);
ArgumentNullException.ThrowIfNull(state);
@ -93,6 +96,7 @@ public sealed class LandblockPresentationPipeline
_state = state;
_onLandblockLoaded = onLandblockLoaded;
_ensureEnvCellMeshes = ensureEnvCellMeshes;
_staticProjectionSink = staticProjectionSink;
if (retirementCoordinator is not null
&& !retirementCoordinator.MatchesState(_state))
{
@ -120,6 +124,27 @@ public sealed class LandblockPresentationPipeline
LandblockPresentationRetirementOwner retirementOwner,
Action<uint>? onLandblockLoaded = null,
Action<EnvCellLandblockBuild>? ensureEnvCellMeshes = null)
: this(
renderPublisher,
physicsPublisher,
staticPublisher,
state,
retirementOwner,
onLandblockLoaded,
ensureEnvCellMeshes,
staticProjectionSink: null)
{
}
internal LandblockPresentationPipeline(
LandblockRenderPublisher renderPublisher,
LandblockPhysicsPublisher physicsPublisher,
LandblockStaticPresentationPublisher staticPublisher,
GpuWorldState state,
LandblockPresentationRetirementOwner retirementOwner,
Action<uint>? onLandblockLoaded,
Action<EnvCellLandblockBuild>? ensureEnvCellMeshes,
IRenderStaticProjectionJournalSink? staticProjectionSink)
{
_renderPublisher = renderPublisher
?? throw new ArgumentNullException(nameof(renderPublisher));
@ -146,10 +171,14 @@ public sealed class LandblockPresentationPipeline
}
_onLandblockLoaded = onLandblockLoaded;
_ensureEnvCellMeshes = ensureEnvCellMeshes;
_staticProjectionSink = staticProjectionSink;
_retirements = LandblockRetirementCoordinator.CreateBudgeted(
state,
retirementOwner.AdvanceOne,
retirementOwner.Advance);
retirementOwner.Advance,
staticProjectionSink is null
? null
: staticProjectionSink.Retire);
}
/// <summary>
@ -785,6 +814,12 @@ public sealed class LandblockPresentationPipeline
transaction.SpatialPublication
?? throw new InvalidOperationException(
"A committed spatial publication has no activation receipt."));
if (transaction.SpatialPublication.RequiresActivation)
{
_staticProjectionSink?.Reconcile(
transaction.Build,
transaction.SpatialPublication);
}
transaction.SpatialPresentationCommitted = true;
}))
{

View file

@ -273,6 +273,7 @@ public sealed class LandblockRetirementCoordinator
LandblockRetirementOperationResult>? _advancePresentationStep;
private readonly Func<LandblockRetirementKind, LandblockRetirementStage>
_requiredPresentationStages;
private readonly Action<GpuLandblockRetirement>? _onDetached;
private readonly Dictionary<uint, List<LandblockRetirementTicket>> _pending = new();
private readonly Queue<LandblockRetirementTicket> _pendingOrder = new();
private readonly List<uint> _completedIds = new();
@ -290,6 +291,7 @@ public sealed class LandblockRetirementCoordinator
_state = state;
_advancePresentation = advancePresentation;
_advancePresentationStep = null;
_onDetached = null;
_requiredPresentationStages = requiredPresentationStages
?? (kind => kind == LandblockRetirementKind.Full
? ProductionPresentationStages
@ -302,7 +304,8 @@ public sealed class LandblockRetirementCoordinator
advancePresentationStep,
Action<LandblockRetirementTicket> advancePresentation,
Func<LandblockRetirementKind, LandblockRetirementStage>
requiredPresentationStages)
requiredPresentationStages,
Action<GpuLandblockRetirement>? onDetached)
{
_state = state ?? throw new ArgumentNullException(nameof(state));
_advancePresentationStep = advancePresentationStep
@ -311,6 +314,7 @@ public sealed class LandblockRetirementCoordinator
?? throw new ArgumentNullException(nameof(advancePresentation));
_requiredPresentationStages = requiredPresentationStages
?? throw new ArgumentNullException(nameof(requiredPresentationStages));
_onDetached = onDetached;
}
public int PendingCount { get; private set; }
@ -335,7 +339,8 @@ public sealed class LandblockRetirementCoordinator
GpuWorldState state,
Func<LandblockRetirementTicket, LandblockRetirementOperationResult>
advancePresentationStep,
Action<LandblockRetirementTicket> advancePresentation)
Action<LandblockRetirementTicket> advancePresentation,
Action<GpuLandblockRetirement>? onDetached = null)
{
return new LandblockRetirementCoordinator(
state,
@ -343,7 +348,8 @@ public sealed class LandblockRetirementCoordinator
advancePresentation,
kind => kind == LandblockRetirementKind.Full
? ProductionPresentationStages
: ProductionPresentationStages & ~LandblockRetirementStage.Terrain);
: ProductionPresentationStages & ~LandblockRetirementStage.Terrain,
onDetached);
}
/// <summary>
@ -531,6 +537,8 @@ public sealed class LandblockRetirementCoordinator
if (_advancePresentationStep is not null)
_pendingOrder.Enqueue(ticket);
_onDetached?.Invoke(stateRetirement);
if (_advancePresentationStep is null)
{
AdvanceTicket(ticket);