perf(rendering): draw retained frame product

Make the incremental render scene the production entity source at the existing retail PView stages while retaining the accepted dispatcher upload and draw executor. Keep diagnostics consumer-gated, retain ordered indices across unchanged frames, refresh only dirty records, and preserve exact mesh-load, selection, alpha, lighting, and route lifecycle semantics.
This commit is contained in:
Erik 2026-07-25 04:12:23 +02:00
parent c7d7848dd2
commit ef1d263337
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

@ -1466,41 +1466,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
@ -2041,11 +2010,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,
@ -2058,7 +2087,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,
@ -2067,8 +2096,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,
@ -2185,15 +2214,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,
@ -2404,6 +2433,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
@ -2559,6 +2589,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,