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,4 +1,3 @@
using System.Numerics;
using AcDream.App.Rendering.Wb;
using AcDream.App.Streaming;
using AcDream.Core.World;
@ -25,7 +24,6 @@ internal sealed class StaticRenderProjectionJournal :
{
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>>
@ -115,7 +113,7 @@ internal sealed class StaticRenderProjectionJournal :
if (accepted != retained.Record)
{
AppendUpdates(retained.Record, accepted);
_journal.AppendDifference(retained.Record, accepted);
current[candidate.Id] = new TrackedProjection(accepted);
}
continue;
@ -217,61 +215,18 @@ internal sealed class StaticRenderProjectionJournal :
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(
return RenderProjectionRecordFactory.ProjectEntity(
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));
landblockId,
fullCellId,
entity,
spatiallyVisible: true);
}
private static RenderProjectionRecord ProjectEnvCellShell(
@ -332,95 +287,6 @@ internal sealed class StaticRenderProjectionJournal :
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)