acdream/src/AcDream.App/Rendering/Scene/RenderSceneContracts.cs
Erik 58c8de264e fix(render): #350 — the shadow ledger's lifetime counters are 64-bit; the 2h42m overflow is closed
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>
2026-08-08 10:29:39 +02:00

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);
}