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

@ -0,0 +1,466 @@
using System.Numerics;
using AcDream.App.Rendering.Wb;
using AcDream.App.Streaming;
using AcDream.Core.World;
namespace AcDream.App.Rendering.Scene;
internal interface IRenderStaticProjectionJournalSink
{
void Reconcile(
LandblockBuild build,
GpuLandblockSpatialPublication publication);
void Retire(GpuLandblockRetirement retirement);
}
/// <summary>
/// Derived, non-authoritative mirror of committed static presentation facts.
/// It owns no mesh, gameplay, GUID, cell, or streaming lifetime. Its only
/// purpose in Slice F2 is to append exact render-scene deltas at the accepted
/// publication and retirement receipts.
/// </summary>
internal sealed class StaticRenderProjectionJournal :
IRenderStaticProjectionJournalSink
{
private const byte StaticEntityDomain = 1;
private const byte EnvCellShellDomain = 2;
private const float DefaultAabbRadius = 5.0f;
private readonly RenderProjectionJournal _journal;
private readonly Dictionary<uint, Dictionary<RenderProjectionId, TrackedProjection>>
_byLandblock = [];
private readonly List<RenderProjectionRecord> _candidates = [];
private readonly List<RenderProjectionId> _removed = [];
private ulong _nextIncarnation = 1;
public StaticRenderProjectionJournal(RenderProjectionJournal journal)
{
_journal = journal ?? throw new ArgumentNullException(nameof(journal));
}
public int ProjectionCount { get; private set; }
public int LandblockCount => _byLandblock.Count;
public void Reconcile(
LandblockBuild build,
GpuLandblockSpatialPublication publication)
{
ArgumentNullException.ThrowIfNull(build);
ArgumentNullException.ThrowIfNull(publication);
uint landblockId = Canonicalize(publication.LandblockId);
if (Canonicalize(build.LandblockId) != landblockId)
{
throw new ArgumentException(
"Static render projection publication belongs to another landblock.",
nameof(build));
}
if (!publication.RequiresActivation)
return;
_candidates.Clear();
for (int i = 0; i < publication.Landblock.Entities.Count; i++)
{
WorldEntity entity = publication.Landblock.Entities[i];
if (entity.ServerGuid == 0)
_candidates.Add(ProjectStaticEntity(landblockId, entity));
}
if (build.EnvCells is { } envCells)
{
for (int i = 0; i < envCells.Shells.Length; i++)
_candidates.Add(ProjectEnvCellShell(landblockId, envCells.Shells[i]));
}
_candidates.Sort(ProjectionRecordComparer.Instance);
if (!_byLandblock.TryGetValue(
landblockId,
out Dictionary<RenderProjectionId, TrackedProjection>? current))
{
current = [];
_byLandblock.Add(landblockId, current);
}
_removed.Clear();
foreach (RenderProjectionId id in current.Keys)
_removed.Add(id);
for (int i = 0; i < _candidates.Count; i++)
{
RenderProjectionRecord candidate = _candidates[i];
if (current.TryGetValue(candidate.Id, out TrackedProjection retained))
{
_removed.Remove(candidate.Id);
RenderProjectionRecord accepted = candidate with
{
OwnerIncarnation = retained.Record.OwnerIncarnation,
PreviousTransform = new PreviousRenderTransform(
retained.Record.Transform.LocalToWorld),
};
if (accepted.ProjectionClass
!= retained.Record.ProjectionClass)
{
_journal.Unregister(
retained.Record.Id,
retained.Record.OwnerIncarnation);
accepted = accepted with
{
OwnerIncarnation = NextIncarnation(),
};
_journal.Register(in accepted);
current[candidate.Id] = new TrackedProjection(accepted);
continue;
}
if (accepted != retained.Record)
{
AppendUpdates(retained.Record, accepted);
current[candidate.Id] = new TrackedProjection(accepted);
}
continue;
}
RenderProjectionRecord registered = candidate with
{
OwnerIncarnation = NextIncarnation(),
};
_journal.Register(in registered);
current.Add(registered.Id, new TrackedProjection(registered));
ProjectionCount++;
}
_removed.Sort(ProjectionIdComparer.Instance);
for (int i = 0; i < _removed.Count; i++)
{
RenderProjectionId id = _removed[i];
TrackedProjection omitted = current[id];
_journal.Unregister(id, omitted.Record.OwnerIncarnation);
current.Remove(id);
ProjectionCount--;
}
if (current.Count == 0)
_byLandblock.Remove(landblockId);
}
public void Retire(GpuLandblockRetirement retirement)
{
ArgumentNullException.ThrowIfNull(retirement);
uint landblockId = Canonicalize(retirement.LandblockId);
if (!_byLandblock.Remove(
landblockId,
out Dictionary<RenderProjectionId, TrackedProjection>? current))
{
return;
}
_removed.Clear();
foreach (RenderProjectionId id in current.Keys)
_removed.Add(id);
_removed.Sort(ProjectionIdComparer.Instance);
for (int i = 0; i < _removed.Count; i++)
{
TrackedProjection tracked = current[_removed[i]];
_journal.Unregister(
tracked.Record.Id,
tracked.Record.OwnerIncarnation);
}
ProjectionCount -= current.Count;
}
public void Clear(RenderSceneGeneration replacementGeneration)
{
_byLandblock.Clear();
_candidates.Clear();
_removed.Clear();
ProjectionCount = 0;
_journal.Clear(replacementGeneration);
}
internal bool TryGet(
RenderProjectionId id,
out RenderProjectionRecord record)
{
foreach (Dictionary<RenderProjectionId, TrackedProjection> projections
in _byLandblock.Values)
{
if (projections.TryGetValue(id, out TrackedProjection tracked))
{
record = tracked.Record;
return true;
}
}
record = default;
return false;
}
internal static RenderProjectionId StaticEntityId(
uint landblockId,
uint localEntityId) =>
CreateProjectionId(
StaticEntityDomain,
Canonicalize(landblockId),
localEntityId);
internal static RenderProjectionId EnvCellShellId(
uint landblockId,
uint cellId) =>
CreateProjectionId(
EnvCellShellDomain,
Canonicalize(landblockId),
cellId);
private RenderProjectionRecord ProjectStaticEntity(
uint landblockId,
WorldEntity entity)
{
CurrentRenderProjectionFingerprint fingerprint =
CurrentRenderSceneOracle.CreateProjectionFingerprint(
landblockId,
entity);
RenderProjectionFlags flags = RenderProjectionFlags.Selectable;
if (entity.IsDrawVisible && entity.IsAncestorDrawVisible)
flags |= RenderProjectionFlags.Draw;
if (!entity.IsDrawVisible)
flags |= RenderProjectionFlags.Hidden;
if (!entity.IsAncestorDrawVisible)
flags |= RenderProjectionFlags.AncestorHidden;
RenderTransform transform = RenderTransform.FromRoot(
entity.Position,
entity.Rotation,
entity.Scale);
(Vector3 minimum, Vector3 maximum) = CalculateBounds(entity);
RenderProjectionId id = StaticEntityId(landblockId, entity.Id);
uint fullCellId = entity.ParentCellId ?? landblockId;
return new RenderProjectionRecord(
id,
InteriorEntityPartition.IsIndoorCellId(entity.ParentCellId)
? RenderProjectionClass.IndoorCellStatic
: RenderProjectionClass.OutdoorStatic,
default,
transform,
new PreviousRenderTransform(transform.LocalToWorld),
new RenderMeshSet(
RenderAssetHandle.FromRaw(
fingerprint.Geometry.Low ^ fingerprint.Geometry.High),
entity.MeshRefs.Count,
0),
new RenderMaterialVariant(
fingerprint.Appearance.Low,
fingerprint.Appearance.High,
1.0f),
new RenderSpatialResidency(
RenderSpatialBucket.FromRaw(fullCellId),
landblockId,
fullCellId),
new RenderWorldBounds(minimum, maximum),
flags,
default,
id.ToSortKey(),
RenderDirtyMask.All,
new RenderSourceMetadata(
entity.Id,
entity.ServerGuid,
entity.SourceGfxObjOrSetupId,
entity.ParentCellId ?? 0,
entity.EffectCellId ?? 0,
entity.BuildingShellAnchorCellId ?? 0,
fingerprint.Transform,
fingerprint.Geometry,
fingerprint.Appearance));
}
private static RenderProjectionRecord ProjectEnvCellShell(
uint landblockId,
EnvCellShellPlacement shell)
{
StableRenderHash128 transformHash = StableRenderHash128.Create();
transformHash.Add(shell.WorldPosition);
transformHash.Add(shell.Rotation);
transformHash.Add(1.0f);
RenderSceneHash128 transformFingerprint = transformHash.Finish();
StableRenderHash128 geometryHash = StableRenderHash128.Create();
geometryHash.Add(shell.GeometryId);
geometryHash.Add(shell.EnvironmentId);
geometryHash.Add(shell.CellStructure);
geometryHash.Add(shell.Surfaces.Length);
for (int i = 0; i < shell.Surfaces.Length; i++)
geometryHash.Add(shell.Surfaces[i]);
RenderSceneHash128 geometryFingerprint = geometryHash.Finish();
RenderProjectionId id = EnvCellShellId(landblockId, shell.CellId);
return new RenderProjectionRecord(
id,
RenderProjectionClass.IndoorCellStatic,
default,
new RenderTransform(
shell.WorldPosition,
shell.Rotation,
1.0f,
shell.Transform),
new PreviousRenderTransform(shell.Transform),
new RenderMeshSet(
RenderAssetHandle.FromRaw(shell.GeometryId),
1,
0),
new RenderMaterialVariant(0, 0, 1.0f),
new RenderSpatialResidency(
RenderSpatialBucket.FromRaw(shell.CellId),
landblockId,
shell.CellId),
new RenderWorldBounds(
shell.WorldBounds.Min,
shell.WorldBounds.Max),
RenderProjectionFlags.Draw,
default,
id.ToSortKey(),
RenderDirtyMask.All,
new RenderSourceMetadata(
shell.CellId,
0,
unchecked((uint)shell.GeometryId),
shell.CellId,
shell.CellId,
0,
transformFingerprint,
geometryFingerprint,
RenderSceneHash128.Empty));
}
private void AppendUpdates(
in RenderProjectionRecord prior,
in RenderProjectionRecord current)
{
if (prior.Transform != current.Transform
|| prior.Bounds != current.Bounds
|| prior.SortKey != current.SortKey
|| prior.Source.TransformFingerprint
!= current.Source.TransformFingerprint)
{
_journal.Update(
RenderProjectionDeltaKind.UpdateTransform,
in current);
}
if (prior.MeshSet != current.MeshSet
|| prior.Material != current.Material
|| prior.DegradeState != current.DegradeState
|| prior.Source.GeometryFingerprint != current.Source.GeometryFingerprint
|| prior.Source.AppearanceFingerprint != current.Source.AppearanceFingerprint)
{
_journal.Update(
RenderProjectionDeltaKind.UpdateAppearance,
in current);
}
if (prior.Residency != current.Residency)
{
_journal.Update(
RenderProjectionDeltaKind.Rebucket,
in current);
}
if (prior.Flags != current.Flags)
{
_journal.Update(
RenderProjectionDeltaKind.UpdateFlags,
in current);
}
}
private static (Vector3 Minimum, Vector3 Maximum) CalculateBounds(
WorldEntity entity)
{
Vector3 position = entity.Position;
if (entity.HasLocalBounds)
{
Vector3 localMin = entity.LocalBoundMin;
Vector3 localMax = entity.LocalBoundMax;
Vector3 minimum = default;
Vector3 maximum = default;
for (int cornerIndex = 0; cornerIndex < 8; cornerIndex++)
{
Vector3 corner = new(
(cornerIndex & 1) == 0 ? localMin.X : localMax.X,
(cornerIndex & 2) == 0 ? localMin.Y : localMax.Y,
(cornerIndex & 4) == 0 ? localMin.Z : localMax.Z);
Vector3 transformed = Vector3.Transform(corner, entity.Rotation);
if (cornerIndex == 0)
{
minimum = transformed;
maximum = transformed;
}
else
{
minimum = Vector3.Min(minimum, transformed);
maximum = Vector3.Max(maximum, transformed);
}
}
Vector3 margin = new(DefaultAabbRadius);
return (
position + minimum - margin,
position + maximum + margin);
}
float radius = DefaultAabbRadius;
for (int i = 0; i < entity.MeshRefs.Count; i++)
{
radius = MathF.Max(
radius,
DefaultAabbRadius
+ entity.MeshRefs[i].PartTransform.Translation.Length());
}
Vector3 extent = new(radius);
return (position - extent, position + extent);
}
private RenderOwnerIncarnation NextIncarnation()
{
if (_nextIncarnation == ulong.MaxValue)
{
throw new InvalidOperationException(
"Static render-projection incarnation exhausted.");
}
return RenderOwnerIncarnation.FromRaw(_nextIncarnation++);
}
private static RenderProjectionId CreateProjectionId(
byte domain,
uint canonicalLandblockId,
uint localId) =>
RenderProjectionId.FromRaw(
((ulong)domain << 56)
| ((ulong)(canonicalLandblockId >> 16) << 32)
| localId);
private static uint Canonicalize(uint landblockId) =>
(landblockId & 0xFFFF0000u) | 0xFFFFu;
private readonly record struct TrackedProjection(
RenderProjectionRecord Record);
private sealed class ProjectionRecordComparer :
IComparer<RenderProjectionRecord>
{
public static ProjectionRecordComparer Instance { get; } = new();
public int Compare(RenderProjectionRecord x, RenderProjectionRecord y) =>
x.Id.CompareTo(y.Id);
}
private sealed class ProjectionIdComparer : IComparer<RenderProjectionId>
{
public static ProjectionIdComparer Instance { get; } = new();
public int Compare(RenderProjectionId x, RenderProjectionId y) =>
x.CompareTo(y);
}
}