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:
parent
212f5a12e5
commit
1da178d9ba
2 changed files with 28 additions and 20 deletions
|
|
@ -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!;
|
||||
|
|
|
|||
|
|
@ -403,8 +403,15 @@ public sealed class OrderPreservingSubmitterTests
|
|||
/// (the old SubmitOrderedStream rebound everything on every call).
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public void DrawOrderedRange_SecondCallInTheSameFrame_BindsNoFurtherStorageSections()
|
||||
public void DrawOrderedRange_EveryCallRebindsTheStorageSections()
|
||||
{
|
||||
// The corrected FW3.4a contract (the dense-Arwic device-lost fix):
|
||||
// between ordered ranges the walk's leaf draws and RetailAlphaQueue
|
||||
// flushes rebind the SAME set-0 slots to THEIR sections, so every
|
||||
// DrawOrderedRange call must re-bind its own — a latched skip draws
|
||||
// the next range against foreign buffers. Only the ring WRITES are
|
||||
// once-per-frame (PrepareOrderedStream); binds repeat per range,
|
||||
// exactly like DrawPreparedAlphaBatchRhi.
|
||||
using var fx = new DispatcherFixture();
|
||||
using DrawScope draw = fx.BeginDraw();
|
||||
|
||||
|
|
@ -420,9 +427,8 @@ public sealed class OrderPreservingSubmitterTests
|
|||
fx.Dispatcher.DrawOrderedRange(draw.Pass, 1, 1);
|
||||
int boundAfterSecond = fx.Device.Calls.OfType<GpuRecordedStorageBind>().Count();
|
||||
|
||||
Assert.Equal(boundAfterFirst, boundAfterSecond);
|
||||
// Both commands still drew — the bind-once optimization changed
|
||||
// nothing about draw coverage.
|
||||
Assert.Equal(boundAfterFirst * 2, boundAfterSecond);
|
||||
// Both commands drew; the rebinds changed nothing about coverage.
|
||||
Assert.Equal([(0, 1), (1, 1)], DecodeDrawRanges(fx.Device));
|
||||
}
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue