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
|
|
@ -19,8 +19,9 @@ retail-faithful gameplay
|
||||||
| F0a — partition-input referee | complete | Diagnostic-only `CurrentRenderSceneOracle`; no production draw decision changed. |
|
| F0a — partition-input referee | complete | Diagnostic-only `CurrentRenderSceneOracle`; no production draw decision changed. |
|
||||||
| F0b — survivor/dispatcher/selection referee | complete | Exact PView routes, dispatcher candidates and final group payloads, material/alpha/clip/light/selection fields, and accepted picking parts. Release gate: 3,690 App tests / 3 skips and 8,174 complete-solution tests / 5 skips. |
|
| F0b — survivor/dispatcher/selection referee | complete | Exact PView routes, dispatcher candidates and final group payloads, material/alpha/clip/light/selection fields, and accepted picking parts. Release gate: 3,690 App tests / 3 skips and 8,174 complete-solution tests / 5 skips. |
|
||||||
| F1 — scene types and contained adapter | complete | `Arch 2.1.0` is pinned in App only behind acdream contracts. Five narrow archetypes, generation/incarnation/sequence gates, borrowed-query invalidation, memory accounting, deterministic digesting, and update-thread enforcement pass 11 focused tests. Release gate: 3,701 App tests / 3 skips and 8,185 complete-solution tests / 5 skips. |
|
| F1 — scene types and contained adapter | complete | `Arch 2.1.0` is pinned in App only behind acdream contracts. Five narrow archetypes, generation/incarnation/sequence gates, borrowed-query invalidation, memory accounting, deterministic digesting, and update-thread enforcement pass 11 focused tests. Release gate: 3,701 App tests / 3 skips and 8,185 complete-solution tests / 5 skips. |
|
||||||
| F2 — static projection journal | active | Static publication/retirement owners do not emit render deltas yet. |
|
| F2 — static projection journal | complete | Static and EnvCell-shell projections now append ordered deltas only after the final activation receipt and exact detach receipt. Same-landblock rehydrate reconciles retained/new/omitted identities; stale Far completions are inert. Seven focused lifecycle tests pass inside the 3,708-App / 8,192-solution Release gate. The scene remains unconstructed and non-drawing in production. |
|
||||||
| F3–F5 | pending | Live/equipped publication, dynamic indices, and continuous shadow comparison have not started. |
|
| F3 — live/equipped projection | active | Live/equipped accepted-state publication and final-frame synchronization have not landed. |
|
||||||
|
| F4–F5 | pending | Dynamic indices and continuous shadow comparison have not started. |
|
||||||
| G0–G5 | pending | No production consumer has switched. |
|
| G0–G5 | pending | No production consumer has switched. |
|
||||||
|
|
||||||
The exact pre-F/G runtime rollback anchor remains `e7d9d6fa`. F0 is
|
The exact pre-F/G runtime rollback anchor remains `e7d9d6fa`. F0 is
|
||||||
|
|
|
||||||
|
|
@ -333,6 +333,7 @@ internal sealed class ArchRenderScene : IRenderScene, IRenderSceneQuerySource
|
||||||
_world.Set(entry.Entity, record.Transform);
|
_world.Set(entry.Entity, record.Transform);
|
||||||
_world.Set(entry.Entity, record.Bounds);
|
_world.Set(entry.Entity, record.Bounds);
|
||||||
_world.Set(entry.Entity, record.SortKey);
|
_world.Set(entry.Entity, record.SortKey);
|
||||||
|
_world.Set(entry.Entity, record.Source);
|
||||||
OrDirty(
|
OrDirty(
|
||||||
entry.Entity,
|
entry.Entity,
|
||||||
RenderDirtyMask.Transform
|
RenderDirtyMask.Transform
|
||||||
|
|
@ -343,14 +344,17 @@ internal sealed class ArchRenderScene : IRenderScene, IRenderSceneQuerySource
|
||||||
_world.Set(entry.Entity, record.MeshSet);
|
_world.Set(entry.Entity, record.MeshSet);
|
||||||
_world.Set(entry.Entity, record.Material);
|
_world.Set(entry.Entity, record.Material);
|
||||||
_world.Set(entry.Entity, record.DegradeState);
|
_world.Set(entry.Entity, record.DegradeState);
|
||||||
|
_world.Set(entry.Entity, record.Source);
|
||||||
OrDirty(entry.Entity, RenderDirtyMask.Appearance);
|
OrDirty(entry.Entity, RenderDirtyMask.Appearance);
|
||||||
break;
|
break;
|
||||||
case RenderProjectionDeltaKind.UpdateFlags:
|
case RenderProjectionDeltaKind.UpdateFlags:
|
||||||
_world.Set(entry.Entity, record.Flags);
|
_world.Set(entry.Entity, record.Flags);
|
||||||
|
_world.Set(entry.Entity, record.Source);
|
||||||
OrDirty(entry.Entity, RenderDirtyMask.Flags);
|
OrDirty(entry.Entity, RenderDirtyMask.Flags);
|
||||||
break;
|
break;
|
||||||
case RenderProjectionDeltaKind.Rebucket:
|
case RenderProjectionDeltaKind.Rebucket:
|
||||||
_world.Set(entry.Entity, record.Residency);
|
_world.Set(entry.Entity, record.Residency);
|
||||||
|
_world.Set(entry.Entity, record.Source);
|
||||||
OrDirty(entry.Entity, RenderDirtyMask.SpatialResidency);
|
OrDirty(entry.Entity, RenderDirtyMask.SpatialResidency);
|
||||||
break;
|
break;
|
||||||
default:
|
default:
|
||||||
|
|
@ -430,6 +434,7 @@ internal sealed class ArchRenderScene : IRenderScene, IRenderSceneQuerySource
|
||||||
record.SortKey,
|
record.SortKey,
|
||||||
record.OwnerIncarnation,
|
record.OwnerIncarnation,
|
||||||
record.DirtyMask,
|
record.DirtyMask,
|
||||||
|
record.Source,
|
||||||
tag);
|
tag);
|
||||||
|
|
||||||
private void WriteRecord(
|
private void WriteRecord(
|
||||||
|
|
@ -447,6 +452,7 @@ internal sealed class ArchRenderScene : IRenderScene, IRenderSceneQuerySource
|
||||||
_world.Set(entity, record.SortKey);
|
_world.Set(entity, record.SortKey);
|
||||||
_world.Set(entity, record.OwnerIncarnation);
|
_world.Set(entity, record.OwnerIncarnation);
|
||||||
_world.Set(entity, record.DirtyMask);
|
_world.Set(entity, record.DirtyMask);
|
||||||
|
_world.Set(entity, record.Source);
|
||||||
}
|
}
|
||||||
|
|
||||||
private RenderProjectionRecord ReadRecord(in SceneEntry entry) =>
|
private RenderProjectionRecord ReadRecord(in SceneEntry entry) =>
|
||||||
|
|
@ -463,7 +469,8 @@ internal sealed class ArchRenderScene : IRenderScene, IRenderSceneQuerySource
|
||||||
_world.Get<RenderProjectionFlags>(entry.Entity),
|
_world.Get<RenderProjectionFlags>(entry.Entity),
|
||||||
_world.Get<RenderDegradeState>(entry.Entity),
|
_world.Get<RenderDegradeState>(entry.Entity),
|
||||||
_world.Get<RenderSortKey>(entry.Entity),
|
_world.Get<RenderSortKey>(entry.Entity),
|
||||||
_world.Get<RenderDirtyMask>(entry.Entity));
|
_world.Get<RenderDirtyMask>(entry.Entity),
|
||||||
|
_world.Get<RenderSourceMetadata>(entry.Entity));
|
||||||
|
|
||||||
private void Destroy(in SceneEntry entry)
|
private void Destroy(in SceneEntry entry)
|
||||||
{
|
{
|
||||||
|
|
@ -549,6 +556,9 @@ internal sealed class ArchRenderScene : IRenderScene, IRenderSceneQuerySource
|
||||||
hash.Add(record.Id.RawValue);
|
hash.Add(record.Id.RawValue);
|
||||||
hash.Add((byte)record.ProjectionClass);
|
hash.Add((byte)record.ProjectionClass);
|
||||||
hash.Add(record.OwnerIncarnation.RawValue);
|
hash.Add(record.OwnerIncarnation.RawValue);
|
||||||
|
hash.Add(record.Transform.Position);
|
||||||
|
hash.Add(record.Transform.Rotation);
|
||||||
|
hash.Add(record.Transform.UniformScale);
|
||||||
hash.Add(record.Transform.LocalToWorld);
|
hash.Add(record.Transform.LocalToWorld);
|
||||||
hash.Add(record.PreviousTransform.LocalToWorld);
|
hash.Add(record.PreviousTransform.LocalToWorld);
|
||||||
hash.Add(record.MeshSet.Handle.RawValue);
|
hash.Add(record.MeshSet.Handle.RawValue);
|
||||||
|
|
@ -566,6 +576,18 @@ internal sealed class ArchRenderScene : IRenderScene, IRenderSceneQuerySource
|
||||||
hash.Add(record.DegradeState.Level);
|
hash.Add(record.DegradeState.Level);
|
||||||
hash.Add(record.DegradeState.Revision);
|
hash.Add(record.DegradeState.Revision);
|
||||||
hash.Add(record.SortKey.Value);
|
hash.Add(record.SortKey.Value);
|
||||||
|
hash.Add(record.Source.LocalEntityId);
|
||||||
|
hash.Add(record.Source.ServerGuid);
|
||||||
|
hash.Add(record.Source.SourceId);
|
||||||
|
hash.Add(record.Source.ParentCellId);
|
||||||
|
hash.Add(record.Source.EffectCellId);
|
||||||
|
hash.Add(record.Source.BuildingShellAnchorCellId);
|
||||||
|
hash.Add(record.Source.TransformFingerprint.Low);
|
||||||
|
hash.Add(record.Source.TransformFingerprint.High);
|
||||||
|
hash.Add(record.Source.GeometryFingerprint.Low);
|
||||||
|
hash.Add(record.Source.GeometryFingerprint.High);
|
||||||
|
hash.Add(record.Source.AppearanceFingerprint.Low);
|
||||||
|
hash.Add(record.Source.AppearanceFingerprint.High);
|
||||||
}
|
}
|
||||||
|
|
||||||
private readonly record struct ProjectionIdentity(RenderProjectionId Id);
|
private readonly record struct ProjectionIdentity(RenderProjectionId Id);
|
||||||
|
|
|
||||||
|
|
@ -701,6 +701,15 @@ internal sealed class CurrentRenderSceneOracle :
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
internal static CurrentRenderProjectionFingerprint
|
||||||
|
CreateProjectionFingerprint(
|
||||||
|
uint landblockId,
|
||||||
|
WorldEntity entity) =>
|
||||||
|
CreateFingerprint(
|
||||||
|
landblockId,
|
||||||
|
entity,
|
||||||
|
ProjectionClassOf(entity));
|
||||||
|
|
||||||
private static CurrentRenderProjectionFingerprint CreateFingerprint(
|
private static CurrentRenderProjectionFingerprint CreateFingerprint(
|
||||||
uint landblockId,
|
uint landblockId,
|
||||||
WorldEntity entity,
|
WorldEntity entity,
|
||||||
|
|
|
||||||
88
src/AcDream.App/Rendering/Scene/RenderProjectionJournal.cs
Normal file
88
src/AcDream.App/Rendering/Scene/RenderProjectionJournal.cs
Normal file
|
|
@ -0,0 +1,88 @@
|
||||||
|
namespace AcDream.App.Rendering.Scene;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Update-thread-owned structural journal between accepted world mutation and
|
||||||
|
/// the disposable render projection. Structural edges are never coalesced.
|
||||||
|
/// A successful frame publication drains the complete retained buffer.
|
||||||
|
/// </summary>
|
||||||
|
internal sealed class RenderProjectionJournal
|
||||||
|
{
|
||||||
|
private readonly List<RenderProjectionDelta> _deltas = [];
|
||||||
|
private ulong _nextSequence = 1;
|
||||||
|
|
||||||
|
public RenderProjectionJournal(RenderSceneGeneration generation)
|
||||||
|
{
|
||||||
|
Generation = generation;
|
||||||
|
}
|
||||||
|
|
||||||
|
public RenderSceneGeneration Generation { get; private set; }
|
||||||
|
public int Count => _deltas.Count;
|
||||||
|
public int Capacity => _deltas.Capacity;
|
||||||
|
|
||||||
|
public void Register(in RenderProjectionRecord record) =>
|
||||||
|
_deltas.Add(RenderProjectionDelta.Register(
|
||||||
|
Generation,
|
||||||
|
NextSequence(),
|
||||||
|
in record));
|
||||||
|
|
||||||
|
public void Update(
|
||||||
|
RenderProjectionDeltaKind kind,
|
||||||
|
in RenderProjectionRecord record) =>
|
||||||
|
_deltas.Add(RenderProjectionDelta.Update(
|
||||||
|
kind,
|
||||||
|
Generation,
|
||||||
|
NextSequence(),
|
||||||
|
in record));
|
||||||
|
|
||||||
|
public void Unregister(
|
||||||
|
RenderProjectionId id,
|
||||||
|
RenderOwnerIncarnation incarnation) =>
|
||||||
|
_deltas.Add(RenderProjectionDelta.Unregister(
|
||||||
|
Generation,
|
||||||
|
NextSequence(),
|
||||||
|
id,
|
||||||
|
incarnation));
|
||||||
|
|
||||||
|
public RenderDeltaApplyResult DrainTo(IRenderScene scene)
|
||||||
|
{
|
||||||
|
ArgumentNullException.ThrowIfNull(scene);
|
||||||
|
if (scene.Generation != Generation)
|
||||||
|
{
|
||||||
|
throw new InvalidOperationException(
|
||||||
|
$"Journal {Generation} cannot drain into scene {scene.Generation}.");
|
||||||
|
}
|
||||||
|
|
||||||
|
RenderDeltaApplyResult result =
|
||||||
|
scene.Apply(System.Runtime.InteropServices.CollectionsMarshal.AsSpan(_deltas));
|
||||||
|
_deltas.Clear();
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void Clear(RenderSceneGeneration replacementGeneration)
|
||||||
|
{
|
||||||
|
if (replacementGeneration.CompareTo(Generation) <= 0)
|
||||||
|
{
|
||||||
|
throw new ArgumentOutOfRangeException(
|
||||||
|
nameof(replacementGeneration),
|
||||||
|
replacementGeneration,
|
||||||
|
"A replacement journal generation must advance.");
|
||||||
|
}
|
||||||
|
|
||||||
|
_deltas.Clear();
|
||||||
|
Generation = replacementGeneration;
|
||||||
|
}
|
||||||
|
|
||||||
|
internal ReadOnlySpan<RenderProjectionDelta> Pending =>
|
||||||
|
System.Runtime.InteropServices.CollectionsMarshal.AsSpan(_deltas);
|
||||||
|
|
||||||
|
private ulong NextSequence()
|
||||||
|
{
|
||||||
|
if (_nextSequence == ulong.MaxValue)
|
||||||
|
{
|
||||||
|
throw new InvalidOperationException(
|
||||||
|
"Render projection journal sequence exhausted.");
|
||||||
|
}
|
||||||
|
|
||||||
|
return _nextSequence++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -11,6 +11,7 @@ internal readonly record struct RenderProjectionId
|
||||||
internal static RenderProjectionId FromRaw(ulong value) => new(value);
|
internal static RenderProjectionId FromRaw(ulong value) => new(value);
|
||||||
|
|
||||||
internal ulong RawValue => _value;
|
internal ulong RawValue => _value;
|
||||||
|
internal RenderSortKey ToSortKey() => new(_value);
|
||||||
|
|
||||||
public int CompareTo(RenderProjectionId other) =>
|
public int CompareTo(RenderProjectionId other) =>
|
||||||
_value.CompareTo(other._value);
|
_value.CompareTo(other._value);
|
||||||
|
|
@ -117,7 +118,32 @@ internal enum RenderDirtyMask : ushort
|
||||||
All = ushort.MaxValue,
|
All = ushort.MaxValue,
|
||||||
}
|
}
|
||||||
|
|
||||||
internal readonly record struct RenderTransform(Matrix4x4 LocalToWorld);
|
internal readonly record struct RenderTransform(
|
||||||
|
Vector3 Position,
|
||||||
|
Quaternion Rotation,
|
||||||
|
float UniformScale,
|
||||||
|
Matrix4x4 LocalToWorld)
|
||||||
|
{
|
||||||
|
public RenderTransform(Matrix4x4 localToWorld)
|
||||||
|
: this(
|
||||||
|
localToWorld.Translation,
|
||||||
|
Quaternion.Identity,
|
||||||
|
1.0f,
|
||||||
|
localToWorld)
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
public static RenderTransform FromRoot(
|
||||||
|
Vector3 position,
|
||||||
|
Quaternion rotation,
|
||||||
|
float uniformScale) =>
|
||||||
|
new(
|
||||||
|
position,
|
||||||
|
rotation,
|
||||||
|
uniformScale,
|
||||||
|
Matrix4x4.CreateFromQuaternion(rotation)
|
||||||
|
* Matrix4x4.CreateTranslation(position));
|
||||||
|
}
|
||||||
|
|
||||||
internal readonly record struct PreviousRenderTransform(Matrix4x4 LocalToWorld);
|
internal readonly record struct PreviousRenderTransform(Matrix4x4 LocalToWorld);
|
||||||
|
|
||||||
|
|
@ -142,6 +168,22 @@ internal readonly record struct RenderDegradeState(byte Level, uint Revision);
|
||||||
|
|
||||||
internal readonly record struct RenderSortKey(ulong Value);
|
internal readonly record struct RenderSortKey(ulong Value);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Immutable source facts required to compare the derived presentation scene
|
||||||
|
/// with the accepted current path. These are diagnostics/render inputs only;
|
||||||
|
/// they never become a second gameplay or server-GUID authority.
|
||||||
|
/// </summary>
|
||||||
|
internal readonly record struct RenderSourceMetadata(
|
||||||
|
uint LocalEntityId,
|
||||||
|
uint ServerGuid,
|
||||||
|
uint SourceId,
|
||||||
|
uint ParentCellId,
|
||||||
|
uint EffectCellId,
|
||||||
|
uint BuildingShellAnchorCellId,
|
||||||
|
RenderSceneHash128 TransformFingerprint,
|
||||||
|
RenderSceneHash128 GeometryFingerprint,
|
||||||
|
RenderSceneHash128 AppearanceFingerprint);
|
||||||
|
|
||||||
internal readonly record struct RenderProjectionRecord(
|
internal readonly record struct RenderProjectionRecord(
|
||||||
RenderProjectionId Id,
|
RenderProjectionId Id,
|
||||||
RenderProjectionClass ProjectionClass,
|
RenderProjectionClass ProjectionClass,
|
||||||
|
|
@ -155,7 +197,8 @@ internal readonly record struct RenderProjectionRecord(
|
||||||
RenderProjectionFlags Flags,
|
RenderProjectionFlags Flags,
|
||||||
RenderDegradeState DegradeState,
|
RenderDegradeState DegradeState,
|
||||||
RenderSortKey SortKey,
|
RenderSortKey SortKey,
|
||||||
RenderDirtyMask DirtyMask);
|
RenderDirtyMask DirtyMask,
|
||||||
|
RenderSourceMetadata Source = default);
|
||||||
|
|
||||||
internal enum RenderProjectionDeltaKind : byte
|
internal enum RenderProjectionDeltaKind : byte
|
||||||
{
|
{
|
||||||
|
|
|
||||||
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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -1,4 +1,5 @@
|
||||||
using AcDream.App.Rendering.Wb;
|
using AcDream.App.Rendering.Wb;
|
||||||
|
using AcDream.App.Rendering.Scene;
|
||||||
using AcDream.Core.Terrain;
|
using AcDream.Core.Terrain;
|
||||||
using AcDream.Core.World;
|
using AcDream.Core.World;
|
||||||
|
|
||||||
|
|
@ -73,6 +74,7 @@ public sealed class LandblockPresentationPipeline
|
||||||
private readonly GpuWorldState _state;
|
private readonly GpuWorldState _state;
|
||||||
private readonly Action<uint>? _onLandblockLoaded;
|
private readonly Action<uint>? _onLandblockLoaded;
|
||||||
private readonly Action<EnvCellLandblockBuild>? _ensureEnvCellMeshes;
|
private readonly Action<EnvCellLandblockBuild>? _ensureEnvCellMeshes;
|
||||||
|
private readonly IRenderStaticProjectionJournalSink? _staticProjectionSink;
|
||||||
private readonly LandblockRetirementCoordinator _retirements;
|
private readonly LandblockRetirementCoordinator _retirements;
|
||||||
private readonly Dictionary<LandblockStreamResult, PublicationTransaction> _publications =
|
private readonly Dictionary<LandblockStreamResult, PublicationTransaction> _publications =
|
||||||
new(ReferenceEqualityComparer.Instance);
|
new(ReferenceEqualityComparer.Instance);
|
||||||
|
|
@ -84,7 +86,8 @@ public sealed class LandblockPresentationPipeline
|
||||||
Action<EnvCellLandblockBuild>? ensureEnvCellMeshes = null,
|
Action<EnvCellLandblockBuild>? ensureEnvCellMeshes = null,
|
||||||
LandblockRetirementCoordinator? retirementCoordinator = null,
|
LandblockRetirementCoordinator? retirementCoordinator = null,
|
||||||
Action<uint>? removeTerrain = null,
|
Action<uint>? removeTerrain = null,
|
||||||
Action<uint>? demoteNearLayer = null)
|
Action<uint>? demoteNearLayer = null,
|
||||||
|
IRenderStaticProjectionJournalSink? staticProjectionSink = null)
|
||||||
{
|
{
|
||||||
ArgumentNullException.ThrowIfNull(publishBeforeSpatialCommit);
|
ArgumentNullException.ThrowIfNull(publishBeforeSpatialCommit);
|
||||||
ArgumentNullException.ThrowIfNull(state);
|
ArgumentNullException.ThrowIfNull(state);
|
||||||
|
|
@ -93,6 +96,7 @@ public sealed class LandblockPresentationPipeline
|
||||||
_state = state;
|
_state = state;
|
||||||
_onLandblockLoaded = onLandblockLoaded;
|
_onLandblockLoaded = onLandblockLoaded;
|
||||||
_ensureEnvCellMeshes = ensureEnvCellMeshes;
|
_ensureEnvCellMeshes = ensureEnvCellMeshes;
|
||||||
|
_staticProjectionSink = staticProjectionSink;
|
||||||
if (retirementCoordinator is not null
|
if (retirementCoordinator is not null
|
||||||
&& !retirementCoordinator.MatchesState(_state))
|
&& !retirementCoordinator.MatchesState(_state))
|
||||||
{
|
{
|
||||||
|
|
@ -120,6 +124,27 @@ public sealed class LandblockPresentationPipeline
|
||||||
LandblockPresentationRetirementOwner retirementOwner,
|
LandblockPresentationRetirementOwner retirementOwner,
|
||||||
Action<uint>? onLandblockLoaded = null,
|
Action<uint>? onLandblockLoaded = null,
|
||||||
Action<EnvCellLandblockBuild>? ensureEnvCellMeshes = 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
|
_renderPublisher = renderPublisher
|
||||||
?? throw new ArgumentNullException(nameof(renderPublisher));
|
?? throw new ArgumentNullException(nameof(renderPublisher));
|
||||||
|
|
@ -146,10 +171,14 @@ public sealed class LandblockPresentationPipeline
|
||||||
}
|
}
|
||||||
_onLandblockLoaded = onLandblockLoaded;
|
_onLandblockLoaded = onLandblockLoaded;
|
||||||
_ensureEnvCellMeshes = ensureEnvCellMeshes;
|
_ensureEnvCellMeshes = ensureEnvCellMeshes;
|
||||||
|
_staticProjectionSink = staticProjectionSink;
|
||||||
_retirements = LandblockRetirementCoordinator.CreateBudgeted(
|
_retirements = LandblockRetirementCoordinator.CreateBudgeted(
|
||||||
state,
|
state,
|
||||||
retirementOwner.AdvanceOne,
|
retirementOwner.AdvanceOne,
|
||||||
retirementOwner.Advance);
|
retirementOwner.Advance,
|
||||||
|
staticProjectionSink is null
|
||||||
|
? null
|
||||||
|
: staticProjectionSink.Retire);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
|
|
@ -785,6 +814,12 @@ public sealed class LandblockPresentationPipeline
|
||||||
transaction.SpatialPublication
|
transaction.SpatialPublication
|
||||||
?? throw new InvalidOperationException(
|
?? throw new InvalidOperationException(
|
||||||
"A committed spatial publication has no activation receipt."));
|
"A committed spatial publication has no activation receipt."));
|
||||||
|
if (transaction.SpatialPublication.RequiresActivation)
|
||||||
|
{
|
||||||
|
_staticProjectionSink?.Reconcile(
|
||||||
|
transaction.Build,
|
||||||
|
transaction.SpatialPublication);
|
||||||
|
}
|
||||||
transaction.SpatialPresentationCommitted = true;
|
transaction.SpatialPresentationCommitted = true;
|
||||||
}))
|
}))
|
||||||
{
|
{
|
||||||
|
|
|
||||||
|
|
@ -273,6 +273,7 @@ public sealed class LandblockRetirementCoordinator
|
||||||
LandblockRetirementOperationResult>? _advancePresentationStep;
|
LandblockRetirementOperationResult>? _advancePresentationStep;
|
||||||
private readonly Func<LandblockRetirementKind, LandblockRetirementStage>
|
private readonly Func<LandblockRetirementKind, LandblockRetirementStage>
|
||||||
_requiredPresentationStages;
|
_requiredPresentationStages;
|
||||||
|
private readonly Action<GpuLandblockRetirement>? _onDetached;
|
||||||
private readonly Dictionary<uint, List<LandblockRetirementTicket>> _pending = new();
|
private readonly Dictionary<uint, List<LandblockRetirementTicket>> _pending = new();
|
||||||
private readonly Queue<LandblockRetirementTicket> _pendingOrder = new();
|
private readonly Queue<LandblockRetirementTicket> _pendingOrder = new();
|
||||||
private readonly List<uint> _completedIds = new();
|
private readonly List<uint> _completedIds = new();
|
||||||
|
|
@ -290,6 +291,7 @@ public sealed class LandblockRetirementCoordinator
|
||||||
_state = state;
|
_state = state;
|
||||||
_advancePresentation = advancePresentation;
|
_advancePresentation = advancePresentation;
|
||||||
_advancePresentationStep = null;
|
_advancePresentationStep = null;
|
||||||
|
_onDetached = null;
|
||||||
_requiredPresentationStages = requiredPresentationStages
|
_requiredPresentationStages = requiredPresentationStages
|
||||||
?? (kind => kind == LandblockRetirementKind.Full
|
?? (kind => kind == LandblockRetirementKind.Full
|
||||||
? ProductionPresentationStages
|
? ProductionPresentationStages
|
||||||
|
|
@ -302,7 +304,8 @@ public sealed class LandblockRetirementCoordinator
|
||||||
advancePresentationStep,
|
advancePresentationStep,
|
||||||
Action<LandblockRetirementTicket> advancePresentation,
|
Action<LandblockRetirementTicket> advancePresentation,
|
||||||
Func<LandblockRetirementKind, LandblockRetirementStage>
|
Func<LandblockRetirementKind, LandblockRetirementStage>
|
||||||
requiredPresentationStages)
|
requiredPresentationStages,
|
||||||
|
Action<GpuLandblockRetirement>? onDetached)
|
||||||
{
|
{
|
||||||
_state = state ?? throw new ArgumentNullException(nameof(state));
|
_state = state ?? throw new ArgumentNullException(nameof(state));
|
||||||
_advancePresentationStep = advancePresentationStep
|
_advancePresentationStep = advancePresentationStep
|
||||||
|
|
@ -311,6 +314,7 @@ public sealed class LandblockRetirementCoordinator
|
||||||
?? throw new ArgumentNullException(nameof(advancePresentation));
|
?? throw new ArgumentNullException(nameof(advancePresentation));
|
||||||
_requiredPresentationStages = requiredPresentationStages
|
_requiredPresentationStages = requiredPresentationStages
|
||||||
?? throw new ArgumentNullException(nameof(requiredPresentationStages));
|
?? throw new ArgumentNullException(nameof(requiredPresentationStages));
|
||||||
|
_onDetached = onDetached;
|
||||||
}
|
}
|
||||||
|
|
||||||
public int PendingCount { get; private set; }
|
public int PendingCount { get; private set; }
|
||||||
|
|
@ -335,7 +339,8 @@ public sealed class LandblockRetirementCoordinator
|
||||||
GpuWorldState state,
|
GpuWorldState state,
|
||||||
Func<LandblockRetirementTicket, LandblockRetirementOperationResult>
|
Func<LandblockRetirementTicket, LandblockRetirementOperationResult>
|
||||||
advancePresentationStep,
|
advancePresentationStep,
|
||||||
Action<LandblockRetirementTicket> advancePresentation)
|
Action<LandblockRetirementTicket> advancePresentation,
|
||||||
|
Action<GpuLandblockRetirement>? onDetached = null)
|
||||||
{
|
{
|
||||||
return new LandblockRetirementCoordinator(
|
return new LandblockRetirementCoordinator(
|
||||||
state,
|
state,
|
||||||
|
|
@ -343,7 +348,8 @@ public sealed class LandblockRetirementCoordinator
|
||||||
advancePresentation,
|
advancePresentation,
|
||||||
kind => kind == LandblockRetirementKind.Full
|
kind => kind == LandblockRetirementKind.Full
|
||||||
? ProductionPresentationStages
|
? ProductionPresentationStages
|
||||||
: ProductionPresentationStages & ~LandblockRetirementStage.Terrain);
|
: ProductionPresentationStages & ~LandblockRetirementStage.Terrain,
|
||||||
|
onDetached);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
|
|
@ -531,6 +537,8 @@ public sealed class LandblockRetirementCoordinator
|
||||||
if (_advancePresentationStep is not null)
|
if (_advancePresentationStep is not null)
|
||||||
_pendingOrder.Enqueue(ticket);
|
_pendingOrder.Enqueue(ticket);
|
||||||
|
|
||||||
|
_onDetached?.Invoke(stateRetirement);
|
||||||
|
|
||||||
if (_advancePresentationStep is null)
|
if (_advancePresentationStep is null)
|
||||||
{
|
{
|
||||||
AdvanceTicket(ticket);
|
AdvanceTicket(ticket);
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,280 @@
|
||||||
|
using System.Collections.Immutable;
|
||||||
|
using System.Numerics;
|
||||||
|
using AcDream.App.Rendering;
|
||||||
|
using AcDream.App.Rendering.Scene;
|
||||||
|
using AcDream.App.Rendering.Scene.Arch;
|
||||||
|
using AcDream.App.Rendering.Wb;
|
||||||
|
using AcDream.App.Streaming;
|
||||||
|
using AcDream.Core.World;
|
||||||
|
using DatReaderWriter.DBObjs;
|
||||||
|
|
||||||
|
namespace AcDream.App.Tests.Rendering;
|
||||||
|
|
||||||
|
public sealed class StaticRenderProjectionJournalTests
|
||||||
|
{
|
||||||
|
private const uint LandblockId = 0x1234FFFFu;
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void Reconcile_RegistersOnlyStaticEntitiesAndEnvCellShells()
|
||||||
|
{
|
||||||
|
RenderSceneGeneration generation = Generation(1);
|
||||||
|
var journal = new RenderProjectionJournal(generation);
|
||||||
|
var statics = new StaticRenderProjectionJournal(journal);
|
||||||
|
WorldEntity staticEntity = Entity(7, position: new Vector3(1, 2, 3));
|
||||||
|
WorldEntity liveEntity = Entity(8, serverGuid: 0x80000008u);
|
||||||
|
LandblockBuild build = Build(
|
||||||
|
LandblockId,
|
||||||
|
[staticEntity, liveEntity],
|
||||||
|
includeShell: true);
|
||||||
|
var publication = Publication(build);
|
||||||
|
|
||||||
|
statics.Reconcile(build, publication);
|
||||||
|
|
||||||
|
Assert.Equal(2, statics.ProjectionCount);
|
||||||
|
Assert.Equal(2, journal.Count);
|
||||||
|
Assert.All(
|
||||||
|
journal.Pending.ToArray(),
|
||||||
|
delta => Assert.Equal(
|
||||||
|
RenderProjectionDeltaKind.Register,
|
||||||
|
delta.Kind));
|
||||||
|
|
||||||
|
using var scene = new ArchRenderScene(generation);
|
||||||
|
RenderDeltaApplyResult applied = journal.DrainTo(scene);
|
||||||
|
Assert.Equal(2, applied.Registered);
|
||||||
|
Assert.Equal(0, journal.Count);
|
||||||
|
Assert.Equal(2, scene.Counts.Total);
|
||||||
|
Assert.Equal(1, scene.Counts.OutdoorStatic);
|
||||||
|
Assert.Equal(1, scene.Counts.IndoorCellStatic);
|
||||||
|
|
||||||
|
RenderProjectionId staticId =
|
||||||
|
StaticRenderProjectionJournal.StaticEntityId(
|
||||||
|
LandblockId,
|
||||||
|
staticEntity.Id);
|
||||||
|
Assert.True(scene.OpenQuery().TryGet(staticId, out var projected));
|
||||||
|
CurrentRenderProjectionFingerprint expected =
|
||||||
|
CurrentRenderSceneOracle.CreateProjectionFingerprint(
|
||||||
|
LandblockId,
|
||||||
|
staticEntity);
|
||||||
|
Assert.Equal(expected.Transform, projected.Source.TransformFingerprint);
|
||||||
|
Assert.Equal(expected.Geometry, projected.Source.GeometryFingerprint);
|
||||||
|
Assert.Equal(expected.Appearance, projected.Source.AppearanceFingerprint);
|
||||||
|
Assert.Equal(LandblockId, projected.Residency.OwnerLandblockId);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void Reconcile_SameLandblockRetainsUpdatesOmitsAndAddsDeterministically()
|
||||||
|
{
|
||||||
|
RenderSceneGeneration generation = Generation(2);
|
||||||
|
var journal = new RenderProjectionJournal(generation);
|
||||||
|
var statics = new StaticRenderProjectionJournal(journal);
|
||||||
|
WorldEntity retained = Entity(1);
|
||||||
|
WorldEntity omitted = Entity(2);
|
||||||
|
LandblockBuild first = Build(
|
||||||
|
LandblockId,
|
||||||
|
[retained, omitted],
|
||||||
|
includeShell: true);
|
||||||
|
statics.Reconcile(first, Publication(first));
|
||||||
|
using var scene = new ArchRenderScene(generation);
|
||||||
|
journal.DrainTo(scene);
|
||||||
|
RenderProjectionId retainedId =
|
||||||
|
StaticRenderProjectionJournal.StaticEntityId(LandblockId, 1);
|
||||||
|
Assert.True(scene.OpenQuery().TryGet(retainedId, out var before));
|
||||||
|
|
||||||
|
WorldEntity moved = Entity(1, position: new Vector3(10, 0, 0));
|
||||||
|
WorldEntity added = Entity(3);
|
||||||
|
LandblockBuild replacement = Build(
|
||||||
|
LandblockId,
|
||||||
|
[moved, added],
|
||||||
|
includeShell: false);
|
||||||
|
statics.Reconcile(replacement, Publication(replacement));
|
||||||
|
|
||||||
|
Assert.Equal(4, journal.Count);
|
||||||
|
Assert.Equal(
|
||||||
|
[
|
||||||
|
RenderProjectionDeltaKind.UpdateTransform,
|
||||||
|
RenderProjectionDeltaKind.Register,
|
||||||
|
RenderProjectionDeltaKind.Unregister,
|
||||||
|
RenderProjectionDeltaKind.Unregister,
|
||||||
|
],
|
||||||
|
journal.Pending.ToArray().Select(delta => delta.Kind).ToArray());
|
||||||
|
|
||||||
|
journal.DrainTo(scene);
|
||||||
|
Assert.Equal(2, scene.Counts.Total);
|
||||||
|
Assert.True(scene.OpenQuery().TryGet(retainedId, out var after));
|
||||||
|
Assert.Equal(before.OwnerIncarnation, after.OwnerIncarnation);
|
||||||
|
Assert.Equal(moved.Position, after.Transform.Position);
|
||||||
|
Assert.False(scene.OpenQuery().TryGet(
|
||||||
|
StaticRenderProjectionJournal.StaticEntityId(LandblockId, 2),
|
||||||
|
out _));
|
||||||
|
Assert.False(scene.OpenQuery().TryGet(
|
||||||
|
StaticRenderProjectionJournal.EnvCellShellId(
|
||||||
|
LandblockId,
|
||||||
|
(LandblockId & 0xFFFF0000u) | 0x0100u),
|
||||||
|
out _));
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void ProjectionIdentity_DistinguishesDuplicateLocalIdsAcrossLandblocks()
|
||||||
|
{
|
||||||
|
const uint otherLandblock = 0x5678FFFFu;
|
||||||
|
var journal = new RenderProjectionJournal(Generation(3));
|
||||||
|
var statics = new StaticRenderProjectionJournal(journal);
|
||||||
|
LandblockBuild first = Build(
|
||||||
|
LandblockId,
|
||||||
|
[Entity(42)],
|
||||||
|
includeShell: false);
|
||||||
|
LandblockBuild second = Build(
|
||||||
|
otherLandblock,
|
||||||
|
[Entity(42)],
|
||||||
|
includeShell: false);
|
||||||
|
|
||||||
|
statics.Reconcile(first, Publication(first));
|
||||||
|
statics.Reconcile(second, Publication(second));
|
||||||
|
|
||||||
|
Assert.Equal(2, statics.ProjectionCount);
|
||||||
|
Assert.NotEqual(
|
||||||
|
StaticRenderProjectionJournal.StaticEntityId(LandblockId, 42),
|
||||||
|
StaticRenderProjectionJournal.StaticEntityId(otherLandblock, 42));
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void StaleFarPublication_DoesNotMutateProjectionJournal()
|
||||||
|
{
|
||||||
|
var journal = new RenderProjectionJournal(Generation(4));
|
||||||
|
var statics = new StaticRenderProjectionJournal(journal);
|
||||||
|
LandblockBuild build = Build(
|
||||||
|
LandblockId,
|
||||||
|
[Entity(1)],
|
||||||
|
includeShell: true);
|
||||||
|
var stale = Publication(build) with { RequiresActivation = false };
|
||||||
|
|
||||||
|
statics.Reconcile(build, stale);
|
||||||
|
|
||||||
|
Assert.Equal(0, statics.ProjectionCount);
|
||||||
|
Assert.Equal(0, journal.Count);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void Retire_IsExactAndIdempotentForFullAndNearLayerReceipts()
|
||||||
|
{
|
||||||
|
var journal = new RenderProjectionJournal(Generation(5));
|
||||||
|
var statics = new StaticRenderProjectionJournal(journal);
|
||||||
|
LandblockBuild build = Build(
|
||||||
|
LandblockId,
|
||||||
|
[Entity(1), Entity(2)],
|
||||||
|
includeShell: true);
|
||||||
|
statics.Reconcile(build, Publication(build));
|
||||||
|
using var scene = new ArchRenderScene(Generation(5));
|
||||||
|
journal.DrainTo(scene);
|
||||||
|
|
||||||
|
var retirement = new GpuLandblockRetirement(
|
||||||
|
LandblockId,
|
||||||
|
LandblockRetirementKind.NearLayer,
|
||||||
|
build.Landblock.Entities);
|
||||||
|
statics.Retire(retirement);
|
||||||
|
statics.Retire(retirement);
|
||||||
|
|
||||||
|
Assert.Equal(3, journal.Count);
|
||||||
|
Assert.Equal(0, statics.ProjectionCount);
|
||||||
|
Assert.All(
|
||||||
|
journal.Pending.ToArray(),
|
||||||
|
delta => Assert.Equal(
|
||||||
|
RenderProjectionDeltaKind.Unregister,
|
||||||
|
delta.Kind));
|
||||||
|
RenderDeltaApplyResult result = journal.DrainTo(scene);
|
||||||
|
Assert.Equal(3, result.Unregistered);
|
||||||
|
Assert.Equal(0, scene.Counts.Total);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void Clear_AdvancesExactGenerationAndDropsRetainedState()
|
||||||
|
{
|
||||||
|
var journal = new RenderProjectionJournal(Generation(6));
|
||||||
|
var statics = new StaticRenderProjectionJournal(journal);
|
||||||
|
LandblockBuild build = Build(
|
||||||
|
LandblockId,
|
||||||
|
[Entity(1)],
|
||||||
|
includeShell: true);
|
||||||
|
statics.Reconcile(build, Publication(build));
|
||||||
|
|
||||||
|
statics.Clear(Generation(7));
|
||||||
|
|
||||||
|
Assert.Equal(Generation(7), journal.Generation);
|
||||||
|
Assert.Equal(0, journal.Count);
|
||||||
|
Assert.Equal(0, statics.ProjectionCount);
|
||||||
|
Assert.Equal(0, statics.LandblockCount);
|
||||||
|
Assert.Throws<ArgumentOutOfRangeException>(
|
||||||
|
() => statics.Clear(Generation(7)));
|
||||||
|
}
|
||||||
|
|
||||||
|
private static GpuLandblockSpatialPublication Publication(
|
||||||
|
LandblockBuild build,
|
||||||
|
bool requiresActivation = true) =>
|
||||||
|
new(
|
||||||
|
build.LandblockId,
|
||||||
|
build.Landblock,
|
||||||
|
build.EnvCells?.Shells.Select(shell => shell.GeometryId).ToArray()
|
||||||
|
?? [],
|
||||||
|
build.Landblock.Entities.Where(entity => entity.ServerGuid == 0).ToArray(),
|
||||||
|
requiresActivation);
|
||||||
|
|
||||||
|
private static LandblockBuild Build(
|
||||||
|
uint landblockId,
|
||||||
|
IReadOnlyList<WorldEntity> entities,
|
||||||
|
bool includeShell)
|
||||||
|
{
|
||||||
|
EnvCellLandblockBuild? envCells = null;
|
||||||
|
if (includeShell)
|
||||||
|
{
|
||||||
|
uint cellId = (landblockId & 0xFFFF0000u) | 0x0100u;
|
||||||
|
envCells = new EnvCellLandblockBuild(
|
||||||
|
landblockId,
|
||||||
|
Array.Empty<LoadedCell>(),
|
||||||
|
[
|
||||||
|
new EnvCellShellPlacement(
|
||||||
|
cellId,
|
||||||
|
0x2_0000_0001UL,
|
||||||
|
0x0D000001u,
|
||||||
|
1,
|
||||||
|
ImmutableArray<ushort>.Empty,
|
||||||
|
new Vector3(4, 5, 6),
|
||||||
|
Quaternion.Identity,
|
||||||
|
Matrix4x4.CreateTranslation(4, 5, 6),
|
||||||
|
new WbBoundingBox(Vector3.Zero, Vector3.One),
|
||||||
|
new WbBoundingBox(
|
||||||
|
new Vector3(4, 5, 6),
|
||||||
|
new Vector3(5, 6, 7))),
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
return new LandblockBuild(
|
||||||
|
new LoadedLandblock(
|
||||||
|
landblockId,
|
||||||
|
new LandBlock(),
|
||||||
|
entities,
|
||||||
|
PhysicsDatBundle.Empty),
|
||||||
|
envCells);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static WorldEntity Entity(
|
||||||
|
uint id,
|
||||||
|
uint serverGuid = 0,
|
||||||
|
Vector3 position = default) =>
|
||||||
|
new()
|
||||||
|
{
|
||||||
|
Id = id,
|
||||||
|
ServerGuid = serverGuid,
|
||||||
|
SourceGfxObjOrSetupId = 0x01000000u + id,
|
||||||
|
Position = position,
|
||||||
|
Rotation = Quaternion.Identity,
|
||||||
|
MeshRefs =
|
||||||
|
[
|
||||||
|
new MeshRef(
|
||||||
|
0x01010000u + id,
|
||||||
|
Matrix4x4.CreateTranslation(0, 0, id)),
|
||||||
|
],
|
||||||
|
};
|
||||||
|
|
||||||
|
private static RenderSceneGeneration Generation(ulong value) =>
|
||||||
|
RenderSceneGeneration.FromRaw(value);
|
||||||
|
}
|
||||||
|
|
@ -1,6 +1,7 @@
|
||||||
using System.Collections.Immutable;
|
using System.Collections.Immutable;
|
||||||
using System.Numerics;
|
using System.Numerics;
|
||||||
using AcDream.App.Rendering;
|
using AcDream.App.Rendering;
|
||||||
|
using AcDream.App.Rendering.Scene;
|
||||||
using AcDream.App.Rendering.Wb;
|
using AcDream.App.Rendering.Wb;
|
||||||
using AcDream.App.Streaming;
|
using AcDream.App.Streaming;
|
||||||
using AcDream.App.World;
|
using AcDream.App.World;
|
||||||
|
|
@ -989,6 +990,40 @@ public sealed class LandblockPresentationPipelineTests
|
||||||
Assert.Equal(2, meshes.PinAttempts);
|
Assert.Equal(2, meshes.PinAttempts);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void StaticRenderProjection_ReconcilesOnlyAfterSuccessfulSpatialActivation()
|
||||||
|
{
|
||||||
|
const uint landblockId = 0x6363FFFFu;
|
||||||
|
var meshes = new FailFirstPreparedPinAdapter();
|
||||||
|
var state = new GpuWorldState(new LandblockSpawnAdapter(meshes));
|
||||||
|
var journal = new RenderProjectionJournal(
|
||||||
|
RenderSceneGeneration.FromRaw(1));
|
||||||
|
var statics = new StaticRenderProjectionJournal(journal);
|
||||||
|
LandblockBuild build = BuildWithShell(landblockId, Entity(32));
|
||||||
|
var result = new LandblockStreamResult.Loaded(
|
||||||
|
landblockId,
|
||||||
|
LandblockStreamTier.Near,
|
||||||
|
build,
|
||||||
|
EmptyMesh());
|
||||||
|
var pipeline = new LandblockPresentationPipeline(
|
||||||
|
static (_, _) => { },
|
||||||
|
state,
|
||||||
|
staticProjectionSink: statics);
|
||||||
|
|
||||||
|
Assert.Throws<InvalidOperationException>(
|
||||||
|
() => pipeline.PublishLoaded(result));
|
||||||
|
Assert.Equal(0, journal.Count);
|
||||||
|
Assert.Equal(0, statics.ProjectionCount);
|
||||||
|
|
||||||
|
pipeline.PublishLoaded(result);
|
||||||
|
|
||||||
|
Assert.Equal(2, journal.Count);
|
||||||
|
Assert.Equal(2, statics.ProjectionCount);
|
||||||
|
Assert.Equal(2, meshes.PinAttempts);
|
||||||
|
pipeline.PublishLoaded(result);
|
||||||
|
Assert.Equal(2, journal.Count);
|
||||||
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
public void PriorityFailure_PreservesFailingResultAndUntouchedDrainedSuffix()
|
public void PriorityFailure_PreservesFailingResultAndUntouchedDrainedSuffix()
|
||||||
{
|
{
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue