Reapply "perf(rendering): draw retained frame product"

This reverts commit 2c848d4167.
This commit is contained in:
Erik 2026-07-25 08:36:11 +02:00
parent 823936ec31
commit 20f9fadb12
13 changed files with 1333 additions and 267 deletions

View file

@ -1,4 +1,5 @@
using System.Numerics;
using AcDream.Core.Rendering;
using AcDream.App.Rendering.Scene;
using AcDream.App.Rendering.Selection;
using AcDream.Core.Lighting;
@ -10,10 +11,10 @@ using DatReaderWriter.Enums;
namespace AcDream.App.Rendering.Wb;
/// <summary>
/// G2 compare-only classification of the scene-built frame product. This file
/// deliberately shares the production dispatcher's exact mesh, texture,
/// light, translucency, selection, grouping, and ordering owners while keeping
/// its group storage separate. It never uploads or draws.
/// 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
{
@ -35,6 +36,10 @@ public sealed unsafe partial class WbDrawDispatcher
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;
@ -51,104 +56,28 @@ public sealed unsafe partial class WbDrawDispatcher
uint tupleLandblockId,
Vector3 cameraWorldPosition)
{
if (_packedGroupFrame == long.MaxValue)
if (_packedProductionFrameOpen)
{
throw new InvalidOperationException(
"Packed dispatcher frame identity was exhausted.");
"The packed dispatcher oracle cannot replace an active production frame.");
}
_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 i = 0; i < entities.Length; i++)
{
if (!_packedEntityById.TryAdd(
entities[i].Projection.Id,
entities[i]))
{
throw new InvalidOperationException(
$"Packed dispatcher received duplicate projection "
+ $"{entities[i].Projection.Id}.");
}
}
ReadOnlySpan<RenderFrameMeshPart> meshParts = view.MeshParts;
ReadOnlySpan<RenderProjectionRecord> routeCandidates =
view.RouteCandidates;
BeginPackedFrameStorage(in view);
ReadOnlySpan<RenderFrameCandidateRange> ranges =
view.RouteRanges;
for (int rangeIndex = 0;
rangeIndex < ranges.Length;
rangeIndex++)
{
_nextPackedInstanceSubmissionOrder = 0;
uint anyVao = 0;
foreach (InstanceGroup group in _packedGroups.Values)
group.ClearPerInstanceData();
RenderFrameCandidateRange range = ranges[rangeIndex];
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 routeIndex = range.Offset;
routeIndex < end;
routeIndex++)
{
RenderProjectionRecord projection =
routeCandidates[routeIndex];
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.");
}
RenderInstanceCandidate candidate =
RenderInstanceCandidate.FromFrame(
in source,
tupleLandblockId);
ClassifyPackedEntity(
in projection,
in candidate,
meshParts.Slice(
source.MeshPartOffset,
source.MeshPartCount),
ref anyVao);
}
PackedRangeClassification classified =
ClassifyPackedRange(
in view,
rangeIndex,
tupleLandblockId,
publishSelection: false);
bool deferTransparent = ShouldDeferPackedTransparent(
anyVao,
classified.AnyVao,
_alphaQueue?.IsCollecting == true);
InstanceLayoutCounts counts = PartitionInstanceGroups(
_packedGroups.Values,
@ -176,11 +105,236 @@ public sealed unsafe partial class WbDrawDispatcher
_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)
ref uint anyVao,
bool publishSelection)
{
(uint slot, bool culled) = ResolveSlotForFrame(
_clipRoutingActive,
@ -231,7 +385,8 @@ public sealed unsafe partial class WbDrawDispatcher
slot,
lights,
indoor,
selectionLighting);
selectionLighting,
publishSelection);
return;
}
@ -272,6 +427,8 @@ public sealed unsafe partial class WbDrawDispatcher
if (renderData is null)
{
reusableAcrossFrames = false;
if (_missRequested.Add(meshRef.GfxObjId))
_meshAdapter.EnsureLoaded(meshRef.GfxObjId);
continue;
}
if (anyVao == 0)
@ -291,6 +448,8 @@ public sealed unsafe partial class WbDrawDispatcher
if (partData is null)
{
reusableAcrossFrames = false;
if (_missRequested.Add(gfxObjId))
_meshAdapter.EnsureLoaded(gfxObjId);
continue;
}
@ -334,7 +493,8 @@ public sealed unsafe partial class WbDrawDispatcher
in entity,
selectionPartIndex,
(uint)gfxObjId,
model);
model,
publishSelection);
}
}
else
@ -374,7 +534,8 @@ public sealed unsafe partial class WbDrawDispatcher
in entity,
partIndex,
meshRef.GfxObjId,
model);
model,
publishSelection);
}
}
@ -481,7 +642,8 @@ public sealed unsafe partial class WbDrawDispatcher
uint slot,
InstanceLightSet lights,
bool indoor,
Vector2 selectionLighting)
Vector2 selectionLighting,
bool publishSelection)
{
for (int i = 0; i < entry.Batches.Count; i++)
{
@ -504,7 +666,8 @@ public sealed unsafe partial class WbDrawDispatcher
in entity,
part.PartIndex,
part.GfxObjId,
part.RestPose * entity.RootWorld);
part.RestPose * entity.RootWorld,
publishSelection);
}
}
@ -593,13 +756,29 @@ public sealed unsafe partial class WbDrawDispatcher
in RenderInstanceCandidate entity,
int partIndex,
uint gfxObjId,
Matrix4x4 localToWorld)
Matrix4x4 localToWorld,
bool publishSelection)
{
if (_selectionSink is not IRetailSelectionRenderOracle oracle
|| !_packedSelectionKeys.Add(new PackedSelectionKey(
if (!_packedSelectionKeys.Add(new PackedSelectionKey(
entity.LocalEntityId,
partIndex,
gfxObjId))
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,

View file

@ -1483,41 +1483,10 @@ public sealed unsafe partial class WbDrawDispatcher : IDisposable
HashSet<uint>? animatedEntityIds = null,
EntitySet set = EntitySet.All)
{
_shader.Use();
_selectionLighting?.TickLighting();
_indoorProbeFrameCounter++;
var vp = camera.View * camera.Projection;
_shader.SetMatrix4("uViewProjection", vp);
// A7 Fix D D-3/D-4: object path — plain Lambert points + sun. MUST set
// explicitly (shared GL uniform; EnvCellRenderer sets it to 1).
_shader.SetInt("uLightingMode", 0);
// #176 stripe-hunt isolation (ACDREAM_LIGHT_DEBUG) — throwaway diagnostic.
_shader.SetInt("uLightDebug", AcDream.Core.Rendering.RenderingDiagnostics.LightDebugMode);
// #128 self-heal: fresh re-request dedup per Draw pass.
_missRequested.Clear();
bool diag = string.Equals(Environment.GetEnvironmentVariable("ACDREAM_WB_DIAG"), "1", StringComparison.Ordinal);
if (diag && !_gpuQueriesInitialized)
{
for (int i = 0; i < GpuQueryRingDepth; i++)
{
_gpuQueryOpaque[i] = _gl.GenQuery();
_gpuQueryTransparent[i] = _gl.GenQuery();
}
_gpuQueriesInitialized = true;
}
// Always run the CPU stopwatch — cheap; only logged under diag.
_cpuStopwatch.Restart();
// Camera world-space position for front-to-back sort (perf #2). The view
// matrix is the inverse of the camera's world transform, so the world
// translation lives in the inverse's translation row.
Vector3 camPos = Vector3.Zero;
if (Matrix4x4.Invert(camera.View, out var invView))
camPos = invView.Translation;
bool diag = BeginEntityDispatch(
camera,
out Matrix4x4 vp,
out Vector3 camPos);
// ── Phase 1: clear groups, walk entities, build groups ──────────────
// Draw is invoked several times per frame (landscape slices, late
@ -2058,11 +2027,71 @@ public sealed unsafe partial class WbDrawDispatcher : IDisposable
if (RenderingDiagnostics.ProbeClipRouteEnabled && _clipRoutingActive)
EmitClipRouteDispatchProbe(probeCulledEntities);
ExecuteClassifiedGroups(
vp,
camPos,
anyVao,
_groups.Values,
set,
walkResult.EntitiesWalked,
_walkScratch.Count,
diag,
observeCurrentPath: true);
}
private bool BeginEntityDispatch(
ICamera camera,
out Matrix4x4 viewProjection,
out Vector3 cameraWorldPosition)
{
_shader.Use();
_selectionLighting?.TickLighting();
_indoorProbeFrameCounter++;
viewProjection = camera.View * camera.Projection;
_shader.SetMatrix4("uViewProjection", viewProjection);
_shader.SetInt("uLightingMode", 0);
_shader.SetInt(
"uLightDebug",
RenderingDiagnostics.LightDebugMode);
_missRequested.Clear();
bool diagnosticsEnabled = string.Equals(
Environment.GetEnvironmentVariable("ACDREAM_WB_DIAG"),
"1",
StringComparison.Ordinal);
if (diagnosticsEnabled && !_gpuQueriesInitialized)
{
for (int index = 0; index < GpuQueryRingDepth; index++)
{
_gpuQueryOpaque[index] = _gl.GenQuery();
_gpuQueryTransparent[index] = _gl.GenQuery();
}
_gpuQueriesInitialized = true;
}
_cpuStopwatch.Restart();
cameraWorldPosition = Vector3.Zero;
if (Matrix4x4.Invert(camera.View, out Matrix4x4 inverseView))
cameraWorldPosition = inverseView.Translation;
return diagnosticsEnabled;
}
private void ExecuteClassifiedGroups(
Matrix4x4 vp,
Vector3 camPos,
uint anyVao,
IEnumerable<InstanceGroup> groups,
EntitySet set,
int entitiesWalked,
int tupleCount,
bool diag,
bool observeCurrentPath)
{
// Nothing visible — skip the GL pass entirely.
if (anyVao == 0)
{
LastDrawStats = new DrawStats(set, walkResult.EntitiesWalked, _walkScratch.Count, 0, 0, 0, 0, 0, 0);
ObserveCurrentDispatcherSubmission(
LastDrawStats = new DrawStats(set, entitiesWalked, tupleCount, 0, 0, 0, 0, 0, 0);
ObserveClassifiedDispatcherSubmission(observeCurrentPath,
visibleInstanceCount: 0,
immediateInstanceCount: 0,
deferTransparent: false,
@ -2075,7 +2104,7 @@ public sealed unsafe partial class WbDrawDispatcher : IDisposable
// ── Phase 3: assign FirstInstance per group, lay matrices contiguously, sort opaque ──
bool deferTransparent = _alphaQueue?.IsCollecting == true;
var instanceCounts = PartitionInstanceGroups(
_groups.Values,
groups,
deferTransparent,
camPos,
_opaqueDraws,
@ -2084,8 +2113,8 @@ public sealed unsafe partial class WbDrawDispatcher : IDisposable
int immediateInstances = instanceCounts.ImmediateInstances;
if (totalInstances == 0)
{
LastDrawStats = new DrawStats(set, walkResult.EntitiesWalked, _walkScratch.Count, 0, 0, 0, 0, 0, 0);
ObserveCurrentDispatcherSubmission(
LastDrawStats = new DrawStats(set, entitiesWalked, tupleCount, 0, 0, 0, 0, 0, 0);
ObserveClassifiedDispatcherSubmission(observeCurrentPath,
visibleInstanceCount: 0,
immediateInstanceCount: 0,
deferTransparent,
@ -2202,15 +2231,15 @@ public sealed unsafe partial class WbDrawDispatcher : IDisposable
_transparentByteOffset = layout.TransparentByteOffset;
LastDrawStats = new DrawStats(
set,
walkResult.EntitiesWalked,
_walkScratch.Count,
entitiesWalked,
tupleCount,
totalInstances,
totalDraws,
cullRuns,
_opaqueDrawCount,
_transparentDrawCount,
totalTriangles);
ObserveCurrentDispatcherSubmission(
ObserveClassifiedDispatcherSubmission(observeCurrentPath,
totalInstances,
immediateInstances,
deferTransparent,
@ -2421,6 +2450,7 @@ public sealed unsafe partial class WbDrawDispatcher : IDisposable
}
}
/// <summary>
/// Phase A8 RR5 (2026-05-26): per-building draw overload. Walks only
/// entities whose ParentCellId is in <paramref name="cellIds"/>, plus
@ -2576,6 +2606,23 @@ public sealed unsafe partial class WbDrawDispatcher : IDisposable
observer.ObserveDispatcherSubmission(in submission);
}
private void ObserveClassifiedDispatcherSubmission(
bool observeCurrentPath,
int visibleInstanceCount,
int immediateInstanceCount,
bool deferTransparent,
Vector3 cameraWorldPosition)
{
if (observeCurrentPath)
{
ObserveCurrentDispatcherSubmission(
visibleInstanceCount,
immediateInstanceCount,
deferTransparent,
cameraWorldPosition);
}
}
internal static CurrentRenderDispatcherSubmission
CreateDispatcherSubmission(
int visibleInstanceCount,