Skip to content

Commit b6e8e8b

Browse files
jcelerierclaude
andcommitted
gfx: ISF/CSF shader port-routing + codegen coherence (PR #2050)
model/renderer port drift for storage/image inputs and synthesized-int uniform shift across visitors, renderer port-advance and GLSL codegen (filter#2,3,11, avnd#6,7); ShaderSource hash/== (filter#4); save-without-document guard (filter#8); CSF oversized-workgroup continue + per-pass barrier (rhi#10,15). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
1 parent 56544c1 commit b6e8e8b

7 files changed

Lines changed: 237 additions & 67 deletions

File tree

src/plugins/score-plugin-gfx/3rdparty/libisf/src/isf.cpp

Lines changed: 49 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -4041,11 +4041,41 @@ void parser::parse_raw_raster_pipeline()
40414041
uniforms += "layout(std140, binding = 2) uniform material_t {\n";
40424042
for(const isf::input& val : d.inputs)
40434043
{
4044-
// Storage buffers / storage images / geometry inputs / UBOs declared
4045-
// separately after samplers — skip them here.
4046-
if(ossia::get_if<isf::storage_input>(&val.data)
4047-
|| ossia::get_if<isf::csf_image_input>(&val.data)
4048-
|| ossia::get_if<isf::geometry_input>(&val.data)
4044+
// Storage buffers / storage images / geometry inputs / UBOs are declared
4045+
// separately after samplers. BUT their synthesized host-side size ints
4046+
// (storage flex-array size, geometry $USER counts) ARE packed into this
4047+
// material blob, so they must be declared here too — otherwise every
4048+
// uniform after them reads shifted. Mirrors the CSF Params block.
4049+
if(auto* storage = ossia::get_if<isf::storage_input>(&val.data))
4050+
{
4051+
if(storage->access.find("write") != std::string::npos
4052+
&& !storage->layout.empty()
4053+
&& storage->layout.back().type.find("[]") != std::string::npos)
4054+
{
4055+
num_uniform++;
4056+
uniforms += "int " + val.name + "_size;\n";
4057+
globalvars += "int " + val.name + "_size = isf_material_uniforms."
4058+
+ val.name + "_size;\n";
4059+
}
4060+
continue;
4061+
}
4062+
if(auto* geo = ossia::get_if<isf::geometry_input>(&val.data))
4063+
{
4064+
auto emit_synth_int = [&](const std::string& nm) {
4065+
num_uniform++;
4066+
uniforms += "int " + nm + ";\n";
4067+
globalvars += "int " + nm + " = isf_material_uniforms." + nm + ";\n";
4068+
};
4069+
if(geo->vertex_count.find("$USER") != std::string::npos)
4070+
emit_synth_int(val.name + "_vertex_count");
4071+
if(geo->instance_count.find("$USER") != std::string::npos)
4072+
emit_synth_int(val.name + "_instance_count");
4073+
for(const auto& aux : geo->auxiliary)
4074+
if(aux.size.find("$USER") != std::string::npos)
4075+
emit_synth_int(val.name + "_" + aux.name + "_size");
4076+
continue;
4077+
}
4078+
if(ossia::get_if<isf::csf_image_input>(&val.data)
40494079
|| ossia::get_if<isf::uniform_input>(&val.data))
40504080
continue;
40514081

@@ -5792,6 +5822,20 @@ void parser::parse_csf()
57925822
}
57935823
}
57945824
}
5825+
else if(auto* storage = ossia::get_if<storage_input>(&inp.data))
5826+
{
5827+
// A writable storage buffer whose LAYOUT ends in a flexible-array
5828+
// member gets a synthesized host-side size int (see ISFVisitors /
5829+
// RenderedCSFNode). Declare it here so this std140 block matches the
5830+
// packed material blob; otherwise every uniform after it reads shifted.
5831+
if(storage->access.find("write") != std::string::npos
5832+
&& !storage->layout.empty()
5833+
&& storage->layout.back().type.find("[]") != std::string::npos)
5834+
{
5835+
k++;
5836+
material_block += " int " + inp.name + "_size;\n";
5837+
}
5838+
}
57955839
}
57965840

57975841
material_block += "};\n\n";

src/plugins/score-plugin-gfx/Gfx/CSF/Process.cpp

Lines changed: 28 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -485,15 +485,23 @@ void Model::setupCSF(const isf::descriptor& desc)
485485
QString::fromStdString(input.name), Id<Process::Port>(output_i++), &self);
486486
self.m_outlets.push_back(port);
487487

488-
auto size_inl = new Process::IntSpinBox{
489-
1,
490-
536870911,
491-
1024,
492-
QString::fromStdString(input.name) + " size",
493-
Id<Process::Port>(input_i++),
494-
&self};
495-
self.m_inlets.push_back(size_inl);
496-
self.controlAdded(size_inl->id());
488+
// Only writable buffers whose layout ends in a flexible-array member
489+
// get a synthesized "size" inlet — this MUST match the renderer
490+
// (isf_input_port_count_vis / isf_input_port_vis) and the generated
491+
// GLSL, or every later control routes to the wrong port.
492+
if(!v.layout.empty()
493+
&& v.layout.back().type.find("[]") != std::string::npos)
494+
{
495+
auto size_inl = new Process::IntSpinBox{
496+
1,
497+
536870911,
498+
1024,
499+
QString::fromStdString(input.name) + " size",
500+
Id<Process::Port>(input_i++),
501+
&self};
502+
self.m_inlets.push_back(size_inl);
503+
self.controlAdded(size_inl->id());
504+
}
497505
}
498506
}
499507

@@ -651,9 +659,17 @@ Process::Descriptor ProcessFactory::descriptor(QString) const noexcept
651659
template <>
652660
void DataStreamReader::read(const Gfx::CSF::Model& proc)
653661
{
654-
auto& ctx = score::IDocument::documentContext(proc);
655-
m_stream << proc.m_compute
656-
<< score::relativizeFilePath(proc.m_scriptPath, ctx);
662+
// documentContext() SCORE_ASSERTs when the model isn't in a document
663+
// (e.g. saving a template / copy). Only relativize against the document
664+
// when there's an actual script path to relativize — mirrors the
665+
// JSON/load guards. The empty case writes an empty path verbatim.
666+
QString relativeScriptPath;
667+
if(!proc.m_scriptPath.isEmpty())
668+
{
669+
auto& ctx = score::IDocument::documentContext(proc);
670+
relativeScriptPath = score::relativizeFilePath(proc.m_scriptPath, ctx);
671+
}
672+
m_stream << proc.m_compute << relativeScriptPath;
657673
readPorts(*this, proc.m_inlets, proc.m_outlets);
658674

659675
insertDelimiter();

src/plugins/score-plugin-gfx/Gfx/Filter/Process.cpp

Lines changed: 11 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -209,9 +209,17 @@ void DataStreamWriter::write(Gfx::ShaderSource& p)
209209
template <>
210210
void DataStreamReader::read(const Gfx::Filter::Model& proc)
211211
{
212-
auto& ctx = score::IDocument::documentContext(proc);
213-
m_stream << proc.m_program
214-
<< score::relativizeFilePath(proc.m_scriptPath, ctx);
212+
// documentContext() SCORE_ASSERTs when the model isn't in a document
213+
// (e.g. saving a template / copy). Only relativize against the document
214+
// when there's an actual script path to relativize — mirrors the
215+
// JSON/load guards. The empty case writes an empty path verbatim.
216+
QString relativeScriptPath;
217+
if(!proc.m_scriptPath.isEmpty())
218+
{
219+
auto& ctx = score::IDocument::documentContext(proc);
220+
relativeScriptPath = score::relativizeFilePath(proc.m_scriptPath, ctx);
221+
}
222+
m_stream << proc.m_program << relativeScriptPath;
215223

216224
readPorts(*this, proc.m_inlets, proc.m_outlets);
217225

src/plugins/score-plugin-gfx/Gfx/Graph/ISFVisitors.hpp

Lines changed: 11 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -199,7 +199,13 @@ struct isf_input_size_vis
199199
// CSF-specific input handlers
200200
void operator()(const isf::storage_input& in) noexcept
201201
{
202-
if(in.access.contains("write"))
202+
// Must match what isf_input_port_vis (ISFNode.cpp) actually writes into the
203+
// blob — and the synthesized "size" int it creates: ONLY a writable buffer
204+
// whose layout ends in a flexible-array member. Reserving for every write
205+
// buffer over-allocated the UBO (harmless, but desynced from the port
206+
// visitor and the generated GLSL Params/material_t block).
207+
if(in.access.contains("write") && !in.layout.empty()
208+
&& in.layout.back().type.find("[]") != std::string::npos)
203209
{
204210
(*this)(isf::long_input{});
205211
}
@@ -215,11 +221,10 @@ struct isf_input_size_vis
215221

216222
void operator()(const isf::csf_image_input& in) noexcept
217223
{
218-
if(in.access.contains("write"))
219-
{
220-
(*this)(isf::point2d_input{});
221-
(*this)(isf::long_input{});
222-
}
224+
// isf_input_port_vis does NOT write anything into the material blob for
225+
// write csf_image inputs (its point2d/long synthesis is commented out), so
226+
// reserve nothing here — keep the size visitor and the port visitor (and
227+
// hence the generated uniform block) in agreement.
223228
}
224229

225230
void operator()(const isf::geometry_input& in) noexcept

src/plugins/score-plugin-gfx/Gfx/Graph/RenderedCSFNode.cpp

Lines changed: 29 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -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

Comments
 (0)