feat(render): Campaign V slice V6i-3 commit 2 — the Vulkan frame gets a world pass, and descriptors bind at draw time
Two structural prerequisites for the world renderers' submission arms. Both are in Gpu/Vk only; the GL backend executes not one changed statement, and the frame this commit produces is bit-identical to the one before it. 1. The clear merges into the world pass (plan §5.5.12 item 5). V6h gave the clear phase a backbuffer pass of its own: clear, resolve, close. Under MSAA that is a trap for whatever comes next. A multisampled backbuffer pass renders into a scratch image and RESOLVES it into the acquired swapchain image, and the scratch's store op is DONT_CARE — so a world pass that followed and declared Load would load undefined contents and lose the clear entirely. The world renderers cannot work around it by each opening their own pass, for the same reason: every pass after the first would load a discarded scratch. So the clear phase now computes the same RenderFrameFoundation from the same world clock and weather owners and publishes only the COLOUR, through VulkanBackbufferClearState; VulkanWorldScenePhase opens the one backbuffer pass and clears as its load op, with Store=Resolve when the backbuffer is multisampled. That makes it the frame's one clear and its one resolve. The retained UI's pass is single-sampled and targets the swapchain image directly, so it composites over the resolved result exactly as it did. The clear stays unconditional because the frame graph makes it so rather than because anything asserts it: RenderFrameOrchestrator runs resource preparation, then the world phase, then private presentation, with no branch between. A frame with no world still opens the pass and leaves a cleared backbuffer — which is precisely the frame captured below, since nothing draws into the pass yet. 2. Descriptor sets bind at DRAW time, not at bind time. V6i-1 derives a descriptor-set scope from the descriptor state itself, which is what closes §5.5.8's one-binding-two-buffers hazard. But the encoder issued vkCmdBindDescriptorSets from inside BindStorageBuffer/BindUniformBuffer, so the arena resolved after EVERY bind. For the retained UI's one or two binds that is free. For a world renderer binding ten buffers it materialises up to ten scopes per draw — nine of them PARTIAL states no draw ever uses, each claiming a real descriptor-set pair out of a fixed-size pool and each paying a full round of vkUpdateDescriptorSets. Recording the state and resolving it once, where the draw needs it, yields exactly one scope per renderer, which is what the arena was designed to produce. It is legal because descriptor-set binding is independent of pipeline binding when the layouts are compatible, and acdream has ONE pipeline layout by design (§4.4) — the same property that lets a bucketed pass change pipeline for free. The pass still opens with all three sets bound, which is V6h's fix for VUID-vkCmdDraw-None-08600 and stays exactly as it was. Gates. Release build green. App tests 4,112 passed / 3 skipped, unchanged from commit 1. Strict GL offline pixel gate against commit 1: 3.73e-05 (21 differing pixels of 563,200), inside the documented 9-31 px control band — expected, since no GL file is touched. One offline Vulkan run with VK_LAYER_KHRONOS_validation proven inserted by the loader: zero validation errors, zero warnings, and no [shutdown] diagnostic on either stream. The captured Vulkan frame is compared against commit 1's rather than merely eyeballed: 0 differing pixels of 921,600, maximum channel delta 0 — bit-identical across the merge, which is the strongest available evidence that moving the clear into the world pass changed nothing about what is drawn. What this does NOT do: draw a world. See the report for the enumerated remainder. No divergence-register row: no retail-facing behaviour changes. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
parent
fe8abacfc6
commit
887de4aec2
3 changed files with 138 additions and 41 deletions
|
|
@ -250,6 +250,11 @@ internal sealed class FrameRootCompositionPhase
|
|||
RenderFrameGlStateController? renderFrameGlState = gl is null
|
||||
? null
|
||||
: new RenderFrameGlStateController(new SilkRenderFrameGlStateApi(gl));
|
||||
// Campaign V slice V6i-3: on Vulkan the frame's clear is a load op of the
|
||||
// world pass rather than a pass of its own, so the two phases share this
|
||||
// one value. See VulkanWorldScenePhase for why the merge is required
|
||||
// rather than tidier.
|
||||
var vulkanClear = new AcDream.App.Rendering.Gpu.Vk.VulkanBackbufferClearState();
|
||||
var renderFrameLivePreparation =
|
||||
new RuntimeRenderFrameLivePreparation(
|
||||
foundation.TextureCache,
|
||||
|
|
@ -275,12 +280,11 @@ internal sealed class FrameRootCompositionPhase
|
|||
"The GL frame root requires the GL state tripwire."),
|
||||
renderFrameGlState!)
|
||||
: new AcDream.App.Rendering.Gpu.Vk.VulkanRenderFrameClearPhase(
|
||||
host.GpuFrameLifetime,
|
||||
d.WorldTime,
|
||||
d.Weather,
|
||||
teleportRenderState,
|
||||
d.ParticleVisibility,
|
||||
() => d.Graphics.Vulkan?.SampleCount ?? 1);
|
||||
vulkanClear);
|
||||
var renderFrameResources = new RenderFrameResourceController(
|
||||
host.FrameSlots,
|
||||
new RuntimeRenderFrameBeginResources(
|
||||
|
|
@ -304,7 +308,10 @@ internal sealed class FrameRootCompositionPhase
|
|||
d.EffectPoses,
|
||||
live.EntityEffects);
|
||||
IWorldSceneFramePhase worldSceneRenderer =
|
||||
AcDream.App.Rendering.Gpu.Vk.VulkanWorldScenePhase.Instance;
|
||||
new AcDream.App.Rendering.Gpu.Vk.VulkanWorldScenePhase(
|
||||
host.GpuFrameLifetime,
|
||||
vulkanClear,
|
||||
() => d.Graphics.Vulkan?.SampleCount ?? 1);
|
||||
CurrentRenderSceneOracle? currentRenderSceneOracle =
|
||||
interaction.RetainedUi?.Screenshots is not null
|
||||
&& d.Options.AutomationArtifactDirectory is not null
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue