refactor(render): delete the packed dispatcher
This commit is contained in:
parent
8d8aa715ee
commit
73ba1d3c32
7 changed files with 115 additions and 1659 deletions
|
|
@ -117,7 +117,6 @@ internal interface IRenderFrameEntityPassExecutor
|
||||||
/// </summary>
|
/// </summary>
|
||||||
internal sealed partial class RetailPViewPassExecutor :
|
internal sealed partial class RetailPViewPassExecutor :
|
||||||
IRetailPViewPassExecutor,
|
IRetailPViewPassExecutor,
|
||||||
IRenderFrameEntityPassExecutor,
|
|
||||||
IOutdoorSceneParticleOwnerSource
|
IOutdoorSceneParticleOwnerSource
|
||||||
{
|
{
|
||||||
private readonly IWorldPassSurface _surface;
|
private readonly IWorldPassSurface _surface;
|
||||||
|
|
@ -214,30 +213,6 @@ internal sealed partial class RetailPViewPassExecutor :
|
||||||
internal (int Width, int Height)? WalkAttachmentExtent =>
|
internal (int Width, int Height)? WalkAttachmentExtent =>
|
||||||
_entities.WalkAttachmentExtent;
|
_entities.WalkAttachmentExtent;
|
||||||
|
|
||||||
public void BeginEntityFrame(in RenderFrameView view) =>
|
|
||||||
_entities.BeginPackedProductionFrame(in view);
|
|
||||||
|
|
||||||
public bool DrawEntityRoute(
|
|
||||||
ICamera camera,
|
|
||||||
in RenderFrameView view,
|
|
||||||
RenderFrameCandidateRoute route,
|
|
||||||
int routeIndex,
|
|
||||||
uint cellId,
|
|
||||||
uint tupleLandblockId) =>
|
|
||||||
_entities.DrawPackedProductionRoute(
|
|
||||||
camera,
|
|
||||||
in view,
|
|
||||||
route,
|
|
||||||
routeIndex,
|
|
||||||
cellId,
|
|
||||||
tupleLandblockId);
|
|
||||||
|
|
||||||
public void CompleteEntityFrame(in RenderFrameView view) =>
|
|
||||||
_entities.CompletePackedProductionFrame(in view);
|
|
||||||
|
|
||||||
public void AbortEntityFrame() =>
|
|
||||||
_entities.AbortPackedProductionFrame();
|
|
||||||
|
|
||||||
public void AbortFrame()
|
public void AbortFrame()
|
||||||
{
|
{
|
||||||
List<Exception>? failures = null;
|
List<Exception>? failures = null;
|
||||||
|
|
@ -406,18 +381,7 @@ internal sealed partial class RetailPViewPassExecutor :
|
||||||
_terrainDiagnostics.Complete();
|
_terrainDiagnostics.Complete();
|
||||||
|
|
||||||
DisableClipDistances();
|
DisableClipDistances();
|
||||||
if (context.EntityDraw is RenderFrameEntityDrawRequest request)
|
if (context.OutdoorEntities.Count > 0)
|
||||||
{
|
|
||||||
RenderFrameView drawView = request.View;
|
|
||||||
_entities.DrawPackedProductionRoute(
|
|
||||||
frame.Camera,
|
|
||||||
in drawView,
|
|
||||||
request.Route,
|
|
||||||
request.RouteIndex,
|
|
||||||
request.CellId,
|
|
||||||
request.TupleLandblockId);
|
|
||||||
}
|
|
||||||
else if (context.OutdoorEntities.Count > 0)
|
|
||||||
{
|
{
|
||||||
var sceneryEntry = (
|
var sceneryEntry = (
|
||||||
frame.PlayerLandblockId ?? 0u,
|
frame.PlayerLandblockId ?? 0u,
|
||||||
|
|
@ -449,18 +413,7 @@ internal sealed partial class RetailPViewPassExecutor :
|
||||||
_surface.BindTerrainClip();
|
_surface.BindTerrainClip();
|
||||||
|
|
||||||
DisableClipDistances();
|
DisableClipDistances();
|
||||||
if (context.EntityDraw is RenderFrameEntityDrawRequest request)
|
if (context.Dynamics.Count > 0)
|
||||||
{
|
|
||||||
RenderFrameView drawView = request.View;
|
|
||||||
_entities.DrawPackedProductionRoute(
|
|
||||||
frame.Camera,
|
|
||||||
in drawView,
|
|
||||||
request.Route,
|
|
||||||
request.RouteIndex,
|
|
||||||
request.CellId,
|
|
||||||
request.TupleLandblockId);
|
|
||||||
}
|
|
||||||
else if (context.Dynamics.Count > 0)
|
|
||||||
{
|
{
|
||||||
var dynamicsEntry = (
|
var dynamicsEntry = (
|
||||||
frame.PlayerLandblockId ?? 0u,
|
frame.PlayerLandblockId ?? 0u,
|
||||||
|
|
@ -558,18 +511,7 @@ internal sealed partial class RetailPViewPassExecutor :
|
||||||
_surface.BindTerrainClip();
|
_surface.BindTerrainClip();
|
||||||
DisableClipDistances();
|
DisableClipDistances();
|
||||||
|
|
||||||
if (context.EntityDraw is RenderFrameEntityDrawRequest request)
|
if (context.BuildingShells.Count > 0)
|
||||||
{
|
|
||||||
RenderFrameView drawView = request.View;
|
|
||||||
_entities.DrawPackedProductionRoute(
|
|
||||||
frame.Camera,
|
|
||||||
in drawView,
|
|
||||||
request.Route,
|
|
||||||
request.RouteIndex,
|
|
||||||
request.CellId,
|
|
||||||
request.TupleLandblockId);
|
|
||||||
}
|
|
||||||
else if (context.BuildingShells.Count > 0)
|
|
||||||
{
|
{
|
||||||
var buildingEntry = (
|
var buildingEntry = (
|
||||||
frame.PlayerLandblockId ?? 0u,
|
frame.PlayerLandblockId ?? 0u,
|
||||||
|
|
|
||||||
|
|
@ -1,251 +0,0 @@
|
||||||
using System.Numerics;
|
|
||||||
using System.Runtime.CompilerServices;
|
|
||||||
using AcDream.App.Rendering.Scene;
|
|
||||||
|
|
||||||
namespace AcDream.App.Rendering.Wb;
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// The appearance-only identity of one retained scene projection. Root
|
|
||||||
/// transform, clip slot, light selection, and selection lighting deliberately
|
|
||||||
/// stay outside this key: G3 updates those instance fields without rebuilding
|
|
||||||
/// mesh/material classification.
|
|
||||||
/// </summary>
|
|
||||||
internal readonly record struct PackedClassificationIdentity(
|
|
||||||
RenderOwnerIncarnation Incarnation,
|
|
||||||
RenderMeshSet MeshSet,
|
|
||||||
RenderMaterialVariant Material,
|
|
||||||
RenderDegradeState DegradeState,
|
|
||||||
RenderSceneHash128 Geometry,
|
|
||||||
RenderSceneHash128 Appearance)
|
|
||||||
{
|
|
||||||
public static PackedClassificationIdentity From(
|
|
||||||
in RenderProjectionRecord projection) =>
|
|
||||||
new(
|
|
||||||
projection.OwnerIncarnation,
|
|
||||||
projection.MeshSet,
|
|
||||||
projection.Material,
|
|
||||||
projection.DegradeState,
|
|
||||||
projection.Source.GeometryFingerprint,
|
|
||||||
projection.Source.AppearanceFingerprint);
|
|
||||||
}
|
|
||||||
|
|
||||||
internal readonly record struct PackedClassifiedBatch(
|
|
||||||
GroupKey Key,
|
|
||||||
Matrix4x4 RestPose,
|
|
||||||
Vector3 LocalSortCenter);
|
|
||||||
|
|
||||||
internal readonly record struct PackedClassifiedSelectionPart(
|
|
||||||
int PartIndex,
|
|
||||||
uint GfxObjId,
|
|
||||||
Matrix4x4 RestPose);
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Reusable variable-size payload for one projection. Lists grow only when an
|
|
||||||
/// appearance first exposes a higher batch/part count and are cleared in
|
|
||||||
/// place on the next appearance rebuild.
|
|
||||||
/// </summary>
|
|
||||||
internal sealed class PackedProjectionClassificationEntry
|
|
||||||
{
|
|
||||||
public PackedClassificationIdentity Identity;
|
|
||||||
public readonly List<PackedClassifiedBatch> Batches = [];
|
|
||||||
public readonly List<PackedClassifiedSelectionPart> SelectionParts = [];
|
|
||||||
public long LastBuiltFrame;
|
|
||||||
public long LastSeenFrame;
|
|
||||||
public bool ReusableAcrossFrames;
|
|
||||||
public long AccountedPayloadBytes;
|
|
||||||
|
|
||||||
public void BeginRebuild(
|
|
||||||
in PackedClassificationIdentity identity,
|
|
||||||
long frame)
|
|
||||||
{
|
|
||||||
Identity = identity;
|
|
||||||
Batches.Clear();
|
|
||||||
SelectionParts.Clear();
|
|
||||||
LastBuiltFrame = frame;
|
|
||||||
LastSeenFrame = frame;
|
|
||||||
ReusableAcrossFrames = false;
|
|
||||||
}
|
|
||||||
|
|
||||||
public void ReleaseStorage()
|
|
||||||
{
|
|
||||||
Batches.Clear();
|
|
||||||
SelectionParts.Clear();
|
|
||||||
Batches.TrimExcess();
|
|
||||||
SelectionParts.TrimExcess();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
internal readonly record struct PackedClassificationCacheSnapshot(
|
|
||||||
int ProjectionCount,
|
|
||||||
int StaticRebuildCount,
|
|
||||||
int SameFrameReuseCount,
|
|
||||||
int CrossFrameReuseCount,
|
|
||||||
int AnimatedClassificationCount,
|
|
||||||
int RetiredProjectionCount,
|
|
||||||
long RetainedPayloadBytes);
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// G3 retained classification owner. Structural classification is rebuilt
|
|
||||||
/// only for first sight, incarnation/appearance changes, incomplete resource
|
|
||||||
/// readiness, or active animation. Warm unchanged projections replay their
|
|
||||||
/// retained batch templates while current transforms, lights, clip slots, and
|
|
||||||
/// selection lighting are refreshed.
|
|
||||||
/// </summary>
|
|
||||||
internal sealed class PackedProjectionClassificationCache
|
|
||||||
{
|
|
||||||
private const RenderDirtyMask ClassificationDirtyMask =
|
|
||||||
RenderDirtyMask.Appearance;
|
|
||||||
|
|
||||||
private readonly Dictionary<
|
|
||||||
RenderProjectionId,
|
|
||||||
PackedProjectionClassificationEntry> _entries = [];
|
|
||||||
private readonly List<RenderProjectionId> _retired = [];
|
|
||||||
private RenderSceneGeneration _generation;
|
|
||||||
private bool _generationSet;
|
|
||||||
private long _frame;
|
|
||||||
private int _staticRebuildCount;
|
|
||||||
private int _sameFrameReuseCount;
|
|
||||||
private int _crossFrameReuseCount;
|
|
||||||
private int _animatedClassificationCount;
|
|
||||||
private int _retiredProjectionCount;
|
|
||||||
private long _retainedPayloadBytes;
|
|
||||||
|
|
||||||
public long Frame => _frame;
|
|
||||||
|
|
||||||
public PackedClassificationCacheSnapshot Snapshot =>
|
|
||||||
new(
|
|
||||||
_entries.Count,
|
|
||||||
_staticRebuildCount,
|
|
||||||
_sameFrameReuseCount,
|
|
||||||
_crossFrameReuseCount,
|
|
||||||
_animatedClassificationCount,
|
|
||||||
_retiredProjectionCount,
|
|
||||||
_retainedPayloadBytes);
|
|
||||||
|
|
||||||
public void BeginFrame(RenderSceneGeneration generation)
|
|
||||||
{
|
|
||||||
if (_frame == long.MaxValue)
|
|
||||||
{
|
|
||||||
throw new InvalidOperationException(
|
|
||||||
"Packed classification frame identity was exhausted.");
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!_generationSet || generation != _generation)
|
|
||||||
{
|
|
||||||
Clear();
|
|
||||||
_generation = generation;
|
|
||||||
_generationSet = true;
|
|
||||||
}
|
|
||||||
|
|
||||||
_frame++;
|
|
||||||
_staticRebuildCount = 0;
|
|
||||||
_sameFrameReuseCount = 0;
|
|
||||||
_crossFrameReuseCount = 0;
|
|
||||||
_animatedClassificationCount = 0;
|
|
||||||
_retiredProjectionCount = 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
public bool TryGetReusable(
|
|
||||||
RenderProjectionId id,
|
|
||||||
in PackedClassificationIdentity identity,
|
|
||||||
RenderDirtyMask dirtyMask,
|
|
||||||
out PackedProjectionClassificationEntry? entry)
|
|
||||||
{
|
|
||||||
if (!_entries.TryGetValue(id, out entry)
|
|
||||||
|| entry.Identity != identity)
|
|
||||||
{
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
entry.LastSeenFrame = _frame;
|
|
||||||
if (entry.LastBuiltFrame == _frame)
|
|
||||||
{
|
|
||||||
_sameFrameReuseCount++;
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!entry.ReusableAcrossFrames
|
|
||||||
|| (dirtyMask & ClassificationDirtyMask) != 0)
|
|
||||||
{
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
_crossFrameReuseCount++;
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
public PackedProjectionClassificationEntry BeginRebuild(
|
|
||||||
RenderProjectionId id,
|
|
||||||
in PackedClassificationIdentity identity)
|
|
||||||
{
|
|
||||||
if (!_entries.TryGetValue(id, out var entry))
|
|
||||||
{
|
|
||||||
entry = new PackedProjectionClassificationEntry();
|
|
||||||
_entries.Add(id, entry);
|
|
||||||
}
|
|
||||||
|
|
||||||
entry.BeginRebuild(in identity, _frame);
|
|
||||||
_staticRebuildCount++;
|
|
||||||
return entry;
|
|
||||||
}
|
|
||||||
|
|
||||||
public void CompleteRebuild(
|
|
||||||
PackedProjectionClassificationEntry entry,
|
|
||||||
bool reusableAcrossFrames)
|
|
||||||
{
|
|
||||||
ArgumentNullException.ThrowIfNull(entry);
|
|
||||||
if (entry.LastBuiltFrame != _frame)
|
|
||||||
{
|
|
||||||
throw new InvalidOperationException(
|
|
||||||
"Only an entry rebuilt in the active frame can be completed.");
|
|
||||||
}
|
|
||||||
|
|
||||||
entry.ReusableAcrossFrames = reusableAcrossFrames;
|
|
||||||
long payloadBytes = checked(
|
|
||||||
(long)entry.Batches.Capacity
|
|
||||||
* Unsafe.SizeOf<PackedClassifiedBatch>()
|
|
||||||
+ (long)entry.SelectionParts.Capacity
|
|
||||||
* Unsafe.SizeOf<PackedClassifiedSelectionPart>());
|
|
||||||
_retainedPayloadBytes = checked(
|
|
||||||
_retainedPayloadBytes
|
|
||||||
- entry.AccountedPayloadBytes
|
|
||||||
+ payloadBytes);
|
|
||||||
entry.AccountedPayloadBytes = payloadBytes;
|
|
||||||
}
|
|
||||||
|
|
||||||
public void RecordAnimatedClassification() =>
|
|
||||||
_animatedClassificationCount++;
|
|
||||||
|
|
||||||
public void EndFrame()
|
|
||||||
{
|
|
||||||
_retired.Clear();
|
|
||||||
foreach ((RenderProjectionId id, var entry) in _entries)
|
|
||||||
{
|
|
||||||
if (entry.LastSeenFrame != _frame)
|
|
||||||
{
|
|
||||||
_retainedPayloadBytes -= entry.AccountedPayloadBytes;
|
|
||||||
entry.AccountedPayloadBytes = 0;
|
|
||||||
entry.ReleaseStorage();
|
|
||||||
_retired.Add(id);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
for (int i = 0; i < _retired.Count; i++)
|
|
||||||
_entries.Remove(_retired[i]);
|
|
||||||
_retiredProjectionCount = _retired.Count;
|
|
||||||
_retired.Clear();
|
|
||||||
}
|
|
||||||
|
|
||||||
public void Clear()
|
|
||||||
{
|
|
||||||
foreach (PackedProjectionClassificationEntry entry
|
|
||||||
in _entries.Values)
|
|
||||||
{
|
|
||||||
entry.ReleaseStorage();
|
|
||||||
}
|
|
||||||
_entries.Clear();
|
|
||||||
_retired.Clear();
|
|
||||||
_frame = 0;
|
|
||||||
_retainedPayloadBytes = 0;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
File diff suppressed because it is too large
Load diff
|
|
@ -2,6 +2,7 @@ using System.Numerics;
|
||||||
using AcDream.App.Rendering.Scene;
|
using AcDream.App.Rendering.Scene;
|
||||||
using AcDream.App.Rendering.Selection;
|
using AcDream.App.Rendering.Selection;
|
||||||
using AcDream.App.Rendering.Walk;
|
using AcDream.App.Rendering.Walk;
|
||||||
|
using AcDream.Core.Lighting;
|
||||||
using AcDream.Core.Meshing;
|
using AcDream.Core.Meshing;
|
||||||
using AcDream.Core.World;
|
using AcDream.Core.World;
|
||||||
using DatReaderWriter.Enums;
|
using DatReaderWriter.Enums;
|
||||||
|
|
@ -166,7 +167,7 @@ public sealed partial class WbDrawDispatcher
|
||||||
/// Classifies one static entity for the walk populator: resolves its clip
|
/// Classifies one static entity for the walk populator: resolves its clip
|
||||||
/// slot / light set / selection lighting exactly as
|
/// slot / light set / selection lighting exactly as
|
||||||
/// <c>ClassifyPackedEntity</c> does (via the shared
|
/// <c>ClassifyPackedEntity</c> does (via the shared
|
||||||
/// <see cref="ResolveSlotForFrame"/> / <c>ResolvePackedLightSet</c>
|
/// <see cref="ResolveSlotForFrame"/> / <c>ResolveWalkLightSet</c>
|
||||||
/// helpers), walks its Setup parts or single mesh (mirroring
|
/// helpers), walks its Setup parts or single mesh (mirroring
|
||||||
/// <c>ClassifyPackedEntity</c>'s own shape), and appends one
|
/// <c>ClassifyPackedEntity</c>'s own shape), and appends one
|
||||||
/// <see cref="WalkClassifiedBatch"/> per surviving batch to
|
/// <see cref="WalkClassifiedBatch"/> per surviving batch to
|
||||||
|
|
@ -204,7 +205,7 @@ public sealed partial class WbDrawDispatcher
|
||||||
if (culled)
|
if (culled)
|
||||||
return;
|
return;
|
||||||
|
|
||||||
ResolvePackedLightSet(in entity, out InstanceLightSet lights, out bool indoor);
|
ResolveWalkLightSet(in entity, out InstanceLightSet lights, out bool indoor);
|
||||||
Vector2 selectionLighting =
|
Vector2 selectionLighting =
|
||||||
_selectionLighting?.TryGetLighting(
|
_selectionLighting?.TryGetLighting(
|
||||||
entity.ServerGuid, entity.LocalEntityId, out RetailSelectionLighting lighting) == true
|
entity.ServerGuid, entity.LocalEntityId, out RetailSelectionLighting lighting) == true
|
||||||
|
|
@ -261,7 +262,7 @@ public sealed partial class WbDrawDispatcher
|
||||||
}
|
}
|
||||||
|
|
||||||
float opacity = liveDynamic
|
float opacity = liveDynamic
|
||||||
? PackedPartOpacity(
|
? WalkPartOpacity(
|
||||||
entity.ServerGuid,
|
entity.ServerGuid,
|
||||||
entity.LocalEntityId,
|
entity.LocalEntityId,
|
||||||
(uint)setupPartIndex)
|
(uint)setupPartIndex)
|
||||||
|
|
@ -298,7 +299,7 @@ public sealed partial class WbDrawDispatcher
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
float opacity = liveDynamic
|
float opacity = liveDynamic
|
||||||
? PackedPartOpacity(
|
? WalkPartOpacity(
|
||||||
entity.ServerGuid,
|
entity.ServerGuid,
|
||||||
entity.LocalEntityId,
|
entity.LocalEntityId,
|
||||||
(uint)partIndex)
|
(uint)partIndex)
|
||||||
|
|
@ -330,6 +331,110 @@ public sealed partial class WbDrawDispatcher
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private float WalkPartOpacity(
|
||||||
|
uint serverGuid,
|
||||||
|
uint localEntityId,
|
||||||
|
uint setupPartIndex)
|
||||||
|
{
|
||||||
|
float opacity = EntityOpacity(serverGuid);
|
||||||
|
if (opacity <= 0f)
|
||||||
|
return 0f;
|
||||||
|
if (!_translucencyFades.TryGetCurrentValue(
|
||||||
|
localEntityId,
|
||||||
|
setupPartIndex,
|
||||||
|
out float translucency))
|
||||||
|
{
|
||||||
|
return opacity;
|
||||||
|
}
|
||||||
|
|
||||||
|
return translucency >= 1f
|
||||||
|
? 0f
|
||||||
|
: opacity * (1f - translucency);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void ResolveWalkLightSet(
|
||||||
|
in RenderInstanceCandidate entity,
|
||||||
|
out InstanceLightSet lights,
|
||||||
|
out bool indoor)
|
||||||
|
{
|
||||||
|
indoor = IndoorObjectReceivesTorches(entity.ParentCell);
|
||||||
|
lights = InstanceLightSet.Disabled;
|
||||||
|
IReadOnlyList<LightSource>? snapshot = _pointSnapshot;
|
||||||
|
if (!indoor || snapshot is null || snapshot.Count == 0)
|
||||||
|
return;
|
||||||
|
|
||||||
|
Vector3 center =
|
||||||
|
(entity.Bounds.Minimum + entity.Bounds.Maximum) * 0.5f;
|
||||||
|
float radius =
|
||||||
|
(entity.Bounds.Maximum - entity.Bounds.Minimum)
|
||||||
|
.Length() * 0.5f;
|
||||||
|
Span<int> selected =
|
||||||
|
stackalloc int[LightManager.MaxLightsPerObject];
|
||||||
|
selected.Fill(-1);
|
||||||
|
LightManager.SelectForObject(
|
||||||
|
snapshot,
|
||||||
|
center,
|
||||||
|
radius,
|
||||||
|
selected);
|
||||||
|
lights = InstanceLightSet.From(selected);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static bool PartVisibleInLookInTurn(
|
||||||
|
IWalkLookInViewSource? lookInViews,
|
||||||
|
int routeIndex,
|
||||||
|
uint cellId,
|
||||||
|
in RenderInstanceCandidate entity,
|
||||||
|
int partIndex,
|
||||||
|
uint gfxObjId,
|
||||||
|
ObjectRenderData renderData,
|
||||||
|
Matrix4x4 localToWorld)
|
||||||
|
{
|
||||||
|
if (lookInViews is null)
|
||||||
|
return true;
|
||||||
|
if (renderData.SelectionSphere is not { Radius: > 0f } sphere)
|
||||||
|
return true;
|
||||||
|
|
||||||
|
return LookInDrawingSphereVisible(
|
||||||
|
lookInViews,
|
||||||
|
routeIndex,
|
||||||
|
sphere,
|
||||||
|
localToWorld,
|
||||||
|
out _,
|
||||||
|
out _);
|
||||||
|
}
|
||||||
|
|
||||||
|
internal static bool LookInDrawingSphereVisible(
|
||||||
|
IWalkLookInViewSource lookInViews,
|
||||||
|
int routeIndex,
|
||||||
|
DatReaderWriter.Types.Sphere sphere,
|
||||||
|
Matrix4x4 localToWorld,
|
||||||
|
out Vector3 center,
|
||||||
|
out float radius)
|
||||||
|
{
|
||||||
|
ArgumentNullException.ThrowIfNull(lookInViews);
|
||||||
|
ArgumentNullException.ThrowIfNull(sphere);
|
||||||
|
|
||||||
|
center = Vector3.Transform(sphere.Origin, localToWorld);
|
||||||
|
float scaleX = new Vector3(
|
||||||
|
localToWorld.M11,
|
||||||
|
localToWorld.M12,
|
||||||
|
localToWorld.M13).Length();
|
||||||
|
float scaleY = new Vector3(
|
||||||
|
localToWorld.M21,
|
||||||
|
localToWorld.M22,
|
||||||
|
localToWorld.M23).Length();
|
||||||
|
float scaleZ = new Vector3(
|
||||||
|
localToWorld.M31,
|
||||||
|
localToWorld.M32,
|
||||||
|
localToWorld.M33).Length();
|
||||||
|
radius = sphere.Radius
|
||||||
|
* MathF.Max(scaleX, MathF.Max(scaleY, scaleZ));
|
||||||
|
return lookInViews.SphereVisibleInLookInTurn(
|
||||||
|
routeIndex,
|
||||||
|
in center,
|
||||||
|
radius);
|
||||||
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Walks one resolved mesh's batches through <see cref="TryClassifyBatch"/>
|
/// Walks one resolved mesh's batches through <see cref="TryClassifyBatch"/>
|
||||||
/// and appends every surviving one to <paramref name="sink"/> at
|
/// and appends every surviving one to <paramref name="sink"/> at
|
||||||
|
|
|
||||||
|
|
@ -2305,18 +2305,9 @@ public sealed partial class WbDrawDispatcher : IDisposable
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Campaign VM VM6 review fix round 3 (N2a): the ONE shared
|
/// Campaign VM VM6 review fix round 3 (N2a): the ONE shared
|
||||||
/// InstanceGroup-from-key construction seam. Before this there were two
|
/// InstanceGroup-from-key construction seam retained by the classic
|
||||||
/// near-identical <c>new InstanceGroup { ... }</c> initializers — the
|
/// diagnostic route. The production walk writes its ordered stream
|
||||||
/// classic route's <see cref="GetOrCreateInstanceGroup"/> and the packed
|
/// directly and the former packed twin was deleted at Campaign FW4.
|
||||||
/// route's <c>GetOrCreatePackedGroup</c> — that had already drifted once
|
|
||||||
/// (the packed route's F1 bug: FoliageFlags copied on one side, dropped
|
|
||||||
/// on the other). <paramref name="registration"/> and
|
|
||||||
/// <paramref name="frame"/> are the only two values that legitimately
|
|
||||||
/// differ per route (each owns its own registration counter and frame
|
|
||||||
/// serial); every other field comes from <paramref name="key"/>, so a
|
|
||||||
/// field neither site remembers to set can no longer exist. Same
|
|
||||||
/// precedent as <see cref="AppendPackedInstance"/> — one writer, tested
|
|
||||||
/// once, called from both routes.
|
|
||||||
/// </summary>
|
/// </summary>
|
||||||
internal static InstanceGroup CreateGroupFromKey(
|
internal static InstanceGroup CreateGroupFromKey(
|
||||||
GroupKey key,
|
GroupKey key,
|
||||||
|
|
|
||||||
|
|
@ -1,115 +0,0 @@
|
||||||
using AcDream.App.Rendering.Wb;
|
|
||||||
using AcDream.App.Rendering.Gpu;
|
|
||||||
using AcDream.Core.Meshing;
|
|
||||||
using System.Numerics;
|
|
||||||
|
|
||||||
namespace AcDream.App.Tests.Rendering.Wb;
|
|
||||||
|
|
||||||
public sealed class PackedDispatcherOracleTests
|
|
||||||
{
|
|
||||||
/// <summary>
|
|
||||||
/// Campaign VM VM6 review fix round 3 (N1): this test does NOT guard
|
|
||||||
/// the packed classifier itself — CreateGroupFromKey's own test
|
|
||||||
/// (InstanceGroupClearTests.CreateGroupFromKey_CopiesFoliageFlagsFromTheKey)
|
|
||||||
/// plus GroupKey.FoliageFlags being a required constructor argument are
|
|
||||||
/// what actually make F1 (the round-2 blocker: the packed classifier's
|
|
||||||
/// InstanceGroup silently carried FoliageFlags == 0) impossible to
|
|
||||||
/// reintroduce. What THIS test proves is the SHARED production step
|
|
||||||
/// downstream of classification: FoliageWindClassification.Classify,
|
|
||||||
/// called with the same argument shape ClassifyPackedBatches uses
|
|
||||||
/// (entity.LocalEntityId, exclusion membership, batch.Translucency,
|
|
||||||
/// entity-scoped HasCutoutSubset), resolves a real procedural-scenery
|
|
||||||
/// entity id (0x8…) to CutoutFoliageFlag (0x2); and a group carrying
|
|
||||||
/// that flags value reaches the literal BatchData.flags word through
|
|
||||||
/// BuildIndirectArrays — the same already-tested production step both
|
|
||||||
/// the classic and packed group lists feed into
|
|
||||||
/// (WbDrawDispatcherIndirectBuilderTests pins bit 0; this pins bits 1/2
|
|
||||||
/// landing alongside it for a real scenery id).
|
|
||||||
/// </summary>
|
|
||||||
[Fact]
|
|
||||||
public void SceneryCutoutEntityClassificationReachesTheBatchDataFlagsWordAsBit0x2()
|
|
||||||
{
|
|
||||||
const uint proceduralSceneryTreeId = 0x80010203u; // top nibble 0x8
|
|
||||||
|
|
||||||
uint foliageFlags = FoliageWindClassification.Classify(
|
|
||||||
proceduralSceneryTreeId,
|
|
||||||
isExcluded: false,
|
|
||||||
TranslucencyKind.ClipMap,
|
|
||||||
meshHasCutoutSubset: true);
|
|
||||||
Assert.Equal(FoliageWindClassification.CutoutFoliageFlag, foliageFlags);
|
|
||||||
Assert.Equal(0x2u, foliageFlags);
|
|
||||||
|
|
||||||
var groups = new List<WbDrawDispatcher.IndirectGroupInput>
|
|
||||||
{
|
|
||||||
new(
|
|
||||||
IndexCount: 12,
|
|
||||||
FirstIndex: 0,
|
|
||||||
BaseVertex: 0,
|
|
||||||
InstanceCount: 1,
|
|
||||||
FirstInstance: 0,
|
|
||||||
TextureIndex: 0x5,
|
|
||||||
TextureLayer: 0,
|
|
||||||
Translucency: TranslucencyKind.ClipMap,
|
|
||||||
FoliageFlags: foliageFlags),
|
|
||||||
};
|
|
||||||
var indirect = new DrawElementsIndirectCommand[4];
|
|
||||||
var batches = new WbDrawDispatcher.BatchDataPublic[4];
|
|
||||||
|
|
||||||
WbDrawDispatcher.BuildIndirectArrays(groups, indirect, batches);
|
|
||||||
|
|
||||||
// Bit 0 (the #226 built-mesh marker) | bit 1 (cutout foliage) = 0x3;
|
|
||||||
// isolating bit 1 with a mask proves the foliage classification
|
|
||||||
// specifically reached the word, not merely that SOME nonzero value
|
|
||||||
// did.
|
|
||||||
Assert.Equal(0x2u, batches[0].Flags & 0x2u);
|
|
||||||
}
|
|
||||||
|
|
||||||
[Theory]
|
|
||||||
[InlineData(false, 0u)]
|
|
||||||
[InlineData(true, 1u)]
|
|
||||||
public void PackedInstanceWriter_AppendsDetailCategoryInParallel(
|
|
||||||
bool buildingDetail,
|
|
||||||
uint expectedCategory)
|
|
||||||
{
|
|
||||||
var group = new WbDrawDispatcher.InstanceGroup();
|
|
||||||
|
|
||||||
WbDrawDispatcher.AppendPackedInstance(
|
|
||||||
group,
|
|
||||||
Matrix4x4.Identity,
|
|
||||||
Vector3.One,
|
|
||||||
submissionOrder: 7,
|
|
||||||
slot: 3u,
|
|
||||||
lights: WbDrawDispatcher.InstanceLightSet.Disabled,
|
|
||||||
indoor: true,
|
|
||||||
buildingDetail: buildingDetail,
|
|
||||||
opacity: 0.5f,
|
|
||||||
selectionLighting: new Vector2(0.25f, 0.75f));
|
|
||||||
|
|
||||||
Assert.Single(group.Matrices);
|
|
||||||
Assert.Single(group.LocalSortCenters);
|
|
||||||
Assert.Single(group.SubmissionOrders);
|
|
||||||
Assert.Single(group.Slots);
|
|
||||||
Assert.Single(group.LightSets);
|
|
||||||
Assert.Single(group.IndoorFlags);
|
|
||||||
Assert.Equal(expectedCategory, Assert.Single(group.DetailCategories));
|
|
||||||
Assert.Single(group.Opacities);
|
|
||||||
Assert.Single(group.SelectionLighting);
|
|
||||||
}
|
|
||||||
|
|
||||||
[Theory]
|
|
||||||
[InlineData(0u, false, false)]
|
|
||||||
[InlineData(0u, true, false)]
|
|
||||||
[InlineData(7u, false, false)]
|
|
||||||
[InlineData(7u, true, true)]
|
|
||||||
public void TransparentDeferral_MatchesProductionNoVaoEarlyReturn(
|
|
||||||
uint anyVao,
|
|
||||||
bool alphaQueueCollecting,
|
|
||||||
bool expected)
|
|
||||||
{
|
|
||||||
Assert.Equal(
|
|
||||||
expected,
|
|
||||||
WbDrawDispatcher.ShouldDeferPackedTransparent(
|
|
||||||
anyVao,
|
|
||||||
alphaQueueCollecting));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,215 +0,0 @@
|
||||||
using AcDream.App.Rendering.Scene;
|
|
||||||
using AcDream.App.Rendering.Wb;
|
|
||||||
using Xunit;
|
|
||||||
|
|
||||||
namespace AcDream.App.Tests.Rendering.Wb;
|
|
||||||
|
|
||||||
public sealed class PackedProjectionClassificationCacheTests
|
|
||||||
{
|
|
||||||
[Fact]
|
|
||||||
public void UnchangedProjection_ReusesClassificationAcrossFrames()
|
|
||||||
{
|
|
||||||
var cache = new PackedProjectionClassificationCache();
|
|
||||||
RenderSceneGeneration generation =
|
|
||||||
RenderSceneGeneration.FromRaw(1);
|
|
||||||
RenderProjectionId id = RenderProjectionId.FromRaw(0x1234);
|
|
||||||
PackedClassificationIdentity identity = Identity(7);
|
|
||||||
|
|
||||||
cache.BeginFrame(generation);
|
|
||||||
PackedProjectionClassificationEntry entry =
|
|
||||||
cache.BeginRebuild(id, in identity);
|
|
||||||
cache.CompleteRebuild(entry, reusableAcrossFrames: true);
|
|
||||||
cache.EndFrame();
|
|
||||||
|
|
||||||
cache.BeginFrame(generation);
|
|
||||||
Assert.True(cache.TryGetReusable(
|
|
||||||
id,
|
|
||||||
in identity,
|
|
||||||
RenderDirtyMask.Transform,
|
|
||||||
out PackedProjectionClassificationEntry? reused));
|
|
||||||
cache.EndFrame();
|
|
||||||
|
|
||||||
Assert.Same(entry, reused);
|
|
||||||
Assert.Equal(0, cache.Snapshot.StaticRebuildCount);
|
|
||||||
Assert.Equal(1, cache.Snapshot.CrossFrameReuseCount);
|
|
||||||
Assert.Equal(1, cache.Snapshot.ProjectionCount);
|
|
||||||
}
|
|
||||||
|
|
||||||
[Fact]
|
|
||||||
public void AppearanceDirty_RequiresRebuildButTransformDirtyDoesNot()
|
|
||||||
{
|
|
||||||
var cache = new PackedProjectionClassificationCache();
|
|
||||||
RenderSceneGeneration generation =
|
|
||||||
RenderSceneGeneration.FromRaw(1);
|
|
||||||
RenderProjectionId id = RenderProjectionId.FromRaw(0x1234);
|
|
||||||
PackedClassificationIdentity identity = Identity(7);
|
|
||||||
|
|
||||||
cache.BeginFrame(generation);
|
|
||||||
PackedProjectionClassificationEntry entry =
|
|
||||||
cache.BeginRebuild(id, in identity);
|
|
||||||
cache.CompleteRebuild(entry, reusableAcrossFrames: true);
|
|
||||||
cache.EndFrame();
|
|
||||||
|
|
||||||
cache.BeginFrame(generation);
|
|
||||||
Assert.False(cache.TryGetReusable(
|
|
||||||
id,
|
|
||||||
in identity,
|
|
||||||
RenderDirtyMask.Appearance,
|
|
||||||
out _));
|
|
||||||
PackedProjectionClassificationEntry rebuilt =
|
|
||||||
cache.BeginRebuild(id, in identity);
|
|
||||||
cache.CompleteRebuild(rebuilt, reusableAcrossFrames: true);
|
|
||||||
|
|
||||||
Assert.Same(entry, rebuilt);
|
|
||||||
Assert.Equal(1, cache.Snapshot.StaticRebuildCount);
|
|
||||||
}
|
|
||||||
|
|
||||||
[Fact]
|
|
||||||
public void IncompleteBuild_CanReplayDuplicateRouteOnlyInSameFrame()
|
|
||||||
{
|
|
||||||
var cache = new PackedProjectionClassificationCache();
|
|
||||||
RenderSceneGeneration generation =
|
|
||||||
RenderSceneGeneration.FromRaw(1);
|
|
||||||
RenderProjectionId id = RenderProjectionId.FromRaw(0x1234);
|
|
||||||
PackedClassificationIdentity identity = Identity(7);
|
|
||||||
|
|
||||||
cache.BeginFrame(generation);
|
|
||||||
PackedProjectionClassificationEntry entry =
|
|
||||||
cache.BeginRebuild(id, in identity);
|
|
||||||
cache.CompleteRebuild(entry, reusableAcrossFrames: false);
|
|
||||||
|
|
||||||
Assert.True(cache.TryGetReusable(
|
|
||||||
id,
|
|
||||||
in identity,
|
|
||||||
RenderDirtyMask.All,
|
|
||||||
out _));
|
|
||||||
Assert.Equal(1, cache.Snapshot.SameFrameReuseCount);
|
|
||||||
cache.EndFrame();
|
|
||||||
|
|
||||||
cache.BeginFrame(generation);
|
|
||||||
Assert.False(cache.TryGetReusable(
|
|
||||||
id,
|
|
||||||
in identity,
|
|
||||||
RenderDirtyMask.None,
|
|
||||||
out _));
|
|
||||||
}
|
|
||||||
|
|
||||||
[Fact]
|
|
||||||
public void IncarnationOrAppearanceChange_CannotReuseRetainedPayload()
|
|
||||||
{
|
|
||||||
var cache = new PackedProjectionClassificationCache();
|
|
||||||
RenderSceneGeneration generation =
|
|
||||||
RenderSceneGeneration.FromRaw(1);
|
|
||||||
RenderProjectionId id = RenderProjectionId.FromRaw(0x1234);
|
|
||||||
PackedClassificationIdentity original = Identity(7);
|
|
||||||
|
|
||||||
cache.BeginFrame(generation);
|
|
||||||
PackedProjectionClassificationEntry entry =
|
|
||||||
cache.BeginRebuild(id, in original);
|
|
||||||
cache.CompleteRebuild(entry, reusableAcrossFrames: true);
|
|
||||||
cache.EndFrame();
|
|
||||||
|
|
||||||
cache.BeginFrame(generation);
|
|
||||||
PackedClassificationIdentity replacement = Identity(17);
|
|
||||||
Assert.False(cache.TryGetReusable(
|
|
||||||
id,
|
|
||||||
in replacement,
|
|
||||||
RenderDirtyMask.None,
|
|
||||||
out _));
|
|
||||||
PackedProjectionClassificationEntry rebuilt =
|
|
||||||
cache.BeginRebuild(id, in replacement);
|
|
||||||
cache.CompleteRebuild(rebuilt, reusableAcrossFrames: true);
|
|
||||||
cache.EndFrame();
|
|
||||||
|
|
||||||
Assert.Same(entry, rebuilt);
|
|
||||||
Assert.Equal(replacement, rebuilt.Identity);
|
|
||||||
Assert.Equal(1, cache.Snapshot.StaticRebuildCount);
|
|
||||||
Assert.Equal(0, cache.Snapshot.CrossFrameReuseCount);
|
|
||||||
}
|
|
||||||
|
|
||||||
[Fact]
|
|
||||||
public void GenerationReplacementAndUnseenProjectionReleaseRetainedEntries()
|
|
||||||
{
|
|
||||||
var cache = new PackedProjectionClassificationCache();
|
|
||||||
RenderProjectionId id = RenderProjectionId.FromRaw(0x1234);
|
|
||||||
PackedClassificationIdentity identity = Identity(7);
|
|
||||||
|
|
||||||
cache.BeginFrame(RenderSceneGeneration.FromRaw(1));
|
|
||||||
PackedProjectionClassificationEntry entry =
|
|
||||||
cache.BeginRebuild(id, in identity);
|
|
||||||
entry.Batches.Capacity = 32;
|
|
||||||
entry.SelectionParts.Capacity = 16;
|
|
||||||
cache.CompleteRebuild(entry, reusableAcrossFrames: true);
|
|
||||||
cache.EndFrame();
|
|
||||||
Assert.True(cache.Snapshot.RetainedPayloadBytes > 0);
|
|
||||||
|
|
||||||
cache.BeginFrame(RenderSceneGeneration.FromRaw(1));
|
|
||||||
cache.EndFrame();
|
|
||||||
|
|
||||||
Assert.Equal(0, cache.Snapshot.ProjectionCount);
|
|
||||||
Assert.Equal(1, cache.Snapshot.RetiredProjectionCount);
|
|
||||||
Assert.Equal(0, entry.Batches.Capacity);
|
|
||||||
Assert.Equal(0, entry.SelectionParts.Capacity);
|
|
||||||
Assert.Equal(0, cache.Snapshot.RetainedPayloadBytes);
|
|
||||||
|
|
||||||
cache.BeginFrame(RenderSceneGeneration.FromRaw(2));
|
|
||||||
Assert.Equal(0, cache.Snapshot.ProjectionCount);
|
|
||||||
}
|
|
||||||
|
|
||||||
[Fact]
|
|
||||||
public void WarmUnchangedProjectionCache_AllocatesZeroBytes()
|
|
||||||
{
|
|
||||||
var cache = new PackedProjectionClassificationCache();
|
|
||||||
RenderSceneGeneration generation =
|
|
||||||
RenderSceneGeneration.FromRaw(1);
|
|
||||||
RenderProjectionId id = RenderProjectionId.FromRaw(0x1234);
|
|
||||||
PackedClassificationIdentity identity = Identity(7);
|
|
||||||
|
|
||||||
cache.BeginFrame(generation);
|
|
||||||
PackedProjectionClassificationEntry entry =
|
|
||||||
cache.BeginRebuild(id, in identity);
|
|
||||||
cache.CompleteRebuild(entry, reusableAcrossFrames: true);
|
|
||||||
cache.EndFrame();
|
|
||||||
|
|
||||||
// #250: the measured window was a 1,000-iteration loop written inline.
|
|
||||||
ZeroAllocationProbe.AssertAllocatesNothing(
|
|
||||||
"PackedProjectionClassificationCache warm cache hit",
|
|
||||||
() => Hit(cache, generation, id, identity));
|
|
||||||
}
|
|
||||||
|
|
||||||
private static void Hit(
|
|
||||||
PackedProjectionClassificationCache cache,
|
|
||||||
RenderSceneGeneration generation,
|
|
||||||
RenderProjectionId id,
|
|
||||||
PackedClassificationIdentity identity)
|
|
||||||
{
|
|
||||||
cache.BeginFrame(generation);
|
|
||||||
if (!cache.TryGetReusable(
|
|
||||||
id,
|
|
||||||
in identity,
|
|
||||||
RenderDirtyMask.None,
|
|
||||||
out _))
|
|
||||||
{
|
|
||||||
throw new InvalidOperationException(
|
|
||||||
"The warmed projection classification was not reusable.");
|
|
||||||
}
|
|
||||||
cache.EndFrame();
|
|
||||||
}
|
|
||||||
|
|
||||||
private static PackedClassificationIdentity Identity(ulong seed) =>
|
|
||||||
new(
|
|
||||||
RenderOwnerIncarnation.FromRaw(seed),
|
|
||||||
new RenderMeshSet(
|
|
||||||
RenderAssetHandle.FromRaw(seed + 1),
|
|
||||||
MeshCount: 2,
|
|
||||||
Revision: seed + 2),
|
|
||||||
new RenderMaterialVariant(
|
|
||||||
PaletteKey: seed + 3,
|
|
||||||
TextureReplacementKey: seed + 4,
|
|
||||||
Opacity: 1f),
|
|
||||||
new RenderDegradeState(
|
|
||||||
Level: 0,
|
|
||||||
Revision: (uint)(seed + 5)),
|
|
||||||
new RenderSceneHash128(seed + 6, seed + 7),
|
|
||||||
new RenderSceneHash128(seed + 8, seed + 9));
|
|
||||||
}
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue