acdream/src/AcDream.App/Rendering/Wb/WbDrawDispatcher.PackedOracle.cs
2026-07-25 08:36:11 +02:00

809 lines
27 KiB
C#

using System.Numerics;
using AcDream.Core.Rendering;
using AcDream.App.Rendering.Scene;
using AcDream.App.Rendering.Selection;
using AcDream.Core.Lighting;
using AcDream.Core.Meshing;
using AcDream.Core.Selection;
using AcDream.Core.World;
using DatReaderWriter.Enums;
namespace AcDream.App.Rendering.Wb;
/// <summary>
/// Scene-frame classification and production submission. The retained group
/// storage is also consumed by the G2/G3 compare oracle, while production
/// routes reuse the dispatcher's exact mesh, texture, light, translucency,
/// selection, upload, and draw owners.
/// </summary>
public sealed unsafe partial class WbDrawDispatcher
{
private readonly Dictionary<RenderProjectionId, RenderFrameEntityCandidate>
_packedEntityById = [];
private readonly Dictionary<GroupKey, InstanceGroup> _packedGroups = [];
private readonly List<GroupKey> _packedRetiredGroupKeys = [];
private readonly List<InstanceGroup> _packedOpaque = [];
private readonly List<InstanceGroup> _packedTransparent = [];
private readonly List<AlphaFingerprint>
_packedAlphaFingerprintScratch = [];
private readonly List<CurrentRenderDispatcherSubmission>
_packedSubmissions = [];
private readonly List<CurrentRenderSelectionFingerprint>
_packedSelectionParts = [];
private readonly HashSet<PackedSelectionKey> _packedSelectionKeys = [];
private readonly PackedProjectionClassificationCache
_packedClassificationCache = new();
private long _packedGroupFrame;
private long _nextPackedGroupRegistration = 1;
private int _nextPackedInstanceSubmissionOrder;
private bool _packedProductionFrameOpen;
private RenderSceneGeneration _packedProductionGeneration;
private ulong _packedProductionFrameSequence;
private int _packedProductionNextRange;
internal IReadOnlyList<CurrentRenderDispatcherSubmission>
PackedDispatcherSubmissions => _packedSubmissions;
internal IReadOnlyList<CurrentRenderSelectionFingerprint>
PackedSelectionParts => _packedSelectionParts;
internal PackedClassificationCacheSnapshot
PackedClassificationSnapshot =>
_packedClassificationCache.Snapshot;
internal void BuildPackedDispatcherOracle(
in RenderFrameView view,
uint tupleLandblockId,
Vector3 cameraWorldPosition)
{
if (_packedProductionFrameOpen)
{
throw new InvalidOperationException(
"The packed dispatcher oracle cannot replace an active production frame.");
}
BeginPackedFrameStorage(in view);
ReadOnlySpan<RenderFrameCandidateRange> ranges =
view.RouteRanges;
for (int rangeIndex = 0;
rangeIndex < ranges.Length;
rangeIndex++)
{
PackedRangeClassification classified =
ClassifyPackedRange(
in view,
rangeIndex,
tupleLandblockId,
publishSelection: false);
bool deferTransparent = ShouldDeferPackedTransparent(
classified.AnyVao,
_alphaQueue?.IsCollecting == true);
InstanceLayoutCounts counts = PartitionInstanceGroups(
_packedGroups.Values,
deferTransparent,
cameraWorldPosition,
_packedOpaque,
_packedTransparent);
_packedOpaque.Sort(CompareOpaqueSubmissionOrder);
if (!deferTransparent)
{
_packedTransparent.Sort(
CompareTransparentSubmissionOrder);
}
_packedSubmissions.Add(CreateDispatcherSubmission(
counts.VisibleInstances,
counts.ImmediateInstances,
deferTransparent,
_packedOpaque,
_packedTransparent,
cameraWorldPosition,
_packedAlphaFingerprintScratch));
}
_packedClassificationCache.EndFrame();
}
internal void BeginPackedProductionFrame(
in RenderFrameView view)
{
if (_packedProductionFrameOpen)
{
throw new InvalidOperationException(
"The packed dispatcher cannot begin a second production frame.");
}
BeginPackedFrameStorage(in view);
_packedProductionFrameOpen = true;
_packedProductionGeneration = view.Generation;
_packedProductionFrameSequence = view.FrameSequence;
_packedProductionNextRange = 0;
}
internal bool DrawPackedProductionRoute(
ICamera camera,
in RenderFrameView view,
RenderFrameCandidateRoute route,
int routeIndex,
uint cellId,
uint tupleLandblockId)
{
ValidatePackedProductionView(in view);
ReadOnlySpan<RenderFrameCandidateRange> ranges =
view.RouteRanges;
if (_packedProductionNextRange >= ranges.Length)
return false;
RenderFrameCandidateRange range =
ranges[_packedProductionNextRange];
if (range.Route != route
|| range.RouteIndex != routeIndex
|| range.CellId != cellId)
{
return false;
}
int rangePosition = _packedProductionNextRange++;
bool diagnosticsEnabled = BeginEntityDispatch(
camera,
out Matrix4x4 viewProjection,
out Vector3 cameraWorldPosition);
PackedRangeClassification classified =
ClassifyPackedRange(
in view,
rangePosition,
tupleLandblockId,
publishSelection: true);
ExecuteClassifiedGroups(
viewProjection,
cameraWorldPosition,
classified.AnyVao,
_packedGroups.Values,
EntitySet.All,
classified.CandidateCount,
classified.MeshPartCount,
diagnosticsEnabled,
observeCurrentPath: false);
return true;
}
internal void CompletePackedProductionFrame(
in RenderFrameView view)
{
ValidatePackedProductionView(in view);
int expected = view.RouteRanges.Length;
if (_packedProductionNextRange != expected)
{
throw new InvalidOperationException(
"The packed dispatcher did not consume the complete route stream: "
+ $"consumed={_packedProductionNextRange} expected={expected}.");
}
_packedClassificationCache.EndFrame();
ResetPackedProductionFrame();
}
internal void AbortPackedProductionFrame()
{
if (!_packedProductionFrameOpen)
return;
_packedClassificationCache.EndFrame();
ResetPackedProductionFrame();
}
private void BeginPackedFrameStorage(
in RenderFrameView view)
{
if (_packedGroupFrame == long.MaxValue)
{
throw new InvalidOperationException(
"Packed dispatcher frame identity was exhausted.");
}
_packedGroupFrame++;
PruneInstanceGroupsUnusedBeforeFrame(
_packedGroups,
_packedRetiredGroupKeys,
_packedGroupFrame - 1);
_packedEntityById.Clear();
_packedSubmissions.Clear();
_packedSelectionParts.Clear();
_packedSelectionKeys.Clear();
_packedClassificationCache.BeginFrame(view.Generation);
ReadOnlySpan<RenderFrameEntityCandidate> entities =
view.EntityCandidates;
for (int index = 0; index < entities.Length; index++)
{
if (!_packedEntityById.TryAdd(
entities[index].Projection.Id,
entities[index]))
{
throw new InvalidOperationException(
"Packed dispatcher received duplicate projection "
+ $"{entities[index].Projection.Id}.");
}
}
}
private PackedRangeClassification ClassifyPackedRange(
in RenderFrameView view,
int rangeIndex,
uint tupleLandblockId,
bool publishSelection)
{
ReadOnlySpan<RenderFrameCandidateRange> ranges =
view.RouteRanges;
if ((uint)rangeIndex >= (uint)ranges.Length)
throw new ArgumentOutOfRangeException(nameof(rangeIndex));
_nextPackedInstanceSubmissionOrder = 0;
foreach (InstanceGroup group in _packedGroups.Values)
group.ClearPerInstanceData();
uint anyVao = 0;
int meshPartCount = 0;
RenderFrameCandidateRange range = ranges[rangeIndex];
ReadOnlySpan<RenderProjectionRecord> routeCandidates =
view.RouteCandidates;
ReadOnlySpan<RenderFrameMeshPart> meshParts = view.MeshParts;
int end = checked(range.Offset + range.Count);
if ((uint)range.Offset > (uint)routeCandidates.Length
|| (uint)end > (uint)routeCandidates.Length)
{
throw new InvalidOperationException(
$"Packed dispatcher route {rangeIndex} exceeds candidate storage.");
}
for (int candidateIndex = range.Offset;
candidateIndex < end;
candidateIndex++)
{
RenderProjectionRecord projection =
routeCandidates[candidateIndex];
if ((projection.Flags & RenderProjectionFlags.Draw) == 0)
continue;
if (!_packedEntityById.TryGetValue(
projection.Id,
out RenderFrameEntityCandidate source))
{
throw new InvalidOperationException(
$"Packed route references absent entity {projection.Id}.");
}
int meshEnd = checked(
source.MeshPartOffset + source.MeshPartCount);
if ((uint)source.MeshPartOffset > (uint)meshParts.Length
|| (uint)meshEnd > (uint)meshParts.Length)
{
throw new InvalidOperationException(
$"Packed entity {projection.Id} exceeds mesh part storage.");
}
meshPartCount = checked(
meshPartCount + source.MeshPartCount);
RenderInstanceCandidate candidate =
RenderInstanceCandidate.FromFrame(
in source,
tupleLandblockId);
ClassifyPackedEntity(
in projection,
in candidate,
meshParts.Slice(
source.MeshPartOffset,
source.MeshPartCount),
ref anyVao,
publishSelection);
}
return new PackedRangeClassification(
anyVao,
range.Count,
meshPartCount);
}
private void ValidatePackedProductionView(
in RenderFrameView view)
{
if (!_packedProductionFrameOpen
|| view.Generation != _packedProductionGeneration
|| view.FrameSequence != _packedProductionFrameSequence)
{
throw new InvalidOperationException(
"The packed dispatcher received a stale or foreign production frame.");
}
}
private void ResetPackedProductionFrame()
{
_packedProductionFrameOpen = false;
_packedProductionGeneration = default;
_packedProductionFrameSequence = 0;
_packedProductionNextRange = 0;
}
private readonly record struct PackedRangeClassification(
uint AnyVao,
int CandidateCount,
int MeshPartCount);
private void ClassifyPackedEntity(
in RenderProjectionRecord projection,
in RenderInstanceCandidate entity,
ReadOnlySpan<RenderFrameMeshPart> meshParts,
ref uint anyVao,
bool publishSelection)
{
(uint slot, bool culled) = ResolveSlotForFrame(
_clipRoutingActive,
entity.ServerGuid,
entity.ParentCell,
_cellIdToSlot,
_outdoorSlot,
_outdoorVisible);
if (culled)
return;
ResolvePackedLightSet(
in entity,
out InstanceLightSet lights,
out bool indoor);
Vector2 selectionLighting =
_selectionLighting?.TryGetLighting(
entity.ServerGuid,
entity.LocalEntityId,
out RetailSelectionLighting lighting) == true
? new Vector2(
lighting.Luminosity,
lighting.Diffuse)
: new Vector2(0f, 1f);
PackedProjectionClassificationEntry? cacheEntry = null;
if (!entity.Animated)
{
PackedClassificationIdentity identity =
PackedClassificationIdentity.From(in projection);
if (_packedClassificationCache.TryGetReusable(
entity.ProjectionId,
in identity,
projection.DirtyMask,
out cacheEntry))
{
if (anyVao == 0 && !meshParts.IsEmpty)
{
ObjectRenderData? firstRenderData =
_meshAdapter.TryGetRenderData(
meshParts[0].MeshRef.GfxObjId);
if (firstRenderData is not null)
anyVao = firstRenderData.VAO;
}
ReplayPackedClassification(
cacheEntry!,
in entity,
slot,
lights,
indoor,
selectionLighting,
publishSelection);
return;
}
cacheEntry = _packedClassificationCache.BeginRebuild(
entity.ProjectionId,
in identity);
}
else
{
_packedClassificationCache.RecordAnimatedClassification();
}
PaletteCompositeIdentity paletteIdentity = default;
if (entity.PaletteOverride is not null)
{
paletteIdentity = TextureCache.GetPaletteIdentity(
entity.PaletteOverride);
}
bool reusableAcrossFrames = !entity.Animated;
for (int meshIndex = 0;
meshIndex < meshParts.Length;
meshIndex++)
{
RenderFrameMeshPart packedPart = meshParts[meshIndex];
if (packedPart.ProjectionId != entity.ProjectionId)
{
throw new InvalidOperationException(
$"Packed mesh part {packedPart.PartIndex} belongs "
+ $"to {packedPart.ProjectionId}, expected "
+ $"{entity.ProjectionId}.");
}
int partIndex = packedPart.PartIndex;
MeshRef meshRef = packedPart.MeshRef;
ObjectRenderData? renderData =
_meshAdapter.TryGetRenderData(meshRef.GfxObjId);
if (renderData is null)
{
reusableAcrossFrames = false;
if (_missRequested.Add(meshRef.GfxObjId))
_meshAdapter.EnsureLoaded(meshRef.GfxObjId);
continue;
}
if (anyVao == 0)
anyVao = renderData.VAO;
if (renderData.IsSetup
&& renderData.SetupParts.Count > 0)
{
for (int setupPartIndex = 0;
setupPartIndex < renderData.SetupParts.Count;
setupPartIndex++)
{
(ulong gfxObjId, Matrix4x4 partTransform) =
renderData.SetupParts[setupPartIndex];
ObjectRenderData? partData =
_meshAdapter.TryGetRenderData(gfxObjId);
if (partData is null)
{
reusableAcrossFrames = false;
if (_missRequested.Add(gfxObjId))
_meshAdapter.EnsureLoaded(gfxObjId);
continue;
}
float opacity = PackedPartOpacity(
entity.LocalEntityId,
(uint)setupPartIndex);
if (opacity < 1f)
reusableAcrossFrames = false;
if (opacity <= 0f)
continue;
Matrix4x4 restPose =
partTransform * meshRef.PartTransform;
Matrix4x4 model =
restPose * entity.RootWorld;
if (!ClassifyPackedBatches(
partData,
restPose,
model,
in entity,
meshRef,
paletteIdentity,
slot,
lights,
indoor,
selectionLighting,
opacity,
cacheEntry))
{
reusableAcrossFrames = false;
}
int selectionPartIndex = unchecked(
(partIndex << 16)
| (setupPartIndex & 0xFFFF));
cacheEntry?.SelectionParts.Add(
new PackedClassifiedSelectionPart(
selectionPartIndex,
(uint)gfxObjId,
restPose));
AddPackedSelectionPart(
in entity,
selectionPartIndex,
(uint)gfxObjId,
model,
publishSelection);
}
}
else
{
float opacity = PackedPartOpacity(
entity.LocalEntityId,
0u);
if (opacity < 1f)
reusableAcrossFrames = false;
if (opacity <= 0f)
continue;
Matrix4x4 restPose = meshRef.PartTransform;
Matrix4x4 model = restPose * entity.RootWorld;
if (!ClassifyPackedBatches(
renderData,
restPose,
model,
in entity,
meshRef,
paletteIdentity,
slot,
lights,
indoor,
selectionLighting,
opacity,
cacheEntry))
{
reusableAcrossFrames = false;
}
cacheEntry?.SelectionParts.Add(
new PackedClassifiedSelectionPart(
partIndex,
meshRef.GfxObjId,
restPose));
AddPackedSelectionPart(
in entity,
partIndex,
meshRef.GfxObjId,
model,
publishSelection);
}
}
if (cacheEntry is not null)
{
_packedClassificationCache.CompleteRebuild(
cacheEntry,
reusableAcrossFrames);
}
}
/// <summary>
/// Mirrors the production dispatcher's no-VAO early return. That return
/// records an empty submission with transparent deferral disabled even
/// when the frame alpha queue is collecting.
/// </summary>
internal static bool ShouldDeferPackedTransparent(
uint anyVao,
bool alphaQueueCollecting) =>
anyVao != 0 && alphaQueueCollecting;
private float PackedPartOpacity(
uint localEntityId,
uint setupPartIndex)
{
if (!_translucencyFades.TryGetCurrentValue(
localEntityId,
setupPartIndex,
out float translucency))
{
return 1f;
}
return translucency >= 1f
? 0f
: 1f - translucency;
}
private bool ClassifyPackedBatches(
ObjectRenderData renderData,
Matrix4x4 restPose,
Matrix4x4 model,
in RenderInstanceCandidate entity,
MeshRef meshRef,
PaletteCompositeIdentity paletteIdentity,
uint slot,
InstanceLightSet lights,
bool indoor,
Vector2 selectionLighting,
float opacity,
PackedProjectionClassificationEntry? cacheEntry)
{
bool reusableAcrossFrames = true;
for (int batchIndex = 0;
batchIndex < renderData.Batches.Count;
batchIndex++)
{
ObjectRenderBatch batch =
renderData.Batches[batchIndex];
TranslucencyKind translucency = batch.Translucency;
if (opacity < 1f && IsOpaque(translucency))
translucency = TranslucencyKind.AlphaBlend;
ResolvedTexture texture = ResolveTexture(
in entity,
meshRef,
batch,
paletteIdentity,
out bool compositePending);
if (compositePending)
reusableAcrossFrames = false;
if (texture.Handle == 0)
continue;
var key = new GroupKey(
batch.FirstIndex,
(int)batch.BaseVertex,
batch.IndexCount,
texture.Handle,
texture.Layer,
translucency,
batch.CullMode);
var classified = new PackedClassifiedBatch(
key,
restPose,
renderData.SortCenter);
cacheEntry?.Batches.Add(classified);
AppendPackedClassification(
in classified,
model,
slot,
lights,
indoor,
opacity,
selectionLighting);
}
return reusableAcrossFrames;
}
private void ReplayPackedClassification(
PackedProjectionClassificationEntry entry,
in RenderInstanceCandidate entity,
uint slot,
InstanceLightSet lights,
bool indoor,
Vector2 selectionLighting,
bool publishSelection)
{
for (int i = 0; i < entry.Batches.Count; i++)
{
PackedClassifiedBatch classified = entry.Batches[i];
AppendPackedClassification(
in classified,
classified.RestPose * entity.RootWorld,
slot,
lights,
indoor,
opacity: 1f,
selectionLighting);
}
for (int i = 0; i < entry.SelectionParts.Count; i++)
{
PackedClassifiedSelectionPart part =
entry.SelectionParts[i];
AddPackedSelectionPart(
in entity,
part.PartIndex,
part.GfxObjId,
part.RestPose * entity.RootWorld,
publishSelection);
}
}
private void AppendPackedClassification(
in PackedClassifiedBatch classified,
Matrix4x4 model,
uint slot,
InstanceLightSet lights,
bool indoor,
float opacity,
Vector2 selectionLighting)
{
InstanceGroup group =
GetOrCreatePackedGroup(classified.Key);
group.Matrices.Add(model);
group.LocalSortCenters.Add(
classified.LocalSortCenter);
group.SubmissionOrders.Add(
_nextPackedInstanceSubmissionOrder++);
group.Slots.Add(slot);
group.LightSets.Add(lights);
group.IndoorFlags.Add(indoor ? 1u : 0u);
group.Opacities.Add(opacity);
group.SelectionLighting.Add(selectionLighting);
}
private InstanceGroup GetOrCreatePackedGroup(GroupKey key)
{
if (_packedGroups.TryGetValue(
key,
out InstanceGroup? group))
{
group.LastUsedFrame = _packedGroupFrame;
return group;
}
if (_nextPackedGroupRegistration == long.MaxValue)
{
throw new InvalidOperationException(
"Packed instance-group registration space was exhausted.");
}
group = new InstanceGroup
{
FirstIndex = key.FirstIndex,
BaseVertex = key.BaseVertex,
IndexCount = key.IndexCount,
BindlessTextureHandle = key.BindlessTextureHandle,
TextureLayer = key.TextureLayer,
Translucency = key.Translucency,
CullMode = key.CullMode,
Registration = _nextPackedGroupRegistration++,
LastUsedFrame = _packedGroupFrame,
};
_packedGroups.Add(key, group);
return group;
}
private void ResolvePackedLightSet(
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 void AddPackedSelectionPart(
in RenderInstanceCandidate entity,
int partIndex,
uint gfxObjId,
Matrix4x4 localToWorld,
bool publishSelection)
{
if (!_packedSelectionKeys.Add(new PackedSelectionKey(
entity.LocalEntityId,
partIndex,
gfxObjId)))
{
return;
}
if (publishSelection)
{
_selectionSink?.AddVisiblePart(
entity.ServerGuid,
entity.LocalEntityId,
partIndex,
gfxObjId,
localToWorld);
return;
}
if (_selectionSink is not IRetailSelectionRenderOracle oracle
|| !oracle.TryCreateVisiblePart(
entity.ServerGuid,
entity.LocalEntityId,
partIndex,
gfxObjId,
localToWorld,
out RetailSelectionPart part))
{
return;
}
_packedSelectionParts.Add(
new CurrentRenderSelectionFingerprint(
Sequence: _packedSelectionParts.Count,
ServerGuid: part.ServerGuid,
LocalEntityId: part.LocalEntityId,
PartIndex: part.PartIndex,
GfxObjId: gfxObjId,
LocalToWorld: part.LocalToWorld,
Geometry: CurrentRenderSceneOracle
.FingerprintSelectionGeometry(part.Mesh)));
}
private readonly record struct PackedSelectionKey(
uint LocalEntityId,
int PartIndex,
uint GfxObjId);
}