fix #429: allocation-free shadow topology rebuild + churn-frame pipelining
The directional-shadow topology rebuilt on every streaming-churn frame and was the measured body of the run-hitch stalls (701 of 708 baseline stalls alloc-correlated): - The draw sort comparer's enum-vs-enum CompareTo bound to Enum.CompareTo(object) and boxed BOTH operands on every comparison — a constant ~38.9 MB of garbage per topology rebuild (~4M boxes), handing the GC a forced gen0 collection mid-frame. The full ~100k-draw sort is replaced outright: draws hash-group by exact DrawKey in one O(n) pass over retained chained-index arrays, and only the few-thousand DISTINCT group keys sort (order-preserving packed material|cull|firstIndex|baseVertex + count|slot|layer|foliage keys, first-appearance tie-break) — bit-identical emission order to the old stable sort, near-zero allocation, and no per-draw comparisons at all. - The caster frame sorts 4-byte indices keyed on SortKey.Value instead of shuffling multi-hundred-byte records through a boxing comparer. - Owner-approved pipelining: on a frame whose shadow inputs just changed (the same frame already paying frame-view/landscape rebuilds), the caster-frame and prepared-draws topology rebuilds defer to the next quieter frame, capped at two consecutive deferrals — inside the GPU fence depth, so retained draws never reference a released arena range. First build, generation change, caster BuildSequence change, and journal overflow force the immediate path; deferred refreshes skip identity-mismatched journal rows. Owner-accepted in both presentation modes: stall frames 5.8/s -> ~0.45/s uncapped (0.49/s capped), median stall 20.3 -> 13.7 ms, >25 ms frames near zero, 275 fps uncapped baseline restored. Allocation gate: a warmed topology rebuild must allocate <2 KiB (DirectionalShadowPreparedDrawTests). docs/ISSUES.md carries the full evidence trail; the residual content-proportional rebuild milliseconds are filed as the incremental-topology successor, and the pre-existing town-view scaling latch is filed as #432. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
parent
4873c10673
commit
ad69558908
6 changed files with 652 additions and 69 deletions
|
|
@ -197,6 +197,10 @@ internal sealed class AtmosphericPostProcessGraph :
|
|||
private readonly bool _fuseLowPostProcess;
|
||||
private readonly AtmosphericCpuStageProfiler? _cpuStageProfiler;
|
||||
private readonly DirectionalShadowCasterFrame _shadowCasters = new();
|
||||
// #429 owner-approved pipelining state — see RenderDirectionalShadows.
|
||||
private ulong _lastObservedSceneShadowRevision;
|
||||
private long _lastObservedAvailabilityVersion;
|
||||
private int _shadowRebuildDeferrals;
|
||||
private TargetSet? _targets;
|
||||
private AtmosphericFrameInputs _lastInputs;
|
||||
private DirectionalSunShadowDiagnostics _lastShadowDiagnostics;
|
||||
|
|
@ -395,7 +399,33 @@ internal sealed class AtmosphericPostProcessGraph :
|
|||
bool measureCpuStages = _cpuStageProfiler is not null
|
||||
&& AtmosphericCpuStageProfiler.ShouldMeasure(frame.Serial);
|
||||
long stageStarted = measureCpuStages ? Stopwatch.GetTimestamp() : 0L;
|
||||
_shadowCasters.Build(in scene);
|
||||
// #429 owner-approved pipelining: on a frame where the shadow inputs
|
||||
// just CHANGED (streaming publish churn — the same frame already pays
|
||||
// the frame-view and landscape rebuilds), keep the retained shadow
|
||||
// topology and let the rebuild land on the next quieter frame. Capped
|
||||
// at two consecutive deferrals: the GPU frame fence is deeper than
|
||||
// that, so retained prepared draws can never reference an arena range
|
||||
// that was released AND reused while deferred. A caster rebuild the
|
||||
// frame forces anyway (first build, generation change, journal
|
||||
// overflow) re-enables the draws rebuild in the same frame — the
|
||||
// prepared draws must never index a caster frame they were not built
|
||||
// from.
|
||||
ulong sceneShadowRevision = scene.DirectionalShadowTopologyRevision;
|
||||
long availabilityVersion = worldMeshes.DirectionalShadowAvailabilityVersion;
|
||||
bool shadowInputsChanged =
|
||||
sceneShadowRevision != _lastObservedSceneShadowRevision
|
||||
|| availabilityVersion != _lastObservedAvailabilityVersion;
|
||||
_lastObservedSceneShadowRevision = sceneShadowRevision;
|
||||
_lastObservedAvailabilityVersion = availabilityVersion;
|
||||
bool allowTopologyRebuild =
|
||||
!shadowInputsChanged || _shadowRebuildDeferrals >= 2;
|
||||
ulong casterSequenceBefore = _shadowCasters.BuildSequence;
|
||||
_shadowCasters.Build(in scene, allowTopologyRebuild);
|
||||
if (_shadowCasters.BuildSequence != casterSequenceBefore)
|
||||
allowTopologyRebuild = true;
|
||||
_shadowRebuildDeferrals = allowTopologyRebuild
|
||||
? 0
|
||||
: _shadowRebuildDeferrals + 1;
|
||||
long casterBuildFinished = measureCpuStages ? Stopwatch.GetTimestamp() : 0L;
|
||||
AuthoredCelestialShadowSource source = world.CelestialShadowSource;
|
||||
var environment = new DirectionalShadowEnvironmentInput(
|
||||
|
|
@ -433,7 +463,8 @@ internal sealed class AtmosphericPostProcessGraph :
|
|||
Preset.Semantic,
|
||||
frame.Serial),
|
||||
MeasureCpuStages: measureCpuStages,
|
||||
AtmosphericFrame: shadowAtmosphericFrame);
|
||||
AtmosphericFrame: shadowAtmosphericFrame,
|
||||
AllowTopologyRebuild: allowTopologyRebuild);
|
||||
long environmentFinished = measureCpuStages ? Stopwatch.GetTimestamp() : 0L;
|
||||
_lastShadowCasterCount = _shadowCasters.Stats.Accepted;
|
||||
_lastShadowClassificationCalls = _shadowCasters.Stats.TopologyRebuilt ? 1 : 0;
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue