RenderDeltaApplyResult's nine counters accumulate for the lifetime of
a world generation (the cumulative sum is never reset in place) yet
were int where every sibling lifetime counter in the same class was
already ulong/long. A 2h42m single-generation session (login to crash
with zero portals) overflowed one through ordinary per-tick churn from
SynchronizeActiveSources' two call sites per frame. Introduced
0eb66485 (2026-07-24); first reached by the vendor buy gate because
parking at a shop for hours produced the project's first multi-hour
unbroken generation. The vendor materializer was exonerated by routing
analysis (shop-item Ingest/Remove reaches inventory deltas only, never
the render journal).
Widened to long; the per-tick builder stays int and widens implicitly;
the arithmetic stays checked. All 68 render-shadow tests green.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
581 lines
17 KiB
C#
581 lines
17 KiB
C#
using System.Numerics;
|
|
using AcDream.Core.World;
|
|
|
|
namespace AcDream.App.Rendering.Scene;
|
|
|
|
internal readonly record struct RenderProjectionId
|
|
{
|
|
private readonly ulong _value;
|
|
|
|
private RenderProjectionId(ulong value) => _value = value;
|
|
|
|
internal static RenderProjectionId FromRaw(ulong value) => new(value);
|
|
|
|
internal ulong RawValue => _value;
|
|
internal byte Domain => (byte)(_value >> 56);
|
|
internal RenderSortKey ToSortKey() => new(_value);
|
|
internal void AddTo(ref StableRenderHash128 hash) => hash.Add(_value);
|
|
|
|
public int CompareTo(RenderProjectionId other) =>
|
|
_value.CompareTo(other._value);
|
|
|
|
public override string ToString() => $"projection:{_value:X16}";
|
|
}
|
|
|
|
internal readonly record struct RenderOwnerIncarnation
|
|
{
|
|
private readonly ulong _value;
|
|
|
|
private RenderOwnerIncarnation(ulong value) => _value = value;
|
|
|
|
internal static RenderOwnerIncarnation FromRaw(ulong value) => new(value);
|
|
|
|
internal ulong RawValue => _value;
|
|
|
|
public int CompareTo(RenderOwnerIncarnation other) =>
|
|
_value.CompareTo(other._value);
|
|
|
|
public override string ToString() => $"incarnation:{_value}";
|
|
}
|
|
|
|
internal readonly record struct RenderSceneGeneration
|
|
{
|
|
private readonly ulong _value;
|
|
|
|
private RenderSceneGeneration(ulong value) => _value = value;
|
|
|
|
internal static RenderSceneGeneration FromRaw(ulong value) => new(value);
|
|
|
|
internal ulong RawValue => _value;
|
|
|
|
public int CompareTo(RenderSceneGeneration other) =>
|
|
_value.CompareTo(other._value);
|
|
|
|
public override string ToString() => $"generation:{_value}";
|
|
}
|
|
|
|
internal readonly record struct RenderSpatialBucket
|
|
{
|
|
private readonly ulong _value;
|
|
|
|
private RenderSpatialBucket(ulong value) => _value = value;
|
|
|
|
internal static RenderSpatialBucket FromRaw(ulong value) => new(value);
|
|
|
|
internal ulong RawValue => _value;
|
|
|
|
public int CompareTo(RenderSpatialBucket other) =>
|
|
_value.CompareTo(other._value);
|
|
|
|
public override string ToString() => $"bucket:{_value:X16}";
|
|
}
|
|
|
|
internal readonly record struct RenderAssetHandle
|
|
{
|
|
private readonly ulong _value;
|
|
|
|
private RenderAssetHandle(ulong value) => _value = value;
|
|
|
|
internal static RenderAssetHandle FromRaw(ulong value) => new(value);
|
|
|
|
internal ulong RawValue => _value;
|
|
|
|
public int CompareTo(RenderAssetHandle other) =>
|
|
_value.CompareTo(other._value);
|
|
|
|
public override string ToString() => $"asset:{_value:X16}";
|
|
}
|
|
|
|
internal enum RenderProjectionClass : byte
|
|
{
|
|
OutdoorStatic,
|
|
IndoorCellStatic,
|
|
LiveDynamicRoot,
|
|
ActiveAnimatedStatic,
|
|
EquippedChild,
|
|
}
|
|
|
|
[Flags]
|
|
internal enum RenderProjectionFlags : uint
|
|
{
|
|
None = 0,
|
|
Draw = 1 << 0,
|
|
Hidden = 1 << 1,
|
|
AncestorHidden = 1 << 2,
|
|
Translucent = 1 << 3,
|
|
Selectable = 1 << 4,
|
|
LightCandidate = 1 << 5,
|
|
PortalStraddling = 1 << 6,
|
|
/// <summary>
|
|
/// The projection currently participates in the accepted spatial
|
|
/// presentation workset. Hidden objects retain this bit; a
|
|
/// portal-withdrawn attachment does not. This is derived render
|
|
/// ownership, not gameplay PhysicsState.
|
|
/// </summary>
|
|
SpatiallyResident = 1 << 7,
|
|
}
|
|
|
|
[Flags]
|
|
internal enum RenderDirtyMask : ushort
|
|
{
|
|
None = 0,
|
|
Transform = 1 << 0,
|
|
Appearance = 1 << 1,
|
|
Flags = 1 << 2,
|
|
SpatialResidency = 1 << 3,
|
|
WorldBounds = 1 << 4,
|
|
SortKey = 1 << 5,
|
|
All = ushort.MaxValue,
|
|
}
|
|
|
|
internal readonly record struct RenderTransform(
|
|
Vector3 Position,
|
|
Quaternion Rotation,
|
|
float UniformScale,
|
|
Matrix4x4 LocalToWorld)
|
|
{
|
|
public RenderTransform(Matrix4x4 localToWorld)
|
|
: this(
|
|
localToWorld.Translation,
|
|
Quaternion.Identity,
|
|
1.0f,
|
|
localToWorld)
|
|
{
|
|
}
|
|
|
|
public static RenderTransform FromRoot(
|
|
Vector3 position,
|
|
Quaternion rotation,
|
|
float uniformScale) =>
|
|
new(
|
|
position,
|
|
rotation,
|
|
uniformScale,
|
|
Matrix4x4.CreateFromQuaternion(rotation)
|
|
* Matrix4x4.CreateTranslation(position));
|
|
}
|
|
|
|
internal readonly record struct PreviousRenderTransform(Matrix4x4 LocalToWorld);
|
|
|
|
internal readonly record struct RenderMeshSet(
|
|
RenderAssetHandle Handle,
|
|
int MeshCount,
|
|
ulong Revision);
|
|
|
|
internal readonly record struct RenderMaterialVariant(
|
|
ulong PaletteKey,
|
|
ulong TextureReplacementKey,
|
|
float Opacity);
|
|
|
|
internal readonly record struct RenderSpatialResidency(
|
|
RenderSpatialBucket Bucket,
|
|
uint OwnerLandblockId,
|
|
uint FullCellId);
|
|
|
|
internal readonly record struct RenderWorldBounds(Vector3 Minimum, Vector3 Maximum);
|
|
|
|
internal readonly record struct RenderDegradeState(byte Level, uint Revision);
|
|
|
|
internal readonly record struct RenderSortKey(ulong Value);
|
|
|
|
/// <summary>
|
|
/// Read-only seam over the spatial owner's exact resident traversal. The
|
|
/// render scene consumes this derived order only; it does not acquire spatial
|
|
/// or gameplay ownership.
|
|
/// </summary>
|
|
internal interface IRenderTraversalOrderSource
|
|
{
|
|
bool TryGetTraversalSortKey(
|
|
WorldEntity entity,
|
|
out RenderSortKey sortKey);
|
|
}
|
|
|
|
internal static class RenderTraversalSortKey
|
|
{
|
|
public static RenderSortKey Compose(
|
|
uint landblockOrder,
|
|
int entityIndex)
|
|
{
|
|
if (landblockOrder == 0)
|
|
throw new ArgumentOutOfRangeException(nameof(landblockOrder));
|
|
if (entityIndex < 0)
|
|
throw new ArgumentOutOfRangeException(nameof(entityIndex));
|
|
|
|
return new RenderSortKey(
|
|
((ulong)landblockOrder << 32)
|
|
| checked((uint)entityIndex));
|
|
}
|
|
}
|
|
|
|
/// <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,
|
|
uint CurrentProjectionFlags = 0);
|
|
|
|
/// <summary>
|
|
/// Borrowed immutable presentation payload captured at a scene publication
|
|
/// edge. WorldEntity replaces its MeshRefs collection when animated or
|
|
/// appearance state changes, so retaining this exact collection reference is
|
|
/// a snapshot rather than a callback into gameplay state. G2 copies it into
|
|
/// the reusable frame arena before any dispatcher consumer observes it.
|
|
/// </summary>
|
|
internal readonly record struct RenderEntityPayload(
|
|
IReadOnlyList<MeshRef> MeshRefs,
|
|
PaletteOverride? PaletteOverride,
|
|
bool IsBuildingShell);
|
|
|
|
internal readonly record struct RenderProjectionRecord(
|
|
RenderProjectionId Id,
|
|
RenderProjectionClass ProjectionClass,
|
|
RenderOwnerIncarnation OwnerIncarnation,
|
|
RenderTransform Transform,
|
|
PreviousRenderTransform PreviousTransform,
|
|
RenderMeshSet MeshSet,
|
|
RenderMaterialVariant Material,
|
|
RenderSpatialResidency Residency,
|
|
RenderWorldBounds Bounds,
|
|
RenderProjectionFlags Flags,
|
|
RenderDegradeState DegradeState,
|
|
RenderSortKey SortKey,
|
|
RenderDirtyMask DirtyMask,
|
|
RenderSourceMetadata Source = default,
|
|
RenderEntityPayload EntityPayload = default);
|
|
|
|
internal enum RenderProjectionDeltaKind : byte
|
|
{
|
|
Register,
|
|
UpdateTransform,
|
|
UpdateAppearance,
|
|
UpdateFlags,
|
|
Rebucket,
|
|
Unregister,
|
|
}
|
|
|
|
internal readonly record struct RenderProjectionDelta(
|
|
RenderProjectionDeltaKind Kind,
|
|
RenderSceneGeneration Generation,
|
|
ulong JournalSequence,
|
|
RenderProjectionRecord Record)
|
|
{
|
|
public static RenderProjectionDelta Register(
|
|
RenderSceneGeneration generation,
|
|
ulong journalSequence,
|
|
in RenderProjectionRecord record) =>
|
|
new(
|
|
RenderProjectionDeltaKind.Register,
|
|
generation,
|
|
journalSequence,
|
|
record);
|
|
|
|
public static RenderProjectionDelta Update(
|
|
RenderProjectionDeltaKind kind,
|
|
RenderSceneGeneration generation,
|
|
ulong journalSequence,
|
|
in RenderProjectionRecord record)
|
|
{
|
|
if (kind is RenderProjectionDeltaKind.Register
|
|
or RenderProjectionDeltaKind.Unregister)
|
|
{
|
|
throw new ArgumentOutOfRangeException(
|
|
nameof(kind),
|
|
kind,
|
|
"Use Register or Unregister for structural deltas.");
|
|
}
|
|
|
|
return new RenderProjectionDelta(kind, generation, journalSequence, record);
|
|
}
|
|
|
|
public static RenderProjectionDelta Unregister(
|
|
RenderSceneGeneration generation,
|
|
ulong journalSequence,
|
|
RenderProjectionId id,
|
|
RenderOwnerIncarnation ownerIncarnation) =>
|
|
new(
|
|
RenderProjectionDeltaKind.Unregister,
|
|
generation,
|
|
journalSequence,
|
|
new RenderProjectionRecord() with
|
|
{
|
|
Id = id,
|
|
OwnerIncarnation = ownerIncarnation,
|
|
});
|
|
}
|
|
|
|
internal readonly record struct DynamicProjectionUpdate(
|
|
RenderProjectionId Id,
|
|
RenderOwnerIncarnation OwnerIncarnation,
|
|
RenderTransform Transform,
|
|
RenderWorldBounds Bounds);
|
|
|
|
internal readonly ref struct DynamicProjectionSyncInput
|
|
{
|
|
public DynamicProjectionSyncInput(
|
|
RenderSceneGeneration generation,
|
|
ReadOnlySpan<DynamicProjectionUpdate> updates)
|
|
{
|
|
Generation = generation;
|
|
Updates = updates;
|
|
}
|
|
|
|
public RenderSceneGeneration Generation { get; }
|
|
|
|
public ReadOnlySpan<DynamicProjectionUpdate> Updates { get; }
|
|
}
|
|
|
|
internal readonly record struct RenderProjectionCounts(
|
|
int Total,
|
|
int OutdoorStatic,
|
|
int IndoorCellStatic,
|
|
int LiveDynamicRoot,
|
|
int ActiveAnimatedStatic,
|
|
int EquippedChild)
|
|
{
|
|
public int For(RenderProjectionClass projectionClass) =>
|
|
projectionClass switch
|
|
{
|
|
RenderProjectionClass.OutdoorStatic => OutdoorStatic,
|
|
RenderProjectionClass.IndoorCellStatic => IndoorCellStatic,
|
|
RenderProjectionClass.LiveDynamicRoot => LiveDynamicRoot,
|
|
RenderProjectionClass.ActiveAnimatedStatic => ActiveAnimatedStatic,
|
|
RenderProjectionClass.EquippedChild => EquippedChild,
|
|
_ => throw new ArgumentOutOfRangeException(
|
|
nameof(projectionClass),
|
|
projectionClass,
|
|
null),
|
|
};
|
|
}
|
|
|
|
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),
|
|
};
|
|
}
|
|
|
|
// #350: these counters accumulate for the LIFETIME of a world generation
|
|
// (RenderSceneShadowRuntime._cumulativeApply is never reset in place), so
|
|
// they are 64-bit like the class's other lifetime counters — a 2h42m
|
|
// single-generation session overflowed the original int through ordinary
|
|
// per-tick churn. The arithmetic stays checked.
|
|
internal readonly record struct RenderDeltaApplyResult(
|
|
long Applied,
|
|
long Registered,
|
|
long Updated,
|
|
long Replaced,
|
|
long Unregistered,
|
|
long RejectedGeneration,
|
|
long RejectedOutOfOrderSequence,
|
|
long RejectedStaleIncarnation,
|
|
long RejectedMissing)
|
|
{
|
|
public long Rejected =>
|
|
RejectedGeneration
|
|
+ RejectedOutOfOrderSequence
|
|
+ RejectedStaleIncarnation
|
|
+ RejectedMissing;
|
|
}
|
|
|
|
internal readonly record struct RenderSceneMemoryAccounting(
|
|
int EntityCount,
|
|
int ArchEntityCapacity,
|
|
int ArchetypeCount,
|
|
int AllocatedChunkCount,
|
|
long EstimatedChunkPayloadBytes,
|
|
int ProjectionLookupCapacity,
|
|
long EstimatedProjectionLookupBytes,
|
|
long EstimatedIndexBytes,
|
|
long EstimatedJournalBufferBytes,
|
|
long EstimatedSynchronizationSourceBytes)
|
|
{
|
|
public long TotalEstimatedBytes =>
|
|
EstimatedChunkPayloadBytes
|
|
+ EstimatedProjectionLookupBytes
|
|
+ EstimatedIndexBytes
|
|
+ EstimatedJournalBufferBytes
|
|
+ EstimatedSynchronizationSourceBytes;
|
|
}
|
|
|
|
internal readonly record struct RenderSceneDigest(
|
|
RenderSceneGeneration Generation,
|
|
RenderProjectionCounts Counts,
|
|
RenderSceneHash128 Hash);
|
|
|
|
internal sealed class RenderSceneDigestBuffer
|
|
{
|
|
internal List<RenderProjectionRecord> Records { get; } = [];
|
|
|
|
internal int Capacity => Records.Capacity;
|
|
}
|
|
|
|
internal interface IRenderSceneQuerySource
|
|
{
|
|
RenderProjectionCounts GetCounts(RenderSceneGeneration generation);
|
|
RenderSceneIndexCounts GetIndexCounts(RenderSceneGeneration generation);
|
|
ulong GetIndexRevision(RenderSceneGeneration generation);
|
|
|
|
bool TryGet(
|
|
RenderSceneGeneration generation,
|
|
RenderProjectionId id,
|
|
out RenderProjectionRecord record);
|
|
|
|
int CopyTo(
|
|
RenderSceneGeneration generation,
|
|
RenderProjectionClass? projectionClass,
|
|
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
|
|
{
|
|
private readonly IRenderSceneQuerySource? _source;
|
|
|
|
internal RenderSceneQuery(
|
|
IRenderSceneQuerySource source,
|
|
RenderSceneGeneration generation)
|
|
{
|
|
_source = source;
|
|
Generation = generation;
|
|
}
|
|
|
|
public RenderSceneGeneration Generation { get; }
|
|
|
|
public RenderProjectionCounts Counts =>
|
|
Source.GetCounts(Generation);
|
|
|
|
public RenderSceneIndexCounts IndexCounts =>
|
|
Source.GetIndexCounts(Generation);
|
|
|
|
public ulong IndexRevision =>
|
|
Source.GetIndexRevision(Generation);
|
|
|
|
public bool TryGet(
|
|
RenderProjectionId id,
|
|
out RenderProjectionRecord record) =>
|
|
Source.TryGet(Generation, id, out record);
|
|
|
|
public int CopyTo(Span<RenderProjectionRecord> destination) =>
|
|
Source.CopyTo(Generation, null, destination);
|
|
|
|
public int CopyTo(
|
|
RenderProjectionClass projectionClass,
|
|
Span<RenderProjectionRecord> 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 =>
|
|
_source
|
|
?? throw new InvalidOperationException("The render-scene query is uninitialized.");
|
|
}
|
|
|
|
internal interface IRenderScene : IDisposable
|
|
{
|
|
RenderSceneGeneration Generation { get; }
|
|
|
|
RenderProjectionCounts Counts { get; }
|
|
|
|
RenderSceneMemoryAccounting Memory { get; }
|
|
|
|
RenderDeltaApplyResult Apply(ReadOnlySpan<RenderProjectionDelta> deltas);
|
|
|
|
void SynchronizeDynamicSources(in DynamicProjectionSyncInput input);
|
|
|
|
RenderSceneDigest BuildDigest(RenderSceneDigestBuffer reuse);
|
|
|
|
RenderSceneQuery OpenQuery();
|
|
|
|
void ClearDirty();
|
|
|
|
void Clear(RenderSceneGeneration replacementGeneration);
|
|
}
|