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. |
|
||||
| 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. |
|
||||
| F2 — static projection journal | active | Static publication/retirement owners do not emit render deltas yet. |
|
||||
| F3–F5 | pending | Live/equipped publication, dynamic indices, and continuous shadow comparison have not started. |
|
||||
| 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 — 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. |
|
||||
|
||||
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.Bounds);
|
||||
_world.Set(entry.Entity, record.SortKey);
|
||||
_world.Set(entry.Entity, record.Source);
|
||||
OrDirty(
|
||||
entry.Entity,
|
||||
RenderDirtyMask.Transform
|
||||
|
|
@ -343,14 +344,17 @@ internal sealed class ArchRenderScene : IRenderScene, IRenderSceneQuerySource
|
|||
_world.Set(entry.Entity, record.MeshSet);
|
||||
_world.Set(entry.Entity, record.Material);
|
||||
_world.Set(entry.Entity, record.DegradeState);
|
||||
_world.Set(entry.Entity, record.Source);
|
||||
OrDirty(entry.Entity, RenderDirtyMask.Appearance);
|
||||
break;
|
||||
case RenderProjectionDeltaKind.UpdateFlags:
|
||||
_world.Set(entry.Entity, record.Flags);
|
||||
_world.Set(entry.Entity, record.Source);
|
||||
OrDirty(entry.Entity, RenderDirtyMask.Flags);
|
||||
break;
|
||||
case RenderProjectionDeltaKind.Rebucket:
|
||||
_world.Set(entry.Entity, record.Residency);
|
||||
_world.Set(entry.Entity, record.Source);
|
||||
OrDirty(entry.Entity, RenderDirtyMask.SpatialResidency);
|
||||
break;
|
||||
default:
|
||||
|
|
@ -430,6 +434,7 @@ internal sealed class ArchRenderScene : IRenderScene, IRenderSceneQuerySource
|
|||
record.SortKey,
|
||||
record.OwnerIncarnation,
|
||||
record.DirtyMask,
|
||||
record.Source,
|
||||
tag);
|
||||
|
||||
private void WriteRecord(
|
||||
|
|
@ -447,6 +452,7 @@ internal sealed class ArchRenderScene : IRenderScene, IRenderSceneQuerySource
|
|||
_world.Set(entity, record.SortKey);
|
||||
_world.Set(entity, record.OwnerIncarnation);
|
||||
_world.Set(entity, record.DirtyMask);
|
||||
_world.Set(entity, record.Source);
|
||||
}
|
||||
|
||||
private RenderProjectionRecord ReadRecord(in SceneEntry entry) =>
|
||||
|
|
@ -463,7 +469,8 @@ internal sealed class ArchRenderScene : IRenderScene, IRenderSceneQuerySource
|
|||
_world.Get<RenderProjectionFlags>(entry.Entity),
|
||||
_world.Get<RenderDegradeState>(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)
|
||||
{
|
||||
|
|
@ -549,6 +556,9 @@ internal sealed class ArchRenderScene : IRenderScene, IRenderSceneQuerySource
|
|||
hash.Add(record.Id.RawValue);
|
||||
hash.Add((byte)record.ProjectionClass);
|
||||
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.PreviousTransform.LocalToWorld);
|
||||
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.Revision);
|
||||
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);
|
||||
|
|
|
|||
|
|
@ -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(
|
||||
uint landblockId,
|
||||
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 ulong RawValue => _value;
|
||||
internal RenderSortKey ToSortKey() => new(_value);
|
||||
|
||||
public int CompareTo(RenderProjectionId other) =>
|
||||
_value.CompareTo(other._value);
|
||||
|
|
@ -117,7 +118,32 @@ internal enum RenderDirtyMask : ushort
|
|||
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);
|
||||
|
||||
|
|
@ -142,6 +168,22 @@ internal readonly record struct RenderDegradeState(byte Level, uint Revision);
|
|||
|
||||
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(
|
||||
RenderProjectionId Id,
|
||||
RenderProjectionClass ProjectionClass,
|
||||
|
|
@ -155,7 +197,8 @@ internal readonly record struct RenderProjectionRecord(
|
|||
RenderProjectionFlags Flags,
|
||||
RenderDegradeState DegradeState,
|
||||
RenderSortKey SortKey,
|
||||
RenderDirtyMask DirtyMask);
|
||||
RenderDirtyMask DirtyMask,
|
||||
RenderSourceMetadata Source = default);
|
||||
|
||||
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.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;
|
||||
}))
|
||||
{
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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.Numerics;
|
||||
using AcDream.App.Rendering;
|
||||
using AcDream.App.Rendering.Scene;
|
||||
using AcDream.App.Rendering.Wb;
|
||||
using AcDream.App.Streaming;
|
||||
using AcDream.App.World;
|
||||
|
|
@ -989,6 +990,40 @@ public sealed class LandblockPresentationPipelineTests
|
|||
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]
|
||||
public void PriorityFailure_PreservesFailingResultAndUntouchedDrainedSuffix()
|
||||
{
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue