diff --git a/src/AcDream.App/Rendering/Wb/WbDrawDispatcher.OrderedStream.cs b/src/AcDream.App/Rendering/Wb/WbDrawDispatcher.OrderedStream.cs index dd70fdf1..468323fa 100644 --- a/src/AcDream.App/Rendering/Wb/WbDrawDispatcher.OrderedStream.cs +++ b/src/AcDream.App/Rendering/Wb/WbDrawDispatcher.OrderedStream.cs @@ -297,7 +297,6 @@ public sealed unsafe partial class WbDrawDispatcher private OrderedDrawStream? _orderedStream; private List _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 /// /// Draws commands [firstCommand, firstCommand + commandCount) of /// the payload the most recent call - /// uploaded. The FIRST call in a frame also binds the pipeline, push - /// constants, and all nine per-instance sections — - /// 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 SubmitOrderedStream 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 + /// (what used to be ~40 full SubmitOrderedStream uploads per /// frame at a town becomes one bind plus ~40 cheap /// 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!; diff --git a/tests/AcDream.App.Tests/Rendering/Walk/OrderPreservingSubmitterTests.cs b/tests/AcDream.App.Tests/Rendering/Walk/OrderPreservingSubmitterTests.cs index 2f69d583..1108d0e3 100644 --- a/tests/AcDream.App.Tests/Rendering/Walk/OrderPreservingSubmitterTests.cs +++ b/tests/AcDream.App.Tests/Rendering/Walk/OrderPreservingSubmitterTests.cs @@ -403,8 +403,15 @@ public sealed class OrderPreservingSubmitterTests /// (the old SubmitOrderedStream rebound everything on every call). /// [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().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)); }