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:
parent
dbd8318417
commit
5d19c56d15
10 changed files with 997 additions and 10 deletions
466
src/AcDream.App/Rendering/Scene/StaticRenderProjectionJournal.cs
Normal file
466
src/AcDream.App/Rendering/Scene/StaticRenderProjectionJournal.cs
Normal 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);
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue