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:
parent
5ecaa5612d
commit
b2b67341ac
9 changed files with 845 additions and 5 deletions
389
src/AcDream.App/Rendering/Scene/CurrentRenderSceneOracle.cs
Normal file
389
src/AcDream.App/Rendering/Scene/CurrentRenderSceneOracle.cs
Normal 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);
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue