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
|
|
@ -1,6 +1,7 @@
|
|||
using AcDream.App.Diagnostics;
|
||||
using AcDream.App.Input;
|
||||
using AcDream.App.Rendering;
|
||||
using AcDream.App.Rendering.Scene;
|
||||
using AcDream.App.Rendering.Vfx;
|
||||
using AcDream.App.Settings;
|
||||
using AcDream.App.Update;
|
||||
|
|
@ -368,6 +369,11 @@ internal sealed class FrameRootCompositionPhase
|
|||
live.SkyRenderer,
|
||||
content.ParticleSystem,
|
||||
live.ParticleRenderer);
|
||||
CurrentRenderSceneOracle? currentRenderSceneOracle =
|
||||
interaction.RetainedUi?.Screenshots is not null
|
||||
&& d.Options.AutomationArtifactDirectory is not null
|
||||
? new CurrentRenderSceneOracle()
|
||||
: null;
|
||||
var worldSceneRenderer = new WorldSceneRenderer(
|
||||
renderFrameResources,
|
||||
renderLoginState,
|
||||
|
|
@ -378,7 +384,7 @@ internal sealed class FrameRootCompositionPhase
|
|||
d.RetailAlphaQueue,
|
||||
d.ParticleVisibility,
|
||||
new WorldScenePViewRenderer(
|
||||
new RetailPViewRenderer(),
|
||||
new RetailPViewRenderer(currentRenderSceneOracle),
|
||||
retailPViewPassExecutor,
|
||||
retailPViewPassExecutor),
|
||||
retailPViewCells,
|
||||
|
|
@ -410,7 +416,8 @@ internal sealed class FrameRootCompositionPhase
|
|||
live.DrawDispatcher,
|
||||
d.FrameProfiler,
|
||||
content.Dats,
|
||||
foundation.Residency);
|
||||
foundation.Residency,
|
||||
currentRenderSceneOracle);
|
||||
lifecycleAutomation =
|
||||
new WorldLifecycleAutomationController(
|
||||
() => session.WorldReveal.Snapshot,
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
using System.Text.Json;
|
||||
using AcDream.App.Rendering;
|
||||
using AcDream.App.Rendering.Residency;
|
||||
using AcDream.App.Rendering.Scene;
|
||||
using AcDream.App.Streaming;
|
||||
using AcDream.App.UI.Testing;
|
||||
|
||||
|
|
@ -38,6 +39,7 @@ internal sealed record WorldLifecycleResourceSnapshot(
|
|||
int TotalLandblocks,
|
||||
int LiveEntities,
|
||||
int MaterializedLiveEntities,
|
||||
CurrentRenderSceneOracleSnapshot RenderSceneOracle,
|
||||
int PendingLiveTeardowns,
|
||||
int PendingLandblockRetirements,
|
||||
int ParticleEmitters,
|
||||
|
|
|
|||
|
|
@ -1,4 +1,5 @@
|
|||
using AcDream.App.Rendering;
|
||||
using AcDream.App.Rendering.Scene;
|
||||
using AcDream.App.Rendering.Vfx;
|
||||
using AcDream.App.Rendering.Wb;
|
||||
using AcDream.App.Rendering.Residency;
|
||||
|
|
@ -38,6 +39,8 @@ internal sealed class WorldLifecycleResourceSnapshotSource
|
|||
private readonly FrameProfiler _frameProfiler;
|
||||
private readonly IDatReaderWriter _dats;
|
||||
private readonly ResidencyManager _residency;
|
||||
private readonly ICurrentRenderSceneOracleSnapshotSource?
|
||||
_renderSceneOracle;
|
||||
|
||||
public WorldLifecycleResourceSnapshotSource(
|
||||
GpuWorldState world,
|
||||
|
|
@ -55,7 +58,8 @@ internal sealed class WorldLifecycleResourceSnapshotSource
|
|||
WbDrawDispatcher dispatcher,
|
||||
FrameProfiler frameProfiler,
|
||||
IDatReaderWriter dats,
|
||||
ResidencyManager residency)
|
||||
ResidencyManager residency,
|
||||
ICurrentRenderSceneOracleSnapshotSource? renderSceneOracle = null)
|
||||
{
|
||||
_world = world ?? throw new ArgumentNullException(nameof(world));
|
||||
_animations = animations
|
||||
|
|
@ -82,6 +86,7 @@ internal sealed class WorldLifecycleResourceSnapshotSource
|
|||
_dats = dats ?? throw new ArgumentNullException(nameof(dats));
|
||||
_residency = residency
|
||||
?? throw new ArgumentNullException(nameof(residency));
|
||||
_renderSceneOracle = renderSceneOracle;
|
||||
}
|
||||
|
||||
public WorldLifecycleResourceSnapshot Capture(RenderFrameOutcome outcome)
|
||||
|
|
@ -122,6 +127,8 @@ internal sealed class WorldLifecycleResourceSnapshotSource
|
|||
TotalLandblocks: outcome.World.TotalLandblocks,
|
||||
LiveEntities: _liveEntities.Count,
|
||||
MaterializedLiveEntities: _liveEntities.MaterializedCount,
|
||||
RenderSceneOracle: _renderSceneOracle?.Snapshot
|
||||
?? CurrentRenderSceneOracleSnapshot.Disabled,
|
||||
PendingLiveTeardowns: _liveEntities.PendingTeardownCount,
|
||||
PendingLandblockRetirements: _streaming.PendingRetirementCount,
|
||||
ParticleEmitters: _particles.ActiveEmitterCount,
|
||||
|
|
|
|||
|
|
@ -30,6 +30,27 @@ namespace AcDream.App.Rendering;
|
|||
/// </summary>
|
||||
public static class InteriorEntityPartition
|
||||
{
|
||||
internal enum ProjectionClass : byte
|
||||
{
|
||||
OutdoorStatic,
|
||||
CellStatic,
|
||||
Dynamic,
|
||||
}
|
||||
|
||||
internal interface IObserver
|
||||
{
|
||||
void BeginFrame();
|
||||
|
||||
void Observe(
|
||||
uint landblockId,
|
||||
WorldEntity entity,
|
||||
ProjectionClass projectionClass);
|
||||
|
||||
void Complete(Result result);
|
||||
|
||||
void AbortFrame();
|
||||
}
|
||||
|
||||
public sealed class Result
|
||||
{
|
||||
public Dictionary<uint, List<WorldEntity>> ByCell { get; } = new();
|
||||
|
|
@ -143,6 +164,79 @@ public static class InteriorEntityPartition
|
|||
result.PruneEmptyCellBuckets();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Current-path referee overload. The observer sees the exact branch that
|
||||
/// populated each accepted partition entry, including its owning
|
||||
/// landblock. A null observer is the production fast path and performs no
|
||||
/// diagnostic allocation or hashing.
|
||||
/// </summary>
|
||||
internal static void Partition(
|
||||
Result result,
|
||||
HashSet<uint> visibleCells,
|
||||
IEnumerable<(uint LandblockId, Vector3 AabbMin, Vector3 AabbMax,
|
||||
IReadOnlyList<WorldEntity> Entities,
|
||||
IReadOnlyDictionary<uint, WorldEntity>? AnimatedById)> landblockEntries,
|
||||
IObserver? observer)
|
||||
{
|
||||
if (observer is null)
|
||||
{
|
||||
Partition(result, visibleCells, landblockEntries);
|
||||
return;
|
||||
}
|
||||
|
||||
observer.BeginFrame();
|
||||
try
|
||||
{
|
||||
result.ClearForReuse();
|
||||
foreach (var entry in landblockEntries)
|
||||
{
|
||||
foreach (var e in entry.Entities)
|
||||
{
|
||||
if (e.MeshRefs.Count == 0) continue;
|
||||
|
||||
// Retail contract: every server-spawned entity is a
|
||||
// DYNAMIC and draws in the last pass, regardless of cell.
|
||||
if (e.ServerGuid != 0)
|
||||
{
|
||||
result.Dynamics.Add(e);
|
||||
observer.Observe(
|
||||
entry.LandblockId,
|
||||
e,
|
||||
ProjectionClass.Dynamic);
|
||||
}
|
||||
else if (e.ParentCellId is uint cell && IsIndoorCellId(cell))
|
||||
{
|
||||
if (!visibleCells.Contains(cell))
|
||||
continue;
|
||||
if (!result.ByCell.TryGetValue(cell, out var list))
|
||||
result.ByCell[cell] = list = new List<WorldEntity>();
|
||||
list.Add(e);
|
||||
observer.Observe(
|
||||
entry.LandblockId,
|
||||
e,
|
||||
ProjectionClass.CellStatic);
|
||||
}
|
||||
else
|
||||
{
|
||||
result.OutdoorStatic.Add(e);
|
||||
observer.Observe(
|
||||
entry.LandblockId,
|
||||
e,
|
||||
ProjectionClass.OutdoorStatic);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
result.PruneEmptyCellBuckets();
|
||||
observer.Complete(result);
|
||||
}
|
||||
catch
|
||||
{
|
||||
observer.AbortFrame();
|
||||
throw;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>Shared indoor classification — keep DrawDynamicsLast, the
|
||||
/// outside-stage assignment (#118), and the partition in lockstep.</summary>
|
||||
public static bool IsIndoorCellId(uint cellId)
|
||||
|
|
|
|||
|
|
@ -12,6 +12,7 @@ namespace AcDream.App.Rendering;
|
|||
/// </summary>
|
||||
public sealed class RetailPViewRenderer
|
||||
{
|
||||
private readonly InteriorEntityPartition.IObserver? _partitionObserver;
|
||||
private readonly PortalVisibilityFrame _mainPortalFrameScratch = new();
|
||||
private readonly ClipFrameAssembly _clipAssemblyScratch = new();
|
||||
private readonly ViewconeCuller _viewconeScratch = new();
|
||||
|
|
@ -63,6 +64,16 @@ public sealed class RetailPViewRenderer
|
|||
private readonly HashSet<uint> _drawableCellsScratch = new();
|
||||
private readonly RetailPViewScratchRetention _scratchRetention = new();
|
||||
|
||||
public RetailPViewRenderer()
|
||||
{
|
||||
}
|
||||
|
||||
internal RetailPViewRenderer(
|
||||
InteriorEntityPartition.IObserver? partitionObserver)
|
||||
{
|
||||
_partitionObserver = partitionObserver;
|
||||
}
|
||||
|
||||
// T2 (BR-4): retail has NO distance constant on the flood-admission chain
|
||||
// (DrawBuilding → portal walk → ConstructView: viewconeCheck + side test +
|
||||
// GetClip + GetVisible only). The old 48 m seed cap is replaced by the
|
||||
|
|
@ -156,7 +167,11 @@ public sealed class RetailPViewRenderer
|
|||
|
||||
passes.PrepareCellBatches(ctx, prepareCells);
|
||||
|
||||
InteriorEntityPartition.Partition(_partitionResult, prepareCells, ctx.LandblockEntries);
|
||||
InteriorEntityPartition.Partition(
|
||||
_partitionResult,
|
||||
prepareCells,
|
||||
ctx.LandblockEntries,
|
||||
_partitionObserver);
|
||||
var partition = _partitionResult;
|
||||
RetailPViewFrameResult result = _frameResultScratch.Reset(
|
||||
pvFrame,
|
||||
|
|
|
|||
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