feat(rendering): index incremental scene queries
Maintain packed render worksets as lifecycle deltas arrive and synchronize active animated statics without scanning the resident world. This keeps Slice F non-drawing while preparing exact spatial and feature queries for shadow comparison. Release: 8,206 passed, 5 skipped.
This commit is contained in:
parent
58e7c2eb99
commit
ff5d86175f
8 changed files with 745 additions and 16 deletions
|
|
@ -21,8 +21,8 @@ retail-faithful gameplay
|
||||||
| 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 | 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. |
|
| 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 | complete | Exact EntityReady/resource teardown, visibility, attachment pose/removal, and active-only final-frame seams now drive generation/incarnation-gated live records. Duplicate CreateObject, pending/loaded rebucket, hidden/appearance, reentrancy, session clear, attachment, and GUID-generation replacement pass 11 focused tests. Production still constructs no render scene. |
|
| F3 — live/equipped projection | complete | Exact EntityReady/resource teardown, visibility, attachment pose/removal, and active-only final-frame seams now drive generation/incarnation-gated live records. Duplicate CreateObject, pending/loaded rebucket, hidden/appearance, reentrancy, session clear, attachment, and GUID-generation replacement pass 11 focused tests. Production still constructs no render scene. |
|
||||||
| F4 — dynamic indices | active | Packed spatial/class/visibility query indices have not landed. |
|
| F4 — dynamic indices | complete | The contained scene now maintains outdoor/static, per-cell static/dynamic, special dynamic-route, translucent, selectable, light-candidate, and dirty indices incrementally. Final-frame live/equipped synchronization remains active-only, and active animated statics are synchronized from the scheduler's active workset rather than a resident-world scan. The production scene remains unconstructed and non-drawing. |
|
||||||
| F5 | pending | Continuous shadow comparison has not started. |
|
| F5 | active | Continuous lifecycle-automation shadow comparison is the next unit; no production consumer has switched. |
|
||||||
| 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
|
||||||
|
|
|
||||||
|
|
@ -334,6 +334,21 @@ internal sealed class RetailStaticAnimatingObjectScheduler : ILiveStaticPartFram
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
internal void CopyActiveDatStaticEntitiesTo(List<WorldEntity> destination)
|
||||||
|
{
|
||||||
|
ArgumentNullException.ThrowIfNull(destination);
|
||||||
|
destination.Clear();
|
||||||
|
foreach (Owner owner in _owners.Values)
|
||||||
|
{
|
||||||
|
if (owner.Entity.ServerGuid == 0
|
||||||
|
&& owner.Sequencer is not null
|
||||||
|
&& _isResident(owner.Entity))
|
||||||
|
{
|
||||||
|
destination.Add(owner.Entity);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
public void Tick(float elapsedSeconds)
|
public void Tick(float elapsedSeconds)
|
||||||
{
|
{
|
||||||
if (!float.IsFinite(elapsedSeconds)
|
if (!float.IsFinite(elapsedSeconds)
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,5 @@
|
||||||
using System.Runtime.CompilerServices;
|
using System.Runtime.CompilerServices;
|
||||||
|
using AcDream.App.Rendering;
|
||||||
using Arch.Core;
|
using Arch.Core;
|
||||||
using ArchWorld = Arch.Core.World;
|
using ArchWorld = Arch.Core.World;
|
||||||
|
|
||||||
|
|
@ -18,6 +19,19 @@ internal sealed class ArchRenderScene : IRenderScene, IRenderSceneQuerySource
|
||||||
|
|
||||||
private readonly int _ownerThreadId;
|
private readonly int _ownerThreadId;
|
||||||
private readonly Dictionary<RenderProjectionId, SceneEntry> _entries = [];
|
private readonly Dictionary<RenderProjectionId, SceneEntry> _entries = [];
|
||||||
|
private readonly HashSet<RenderProjectionId> _outdoorStatics = [];
|
||||||
|
private readonly HashSet<RenderProjectionId> _indoorCellStatics = [];
|
||||||
|
private readonly HashSet<RenderProjectionId> _dynamics = [];
|
||||||
|
private readonly HashSet<RenderProjectionId> _outdoorDynamics = [];
|
||||||
|
private readonly HashSet<RenderProjectionId> _portalStraddlingDynamics = [];
|
||||||
|
private readonly HashSet<RenderProjectionId> _translucent = [];
|
||||||
|
private readonly HashSet<RenderProjectionId> _selectable = [];
|
||||||
|
private readonly HashSet<RenderProjectionId> _lightCandidates = [];
|
||||||
|
private readonly HashSet<RenderProjectionId> _dirty = [];
|
||||||
|
private readonly Dictionary<uint, HashSet<RenderProjectionId>>
|
||||||
|
_cellStatics = [];
|
||||||
|
private readonly Dictionary<uint, HashSet<RenderProjectionId>>
|
||||||
|
_cellDynamics = [];
|
||||||
private ArchWorld _world;
|
private ArchWorld _world;
|
||||||
private RenderProjectionCounts _counts;
|
private RenderProjectionCounts _counts;
|
||||||
private ulong _lastAppliedJournalSequence;
|
private ulong _lastAppliedJournalSequence;
|
||||||
|
|
@ -59,6 +73,7 @@ internal sealed class ArchRenderScene : IRenderScene, IRenderSceneQuerySource
|
||||||
int lookupCapacity = _entries.EnsureCapacity(0);
|
int lookupCapacity = _entries.EnsureCapacity(0);
|
||||||
long lookupBytes =
|
long lookupBytes =
|
||||||
(long)lookupCapacity * Unsafe.SizeOf<ProjectionLookupSlotEstimate>();
|
(long)lookupCapacity * Unsafe.SizeOf<ProjectionLookupSlotEstimate>();
|
||||||
|
long indexBytes = EstimateIndexBytes();
|
||||||
|
|
||||||
return new RenderSceneMemoryAccounting(
|
return new RenderSceneMemoryAccounting(
|
||||||
EntityCount: _world.Size,
|
EntityCount: _world.Size,
|
||||||
|
|
@ -68,7 +83,7 @@ internal sealed class ArchRenderScene : IRenderScene, IRenderSceneQuerySource
|
||||||
EstimatedChunkPayloadBytes: estimatedChunkPayloadBytes,
|
EstimatedChunkPayloadBytes: estimatedChunkPayloadBytes,
|
||||||
ProjectionLookupCapacity: lookupCapacity,
|
ProjectionLookupCapacity: lookupCapacity,
|
||||||
EstimatedProjectionLookupBytes: lookupBytes,
|
EstimatedProjectionLookupBytes: lookupBytes,
|
||||||
EstimatedIndexBytes: 0,
|
EstimatedIndexBytes: indexBytes,
|
||||||
EstimatedJournalBufferBytes: 0,
|
EstimatedJournalBufferBytes: 0,
|
||||||
EstimatedSynchronizationSourceBytes: 0);
|
EstimatedSynchronizationSourceBytes: 0);
|
||||||
}
|
}
|
||||||
|
|
@ -158,6 +173,7 @@ internal sealed class ArchRenderScene : IRenderScene, IRenderSceneQuerySource
|
||||||
ref RenderDirtyMask dirty =
|
ref RenderDirtyMask dirty =
|
||||||
ref _world.Get<RenderDirtyMask>(entry.Entity);
|
ref _world.Get<RenderDirtyMask>(entry.Entity);
|
||||||
dirty |= RenderDirtyMask.Transform | RenderDirtyMask.WorldBounds;
|
dirty |= RenderDirtyMask.Transform | RenderDirtyMask.WorldBounds;
|
||||||
|
_dirty.Add(update.Id);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -205,6 +221,7 @@ internal sealed class ArchRenderScene : IRenderScene, IRenderSceneQuerySource
|
||||||
ArchWorld.Destroy(_world);
|
ArchWorld.Destroy(_world);
|
||||||
_world = CreateWorld();
|
_world = CreateWorld();
|
||||||
_entries.Clear();
|
_entries.Clear();
|
||||||
|
ClearIndices();
|
||||||
_counts = default;
|
_counts = default;
|
||||||
_lastAppliedJournalSequence = 0;
|
_lastAppliedJournalSequence = 0;
|
||||||
Generation = replacementGeneration;
|
Generation = replacementGeneration;
|
||||||
|
|
@ -218,10 +235,22 @@ internal sealed class ArchRenderScene : IRenderScene, IRenderSceneQuerySource
|
||||||
EnsureMutationThread();
|
EnsureMutationThread();
|
||||||
ArchWorld.Destroy(_world);
|
ArchWorld.Destroy(_world);
|
||||||
_entries.Clear();
|
_entries.Clear();
|
||||||
|
ClearIndices();
|
||||||
_counts = default;
|
_counts = default;
|
||||||
_disposed = true;
|
_disposed = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public void ClearDirty()
|
||||||
|
{
|
||||||
|
EnsureMutationThread();
|
||||||
|
foreach (RenderProjectionId id in _dirty)
|
||||||
|
{
|
||||||
|
if (_entries.TryGetValue(id, out SceneEntry entry))
|
||||||
|
_world.Set(entry.Entity, RenderDirtyMask.None);
|
||||||
|
}
|
||||||
|
_dirty.Clear();
|
||||||
|
}
|
||||||
|
|
||||||
RenderProjectionCounts IRenderSceneQuerySource.GetCounts(
|
RenderProjectionCounts IRenderSceneQuerySource.GetCounts(
|
||||||
RenderSceneGeneration generation)
|
RenderSceneGeneration generation)
|
||||||
{
|
{
|
||||||
|
|
@ -229,6 +258,22 @@ internal sealed class ArchRenderScene : IRenderScene, IRenderSceneQuerySource
|
||||||
return _counts;
|
return _counts;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
RenderSceneIndexCounts IRenderSceneQuerySource.GetIndexCounts(
|
||||||
|
RenderSceneGeneration generation)
|
||||||
|
{
|
||||||
|
EnsureQueryGeneration(generation);
|
||||||
|
return new RenderSceneIndexCounts(
|
||||||
|
_outdoorStatics.Count,
|
||||||
|
_indoorCellStatics.Count,
|
||||||
|
_dynamics.Count,
|
||||||
|
_outdoorDynamics.Count,
|
||||||
|
_portalStraddlingDynamics.Count,
|
||||||
|
_translucent.Count,
|
||||||
|
_selectable.Count,
|
||||||
|
_lightCandidates.Count,
|
||||||
|
_dirty.Count);
|
||||||
|
}
|
||||||
|
|
||||||
bool IRenderSceneQuerySource.TryGet(
|
bool IRenderSceneQuerySource.TryGet(
|
||||||
RenderSceneGeneration generation,
|
RenderSceneGeneration generation,
|
||||||
RenderProjectionId id,
|
RenderProjectionId id,
|
||||||
|
|
@ -276,6 +321,41 @@ internal sealed class ArchRenderScene : IRenderScene, IRenderSceneQuerySource
|
||||||
return count;
|
return count;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
int IRenderSceneQuerySource.CopyIndexTo(
|
||||||
|
RenderSceneGeneration generation,
|
||||||
|
RenderSceneIndex index,
|
||||||
|
Span<RenderProjectionRecord> destination)
|
||||||
|
{
|
||||||
|
EnsureQueryGeneration(generation);
|
||||||
|
HashSet<RenderProjectionId> source = Index(index);
|
||||||
|
return CopyIdsTo(source, destination);
|
||||||
|
}
|
||||||
|
|
||||||
|
int IRenderSceneQuerySource.GetCellCount(
|
||||||
|
RenderSceneGeneration generation,
|
||||||
|
uint fullCellId,
|
||||||
|
bool dynamic)
|
||||||
|
{
|
||||||
|
EnsureQueryGeneration(generation);
|
||||||
|
Dictionary<uint, HashSet<RenderProjectionId>> index =
|
||||||
|
dynamic ? _cellDynamics : _cellStatics;
|
||||||
|
return index.TryGetValue(fullCellId, out var ids) ? ids.Count : 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
int IRenderSceneQuerySource.CopyCellTo(
|
||||||
|
RenderSceneGeneration generation,
|
||||||
|
uint fullCellId,
|
||||||
|
bool dynamic,
|
||||||
|
Span<RenderProjectionRecord> destination)
|
||||||
|
{
|
||||||
|
EnsureQueryGeneration(generation);
|
||||||
|
Dictionary<uint, HashSet<RenderProjectionId>> index =
|
||||||
|
dynamic ? _cellDynamics : _cellStatics;
|
||||||
|
return index.TryGetValue(fullCellId, out var ids)
|
||||||
|
? CopyIdsTo(ids, destination)
|
||||||
|
: 0;
|
||||||
|
}
|
||||||
|
|
||||||
private static ArchWorld CreateWorld() =>
|
private static ArchWorld CreateWorld() =>
|
||||||
ArchWorld.Create(
|
ArchWorld.Create(
|
||||||
archetypeCapacity: InitialArchetypeCapacity,
|
archetypeCapacity: InitialArchetypeCapacity,
|
||||||
|
|
@ -298,7 +378,9 @@ internal sealed class ArchRenderScene : IRenderScene, IRenderSceneQuerySource
|
||||||
if (incarnationOrder == 0
|
if (incarnationOrder == 0
|
||||||
&& record.ProjectionClass == existing.ProjectionClass)
|
&& record.ProjectionClass == existing.ProjectionClass)
|
||||||
{
|
{
|
||||||
|
RenderProjectionRecord prior = ReadRecord(in existing);
|
||||||
WriteRecord(existing.Entity, in record);
|
WriteRecord(existing.Entity, in record);
|
||||||
|
UpdateIndices(in prior, in record);
|
||||||
result.Applied++;
|
result.Applied++;
|
||||||
result.Updated++;
|
result.Updated++;
|
||||||
return;
|
return;
|
||||||
|
|
@ -314,6 +396,7 @@ internal sealed class ArchRenderScene : IRenderScene, IRenderSceneQuerySource
|
||||||
record.OwnerIncarnation,
|
record.OwnerIncarnation,
|
||||||
record.ProjectionClass);
|
record.ProjectionClass);
|
||||||
IncrementCount(record.ProjectionClass);
|
IncrementCount(record.ProjectionClass);
|
||||||
|
AddToIndices(in record);
|
||||||
result.Applied++;
|
result.Applied++;
|
||||||
result.Registered++;
|
result.Registered++;
|
||||||
}
|
}
|
||||||
|
|
@ -326,6 +409,7 @@ internal sealed class ArchRenderScene : IRenderScene, IRenderSceneQuerySource
|
||||||
if (!TryGetCurrent(in record, ref result, out SceneEntry entry))
|
if (!TryGetCurrent(in record, ref result, out SceneEntry entry))
|
||||||
return;
|
return;
|
||||||
|
|
||||||
|
RenderProjectionRecord prior = ReadRecord(in entry);
|
||||||
switch (kind)
|
switch (kind)
|
||||||
{
|
{
|
||||||
case RenderProjectionDeltaKind.UpdateTransform:
|
case RenderProjectionDeltaKind.UpdateTransform:
|
||||||
|
|
@ -336,6 +420,7 @@ internal sealed class ArchRenderScene : IRenderScene, IRenderSceneQuerySource
|
||||||
_world.Set(entry.Entity, record.Source);
|
_world.Set(entry.Entity, record.Source);
|
||||||
OrDirty(
|
OrDirty(
|
||||||
entry.Entity,
|
entry.Entity,
|
||||||
|
record.Id,
|
||||||
RenderDirtyMask.Transform
|
RenderDirtyMask.Transform
|
||||||
| RenderDirtyMask.WorldBounds
|
| RenderDirtyMask.WorldBounds
|
||||||
| RenderDirtyMask.SortKey);
|
| RenderDirtyMask.SortKey);
|
||||||
|
|
@ -345,22 +430,30 @@ internal sealed class ArchRenderScene : IRenderScene, IRenderSceneQuerySource
|
||||||
_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);
|
_world.Set(entry.Entity, record.Source);
|
||||||
OrDirty(entry.Entity, RenderDirtyMask.Appearance);
|
OrDirty(
|
||||||
|
entry.Entity,
|
||||||
|
record.Id,
|
||||||
|
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);
|
_world.Set(entry.Entity, record.Source);
|
||||||
OrDirty(entry.Entity, RenderDirtyMask.Flags);
|
OrDirty(entry.Entity, record.Id, 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);
|
_world.Set(entry.Entity, record.Source);
|
||||||
OrDirty(entry.Entity, RenderDirtyMask.SpatialResidency);
|
OrDirty(
|
||||||
|
entry.Entity,
|
||||||
|
record.Id,
|
||||||
|
RenderDirtyMask.SpatialResidency);
|
||||||
break;
|
break;
|
||||||
default:
|
default:
|
||||||
throw new ArgumentOutOfRangeException(nameof(kind), kind, null);
|
throw new ArgumentOutOfRangeException(nameof(kind), kind, null);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
RenderProjectionRecord current = ReadRecord(in entry);
|
||||||
|
UpdateIndices(in prior, in current);
|
||||||
result.Applied++;
|
result.Applied++;
|
||||||
result.Updated++;
|
result.Updated++;
|
||||||
}
|
}
|
||||||
|
|
@ -474,14 +567,196 @@ internal sealed class ArchRenderScene : IRenderScene, IRenderSceneQuerySource
|
||||||
|
|
||||||
private void Destroy(in SceneEntry entry)
|
private void Destroy(in SceneEntry entry)
|
||||||
{
|
{
|
||||||
|
RenderProjectionRecord record = ReadRecord(in entry);
|
||||||
|
RemoveFromIndices(in record);
|
||||||
_world.Destroy(entry.Entity);
|
_world.Destroy(entry.Entity);
|
||||||
DecrementCount(entry.ProjectionClass);
|
DecrementCount(entry.ProjectionClass);
|
||||||
}
|
}
|
||||||
|
|
||||||
private void OrDirty(Entity entity, RenderDirtyMask value)
|
private void OrDirty(
|
||||||
|
Entity entity,
|
||||||
|
RenderProjectionId id,
|
||||||
|
RenderDirtyMask value)
|
||||||
{
|
{
|
||||||
ref RenderDirtyMask dirty = ref _world.Get<RenderDirtyMask>(entity);
|
ref RenderDirtyMask dirty = ref _world.Get<RenderDirtyMask>(entity);
|
||||||
dirty |= value;
|
dirty |= value;
|
||||||
|
_dirty.Add(id);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void UpdateIndices(
|
||||||
|
in RenderProjectionRecord prior,
|
||||||
|
in RenderProjectionRecord current)
|
||||||
|
{
|
||||||
|
RemoveFromIndices(in prior);
|
||||||
|
AddToIndices(in current);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void AddToIndices(in RenderProjectionRecord record)
|
||||||
|
{
|
||||||
|
bool dynamic = IsDynamic(record.ProjectionClass);
|
||||||
|
bool staticProjection = !dynamic;
|
||||||
|
bool indoor = InteriorEntityPartition.IsIndoorCellId(
|
||||||
|
record.Source.ParentCellId == 0
|
||||||
|
? null
|
||||||
|
: record.Source.ParentCellId);
|
||||||
|
|
||||||
|
if (staticProjection)
|
||||||
|
{
|
||||||
|
if (indoor)
|
||||||
|
{
|
||||||
|
_indoorCellStatics.Add(record.Id);
|
||||||
|
AddCell(_cellStatics, record.Residency.FullCellId, record.Id);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
_outdoorStatics.Add(record.Id);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
_dynamics.Add(record.Id);
|
||||||
|
if (indoor)
|
||||||
|
AddCell(_cellDynamics, record.Residency.FullCellId, record.Id);
|
||||||
|
else
|
||||||
|
_outdoorDynamics.Add(record.Id);
|
||||||
|
if ((record.Flags & RenderProjectionFlags.PortalStraddling) != 0)
|
||||||
|
_portalStraddlingDynamics.Add(record.Id);
|
||||||
|
}
|
||||||
|
|
||||||
|
if ((record.Flags & RenderProjectionFlags.Translucent) != 0)
|
||||||
|
_translucent.Add(record.Id);
|
||||||
|
if ((record.Flags & RenderProjectionFlags.Selectable) != 0)
|
||||||
|
_selectable.Add(record.Id);
|
||||||
|
if ((record.Flags & RenderProjectionFlags.LightCandidate) != 0)
|
||||||
|
_lightCandidates.Add(record.Id);
|
||||||
|
if (record.DirtyMask != RenderDirtyMask.None)
|
||||||
|
_dirty.Add(record.Id);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void RemoveFromIndices(in RenderProjectionRecord record)
|
||||||
|
{
|
||||||
|
_outdoorStatics.Remove(record.Id);
|
||||||
|
_indoorCellStatics.Remove(record.Id);
|
||||||
|
_dynamics.Remove(record.Id);
|
||||||
|
_outdoorDynamics.Remove(record.Id);
|
||||||
|
_portalStraddlingDynamics.Remove(record.Id);
|
||||||
|
_translucent.Remove(record.Id);
|
||||||
|
_selectable.Remove(record.Id);
|
||||||
|
_lightCandidates.Remove(record.Id);
|
||||||
|
_dirty.Remove(record.Id);
|
||||||
|
RemoveCell(_cellStatics, record.Residency.FullCellId, record.Id);
|
||||||
|
RemoveCell(_cellDynamics, record.Residency.FullCellId, record.Id);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static bool IsDynamic(RenderProjectionClass projectionClass) =>
|
||||||
|
projectionClass is RenderProjectionClass.LiveDynamicRoot
|
||||||
|
or RenderProjectionClass.EquippedChild;
|
||||||
|
|
||||||
|
private static void AddCell(
|
||||||
|
Dictionary<uint, HashSet<RenderProjectionId>> index,
|
||||||
|
uint fullCellId,
|
||||||
|
RenderProjectionId id)
|
||||||
|
{
|
||||||
|
if (fullCellId == 0)
|
||||||
|
return;
|
||||||
|
if (!index.TryGetValue(fullCellId, out HashSet<RenderProjectionId>? ids))
|
||||||
|
{
|
||||||
|
ids = [];
|
||||||
|
index.Add(fullCellId, ids);
|
||||||
|
}
|
||||||
|
ids.Add(id);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void RemoveCell(
|
||||||
|
Dictionary<uint, HashSet<RenderProjectionId>> index,
|
||||||
|
uint fullCellId,
|
||||||
|
RenderProjectionId id)
|
||||||
|
{
|
||||||
|
if (!index.TryGetValue(fullCellId, out HashSet<RenderProjectionId>? ids))
|
||||||
|
return;
|
||||||
|
ids.Remove(id);
|
||||||
|
if (ids.Count == 0)
|
||||||
|
index.Remove(fullCellId);
|
||||||
|
}
|
||||||
|
|
||||||
|
private HashSet<RenderProjectionId> Index(RenderSceneIndex index) =>
|
||||||
|
index switch
|
||||||
|
{
|
||||||
|
RenderSceneIndex.OutdoorStatic => _outdoorStatics,
|
||||||
|
RenderSceneIndex.IndoorCellStatic => _indoorCellStatics,
|
||||||
|
RenderSceneIndex.Dynamic => _dynamics,
|
||||||
|
RenderSceneIndex.OutdoorDynamic => _outdoorDynamics,
|
||||||
|
RenderSceneIndex.PortalStraddlingDynamic =>
|
||||||
|
_portalStraddlingDynamics,
|
||||||
|
RenderSceneIndex.Translucent => _translucent,
|
||||||
|
RenderSceneIndex.Selectable => _selectable,
|
||||||
|
RenderSceneIndex.LightCandidate => _lightCandidates,
|
||||||
|
RenderSceneIndex.Dirty => _dirty,
|
||||||
|
_ => throw new ArgumentOutOfRangeException(
|
||||||
|
nameof(index),
|
||||||
|
index,
|
||||||
|
null),
|
||||||
|
};
|
||||||
|
|
||||||
|
private int CopyIdsTo(
|
||||||
|
HashSet<RenderProjectionId> source,
|
||||||
|
Span<RenderProjectionRecord> destination)
|
||||||
|
{
|
||||||
|
if (destination.Length < source.Count)
|
||||||
|
{
|
||||||
|
throw new ArgumentException(
|
||||||
|
$"Destination holds {destination.Length} records; {source.Count} required.",
|
||||||
|
nameof(destination));
|
||||||
|
}
|
||||||
|
|
||||||
|
int count = 0;
|
||||||
|
foreach (RenderProjectionId id in source)
|
||||||
|
{
|
||||||
|
if (!_entries.TryGetValue(id, out SceneEntry entry))
|
||||||
|
{
|
||||||
|
throw new InvalidOperationException(
|
||||||
|
$"Render index retained missing {id}.");
|
||||||
|
}
|
||||||
|
destination[count++] = ReadRecord(in entry);
|
||||||
|
}
|
||||||
|
return count;
|
||||||
|
}
|
||||||
|
|
||||||
|
private long EstimateIndexBytes()
|
||||||
|
{
|
||||||
|
long slots =
|
||||||
|
_outdoorStatics.EnsureCapacity(0)
|
||||||
|
+ _indoorCellStatics.EnsureCapacity(0)
|
||||||
|
+ _dynamics.EnsureCapacity(0)
|
||||||
|
+ _outdoorDynamics.EnsureCapacity(0)
|
||||||
|
+ _portalStraddlingDynamics.EnsureCapacity(0)
|
||||||
|
+ _translucent.EnsureCapacity(0)
|
||||||
|
+ _selectable.EnsureCapacity(0)
|
||||||
|
+ _lightCandidates.EnsureCapacity(0)
|
||||||
|
+ _dirty.EnsureCapacity(0);
|
||||||
|
foreach (HashSet<RenderProjectionId> ids in _cellStatics.Values)
|
||||||
|
slots += ids.EnsureCapacity(0);
|
||||||
|
foreach (HashSet<RenderProjectionId> ids in _cellDynamics.Values)
|
||||||
|
slots += ids.EnsureCapacity(0);
|
||||||
|
long cellLookupSlots =
|
||||||
|
_cellStatics.EnsureCapacity(0)
|
||||||
|
+ _cellDynamics.EnsureCapacity(0);
|
||||||
|
return checked(slots * 24L + cellLookupSlots * 40L);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void ClearIndices()
|
||||||
|
{
|
||||||
|
_outdoorStatics.Clear();
|
||||||
|
_indoorCellStatics.Clear();
|
||||||
|
_dynamics.Clear();
|
||||||
|
_outdoorDynamics.Clear();
|
||||||
|
_portalStraddlingDynamics.Clear();
|
||||||
|
_translucent.Clear();
|
||||||
|
_selectable.Clear();
|
||||||
|
_lightCandidates.Clear();
|
||||||
|
_dirty.Clear();
|
||||||
|
_cellStatics.Clear();
|
||||||
|
_cellDynamics.Clear();
|
||||||
}
|
}
|
||||||
|
|
||||||
private void IncrementCount(RenderProjectionClass projectionClass)
|
private void IncrementCount(RenderProjectionClass projectionClass)
|
||||||
|
|
|
||||||
|
|
@ -304,6 +304,50 @@ internal readonly record struct RenderProjectionCounts(
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
internal enum RenderSceneIndex : byte
|
||||||
|
{
|
||||||
|
OutdoorStatic,
|
||||||
|
IndoorCellStatic,
|
||||||
|
Dynamic,
|
||||||
|
OutdoorDynamic,
|
||||||
|
PortalStraddlingDynamic,
|
||||||
|
Translucent,
|
||||||
|
Selectable,
|
||||||
|
LightCandidate,
|
||||||
|
Dirty,
|
||||||
|
}
|
||||||
|
|
||||||
|
internal readonly record struct RenderSceneIndexCounts(
|
||||||
|
int OutdoorStatic,
|
||||||
|
int IndoorCellStatic,
|
||||||
|
int Dynamic,
|
||||||
|
int OutdoorDynamic,
|
||||||
|
int PortalStraddlingDynamic,
|
||||||
|
int Translucent,
|
||||||
|
int Selectable,
|
||||||
|
int LightCandidate,
|
||||||
|
int Dirty)
|
||||||
|
{
|
||||||
|
public int For(RenderSceneIndex index) =>
|
||||||
|
index switch
|
||||||
|
{
|
||||||
|
RenderSceneIndex.OutdoorStatic => OutdoorStatic,
|
||||||
|
RenderSceneIndex.IndoorCellStatic => IndoorCellStatic,
|
||||||
|
RenderSceneIndex.Dynamic => Dynamic,
|
||||||
|
RenderSceneIndex.OutdoorDynamic => OutdoorDynamic,
|
||||||
|
RenderSceneIndex.PortalStraddlingDynamic =>
|
||||||
|
PortalStraddlingDynamic,
|
||||||
|
RenderSceneIndex.Translucent => Translucent,
|
||||||
|
RenderSceneIndex.Selectable => Selectable,
|
||||||
|
RenderSceneIndex.LightCandidate => LightCandidate,
|
||||||
|
RenderSceneIndex.Dirty => Dirty,
|
||||||
|
_ => throw new ArgumentOutOfRangeException(
|
||||||
|
nameof(index),
|
||||||
|
index,
|
||||||
|
null),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
internal readonly record struct RenderDeltaApplyResult(
|
internal readonly record struct RenderDeltaApplyResult(
|
||||||
int Applied,
|
int Applied,
|
||||||
int Registered,
|
int Registered,
|
||||||
|
|
@ -357,6 +401,7 @@ internal sealed class RenderSceneDigestBuffer
|
||||||
internal interface IRenderSceneQuerySource
|
internal interface IRenderSceneQuerySource
|
||||||
{
|
{
|
||||||
RenderProjectionCounts GetCounts(RenderSceneGeneration generation);
|
RenderProjectionCounts GetCounts(RenderSceneGeneration generation);
|
||||||
|
RenderSceneIndexCounts GetIndexCounts(RenderSceneGeneration generation);
|
||||||
|
|
||||||
bool TryGet(
|
bool TryGet(
|
||||||
RenderSceneGeneration generation,
|
RenderSceneGeneration generation,
|
||||||
|
|
@ -367,6 +412,22 @@ internal interface IRenderSceneQuerySource
|
||||||
RenderSceneGeneration generation,
|
RenderSceneGeneration generation,
|
||||||
RenderProjectionClass? projectionClass,
|
RenderProjectionClass? projectionClass,
|
||||||
Span<RenderProjectionRecord> destination);
|
Span<RenderProjectionRecord> destination);
|
||||||
|
|
||||||
|
int CopyIndexTo(
|
||||||
|
RenderSceneGeneration generation,
|
||||||
|
RenderSceneIndex index,
|
||||||
|
Span<RenderProjectionRecord> destination);
|
||||||
|
|
||||||
|
int GetCellCount(
|
||||||
|
RenderSceneGeneration generation,
|
||||||
|
uint fullCellId,
|
||||||
|
bool dynamic);
|
||||||
|
|
||||||
|
int CopyCellTo(
|
||||||
|
RenderSceneGeneration generation,
|
||||||
|
uint fullCellId,
|
||||||
|
bool dynamic,
|
||||||
|
Span<RenderProjectionRecord> destination);
|
||||||
}
|
}
|
||||||
|
|
||||||
internal readonly struct RenderSceneQuery
|
internal readonly struct RenderSceneQuery
|
||||||
|
|
@ -386,6 +447,9 @@ internal readonly struct RenderSceneQuery
|
||||||
public RenderProjectionCounts Counts =>
|
public RenderProjectionCounts Counts =>
|
||||||
Source.GetCounts(Generation);
|
Source.GetCounts(Generation);
|
||||||
|
|
||||||
|
public RenderSceneIndexCounts IndexCounts =>
|
||||||
|
Source.GetIndexCounts(Generation);
|
||||||
|
|
||||||
public bool TryGet(
|
public bool TryGet(
|
||||||
RenderProjectionId id,
|
RenderProjectionId id,
|
||||||
out RenderProjectionRecord record) =>
|
out RenderProjectionRecord record) =>
|
||||||
|
|
@ -399,6 +463,35 @@ internal readonly struct RenderSceneQuery
|
||||||
Span<RenderProjectionRecord> destination) =>
|
Span<RenderProjectionRecord> destination) =>
|
||||||
Source.CopyTo(Generation, projectionClass, destination);
|
Source.CopyTo(Generation, projectionClass, destination);
|
||||||
|
|
||||||
|
public int CopyIndexTo(
|
||||||
|
RenderSceneIndex index,
|
||||||
|
Span<RenderProjectionRecord> destination) =>
|
||||||
|
Source.CopyIndexTo(Generation, index, destination);
|
||||||
|
|
||||||
|
public int GetCellStaticCount(uint fullCellId) =>
|
||||||
|
Source.GetCellCount(Generation, fullCellId, dynamic: false);
|
||||||
|
|
||||||
|
public int CopyCellStaticsTo(
|
||||||
|
uint fullCellId,
|
||||||
|
Span<RenderProjectionRecord> destination) =>
|
||||||
|
Source.CopyCellTo(
|
||||||
|
Generation,
|
||||||
|
fullCellId,
|
||||||
|
dynamic: false,
|
||||||
|
destination);
|
||||||
|
|
||||||
|
public int GetCellDynamicCount(uint fullCellId) =>
|
||||||
|
Source.GetCellCount(Generation, fullCellId, dynamic: true);
|
||||||
|
|
||||||
|
public int CopyCellDynamicsTo(
|
||||||
|
uint fullCellId,
|
||||||
|
Span<RenderProjectionRecord> destination) =>
|
||||||
|
Source.CopyCellTo(
|
||||||
|
Generation,
|
||||||
|
fullCellId,
|
||||||
|
dynamic: true,
|
||||||
|
destination);
|
||||||
|
|
||||||
private IRenderSceneQuerySource Source =>
|
private IRenderSceneQuerySource Source =>
|
||||||
_source
|
_source
|
||||||
?? throw new InvalidOperationException("The render-scene query is uninitialized.");
|
?? throw new InvalidOperationException("The render-scene query is uninitialized.");
|
||||||
|
|
@ -420,5 +513,7 @@ internal interface IRenderScene : IDisposable
|
||||||
|
|
||||||
RenderSceneQuery OpenQuery();
|
RenderSceneQuery OpenQuery();
|
||||||
|
|
||||||
|
void ClearDirty();
|
||||||
|
|
||||||
void Clear(RenderSceneGeneration replacementGeneration);
|
void Clear(RenderSceneGeneration replacementGeneration);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -28,6 +28,12 @@ internal sealed class StaticRenderProjectionJournal :
|
||||||
private readonly RenderProjectionJournal _journal;
|
private readonly RenderProjectionJournal _journal;
|
||||||
private readonly Dictionary<uint, Dictionary<RenderProjectionId, TrackedProjection>>
|
private readonly Dictionary<uint, Dictionary<RenderProjectionId, TrackedProjection>>
|
||||||
_byLandblock = [];
|
_byLandblock = [];
|
||||||
|
private readonly Dictionary<RenderProjectionId, TrackedProjection>
|
||||||
|
_trackedById = [];
|
||||||
|
private readonly Dictionary<WorldEntity, RenderProjectionId> _idByEntity =
|
||||||
|
new(ReferenceEqualityComparer.Instance);
|
||||||
|
private readonly Dictionary<uint, List<WorldEntity>> _entitiesByLandblock =
|
||||||
|
[];
|
||||||
private readonly List<RenderProjectionRecord> _candidates = [];
|
private readonly List<RenderProjectionRecord> _candidates = [];
|
||||||
private readonly List<RenderProjectionId> _removed = [];
|
private readonly List<RenderProjectionId> _removed = [];
|
||||||
private ulong _nextIncarnation = 1;
|
private ulong _nextIncarnation = 1;
|
||||||
|
|
@ -87,7 +93,10 @@ internal sealed class StaticRenderProjectionJournal :
|
||||||
for (int i = 0; i < _candidates.Count; i++)
|
for (int i = 0; i < _candidates.Count; i++)
|
||||||
{
|
{
|
||||||
RenderProjectionRecord candidate = _candidates[i];
|
RenderProjectionRecord candidate = _candidates[i];
|
||||||
if (current.TryGetValue(candidate.Id, out TrackedProjection retained))
|
if (current.TryGetValue(
|
||||||
|
candidate.Id,
|
||||||
|
out TrackedProjection? retained)
|
||||||
|
&& retained is not null)
|
||||||
{
|
{
|
||||||
_removed.Remove(candidate.Id);
|
_removed.Remove(candidate.Id);
|
||||||
RenderProjectionRecord accepted = candidate with
|
RenderProjectionRecord accepted = candidate with
|
||||||
|
|
@ -107,14 +116,14 @@ internal sealed class StaticRenderProjectionJournal :
|
||||||
OwnerIncarnation = NextIncarnation(),
|
OwnerIncarnation = NextIncarnation(),
|
||||||
};
|
};
|
||||||
_journal.Register(in accepted);
|
_journal.Register(in accepted);
|
||||||
current[candidate.Id] = new TrackedProjection(accepted);
|
retained.Record = accepted;
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (accepted != retained.Record)
|
if (accepted != retained.Record)
|
||||||
{
|
{
|
||||||
_journal.AppendDifference(retained.Record, accepted);
|
_journal.AppendDifference(retained.Record, accepted);
|
||||||
current[candidate.Id] = new TrackedProjection(accepted);
|
retained.Record = accepted;
|
||||||
}
|
}
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
@ -124,7 +133,9 @@ internal sealed class StaticRenderProjectionJournal :
|
||||||
OwnerIncarnation = NextIncarnation(),
|
OwnerIncarnation = NextIncarnation(),
|
||||||
};
|
};
|
||||||
_journal.Register(in registered);
|
_journal.Register(in registered);
|
||||||
current.Add(registered.Id, new TrackedProjection(registered));
|
var tracked = new TrackedProjection(registered);
|
||||||
|
current.Add(registered.Id, tracked);
|
||||||
|
_trackedById.Add(registered.Id, tracked);
|
||||||
ProjectionCount++;
|
ProjectionCount++;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -135,11 +146,13 @@ internal sealed class StaticRenderProjectionJournal :
|
||||||
TrackedProjection omitted = current[id];
|
TrackedProjection omitted = current[id];
|
||||||
_journal.Unregister(id, omitted.Record.OwnerIncarnation);
|
_journal.Unregister(id, omitted.Record.OwnerIncarnation);
|
||||||
current.Remove(id);
|
current.Remove(id);
|
||||||
|
_trackedById.Remove(id);
|
||||||
ProjectionCount--;
|
ProjectionCount--;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (current.Count == 0)
|
if (current.Count == 0)
|
||||||
_byLandblock.Remove(landblockId);
|
_byLandblock.Remove(landblockId);
|
||||||
|
ReplaceStaticEntityMap(landblockId, publication.Landblock.Entities);
|
||||||
}
|
}
|
||||||
|
|
||||||
public void Retire(GpuLandblockRetirement retirement)
|
public void Retire(GpuLandblockRetirement retirement)
|
||||||
|
|
@ -166,17 +179,66 @@ internal sealed class StaticRenderProjectionJournal :
|
||||||
}
|
}
|
||||||
|
|
||||||
ProjectionCount -= current.Count;
|
ProjectionCount -= current.Count;
|
||||||
|
foreach (RenderProjectionId id in current.Keys)
|
||||||
|
_trackedById.Remove(id);
|
||||||
|
RemoveStaticEntityMap(landblockId);
|
||||||
}
|
}
|
||||||
|
|
||||||
public void Clear(RenderSceneGeneration replacementGeneration)
|
public void Clear(RenderSceneGeneration replacementGeneration)
|
||||||
{
|
{
|
||||||
_byLandblock.Clear();
|
_byLandblock.Clear();
|
||||||
|
_trackedById.Clear();
|
||||||
|
_idByEntity.Clear();
|
||||||
|
_entitiesByLandblock.Clear();
|
||||||
_candidates.Clear();
|
_candidates.Clear();
|
||||||
_removed.Clear();
|
_removed.Clear();
|
||||||
ProjectionCount = 0;
|
ProjectionCount = 0;
|
||||||
_journal.Clear(replacementGeneration);
|
_journal.Clear(replacementGeneration);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public void SynchronizeActiveAnimatedSources(
|
||||||
|
IReadOnlyList<WorldEntity> activeEntities)
|
||||||
|
{
|
||||||
|
ArgumentNullException.ThrowIfNull(activeEntities);
|
||||||
|
for (int i = 0; i < activeEntities.Count; i++)
|
||||||
|
{
|
||||||
|
WorldEntity entity = activeEntities[i];
|
||||||
|
if (!_idByEntity.TryGetValue(
|
||||||
|
entity,
|
||||||
|
out RenderProjectionId id)
|
||||||
|
|| !_trackedById.TryGetValue(
|
||||||
|
id,
|
||||||
|
out TrackedProjection? tracked))
|
||||||
|
{
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
RenderProjectionRecord current =
|
||||||
|
RenderProjectionRecordFactory.ProjectEntity(
|
||||||
|
id,
|
||||||
|
RenderProjectionClass.ActiveAnimatedStatic,
|
||||||
|
tracked.Record.OwnerIncarnation,
|
||||||
|
tracked.Record.Residency.OwnerLandblockId,
|
||||||
|
tracked.Record.Residency.FullCellId,
|
||||||
|
entity,
|
||||||
|
spatiallyVisible: true) with
|
||||||
|
{
|
||||||
|
PreviousTransform = new PreviousRenderTransform(
|
||||||
|
tracked.Record.Transform.LocalToWorld),
|
||||||
|
};
|
||||||
|
if (tracked.Record.ProjectionClass
|
||||||
|
!= RenderProjectionClass.ActiveAnimatedStatic)
|
||||||
|
{
|
||||||
|
_journal.Register(in current);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
_journal.AppendDifference(tracked.Record, current);
|
||||||
|
}
|
||||||
|
tracked.Record = current;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
internal bool TryGet(
|
internal bool TryGet(
|
||||||
RenderProjectionId id,
|
RenderProjectionId id,
|
||||||
out RenderProjectionRecord record)
|
out RenderProjectionRecord record)
|
||||||
|
|
@ -184,7 +246,10 @@ internal sealed class StaticRenderProjectionJournal :
|
||||||
foreach (Dictionary<RenderProjectionId, TrackedProjection> projections
|
foreach (Dictionary<RenderProjectionId, TrackedProjection> projections
|
||||||
in _byLandblock.Values)
|
in _byLandblock.Values)
|
||||||
{
|
{
|
||||||
if (projections.TryGetValue(id, out TrackedProjection tracked))
|
if (projections.TryGetValue(
|
||||||
|
id,
|
||||||
|
out TrackedProjection? tracked)
|
||||||
|
&& tracked is not null)
|
||||||
{
|
{
|
||||||
record = tracked.Record;
|
record = tracked.Record;
|
||||||
return true;
|
return true;
|
||||||
|
|
@ -310,8 +375,43 @@ internal sealed class StaticRenderProjectionJournal :
|
||||||
private static uint Canonicalize(uint landblockId) =>
|
private static uint Canonicalize(uint landblockId) =>
|
||||||
(landblockId & 0xFFFF0000u) | 0xFFFFu;
|
(landblockId & 0xFFFF0000u) | 0xFFFFu;
|
||||||
|
|
||||||
private readonly record struct TrackedProjection(
|
private void ReplaceStaticEntityMap(
|
||||||
RenderProjectionRecord Record);
|
uint landblockId,
|
||||||
|
IReadOnlyList<WorldEntity> entities)
|
||||||
|
{
|
||||||
|
RemoveStaticEntityMap(landblockId);
|
||||||
|
var retained = new List<WorldEntity>();
|
||||||
|
for (int i = 0; i < entities.Count; i++)
|
||||||
|
{
|
||||||
|
WorldEntity entity = entities[i];
|
||||||
|
if (entity.ServerGuid != 0)
|
||||||
|
continue;
|
||||||
|
RenderProjectionId id = StaticEntityId(landblockId, entity.Id);
|
||||||
|
if (!_trackedById.ContainsKey(id))
|
||||||
|
continue;
|
||||||
|
_idByEntity.Add(entity, id);
|
||||||
|
retained.Add(entity);
|
||||||
|
}
|
||||||
|
if (retained.Count > 0)
|
||||||
|
_entitiesByLandblock.Add(landblockId, retained);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void RemoveStaticEntityMap(uint landblockId)
|
||||||
|
{
|
||||||
|
if (!_entitiesByLandblock.Remove(
|
||||||
|
landblockId,
|
||||||
|
out List<WorldEntity>? entities))
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
for (int i = 0; i < entities.Count; i++)
|
||||||
|
_idByEntity.Remove(entities[i]);
|
||||||
|
}
|
||||||
|
|
||||||
|
private sealed class TrackedProjection(RenderProjectionRecord record)
|
||||||
|
{
|
||||||
|
public RenderProjectionRecord Record { get; set; } = record;
|
||||||
|
}
|
||||||
|
|
||||||
private sealed class ProjectionRecordComparer :
|
private sealed class ProjectionRecordComparer :
|
||||||
IComparer<RenderProjectionRecord>
|
IComparer<RenderProjectionRecord>
|
||||||
|
|
|
||||||
|
|
@ -138,6 +138,8 @@ internal sealed class LiveObjectFrameController : ILiveObjectFramePhase
|
||||||
private readonly EquippedChildRenderController _equippedChildren;
|
private readonly EquippedChildRenderController _equippedChildren;
|
||||||
private readonly LiveEffectFrameController _effects;
|
private readonly LiveEffectFrameController _effects;
|
||||||
private readonly ILiveRenderProjectionSink? _renderProjections;
|
private readonly ILiveRenderProjectionSink? _renderProjections;
|
||||||
|
private readonly StaticRenderProjectionJournal? _staticRenderProjections;
|
||||||
|
private readonly List<WorldEntity> _activeStaticProjectionScratch = [];
|
||||||
|
|
||||||
public LiveObjectFrameController(
|
public LiveObjectFrameController(
|
||||||
RetailInboundEventDispatcher inboundEvents,
|
RetailInboundEventDispatcher inboundEvents,
|
||||||
|
|
@ -152,7 +154,8 @@ internal sealed class LiveObjectFrameController : ILiveObjectFramePhase
|
||||||
LiveEntityAnimationRuntimeView<LiveEntityAnimationState> animatedEntities,
|
LiveEntityAnimationRuntimeView<LiveEntityAnimationState> animatedEntities,
|
||||||
EquippedChildRenderController equippedChildren,
|
EquippedChildRenderController equippedChildren,
|
||||||
LiveEffectFrameController effects,
|
LiveEffectFrameController effects,
|
||||||
ILiveRenderProjectionSink? renderProjections = null)
|
ILiveRenderProjectionSink? renderProjections = null,
|
||||||
|
StaticRenderProjectionJournal? staticRenderProjections = null)
|
||||||
{
|
{
|
||||||
_inboundEvents = inboundEvents ?? throw new ArgumentNullException(nameof(inboundEvents));
|
_inboundEvents = inboundEvents ?? throw new ArgumentNullException(nameof(inboundEvents));
|
||||||
_localPlayerFrame = localPlayerFrame
|
_localPlayerFrame = localPlayerFrame
|
||||||
|
|
@ -172,6 +175,7 @@ internal sealed class LiveObjectFrameController : ILiveObjectFramePhase
|
||||||
?? throw new ArgumentNullException(nameof(equippedChildren));
|
?? throw new ArgumentNullException(nameof(equippedChildren));
|
||||||
_effects = effects ?? throw new ArgumentNullException(nameof(effects));
|
_effects = effects ?? throw new ArgumentNullException(nameof(effects));
|
||||||
_renderProjections = renderProjections;
|
_renderProjections = renderProjections;
|
||||||
|
_staticRenderProjections = staticRenderProjections;
|
||||||
}
|
}
|
||||||
|
|
||||||
public void Tick(float deltaSeconds) =>
|
public void Tick(float deltaSeconds) =>
|
||||||
|
|
@ -207,6 +211,13 @@ internal sealed class LiveObjectFrameController : ILiveObjectFramePhase
|
||||||
_animationPresenter.Present(schedules);
|
_animationPresenter.Present(schedules);
|
||||||
_equippedChildren.Tick();
|
_equippedChildren.Tick();
|
||||||
_renderProjections?.SynchronizeActiveSources();
|
_renderProjections?.SynchronizeActiveSources();
|
||||||
|
if (_staticRenderProjections is not null)
|
||||||
|
{
|
||||||
|
_staticAnimations.CopyActiveDatStaticEntitiesTo(
|
||||||
|
_activeStaticProjectionScratch);
|
||||||
|
_staticRenderProjections.SynchronizeActiveAnimatedSources(
|
||||||
|
_activeStaticProjectionScratch);
|
||||||
|
}
|
||||||
// Retail CPhysicsObj::animate_static_object @ 0x00513DF0 runs
|
// Retail CPhysicsObj::animate_static_object @ 0x00513DF0 runs
|
||||||
// Script -> Particle -> process_hooks. acdream intentionally retains
|
// Script -> Particle -> process_hooks. acdream intentionally retains
|
||||||
// the TS-51 static-order difference here: ProcessHooks precedes the
|
// the TS-51 static-order difference here: ProcessHooks precedes the
|
||||||
|
|
|
||||||
|
|
@ -253,7 +253,8 @@ public sealed class ArchRenderSceneTests
|
||||||
Assert.True(memory.EstimatedProjectionLookupBytes > 0);
|
Assert.True(memory.EstimatedProjectionLookupBytes > 0);
|
||||||
Assert.Equal(
|
Assert.Equal(
|
||||||
memory.EstimatedChunkPayloadBytes
|
memory.EstimatedChunkPayloadBytes
|
||||||
+ memory.EstimatedProjectionLookupBytes,
|
+ memory.EstimatedProjectionLookupBytes
|
||||||
|
+ memory.EstimatedIndexBytes,
|
||||||
memory.TotalEstimatedBytes);
|
memory.TotalEstimatedBytes);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -359,6 +360,182 @@ public sealed class ArchRenderSceneTests
|
||||||
Assert.Equal(0, scene.Counts.Total);
|
Assert.Equal(0, scene.Counts.Total);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void IncrementalIndices_PreserveOutdoorCellDynamicAndFeatureMembership()
|
||||||
|
{
|
||||||
|
const uint indoorCell = 0x12340100u;
|
||||||
|
RenderSceneGeneration generation = Generation(16);
|
||||||
|
using var scene = new ArchRenderScene(generation);
|
||||||
|
RenderProjectionRecord outdoorStatic = Record(
|
||||||
|
1,
|
||||||
|
1,
|
||||||
|
RenderProjectionClass.OutdoorStatic);
|
||||||
|
RenderProjectionRecord indoorStatic = Record(
|
||||||
|
2,
|
||||||
|
1,
|
||||||
|
RenderProjectionClass.IndoorCellStatic) with
|
||||||
|
{
|
||||||
|
Residency = new RenderSpatialResidency(
|
||||||
|
Bucket(indoorCell),
|
||||||
|
0x1234FFFFu,
|
||||||
|
indoorCell),
|
||||||
|
Source = new RenderSourceMetadata() with
|
||||||
|
{
|
||||||
|
ParentCellId = indoorCell,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
RenderProjectionRecord animatedStatic = Record(
|
||||||
|
3,
|
||||||
|
1,
|
||||||
|
RenderProjectionClass.ActiveAnimatedStatic);
|
||||||
|
RenderProjectionRecord outdoorDynamic = Record(
|
||||||
|
4,
|
||||||
|
1,
|
||||||
|
RenderProjectionClass.LiveDynamicRoot);
|
||||||
|
RenderProjectionRecord cellDynamic = Record(
|
||||||
|
5,
|
||||||
|
1,
|
||||||
|
RenderProjectionClass.LiveDynamicRoot) with
|
||||||
|
{
|
||||||
|
Residency = new RenderSpatialResidency(
|
||||||
|
Bucket(indoorCell),
|
||||||
|
0x1234FFFFu,
|
||||||
|
indoorCell),
|
||||||
|
Source = new RenderSourceMetadata() with
|
||||||
|
{
|
||||||
|
ParentCellId = indoorCell,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
RenderProjectionRecord equipped = Record(
|
||||||
|
6,
|
||||||
|
1,
|
||||||
|
RenderProjectionClass.EquippedChild) with
|
||||||
|
{
|
||||||
|
Flags = RenderProjectionFlags.Draw
|
||||||
|
| RenderProjectionFlags.Selectable
|
||||||
|
| RenderProjectionFlags.Translucent
|
||||||
|
| RenderProjectionFlags.LightCandidate
|
||||||
|
| RenderProjectionFlags.PortalStraddling,
|
||||||
|
};
|
||||||
|
RenderProjectionRecord[] records =
|
||||||
|
[
|
||||||
|
outdoorStatic,
|
||||||
|
indoorStatic,
|
||||||
|
animatedStatic,
|
||||||
|
outdoorDynamic,
|
||||||
|
cellDynamic,
|
||||||
|
equipped,
|
||||||
|
];
|
||||||
|
var deltas = new RenderProjectionDelta[records.Length];
|
||||||
|
for (int i = 0; i < records.Length; i++)
|
||||||
|
{
|
||||||
|
deltas[i] = RenderProjectionDelta.Register(
|
||||||
|
generation,
|
||||||
|
(ulong)i + 1,
|
||||||
|
records[i]);
|
||||||
|
}
|
||||||
|
|
||||||
|
scene.Apply(deltas);
|
||||||
|
RenderSceneQuery query = scene.OpenQuery();
|
||||||
|
|
||||||
|
Assert.Equal(
|
||||||
|
new RenderSceneIndexCounts(
|
||||||
|
OutdoorStatic: 2,
|
||||||
|
IndoorCellStatic: 1,
|
||||||
|
Dynamic: 3,
|
||||||
|
OutdoorDynamic: 2,
|
||||||
|
PortalStraddlingDynamic: 1,
|
||||||
|
Translucent: 1,
|
||||||
|
Selectable: 6,
|
||||||
|
LightCandidate: 1,
|
||||||
|
Dirty: 6),
|
||||||
|
query.IndexCounts);
|
||||||
|
Assert.Equal(1, query.GetCellStaticCount(indoorCell));
|
||||||
|
Assert.Equal(1, query.GetCellDynamicCount(indoorCell));
|
||||||
|
var cellRecords = new RenderProjectionRecord[1];
|
||||||
|
Assert.Equal(1, query.CopyCellStaticsTo(indoorCell, cellRecords));
|
||||||
|
Assert.Equal(indoorStatic.Id, cellRecords[0].Id);
|
||||||
|
Assert.Equal(1, query.CopyCellDynamicsTo(indoorCell, cellRecords));
|
||||||
|
Assert.Equal(cellDynamic.Id, cellRecords[0].Id);
|
||||||
|
Assert.True(scene.Memory.EstimatedIndexBytes > 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void IncrementalIndices_UpdateRebucketReplaceRemoveAndDirtyAcknowledge()
|
||||||
|
{
|
||||||
|
const uint indoorCell = 0x22220100u;
|
||||||
|
RenderSceneGeneration generation = Generation(17);
|
||||||
|
using var scene = new ArchRenderScene(generation);
|
||||||
|
RenderProjectionRecord original = Record(
|
||||||
|
17,
|
||||||
|
1,
|
||||||
|
RenderProjectionClass.LiveDynamicRoot);
|
||||||
|
scene.Apply(
|
||||||
|
[RenderProjectionDelta.Register(generation, 1, original)]);
|
||||||
|
scene.ClearDirty();
|
||||||
|
Assert.Equal(0, scene.OpenQuery().IndexCounts.Dirty);
|
||||||
|
|
||||||
|
RenderProjectionRecord flagged = original with
|
||||||
|
{
|
||||||
|
Flags = original.Flags
|
||||||
|
| RenderProjectionFlags.Translucent
|
||||||
|
| RenderProjectionFlags.LightCandidate
|
||||||
|
| RenderProjectionFlags.PortalStraddling,
|
||||||
|
Source = original.Source with { ParentCellId = indoorCell },
|
||||||
|
};
|
||||||
|
RenderProjectionRecord rebucketed = flagged with
|
||||||
|
{
|
||||||
|
Residency = new RenderSpatialResidency(
|
||||||
|
Bucket(indoorCell),
|
||||||
|
0x2222FFFFu,
|
||||||
|
indoorCell),
|
||||||
|
};
|
||||||
|
scene.Apply(
|
||||||
|
[
|
||||||
|
RenderProjectionDelta.Update(
|
||||||
|
RenderProjectionDeltaKind.UpdateFlags,
|
||||||
|
generation,
|
||||||
|
2,
|
||||||
|
flagged),
|
||||||
|
RenderProjectionDelta.Update(
|
||||||
|
RenderProjectionDeltaKind.Rebucket,
|
||||||
|
generation,
|
||||||
|
3,
|
||||||
|
rebucketed),
|
||||||
|
]);
|
||||||
|
|
||||||
|
RenderSceneQuery updated = scene.OpenQuery();
|
||||||
|
Assert.Equal(0, updated.IndexCounts.OutdoorDynamic);
|
||||||
|
Assert.Equal(1, updated.IndexCounts.PortalStraddlingDynamic);
|
||||||
|
Assert.Equal(1, updated.IndexCounts.Translucent);
|
||||||
|
Assert.Equal(1, updated.IndexCounts.LightCandidate);
|
||||||
|
Assert.Equal(1, updated.IndexCounts.Dirty);
|
||||||
|
Assert.Equal(1, updated.GetCellDynamicCount(indoorCell));
|
||||||
|
|
||||||
|
RenderProjectionRecord replacement = rebucketed with
|
||||||
|
{
|
||||||
|
ProjectionClass = RenderProjectionClass.IndoorCellStatic,
|
||||||
|
OwnerIncarnation = Incarnation(2),
|
||||||
|
};
|
||||||
|
scene.Apply(
|
||||||
|
[RenderProjectionDelta.Register(generation, 4, replacement)]);
|
||||||
|
RenderSceneQuery replaced = scene.OpenQuery();
|
||||||
|
Assert.Equal(0, replaced.IndexCounts.Dynamic);
|
||||||
|
Assert.Equal(1, replaced.IndexCounts.IndoorCellStatic);
|
||||||
|
Assert.Equal(1, replaced.GetCellStaticCount(indoorCell));
|
||||||
|
|
||||||
|
scene.Apply(
|
||||||
|
[
|
||||||
|
RenderProjectionDelta.Unregister(
|
||||||
|
generation,
|
||||||
|
5,
|
||||||
|
replacement.Id,
|
||||||
|
replacement.OwnerIncarnation),
|
||||||
|
]);
|
||||||
|
Assert.Equal(default, scene.OpenQuery().IndexCounts);
|
||||||
|
Assert.True(scene.Memory.EstimatedIndexBytes > 0);
|
||||||
|
}
|
||||||
|
|
||||||
private static RenderProjectionRecord Record(
|
private static RenderProjectionRecord Record(
|
||||||
ulong id,
|
ulong id,
|
||||||
ulong incarnation,
|
ulong incarnation,
|
||||||
|
|
|
||||||
|
|
@ -207,6 +207,62 @@ public sealed class StaticRenderProjectionJournalTests
|
||||||
() => statics.Clear(Generation(7)));
|
() => statics.Clear(Generation(7)));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void ActiveAnimatedSynchronization_ReclassifiesAndUpdatesOnlyActiveSource()
|
||||||
|
{
|
||||||
|
RenderSceneGeneration generation = Generation(8);
|
||||||
|
var journal = new RenderProjectionJournal(generation);
|
||||||
|
var statics = new StaticRenderProjectionJournal(journal);
|
||||||
|
WorldEntity animated = Entity(1);
|
||||||
|
WorldEntity ordinary = Entity(2);
|
||||||
|
LandblockBuild build = Build(
|
||||||
|
LandblockId,
|
||||||
|
[animated, ordinary],
|
||||||
|
includeShell: false);
|
||||||
|
statics.Reconcile(build, Publication(build));
|
||||||
|
using var scene = new ArchRenderScene(generation);
|
||||||
|
journal.DrainTo(scene);
|
||||||
|
RenderProjectionId animatedId =
|
||||||
|
StaticRenderProjectionJournal.StaticEntityId(
|
||||||
|
LandblockId,
|
||||||
|
animated.Id);
|
||||||
|
Assert.True(scene.OpenQuery().TryGet(animatedId, out var before));
|
||||||
|
|
||||||
|
statics.SynchronizeActiveAnimatedSources([animated]);
|
||||||
|
Assert.Single(journal.Pending.ToArray());
|
||||||
|
Assert.Equal(
|
||||||
|
RenderProjectionDeltaKind.Register,
|
||||||
|
journal.Pending[0].Kind);
|
||||||
|
Assert.Equal(
|
||||||
|
RenderProjectionClass.ActiveAnimatedStatic,
|
||||||
|
journal.Pending[0].Record.ProjectionClass);
|
||||||
|
Assert.Equal(
|
||||||
|
before.OwnerIncarnation,
|
||||||
|
journal.Pending[0].Record.OwnerIncarnation);
|
||||||
|
journal.DrainTo(scene);
|
||||||
|
|
||||||
|
statics.SynchronizeActiveAnimatedSources([animated]);
|
||||||
|
Assert.Equal(0, journal.Count);
|
||||||
|
|
||||||
|
animated.Rotation =
|
||||||
|
Quaternion.CreateFromAxisAngle(Vector3.UnitZ, 0.75f);
|
||||||
|
statics.SynchronizeActiveAnimatedSources([animated]);
|
||||||
|
Assert.Single(journal.Pending.ToArray());
|
||||||
|
Assert.Equal(
|
||||||
|
RenderProjectionDeltaKind.UpdateTransform,
|
||||||
|
journal.Pending[0].Kind);
|
||||||
|
Assert.True(statics.TryGet(animatedId, out var updated));
|
||||||
|
Assert.Equal(animated.Rotation, updated.Transform.Rotation);
|
||||||
|
RenderProjectionId ordinaryId =
|
||||||
|
StaticRenderProjectionJournal.StaticEntityId(
|
||||||
|
LandblockId,
|
||||||
|
ordinary.Id);
|
||||||
|
Assert.True(statics.TryGet(ordinaryId, out var unchanged));
|
||||||
|
Assert.Equal(
|
||||||
|
RenderProjectionClass.OutdoorStatic,
|
||||||
|
unchanged.ProjectionClass);
|
||||||
|
}
|
||||||
|
|
||||||
private static GpuLandblockSpatialPublication Publication(
|
private static GpuLandblockSpatialPublication Publication(
|
||||||
LandblockBuild build,
|
LandblockBuild build,
|
||||||
bool requiresActivation = true) =>
|
bool requiresActivation = true) =>
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue