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 OrderedDrawStream? _orderedStream;
|
||||||
private List<OrderedMergeRun> _orderedRuns = new();
|
private List<OrderedMergeRun> _orderedRuns = new();
|
||||||
private int _orderedPreparedCount;
|
private int _orderedPreparedCount;
|
||||||
private bool _orderedSectionsBound;
|
|
||||||
// Caller-supplied, exactly like SubmitOrderedStream's own frame/encoder
|
// Caller-supplied, exactly like SubmitOrderedStream's own frame/encoder
|
||||||
// parameters were (see this file's type doc comment: the walk submitter
|
// parameters were (see this file's type doc comment: the walk submitter
|
||||||
// draws into whatever pass its caller has open, never pulled from
|
// draws into whatever pass its caller has open, never pulled from
|
||||||
|
|
@ -355,7 +354,6 @@ public sealed unsafe partial class WbDrawDispatcher
|
||||||
|
|
||||||
_orderedStream = stream;
|
_orderedStream = stream;
|
||||||
_orderedFrame = frame;
|
_orderedFrame = frame;
|
||||||
_orderedSectionsBound = false;
|
|
||||||
_orderedPreparedCount = 0;
|
_orderedPreparedCount = 0;
|
||||||
|
|
||||||
// Fail loud before any GPU work: a PortalPunch command has no
|
// Fail loud before any GPU work: a PortalPunch command has no
|
||||||
|
|
@ -443,11 +441,12 @@ public sealed unsafe partial class WbDrawDispatcher
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Draws commands <c>[firstCommand, firstCommand + commandCount)</c> of
|
/// Draws commands <c>[firstCommand, firstCommand + commandCount)</c> of
|
||||||
/// the payload the most recent <see cref="PrepareOrderedStream"/> call
|
/// the payload the most recent <see cref="PrepareOrderedStream"/> call
|
||||||
/// uploaded. The FIRST call in a frame also binds the pipeline, push
|
/// uploaded. Every call re-binds the pipeline, push constants, and the
|
||||||
/// constants, and all nine per-instance sections — <see cref="_orderedSectionsBound"/>
|
/// per-instance sections — leaf draws and alpha flushes between ranges
|
||||||
/// gates that so every later call in the same frame is just the merge-run
|
/// rebind the same set-0 slots to THEIR buffers, so a latched skip draws
|
||||||
/// walk-and-draw loop, never a rebind (the whole point of the FW3.4a
|
/// against foreign sections (the dense-Arwic device-lost). The FW3.4a
|
||||||
/// split: what used to be ~40 full <c>SubmitOrderedStream</c> rebinds per
|
/// 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
|
/// frame at a town becomes one bind plus ~40 cheap
|
||||||
/// <see cref="DrawIndirectRangeRhi"/> calls). Section binds survive
|
/// <see cref="DrawIndirectRangeRhi"/> calls). Section binds survive
|
||||||
/// pipeline switches (every mesh pipeline shares one layout — the same
|
/// pipeline switches (every mesh pipeline shares one layout — the same
|
||||||
|
|
@ -505,18 +504,23 @@ public sealed unsafe partial class WbDrawDispatcher
|
||||||
ParamB = 0f,
|
ParamB = 0f,
|
||||||
};
|
};
|
||||||
|
|
||||||
if (!_orderedSectionsBound)
|
|
||||||
{
|
{
|
||||||
IGpuFrame frame = _orderedFrame
|
IGpuFrame frame = _orderedFrame
|
||||||
?? throw new InvalidOperationException(
|
?? 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.");
|
+ "scene-lighting sections against — PrepareOrderedStream must run first.");
|
||||||
|
|
||||||
// Bind the opaque variant first so the storage/uniform binds
|
// Bind the SECTIONS on EVERY range call — never latch them
|
||||||
// below land on a live program (SubmitRhi's own rationale) —
|
// across calls. Between ordered ranges the walk's leaf draws run
|
||||||
// every mesh pipeline shares one layout, so these bindings
|
// (terrain, cell shells, sky, punch fans) and RetailAlphaQueue
|
||||||
// survive every per-run pipeline switch below, across every
|
// flushes rebind the SAME set-0 storage bindings to THEIR
|
||||||
// DrawOrderedRange call this frame.
|
// 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);
|
BindPipelineWithMesh(encoder, pipelines.Opaque, global);
|
||||||
encoder.SetPushConstants(in pushConstants);
|
encoder.SetPushConstants(in pushConstants);
|
||||||
BindSection(encoder, GpuBindingModel.StorageInstances, _orderedInstances);
|
BindSection(encoder, GpuBindingModel.StorageInstances, _orderedInstances);
|
||||||
|
|
@ -534,8 +538,6 @@ public sealed unsafe partial class WbDrawDispatcher
|
||||||
encoder, _scope!.Sections, frame);
|
encoder, _scope!.Sections, frame);
|
||||||
AcDream.App.Rendering.WorldFrameSectionBinding.BindSceneLighting(
|
AcDream.App.Rendering.WorldFrameSectionBinding.BindSceneLighting(
|
||||||
encoder, _scope!.Sections, frame);
|
encoder, _scope!.Sections, frame);
|
||||||
|
|
||||||
_orderedSectionsBound = true;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
IGpuBuffer commandBuffer = _orderedCommands.Buffer!;
|
IGpuBuffer commandBuffer = _orderedCommands.Buffer!;
|
||||||
|
|
|
||||||
|
|
@ -403,8 +403,15 @@ public sealed class OrderPreservingSubmitterTests
|
||||||
/// (the old SubmitOrderedStream rebound everything on every call).
|
/// (the old SubmitOrderedStream rebound everything on every call).
|
||||||
/// </summary>
|
/// </summary>
|
||||||
[Fact]
|
[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 var fx = new DispatcherFixture();
|
||||||
using DrawScope draw = fx.BeginDraw();
|
using DrawScope draw = fx.BeginDraw();
|
||||||
|
|
||||||
|
|
@ -420,9 +427,8 @@ public sealed class OrderPreservingSubmitterTests
|
||||||
fx.Dispatcher.DrawOrderedRange(draw.Pass, 1, 1);
|
fx.Dispatcher.DrawOrderedRange(draw.Pass, 1, 1);
|
||||||
int boundAfterSecond = fx.Device.Calls.OfType<GpuRecordedStorageBind>().Count();
|
int boundAfterSecond = fx.Device.Calls.OfType<GpuRecordedStorageBind>().Count();
|
||||||
|
|
||||||
Assert.Equal(boundAfterFirst, boundAfterSecond);
|
Assert.Equal(boundAfterFirst * 2, boundAfterSecond);
|
||||||
// Both commands still drew — the bind-once optimization changed
|
// Both commands drew; the rebinds changed nothing about coverage.
|
||||||
// nothing about draw coverage.
|
|
||||||
Assert.Equal([(0, 1), (1, 1)], DecodeDrawRanges(fx.Device));
|
Assert.Equal([(0, 1), (1, 1)], DecodeDrawRanges(fx.Device));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue