fix(render) Campaign FW3.4a: rebind sections per DrawOrderedRange - the device-lost fix

The dense-Arwic re-measure crashed with VK_ERROR_DEVICE_LOST: between
ordered ranges the walk leaf draws (terrain, shells, sky, punch fans)
and RetailAlphaQueue flushes rebind the SAME set-0 storage slots to
their own sections, so the bind-once latch made the next range draw
against foreign buffers - out-of-bounds instance reads and a GPU
fault. Sections now re-bind on every DrawOrderedRange call, exactly
like the proven DrawPreparedAlphaBatchRhi; the once-per-frame ring
WRITES in PrepareOrderedStream (the actual measured cost) are
unchanged. The bind-once referee test flips to assert per-range
rebinds with unchanged draw coverage.

Suites: full Release build 0 warnings; hermetic 6,758/0.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
Erik 2026-08-30 17:07:29 +02:00
parent 212f5a12e5
commit 1da178d9ba
2 changed files with 28 additions and 20 deletions

View file

@ -297,7 +297,6 @@ public sealed unsafe partial class WbDrawDispatcher
private OrderedDrawStream? _orderedStream;
private List<OrderedMergeRun> _orderedRuns = new();
private int _orderedPreparedCount;
private bool _orderedSectionsBound;
// Caller-supplied, exactly like SubmitOrderedStream's own frame/encoder
// parameters were (see this file's type doc comment: the walk submitter
// draws into whatever pass its caller has open, never pulled from
@ -355,7 +354,6 @@ public sealed unsafe partial class WbDrawDispatcher
_orderedStream = stream;
_orderedFrame = frame;
_orderedSectionsBound = false;
_orderedPreparedCount = 0;
// Fail loud before any GPU work: a PortalPunch command has no
@ -443,11 +441,12 @@ public sealed unsafe partial class WbDrawDispatcher
/// <summary>
/// Draws commands <c>[firstCommand, firstCommand + commandCount)</c> of
/// the payload the most recent <see cref="PrepareOrderedStream"/> call
/// uploaded. The FIRST call in a frame also binds the pipeline, push
/// constants, and all nine per-instance sections — <see cref="_orderedSectionsBound"/>
/// gates that so every later call in the same frame is just the merge-run
/// walk-and-draw loop, never a rebind (the whole point of the FW3.4a
/// split: what used to be ~40 full <c>SubmitOrderedStream</c> rebinds per
/// uploaded. Every call re-binds the pipeline, push constants, and the
/// per-instance sections — leaf draws and alpha flushes between ranges
/// rebind the same set-0 slots to THEIR buffers, so a latched skip draws
/// against foreign sections (the dense-Arwic device-lost). The FW3.4a
/// win is the once-per-frame ring WRITES in <see cref="PrepareOrderedStream"/>
/// (what used to be ~40 full <c>SubmitOrderedStream</c> uploads per
/// frame at a town becomes one bind plus ~40 cheap
/// <see cref="DrawIndirectRangeRhi"/> calls). Section binds survive
/// pipeline switches (every mesh pipeline shares one layout — the same
@ -505,18 +504,23 @@ public sealed unsafe partial class WbDrawDispatcher
ParamB = 0f,
};
if (!_orderedSectionsBound)
{
IGpuFrame frame = _orderedFrame
?? throw new InvalidOperationException(
"DrawOrderedRange's first call this frame has no frame to bind clip-region/"
"DrawOrderedRange has no frame to bind clip-region/"
+ "scene-lighting sections against — PrepareOrderedStream must run first.");
// Bind the opaque variant first so the storage/uniform binds
// below land on a live program (SubmitRhi's own rationale) —
// every mesh pipeline shares one layout, so these bindings
// survive every per-run pipeline switch below, across every
// DrawOrderedRange call this frame.
// Bind the SECTIONS on EVERY range call — never latch them
// across calls. Between ordered ranges the walk's leaf draws run
// (terrain, cell shells, sky, punch fans) and RetailAlphaQueue
// flushes rebind the SAME set-0 storage bindings to THEIR
// sections; a latched skip here draws the next range against the
// alpha path's buffers — out-of-bounds instance reads and a
// VK_ERROR_DEVICE_LOST at dense Arwic (the FW3.4a re-measure
// crash). The expensive part — the ring WRITES — already happens
// once per frame in PrepareOrderedStream; these are descriptor
// binds only, the same per-batch rebinding the proven
// DrawPreparedAlphaBatchRhi does for the same reason.
BindPipelineWithMesh(encoder, pipelines.Opaque, global);
encoder.SetPushConstants(in pushConstants);
BindSection(encoder, GpuBindingModel.StorageInstances, _orderedInstances);
@ -534,8 +538,6 @@ public sealed unsafe partial class WbDrawDispatcher
encoder, _scope!.Sections, frame);
AcDream.App.Rendering.WorldFrameSectionBinding.BindSceneLighting(
encoder, _scope!.Sections, frame);
_orderedSectionsBound = true;
}
IGpuBuffer commandBuffer = _orderedCommands.Buffer!;