@@ -2970,7 +2970,7 @@ void RenderedCSFNode::buildComputeSrbBindings(
29702970 for (const auto & input : n.m_descriptor .inputs )
29712971 {
29722972 // Storage buffers
2973- if (ossia::get_if<isf::storage_input>(&input.data ))
2973+ if (auto * storage_in = ossia::get_if<isf::storage_input>(&input.data ))
29742974 {
29752975 // Find the corresponding storage buffer
29762976 auto it = std::find_if (m_storageBuffers.begin (), m_storageBuffers.end (),
@@ -3026,6 +3026,20 @@ void RenderedCSFNode::buildComputeSrbBindings(
30263026 << QString::fromStdString (input.name );
30273027 bindingIndex++;
30283028 }
3029+
3030+ // Write-access buffers whose layout ends in a flexible-array member get a
3031+ // synthesized "size" INPUT port on the model (setupCSF / isf_input_port_-
3032+ // vis). The read_only branch advanced input_port_index for its own inlet,
3033+ // but the write branches above only touched output_port_index — so this
3034+ // sizing inlet was never skipped and every later storage input resolved
3035+ // the wrong port (its upstream buffer silently never bound). The geometry
3036+ // branch already does the equivalent for its $USER ports. Advance here
3037+ // under the SAME flex-array condition used everywhere else.
3038+ if (storage_in->access .contains (" write" ) && !storage_in->layout .empty ()
3039+ && storage_in->layout .back ().type .find (" []" ) != std::string::npos)
3040+ {
3041+ input_port_index++;
3042+ }
30293043 }
30303044 // Regular textures (sampled)
30313045 else if (ossia::get_if<isf::texture_input>(&input.data ))
@@ -4653,11 +4667,14 @@ void RenderedCSFNode::runInitialPasses(
46534667
46544668 if (totalWorkgroups > maxWorkgroups * maxWorkgroups * maxWorkgroups)
46554669 {
4656- // Workgroup count overflow: skip this pass. We haven't yet
4670+ // Workgroup count overflow: skip THIS pass only . We haven't yet
46574671 // opened a compute pass at this point (the begin/end for this
46584672 // dispatch is now hoisted *after* the size calculation), so
4659- // there is nothing to close — just bail to the next pass.
4660- return ;
4673+ // there is nothing to close — continue to the next pass. Using
4674+ // `return` here aborted every remaining pass and desynced the
4675+ // ping-pong buffer swaps; mirror the dispatch(0,0,0) guard below
4676+ // which already uses `continue`.
4677+ continue ;
46614678 }
46624679 if (totalWorkgroups > maxWorkgroups * maxWorkgroups)
46634680 {
@@ -4715,22 +4732,20 @@ void RenderedCSFNode::runInitialPasses(
47154732 pass.processUBO , 0 , sizeof (ProcessUBO), &n.standardUBO );
47164733 }
47174734
4718- // Begin compute pass with ExternalContent flag so we can insert
4719- // native memory barriers between dispatches via beginExternal/endExternal.
4720- commands.beginComputePass (res, QRhiCommandBuffer::BeginPassFlag::ExternalContent);
4735+ // Each CSF pass issues exactly ONE dispatch in its own begin/endComputePass.
4736+ // QRhi automatically inserts the compute→compute memory barrier between
4737+ // consecutive passes that touch the same SSBO/image, so the previous
4738+ // per-pass ExternalContent flag + native barrier was redundant here — and
4739+ // ExternalContent needlessly forced Vulkan secondary command buffers. The
4740+ // native-barrier path stays for the genuinely multi-dispatch scatter loop
4741+ // (above), which issues several dispatches inside a single pass.
4742+ commands.beginComputePass (res);
47214743 res = nullptr ;
47224744
47234745 commands.setComputePipeline (pass.pipeline );
47244746 commands.setShaderResources (pass.srb );
47254747 commands.dispatch (dispatchX, dispatchY, dispatchZ);
47264748
4727- // Insert a compute→compute memory barrier so that SSBO writes from
4728- // this dispatch are visible to the next dispatch. QRhi does not
4729- // insert these automatically between consecutive compute passes.
4730- commands.beginExternal ();
4731- insertComputeBarrier (*renderer.state .rhi , commands);
4732- commands.endExternal ();
4733-
47344749 commands.endComputePass ();
47354750 }
47364751
0 commit comments