Loading...
Searching...
No Matches
RenderedCSFNode.hpp
1#pragma once
2#include <Gfx/Graph/GPUBufferScatter.hpp>
3#include <Gfx/Graph/ISFNode.hpp>
4#include <Gfx/Graph/NodeRenderer.hpp>
5#include <Gfx/Graph/RenderedISFUtils.hpp>
6
7#include <ossia/detail/hash_map.hpp>
8#include <ossia/detail/small_flat_map.hpp>
9#include <ossia/detail/small_vector.hpp>
10
11namespace ossia { class math_expression; }
12
13namespace score::gfx
14{
18SCORE_PLUGIN_GFX_EXPORT
19decltype(ossia::geometry::attribute::format) csfAttributeFormat(std::string_view type) noexcept;
20
25SCORE_PLUGIN_GFX_EXPORT
27 ossia::geometry::attribute& attr, std::string_view type,
28 const isf::descriptor& d) noexcept;
29
35SCORE_PLUGIN_GFX_EXPORT
37 const ossia::geometry::attribute& upstream, std::string_view type,
38 const isf::descriptor& d);
39
40
41struct SCORE_PLUGIN_GFX_TEST_EXPORT RenderedCSFNode : score::gfx::NodeRenderer
42{
43 explicit RenderedCSFNode(const ISFNode& node) noexcept;
44
45 virtual ~RenderedCSFNode();
46
47 void updateInputTexture(const Port& input, QRhiTexture* tex, QRhiTexture* depthTex = nullptr) override;
48 void updateInputSamplerFilter(const Port& input, const RenderTargetSpecs& spec) override;
49 QRhiTexture* textureForOutput(const Port& output) override;
50
51 void init(RenderList& renderer, QRhiResourceUpdateBatch& res) override;
52 void update(RenderList& renderer, QRhiResourceUpdateBatch& res, Edge* edge) override;
53 void release(RenderList& r) override;
54
55 void initState(RenderList& renderer, QRhiResourceUpdateBatch& res) override;
56 void releaseState(RenderList& renderer) override;
57 void addOutputPass(
58 RenderList& renderer, Edge& edge, QRhiResourceUpdateBatch& res) override;
59 void removeOutputPass(RenderList& renderer, Edge& edge) override;
60 bool hasOutputPassForEdge(Edge& edge) const override;
61 void
62 addInputEdge(RenderList& renderer, Edge& edge, QRhiResourceUpdateBatch& res) override;
63 void removeInputEdge(RenderList& renderer, Edge& edge) override;
64 bool followsRenderSize() const noexcept override;
65
66 void runInitialPasses(
67 RenderList&, QRhiCommandBuffer& commands, QRhiResourceUpdateBatch*& res,
68 Edge& edge) override;
69
70 void runRenderPass(RenderList&, QRhiCommandBuffer& commands, Edge& edge) override;
71
72 std::vector<Sampler> allSamplers() const noexcept;
73
74private:
75 void initComputeSRBAndPasses(RenderList& renderer, QRhiResourceUpdateBatch& res);
76 void createComputePipeline(RenderList& renderer);
79 struct CompiledComputePipelines
80 {
81 std::vector<std::unique_ptr<QRhiComputePipeline>> owned;
82 std::vector<QRhiComputePipeline*> perPass;
83 QString source;
84 std::vector<std::vector<uint32_t>> spec;
85 };
88 bool compileComputePipelines(RenderList& renderer, CompiledComputePipelines& out);
90 void commitComputePipelines(CompiledComputePipelines& c);
91 void createGraphicsPass(const TextureRenderTarget& rt, RenderList& renderer, Edge& edge, QRhiResourceUpdateBatch& res);
92 void updateDescriptorSet(RenderList& renderer, Edge& edge);
93 void bindInputSampler(std::size_t samplerIndex, QRhiTexture* tex);
94 void prepareSelfFeedbackInputs(RenderList& renderer, QRhiResourceUpdateBatch& res);
95
96 // Expression evaluation helper
97 void registerCommonExpressionVariables(
98 ossia::math_expression& e, ossia::small_pod_vector<double, 16>& data) const;
99
100 // Upper bound on the number of doubles registerCommonExpressionVariables (+
101 // the small extra a caller adds, e.g. $USER) will emplace into the backing
102 // vector. ossia::math_expression::add_constant stores a double& INTO that
103 // vector, so the reserve MUST cover the full count: any emplace_back past
104 // capacity reallocates and dangles every previously-registered reference.
105 std::size_t expressionSymbolReserveCount() const noexcept;
106
107 // Image management
110 QSize getImageSize(const isf::csf_image_input&, QSize fallback) const noexcept;
111 QSize computeTextureSize(const isf::csf_image_input& img) const noexcept;
112
113 // Buffer management methods
114 int calculateStorageBufferSize(std::span<const isf::storage_input::layout_field> layout, int arrayCount) const;
115 BufferView createStorageBuffer(
116 RenderList& renderer, const QString& name, const QString& access, int64_t size);
117 void updateStorageBuffers(RenderList& renderer, QRhiResourceUpdateBatch& res);
118 void recreateShaderResourceBindings(RenderList& renderer, QRhiResourceUpdateBatch& res);
119
121 std::vector<QRhiBuffer*> m_srbAdoptedBuffers;
123 std::vector<QRhiBuffer*> m_srbPreviousAdoptions;
124
126 void dropSrbAdoptions(RenderList& renderer);
128 void adoptForSrb(QRhiBuffer* buf);
129
130 // Single source of truth for the CSF compute SRB binding list. Walks the
131 // descriptor's INPUTS / RESOURCES / AUXILIARIES in order and emits one
132 // QRhiShaderResourceBinding per shader binding slot. Both
133 // initComputeSRBAndPasses (init path) and recreateShaderResourceBindings
134 // (re-emit path) call this so the two paths can never drift in their
135 // emission order, indices, or fallback-on-missing-resource policy.
136 // Binding 1 (ProcessUBO) is left as a nullptr placeholder; each caller
137 // patches it per-pass. Output: appended to `bindings`.
138 void buildComputeSrbBindings(
139 RenderList& renderer, QRhiResourceUpdateBatch& res,
140 QList<QRhiShaderResourceBinding>& bindings);
141 int getArraySizeFromUI(const QString& bufferName) const;
142 QString updateShaderWithImageFormats(QString current);
143
144 // Geometry buffer management
145 void updateGeometryBindings(RenderList& renderer, QRhiResourceUpdateBatch& res);
146
147 void pushOutputGeometry(RenderList& renderer, QRhiResourceUpdateBatch& res, Edge& edge);
148 int resolveCountExpression(
149 const std::string& expr, const isf::geometry_input& geo,
150 const std::string& fieldName) const;
151 int resolveDispatchExpression(const std::string& expr) const;
152
153 BufferView bufferForOutput(const score::gfx::Port& output) override;
154
155 struct ComputePass
156 {
157 QRhiComputePipeline* pipeline{};
158 QRhiShaderResourceBindings* srb{};
159 QRhiBuffer* processUBO{};
160 // Hash of the last bindings vector applied to `srb`. Compared in
161 // recreateShaderResourceBindings to skip a destroy+setBindings+
162 // create cycle when the bindings haven't actually changed since the
163 // previous frame. 0 = "never built / unknown" — first call always
164 // rebuilds. See RenderedCSFNode.cpp recreateShaderResourceBindings.
165 size_t srbBindingsHash{0};
166 };
167
168 struct GraphicsPass
169 {
170 Pipeline pipeline;
171 QRhiSampler* outputSampler{};
172 MeshBuffers meshBuffers;
173 };
174
175 ossia::small_vector<std::pair<Edge*, ComputePass>, 2> m_computePasses;
176 ossia::small_vector<std::pair<Edge*, GraphicsPass>, 2> m_graphicsPasses;
177
178 ISFNode& n;
179
180 std::vector<Sampler> m_inputSamplers;
181 std::vector<Sampler> m_audioSamplers;
182
183 struct SelfFeedbackInput
184 {
185 std::size_t sampler{};
186 std::size_t storage{};
187 QRhiTexture* snapshot{};
188 bool filled{false};
189 };
190 std::vector<SelfFeedbackInput> m_selfFeedbackInputs;
191 AudioTextureUpload m_audioTex;
192
193 // Storage buffers for compute shaders
194 struct StorageBuffer
195 {
196 QRhiBuffer* buffer{};
197 int64_t size{};
198 int64_t offset{};
199 int64_t lastKnownSize{}; // For dynamic resizing
200 QString name;
201 QString access; // "read_only", "write_only", "read_write"
202 std::vector<isf::storage_input::layout_field> layout; // For size calculation
203 bool owned{true}; // false when buffer comes from geometry auxiliary
204 std::string buffer_usage; // "", "indirect_draw", "indirect_draw_indexed", "dispatch_args"
207 bool persistent{false};
208 QRhiBuffer* prev{};
209 };
212 bool m_auxSizeUnresolved{false};
213 std::vector<StorageBuffer> m_storageBuffers; // Contains both ins and outs
216 ossia::hash_map<std::string, QRhiBuffer*> m_uniformPlaceholders;
219 ossia::hash_map<std::string, QString> m_uniformRefusals;
220 bool m_warnedIndirectArgs{false};
221 bool m_warnedIndirectNumWorkGroups{false};
222 // Whether this node's compute shaders were built reading gl_NumWorkGroups
223 // through the NUMWORKGROUPS_ uniform: decided when they are compiled (the
224 // environment switch is read then), not again per dispatch.
225 bool m_numWorkGroupsUniform{false};
226 bool m_warnedDispatchTooLarge{false};
229 bool m_storageUnallocated{false};
230 bool m_warnedStorageUnallocated{false};
231
232
233 // Only outs, matched with index in m_storageBuffers
234 std::vector<std::pair<const score::gfx::Port*, int>> m_outStorageBuffers;
235
236 // Storage images for compute shaders
237 struct StorageImage
238 {
239 QRhiTexture* texture{};
240 QRhiTexture* read_texture{};
241 QString name;
242 QString access; // "read_only", "write_only", "read_write"
243 QRhiTexture::Format format{QRhiTexture::RGBA8};
244 bool is3D{false};
245 bool isCube{false};
246 bool persistent{false};
247 bool pending_initial_copy{false};
248 bool generate_mips{false};
249
250 // Recorded binding slots in the compute SRB so that end-of-frame
251 // swapping can call replaceTexture() without having to re-walk the
252 // descriptor layout.
253 int binding{-1};
254 int prev_binding{-1};
255 };
256 std::vector<StorageImage> m_storageImages;
257
258 // Only outs, matched with index in m_storageImages
259 std::vector<std::pair<const score::gfx::Port*, int>> m_outStorageImages;
260
261 // Geometry input bindings: SoA SSBOs created from incoming ossia::geometry
262 struct GeometryBinding
263 {
264 // One SSBO per declared attribute in the geometry_input
266 {
267 QRhiBuffer* buffer{}; // GPU SSBO for this attribute (write target / primary)
268 QRhiBuffer* read_buffer{}; // Separate read buffer for ping-pong (nullptr = use buffer for both)
271 bool read_buffer_is_snapshot{false};
272 bool warned_size_mismatch{false};
273 bool warned_borrowed_pair{false};
274 int64_t size{}; // Current buffer size in bytes
275 int64_t offset{}; // Start of the attribute data in a borrowed buffer
276 bool owned{true}; // true = we created it; false = referencing upstream gpu_buffer
277
280 QRhiBuffer* restore_source{};
281 int64_t restore_offset{};
282 int64_t restore_size{};
283 bool restore_seen{false};
284 std::string name; // e.g. "position", "velocity"
285 std::string access; // "read_only", "write_only", "read_write"
286 bool per_instance{false}; // true = sized by instance_count, false = sized by vertex_count
287 const void* lastUploadSrc{};// CPU data pointer from last upload (for dedup)
288
289 // GPU scatter state (used when format conversion is needed)
290 QRhiBuffer* scatterStaging{}; // Staging SSBO for raw CPU data
292 QRhiBuffer* scatter_source{};
293 bool scatter_seen{false};
294 int64_t scatterStagingSize{};
296 GPUBufferScatter::Params scatterParams;
297 bool scatterPending{false}; // true = needs dispatch this frame
298 };
299
300 // Structured SSBOs (or UBOs) that travel with the geometry (matched
301 // by name against ossia::geometry::auxiliary_buffer entries). The
302 // `is_uniform` flag mirrors the AUXILIARY request's kind: when true,
303 // the buffer is bound as a std140 uniform block via
304 // QRhiShaderResourceBinding::uniformBuffer; when false, as an std430
305 // SSBO via bufferLoad / bufferStore / bufferLoadStore.
307 {
310 QRhiBuffer* buffer{};
311 int64_t size{};
312 int64_t offset{};
313 bool owned{true};
316 QRhiBuffer* restore_source{};
317 int64_t restore_offset{};
318 int64_t restore_size{};
319 bool restore_seen{false};
320 bool is_uniform{false}; // true = std140 UBO, false = std430 SSBO
321 std::string name;
322 std::string access;
323 std::vector<isf::storage_input::layout_field> layout;
324 std::string size_expr; // expression for flexible array count, may contain $USER
325
328 int column_block{0};
330 std::vector<uint32_t> column_table;
331 QRhiBuffer* column_table_buffer{};
340 {
341 std::string name;
342 uint32_t byte_offset{};
343 int format{};
344 int binding{};
345 int semantic{};
346 };
347 std::vector<ColumnAttribute> column_attributes;
348 std::vector<int64_t> column_layout;
349 std::vector<std::pair<const void*, int64_t>> column_buffers;
351 bool column_from_mesh{};
352 QRhiBuffer* column_bound{};
353 };
354
355 // Auxiliary textures that travel with the geometry (resolved from
356 // ossia::geometry::auxiliary_textures by name). Either sampled
357 // (sampler*) or storage-image (image*). Shape-matched placeholder
358 // used as fallback when no match exists on the incoming geometry.
360 {
361 QRhiSampler* sampler{}; // null for storage-image entries
362 QRhiTexture* texture{}; // current bound handle (placeholder or upstream)
363 bool mips_follow_texture{false}; // MIPMAP_MODE left out: see followTextureMips
364 QRhiTexture* placeholder{}; // shape-matched empty from RenderList
365 std::string name;
366 int binding{-1}; // assigned at SRB build
367 bool is_storage{false};
368 std::string access; // "read_only" / "write_only" / "read_write"
369
370 // True when this binding allocated `texture` itself (write_only /
371 // read_write storage image declared as a nested aux on a geometry
372 // input — same lifecycle role as m_storageImages plays for top-
373 // level csf_image_input outputs). Owned textures:
374 // - skip the per-frame upstream-resolution overwrite (we own
375 // the data, no upstream contributes);
376 // - get pushed into out_geo.auxiliary_textures by name so
377 // downstream consumers can resolve the live handle;
378 // - get deleted on release().
379 bool owned{false};
380 };
381
382 std::vector<AttributeSSBO> attribute_ssbos;
383 std::vector<AuxiliarySSBO> auxiliary_ssbos;
384 std::vector<AuxiliaryTexture> auxiliary_textures;
385 std::string input_name; // RESOURCES[].NAME (e.g. "geoIn", "geoOut") — used by PER_VERTEX/PER_INSTANCE TARGET filtering
386 int vertex_count{0}; // Number of elements (vertices) in the geometry
387 int instance_count{1}; // Number of instances
388 int input_port_index{-1}; // Input port index for this binding (-1 = no input port, e.g. write_only generator)
389 int outlet_index{-1}; // Geometry outlet this binding publishes on (-1 = none)
392 bool closesCycle{false};
393 uint64_t closesCycleGeneration{~uint64_t{0}};
394 bool has_output{false}; // true if any attribute is writable
395 bool has_vertex_count_spec{false}; // true if vertex_count expression is set
396 bool warned_multiple_cables{false}; // several cables on the input, reported once
397 bool has_instance_count_spec{false}; // true if instance_count expression is set
398 bool is_feedback_receiver{false}; // true = uses ping-pong double buffering for read_write attrs
399 bool pending_initial_copy{false}; // first frame after read_buffer allocated: use same-buffer mode, then copy buffer→read_buffer
400
401 // GPU buffers allocated by COPY_FROM (CPU→GPU upload). Owned by this binding,
402 // must be released via renderer.releaseBuffer() since they escape into output geometry.
403 std::vector<QRhiBuffer*> copyFromBuffers;
404
405 // Persistent output geometry — reused across frames to avoid per-frame shared_ptr allocation.
406 // Updated in-place; dirty_index incremented when structure or handles change.
407 ossia::geometry_spec outputGeometry;
408 int prev_vertex_count{-1}; // Track structural changes
409 int prev_instance_count{-1};
410 int prev_attribute_count{-1};
411 int prev_upstream_attr_count{-1};
412 int prev_upstream_aux_count{-1};
413 int prev_mesh_count{-1};
416 std::vector<std::pair<const ossia::mesh_list*, int64_t>> mesh_seen;
417
419 {
420 std::vector<int> declared;
421 std::vector<std::pair<int, int>> forwarded;
422 std::vector<int> auxiliary;
423 std::vector<int> storage;
424 int forwarded_aux_begin{0};
425 int forwarded_aux_end{0};
426 } outputSlots;
427
428 QRhiBuffer* indirectBuffer{}; // StorageBuffer (+ IndirectBuffer on Qt 6.12+)
429 int64_t indirectBufferSize{};
430 int indirectCountResult{0}; // Resolved command count
431 std::string indirectCountExpr; // Expression string for dynamic re-resolve
432 bool uses_indirect_draw{false};
433 // INDIRECT: { DRAW_COUNT: true } — 16-byte buffer whose word 0 is the
434 // GPU-written draw count, published via the "_indirect_draw_count"
435 // auxiliary and consumed by the drawIndexedIndirectCount rung.
436 QRhiBuffer* indirectCountBuffer{};
437 bool uses_indirect_count{false};
438 };
439 std::vector<GeometryBinding> m_geometryBindings;
444 std::vector<std::vector<uint32_t>> columnSpecialization() const;
447 QString specializeColumns(
448 QString src, const std::vector<std::vector<uint32_t>>& spec) const;
450 void rebuildComputePipelines(RenderList& renderer);
451 std::vector<std::vector<uint32_t>> m_columnSpec;
454 uint64_t m_columnTablesGeneration{1};
456 std::vector<uint32_t> m_columnTableScratch;
457 uint64_t m_columnSpecGeneration{0};
458 bool m_hasColumns{false};
460 bool m_specializeColumns{true};
461
463 void resolveColumns(
464 RenderList& renderer, QRhiResourceUpdateBatch& res, GeometryBinding& binding,
465 const isf::geometry_input& geo, const ossia::geometry* mesh);
466
467 // The meshes after the first of a multi-mesh upstream. Each one runs the
468 // passes over its own geometry bindings and SRBs, which are swapped in
469 // place of the first mesh's while it is updated, dispatched and pushed.
470 struct MeshLayer
471 {
472 std::vector<GeometryBinding> bindings;
473 std::vector<ComputePass> passes;
474 int meshIndex{};
475 bool auxSizeUnresolved{false};
477 bool passthrough{false};
478 };
479 std::vector<MeshLayer> m_meshLayers;
481 int m_meshIndex{0};
482
483 struct MeshLayerScope
484 {
485 RenderedCSFNode& self;
486 MeshLayer& layer;
487 MeshLayerScope(RenderedCSFNode& s, MeshLayer& l) noexcept;
488 ~MeshLayerScope();
489 };
490 void swapMeshLayer(MeshLayer& layer) noexcept;
491 int processedMeshCount() const;
492 void updateMeshLayers(RenderList& renderer, QRhiResourceUpdateBatch& res);
493 void releaseMeshLayer(RenderList& renderer, MeshLayer& layer);
494 const ossia::geometry* upstreamMesh(int port) const noexcept;
495 void runGeometryPasses(
496 RenderList& renderer, QRhiCommandBuffer& commands, QRhiResourceUpdateBatch*& res,
497 Edge& edge, bool firstMesh);
498
499 // One-time "CSF indirect dispatch: gpu|cpu-fallback" log guard (per node
500 // instance, so per test session); see the dispatch site.
501 bool m_loggedIndirectDispatch{false};
503 QByteArray m_liveCheckLabel;
504
505 QRhiBuffer* m_materialUBO{};
506 int m_materialSize{};
507
510 std::size_t m_srbRangeHash{};
511
512 // Output texture for compute shader results
513 QRhiTexture* m_outputTexture{};
514 QRhiTexture::Format m_outputFormat{QRhiTexture::RGBA8};
515
516 // Compute shader specifics
517 QRhiComputePipeline* m_computePipeline{}; // Points to first pass pipeline (backward compat)
518 QShader m_computeShader;
519 QString m_computeShaderSource; // Template with ISF_LOCAL_SIZE_X/Y/Z placeholders
520 std::vector<QRhiComputePipeline*> m_perPassPipelines; // One entry per pass (may share pipelines)
521 std::vector<QRhiComputePipeline*> m_ownedPipelines; // Unique pipelines for cleanup
522 bool m_pipelinesDirty{true};
523
524 // GPU buffer scatter (format conversion on GPU)
525 GPUBufferScatter m_gpuScatter;
526 bool m_gpuScatterAvailable{false};
527
528 // True once at least one frame's worth of upstream rendering has happened
529 // for this renderer's input textures. Used to gate generateMips() so we
530 // don't trip a Vulkan validation error on freshly-allocated textures whose
531 // layout is still PREINITIALIZED. Reset on init() / after release() so a
532 // RenderList rebuild starts the cycle over.
533 bool m_inputsHaveBeenWritten{false};
534
535 // Once-per-frame guard for runInitialPasses. RenderList calls update() +
536 // runInitialPasses() once per incoming edge of every sink port, so a CSF
537 // feeding >=2 sinks would otherwise re-dispatch every compute pass and
538 // double-swap the feedback SSBOs / persistent images per frame (simulation
539 // advancing at N x). Keyed on renderer.frame (a monotonic counter) rather
540 // than a reset-in-update() bool, because update() is interleaved per-port
541 // before each runInitialPasses and would reset such a bool between edges.
542 // Mirrors SimpleRenderedISFNode::m_lastMRTRenderFrame. Reset in release().
543 int64_t m_lastRunFrame{-1};
544
545 int64_t m_frameIndexFrame{-1};
546};
547
548}
Definition ISFNode.hpp:39
Renderer for a given node.
Definition NodeRenderer.hpp:11
List of nodes to be rendered to an output.
Definition RenderList.hpp:36
Graphics rendering pipeline for ossia score.
Definition Filter/PreviewWidget.hpp:11
QString csfAttributeRefusal(const ossia::geometry::attribute &upstream, std::string_view type, const isf::descriptor &d)
Definition RenderedCSFNode.cpp:77
void describeCsfAttribute(ossia::geometry::attribute &attr, std::string_view type, const isf::descriptor &d) noexcept
Definition RenderedCSFNode.cpp:61
void updateInputSamplerFilter(std::span< Sampler > samplers, const score::gfx::Node &node, const Port &input, const RenderTargetSpecs &spec)
Give the sampler initInputSamplers() created for input, one of node's input ports,...
Definition score-plugin-gfx/Gfx/Graph/Utils.cpp:2213
decltype(ossia::geometry::attribute::format) csfAttributeFormat(std::string_view type) noexcept
Definition RenderedCSFNode.cpp:35
Definition RenderedISFUtils.hpp:35
Definition Mesh.hpp:18
Connection between two score::gfx::Port.
Definition score-plugin-gfx/Gfx/Graph/Utils.hpp:236
Definition GPUBufferScatter.hpp:49
Create or update an SRB+UBO for a specific scatter operation.
Definition GPUBufferScatter.hpp:75
Definition Mesh.hpp:95
Useful abstraction for storing a graphics pipeline and associated resource bindings.
Definition score-plugin-gfx/Gfx/Graph/Utils.hpp:278
Port of a score::gfx::Node.
Definition score-plugin-gfx/Gfx/Graph/Utils.hpp:215
Definition score-plugin-gfx/Gfx/Graph/Node.hpp:71
QString column_refusal
Why columns read as absent, as last reported.
Definition RenderedCSFNode.hpp:333
std::vector< uint32_t > column_table
Column table: what was last uploaded, and into which buffer.
Definition RenderedCSFNode.hpp:330
Definition RenderedCSFNode.hpp:42
Useful abstraction for storing all the data related to a render target.
Definition score-plugin-gfx/Gfx/Graph/Utils.hpp:307