feat(rendering): establish current-path scene referee

Capture deterministic, landblock-aware fingerprints from the accepted partition only during lifecycle automation. This gives the F/G shadow scene an independent oracle without changing normal draw decisions or adding disabled-path per-entity cost.
This commit is contained in:
Erik 2026-07-24 21:13:11 +02:00
parent 5ecaa5612d
commit b2b67341ac
9 changed files with 845 additions and 5 deletions

View file

@ -0,0 +1,389 @@
using System.Numerics;
using AcDream.Core.World;
namespace AcDream.App.Rendering.Scene;
internal readonly record struct RenderSceneHash128(ulong Low, ulong High)
{
public static RenderSceneHash128 Empty { get; } = new(0, 0);
public override string ToString() => $"{High:X16}{Low:X16}";
}
internal readonly record struct CurrentRenderProjectionFingerprint(
InteriorEntityPartition.ProjectionClass ProjectionClass,
uint LandblockId,
uint EntityId,
uint ServerGuid,
uint SourceId,
uint ParentCellId,
uint EffectCellId,
uint BuildingShellAnchorCellId,
uint Flags,
int MeshCount,
RenderSceneHash128 Transform,
RenderSceneHash128 Geometry,
RenderSceneHash128 Appearance);
internal readonly record struct CurrentRenderSceneOracleSnapshot(
bool Enabled,
ulong CompletedFrameSequence,
int AbortedFrames,
int ProjectionCount,
int OutdoorStaticCount,
int CellStaticCount,
int DynamicCount,
int CellBucketCount,
RenderSceneHash128 Digest)
{
public static CurrentRenderSceneOracleSnapshot Disabled { get; } = new(
Enabled: false,
CompletedFrameSequence: 0,
AbortedFrames: 0,
ProjectionCount: 0,
OutdoorStaticCount: 0,
CellStaticCount: 0,
DynamicCount: 0,
CellBucketCount: 0,
Digest: RenderSceneHash128.Empty);
}
internal interface ICurrentRenderSceneOracleSnapshotSource
{
CurrentRenderSceneOracleSnapshot Snapshot { get; }
}
/// <summary>
/// Deterministic fingerprint of the already-accepted
/// <see cref="InteriorEntityPartition"/> output. This is the Slice-F referee:
/// it records current behavior before a shadow render scene exists, and never
/// makes a draw or visibility decision itself.
///
/// Collection storage is retained across frames. Production composition does
/// not construct this owner unless lifecycle automation is enabled, so normal
/// rendering pays no hashing, sorting, or allocation cost.
/// </summary>
internal sealed class CurrentRenderSceneOracle :
InteriorEntityPartition.IObserver,
ICurrentRenderSceneOracleSnapshotSource
{
private readonly List<CurrentRenderProjectionFingerprint> _projections = [];
private ulong _frameSequence;
private int _abortedFrames;
private int _outdoorStaticCount;
private int _cellStaticCount;
private int _dynamicCount;
private bool _frameOpen;
public CurrentRenderSceneOracleSnapshot Snapshot { get; private set; } =
CurrentRenderSceneOracleSnapshot.Disabled with { Enabled = true };
internal IReadOnlyList<CurrentRenderProjectionFingerprint> Projections =>
_projections;
public void BeginFrame()
{
if (_frameOpen)
{
throw new InvalidOperationException(
"The current render-scene oracle cannot begin a second frame before completing or aborting the first.");
}
_frameOpen = true;
_projections.Clear();
_outdoorStaticCount = 0;
_cellStaticCount = 0;
_dynamicCount = 0;
}
public void Observe(
uint landblockId,
WorldEntity entity,
InteriorEntityPartition.ProjectionClass projectionClass)
{
if (!_frameOpen)
{
throw new InvalidOperationException(
"The current render-scene oracle received a projection outside an open frame.");
}
ArgumentNullException.ThrowIfNull(entity);
switch (projectionClass)
{
case InteriorEntityPartition.ProjectionClass.OutdoorStatic:
_outdoorStaticCount++;
break;
case InteriorEntityPartition.ProjectionClass.CellStatic:
_cellStaticCount++;
break;
case InteriorEntityPartition.ProjectionClass.Dynamic:
_dynamicCount++;
break;
default:
throw new ArgumentOutOfRangeException(
nameof(projectionClass),
projectionClass,
"Unknown current render projection class.");
}
_projections.Add(CreateFingerprint(
landblockId,
entity,
projectionClass));
}
public void Complete(InteriorEntityPartition.Result result)
{
ArgumentNullException.ThrowIfNull(result);
if (!_frameOpen)
{
throw new InvalidOperationException(
"The current render-scene oracle cannot complete without an open frame.");
}
int cellStaticCount = 0;
foreach (List<WorldEntity> entities in result.ByCell.Values)
cellStaticCount = checked(cellStaticCount + entities.Count);
if (result.OutdoorStatic.Count != _outdoorStaticCount
|| cellStaticCount != _cellStaticCount
|| result.Dynamics.Count != _dynamicCount
|| _projections.Count
!= checked(_outdoorStaticCount + _cellStaticCount + _dynamicCount))
{
throw new InvalidOperationException(
"The current render-scene oracle diverged from the partition it observed.");
}
_projections.Sort(CurrentRenderProjectionFingerprintComparer.Instance);
StableRenderHash128 aggregate = StableRenderHash128.Create();
aggregate.Add(_projections.Count);
foreach (CurrentRenderProjectionFingerprint projection in _projections)
AddProjection(ref aggregate, in projection);
_frameOpen = false;
Snapshot = new CurrentRenderSceneOracleSnapshot(
Enabled: true,
CompletedFrameSequence: checked(++_frameSequence),
AbortedFrames: _abortedFrames,
ProjectionCount: _projections.Count,
OutdoorStaticCount: _outdoorStaticCount,
CellStaticCount: _cellStaticCount,
DynamicCount: _dynamicCount,
CellBucketCount: result.ByCell.Count,
Digest: aggregate.Finish());
}
public void AbortFrame()
{
if (!_frameOpen)
return;
_frameOpen = false;
_projections.Clear();
_outdoorStaticCount = 0;
_cellStaticCount = 0;
_dynamicCount = 0;
_abortedFrames = checked(_abortedFrames + 1);
Snapshot = Snapshot with { AbortedFrames = _abortedFrames };
}
private static CurrentRenderProjectionFingerprint CreateFingerprint(
uint landblockId,
WorldEntity entity,
InteriorEntityPartition.ProjectionClass projectionClass)
{
StableRenderHash128 transform = StableRenderHash128.Create();
transform.Add(entity.Position);
transform.Add(entity.Rotation);
transform.Add(entity.Scale);
StableRenderHash128 geometry = StableRenderHash128.Create();
geometry.Add(entity.MeshRefs.Count);
foreach (MeshRef mesh in entity.MeshRefs)
{
geometry.Add(mesh.GfxObjId);
geometry.Add(mesh.PartTransform);
}
StableRenderHash128 appearance = StableRenderHash128.Create();
if (entity.PaletteOverride is { } palette)
{
appearance.Add(true);
appearance.Add(palette.BasePaletteId);
appearance.Add(palette.SubPalettes.Count);
foreach (PaletteOverride.SubPaletteRange range in palette.SubPalettes)
{
appearance.Add(range.SubPaletteId);
appearance.Add(range.Offset);
appearance.Add(range.Length);
}
}
else
{
appearance.Add(false);
}
appearance.Add(entity.PartOverrides.Count);
foreach (PartOverride part in entity.PartOverrides)
{
appearance.Add(part.PartIndex);
appearance.Add(part.GfxObjId);
}
appearance.Add(entity.HiddenPartsMask);
uint flags = 0;
if (entity.IsDrawVisible)
flags |= 1u << 0;
if (entity.IsAncestorDrawVisible)
flags |= 1u << 1;
if (entity.IsBuildingShell)
flags |= 1u << 2;
if (entity.ParentCellId.HasValue)
flags |= 1u << 3;
if (entity.EffectCellId.HasValue)
flags |= 1u << 4;
if (entity.BuildingShellAnchorCellId.HasValue)
flags |= 1u << 5;
return new CurrentRenderProjectionFingerprint(
ProjectionClass: projectionClass,
LandblockId: landblockId,
EntityId: entity.Id,
ServerGuid: entity.ServerGuid,
SourceId: entity.SourceGfxObjOrSetupId,
ParentCellId: entity.ParentCellId ?? 0,
EffectCellId: entity.EffectCellId ?? 0,
BuildingShellAnchorCellId: entity.BuildingShellAnchorCellId ?? 0,
Flags: flags,
MeshCount: entity.MeshRefs.Count,
Transform: transform.Finish(),
Geometry: geometry.Finish(),
Appearance: appearance.Finish());
}
private static void AddProjection(
ref StableRenderHash128 hash,
in CurrentRenderProjectionFingerprint projection)
{
hash.Add((byte)projection.ProjectionClass);
hash.Add(projection.LandblockId);
hash.Add(projection.EntityId);
hash.Add(projection.ServerGuid);
hash.Add(projection.SourceId);
hash.Add(projection.ParentCellId);
hash.Add(projection.EffectCellId);
hash.Add(projection.BuildingShellAnchorCellId);
hash.Add(projection.Flags);
hash.Add(projection.MeshCount);
hash.Add(projection.Transform.Low);
hash.Add(projection.Transform.High);
hash.Add(projection.Geometry.Low);
hash.Add(projection.Geometry.High);
hash.Add(projection.Appearance.Low);
hash.Add(projection.Appearance.High);
}
private sealed class CurrentRenderProjectionFingerprintComparer :
IComparer<CurrentRenderProjectionFingerprint>
{
public static CurrentRenderProjectionFingerprintComparer Instance { get; } =
new();
public int Compare(
CurrentRenderProjectionFingerprint x,
CurrentRenderProjectionFingerprint y)
{
int value = x.ProjectionClass.CompareTo(y.ProjectionClass);
if (value != 0) return value;
value = x.LandblockId.CompareTo(y.LandblockId);
if (value != 0) return value;
value = x.ParentCellId.CompareTo(y.ParentCellId);
if (value != 0) return value;
value = x.EntityId.CompareTo(y.EntityId);
if (value != 0) return value;
value = x.ServerGuid.CompareTo(y.ServerGuid);
if (value != 0) return value;
value = x.SourceId.CompareTo(y.SourceId);
if (value != 0) return value;
value = x.EffectCellId.CompareTo(y.EffectCellId);
if (value != 0) return value;
value = x.BuildingShellAnchorCellId.CompareTo(
y.BuildingShellAnchorCellId);
if (value != 0) return value;
value = x.Flags.CompareTo(y.Flags);
if (value != 0) return value;
value = x.MeshCount.CompareTo(y.MeshCount);
if (value != 0) return value;
value = x.Transform.High.CompareTo(y.Transform.High);
if (value != 0) return value;
value = x.Transform.Low.CompareTo(y.Transform.Low);
if (value != 0) return value;
value = x.Geometry.High.CompareTo(y.Geometry.High);
if (value != 0) return value;
value = x.Geometry.Low.CompareTo(y.Geometry.Low);
if (value != 0) return value;
value = x.Appearance.High.CompareTo(y.Appearance.High);
if (value != 0) return value;
return x.Appearance.Low.CompareTo(y.Appearance.Low);
}
}
}
internal struct StableRenderHash128
{
private const ulong LowOffset = 14695981039346656037UL;
private const ulong LowPrime = 1099511628211UL;
private const ulong HighOffset = 7809847782465536322UL;
private const ulong HighPrime = 14029467366897019727UL;
private ulong _low;
private ulong _high;
public static StableRenderHash128 Create() => new()
{
_low = LowOffset,
_high = HighOffset,
};
public readonly RenderSceneHash128 Finish() => new(_low, _high);
public void Add(bool value) => Add(value ? (byte)1 : (byte)0);
public void Add(byte value) => Add((ulong)value);
public void Add(int value) => Add(unchecked((uint)value));
public void Add(uint value) => Add((ulong)value);
public void Add(float value) => Add(BitConverter.SingleToUInt32Bits(value));
public void Add(ulong value)
{
_low = unchecked((_low ^ value) * LowPrime);
ulong folded = value ^ BitOperations.RotateLeft(value, 29);
_high = unchecked((_high ^ folded) * HighPrime);
}
public void Add(Vector3 value)
{
Add(value.X);
Add(value.Y);
Add(value.Z);
}
public void Add(Quaternion value)
{
Add(value.X);
Add(value.Y);
Add(value.Z);
Add(value.W);
}
public void Add(Matrix4x4 value)
{
Add(value.M11); Add(value.M12); Add(value.M13); Add(value.M14);
Add(value.M21); Add(value.M22); Add(value.M23); Add(value.M24);
Add(value.M31); Add(value.M32); Add(value.M33); Add(value.M34);
Add(value.M41); Add(value.M42); Add(value.M43); Add(value.M44);
}
}