Loading...
Searching...
No Matches
score-plugin-avnd/Crousti/GpuNode.hpp
1#pragma once
2
3#if SCORE_PLUGIN_GFX
4#include <Crousti/Concepts.hpp>
5#include <Crousti/GpuUtils.hpp>
6#include <Crousti/Metadatas.hpp>
7#include <Gfx/Graph/NodeRenderer.hpp>
8#include <Gfx/Graph/RenderList.hpp>
9#include <ossia/detail/algorithms.hpp>
10#include <Gfx/Graph/Uniforms.hpp>
11
12// #include <gpp/ports.hpp>
13
14namespace oscr
15{
16
17template <typename Node_T>
18struct CustomGpuRenderer final
20 , GpuRendererFiles<Node_T>
21{
22 using texture_inputs = avnd::texture_input_introspection<Node_T>;
23 using texture_outputs = avnd::texture_output_introspection<Node_T>;
24 std::vector<std::shared_ptr<Node_T>> states;
25 GpuMessageState m_last_message{};
26 ossia::small_flat_map<const score::gfx::Port*, score::gfx::TextureRenderTarget, 2>
27 m_rts;
28
29 ossia::time_value m_last_time{-1};
30
31 score::gfx::PassMap m_p;
32
33 // Per-pass "pipeline + SRB created" flags, kept index-parallel with m_p
34 // and `states` (same push_back in addOutputPass / same erase in
35 // removeOutputPass). Each pass gates its own srb->create() /
36 // pipeline->create(): a single global flag cannot handle a pass added live
37 // onto an update()-driven node, where the first frame would either recreate
38 // the already-live passes or skip the new one entirely.
39 ossia::small_vector<bool, 2> m_passCreated;
40
41 score::gfx::MeshBuffers m_meshBuffer{};
42
43 QRhiShaderResourceBindings* m_srb{};
44
45 ossia::flat_map<int, QRhiBuffer*> createdUbos;
46 ossia::flat_map<int, QRhiSampler*> createdSamplers;
47 ossia::flat_map<int, QRhiTexture*> createdTexs;
48 ossia::flat_map<int, score::gfx::RenderTargetSpecs> samplerSpecs;
49 ossia::flat_map<const score::gfx::Port*, int> inputSamplerBindings;
50
51 const CustomGpuNodeBase& node() const noexcept
52 {
53 return static_cast<const CustomGpuNodeBase&>(score::gfx::NodeRenderer::node);
54 }
55
56 CustomGpuRenderer(const CustomGpuNodeBase& p)
57 : NodeRenderer{p}
58 {
59 }
60
62 renderTargetForInput(const score::gfx::Port& p) override
63 {
64 auto it = m_rts.find(&p);
65 if(it == m_rts.end())
66 return {};
67 return it->second;
68 }
69
70 QRhiTexture* createInput(
71 score::gfx::RenderList& renderer, int k, const score::gfx::RenderTargetSpecs& spec)
72 {
73 auto& parent = node();
74 auto port = parent.input[k];
75 QRhiTexture::Flags flags = QRhiTexture::RenderTarget;
76 if(spec.mipmap_mode != QRhiSampler::None)
77 flags |= QRhiTexture::MipMapped | QRhiTexture::UsedWithGenerateMips;
78 auto texture = renderer.state.rhi->newTexture(spec.format, spec.size, 1, flags);
79 SCORE_ASSERT(texture->create());
81 renderer.state, texture, renderer.samples(), renderer.requiresDepth(*port));
82 return texture;
83 }
84
85 template <typename F>
86 QRhiShaderResourceBinding initBinding(score::gfx::RenderList& renderer, F field)
87 {
88 static constexpr auto bindingStages = QRhiShaderResourceBinding::VertexStage
89 | QRhiShaderResourceBinding::FragmentStage;
90 if constexpr(requires { F::ubo; })
91 {
92 auto it = createdUbos.find(F::binding());
93 QRhiBuffer* buffer = it != createdUbos.end() ? it->second : nullptr;
94 return QRhiShaderResourceBinding::uniformBuffer(
95 F::binding(), bindingStages, buffer);
96 }
97 else if constexpr(requires { F::sampler2D; })
98 {
99 auto tex_it = createdTexs.find(F::binding());
100 QRhiTexture* tex
101 = tex_it != createdTexs.end() ? tex_it->second : &renderer.emptyTexture();
102
104 spec.address_u = QRhiSampler::ClampToEdge;
105 spec.address_v = QRhiSampler::ClampToEdge;
106 spec.address_w = QRhiSampler::ClampToEdge;
107 if(auto spec_it = samplerSpecs.find(F::binding()); spec_it != samplerSpecs.end())
108 spec = spec_it->second;
109
110 QRhiSampler* sampler = renderer.state.rhi->newSampler(
111 spec.mag_filter, spec.min_filter, spec.mipmap_mode, spec.address_u,
112 spec.address_v, spec.address_w);
113 sampler->create();
114 createdSamplers[F::binding()] = sampler;
115
116 return QRhiShaderResourceBinding::sampledTexture(
117 F::binding(), bindingStages, tex, sampler);
118 }
119 else
120 {
121 static_assert(F::nope);
122 throw;
123 }
124 }
125
126 auto initBindings(score::gfx::RenderList& renderer)
127 {
128 auto& rhi = *renderer.state.rhi;
129 // Shader resource bindings
130 auto srb = rhi.newShaderResourceBindings();
131 SCORE_ASSERT(srb);
132
133 QVarLengthArray<QRhiShaderResourceBinding, 8> bindings;
134
135 if constexpr(requires { decltype(Node_T::layout::bindings){}; })
136 {
137 using bindings_type = decltype(Node_T::layout::bindings);
138 avnd::pfr::for_each_field(bindings_type{}, [&](auto f) {
139 bindings.push_back(initBinding(renderer, f));
140 });
141 }
142 else if constexpr(requires { sizeof(typename Node_T::layout::bindings); })
143 {
144 using bindings_type = typename Node_T::layout::bindings;
145 avnd::pfr::for_each_field(bindings_type{}, [&](auto f) {
146 bindings.push_back(initBinding(renderer, f));
147 });
148 }
149
150 srb->setBindings(bindings.begin(), bindings.end());
151 return srb;
152 }
153
154 QRhiGraphicsPipeline* createRenderPipeline(
156 {
157 auto& parent = node();
158 auto& rhi = *renderer.state.rhi;
159 auto& mesh = renderer.defaultTriangle();
160 auto ps = rhi.newGraphicsPipeline();
161 ps->setName("createRenderPipeline");
162 SCORE_ASSERT(ps);
163 QRhiGraphicsPipeline::TargetBlend premulAlphaBlend;
164 premulAlphaBlend.enable = true;
165 premulAlphaBlend.srcColor = QRhiGraphicsPipeline::BlendFactor::SrcAlpha;
166 premulAlphaBlend.dstColor = QRhiGraphicsPipeline::BlendFactor::OneMinusSrcAlpha;
167 premulAlphaBlend.srcAlpha = QRhiGraphicsPipeline::BlendFactor::SrcAlpha;
168 premulAlphaBlend.dstAlpha = QRhiGraphicsPipeline::BlendFactor::OneMinusSrcAlpha;
169 ps->setTargetBlends({premulAlphaBlend});
170
171 // Match the destination render target's actual sample count, with the
172 // standard -1 fallback to renderer.samples() for placeholder RTs that
173 // only carry a renderPass descriptor. Without this, MSAA renderlists
174 // create RTs with samples=N but this pipeline would default to 1, and
175 // Vulkan rejects the render pass for sample-count mismatch.
176 {
177 const int rtS = rt.sampleCount();
178 ps->setSampleCount(rtS > 0 ? rtS : renderer.samples());
179 }
180
181 mesh.preparePipeline(*ps);
182
183 auto [v, f]
184 = score::gfx::makeShaders(renderer.state, parent.vertex, parent.fragment);
185 ps->setShaderStages({{QRhiShaderStage::Vertex, v}, {QRhiShaderStage::Fragment, f}});
186
187 SCORE_ASSERT(rt.renderPass);
188 ps->setRenderPassDescriptor(rt.renderPass);
189
190 return ps;
191 }
192
193 void init_input(score::gfx::RenderList& renderer, auto field)
194 {
195 //using input_type = std::decay_t<F>;
196 }
197
198 template <std::size_t Idx, typename F>
199 requires avnd::sampler_port<F>
200 void init_input(score::gfx::RenderList& renderer, avnd::field_reflection<Idx, F> field)
201 {
202 const auto spec = renderer.resolveInputRenderTargetSpecs(node(), Idx);
203 auto tex = createInput(renderer, Idx, spec);
204
205 using sampler_type = typename avnd::member_reflection<F::sampler()>::member_type;
206 createdTexs[sampler_type::binding()] = tex;
207 samplerSpecs[sampler_type::binding()] = spec;
208 inputSamplerBindings[node().input[Idx]] = sampler_type::binding();
209 }
210
212 const score::gfx::Port& input, const score::gfx::RenderTargetSpecs& spec) override
213 {
214 auto binding_it = inputSamplerBindings.find(&input);
215 if(binding_it == inputSamplerBindings.end())
216 return;
217 auto sampler_it = createdSamplers.find(binding_it->second);
218 if(sampler_it == createdSamplers.end() || !sampler_it->second)
219 return;
220 auto* sampler = sampler_it->second;
221 const auto tex_it = createdTexs.find(binding_it->second);
222 const bool mipmapped = tex_it != createdTexs.end() && tex_it->second
223 && tex_it->second->flags().testFlag(QRhiTexture::MipMapped);
224 const auto mip = mipmapped ? spec.mipmap_mode : QRhiSampler::None;
225 if(sampler->magFilter() == spec.mag_filter && sampler->minFilter() == spec.min_filter
226 && sampler->mipmapMode() == mip && sampler->addressU() == spec.address_u
227 && sampler->addressV() == spec.address_v && sampler->addressW() == spec.address_w)
228 return;
229 sampler->destroy();
230 sampler->setMagFilter(spec.mag_filter);
231 sampler->setMinFilter(spec.min_filter);
232 sampler->setMipmapMode(mip);
233 sampler->setAddressU(spec.address_u);
234 sampler->setAddressV(spec.address_v);
235 sampler->setAddressW(spec.address_w);
236 sampler->create();
237 }
238
239 template <std::size_t Idx, typename F>
240 requires avnd::uniform_port<F>
241 void init_input(score::gfx::RenderList& renderer, avnd::field_reflection<Idx, F> field)
242 {
243 using ubo_type = typename avnd::member_reflection<F::uniform()>::class_type;
244
245 // We must mark the UBO to construct.
246 if(createdUbos.find(ubo_type::binding()) != createdUbos.end())
247 return;
248
249 auto ubo = renderer.state.rhi->newBuffer(
250 QRhiBuffer::Dynamic, QRhiBuffer::UniformBuffer, gpp::std140_size<ubo_type>());
251 ubo->setName(oscr::getUtf8Name<F>());
252 ubo->create();
253
254 createdUbos[ubo_type::binding()] = ubo;
255 }
256
257 void initState(score::gfx::RenderList& renderer, QRhiResourceUpdateBatch& res) override
258 {
259 if(m_initialized)
260 return;
261
262 // prepare()/processControlIn for graphics nodes is not invoked here:
263 // `states` is empty at initState time (states are constructed per-edge in
264 // addOutputPass), so there is nothing to prepare. The prepare/control-in
265 // call happens in addOutputPass, right after each state is built.
266
267 if(m_meshBuffer.buffers.empty())
268 {
269 auto& mesh = renderer.defaultTriangle();
270 m_meshBuffer = renderer.initMeshBuffer(mesh, res);
271 }
272
273 // Create the global shared inputs
274 avnd::input_introspection<Node_T>::for_all(
275 [this, &renderer](auto f) { init_input(renderer, f); });
276
277 // Create the shared shader resource bindings
278 m_srb = initBindings(renderer);
279
280 m_initialized = true;
281 }
282
283 void addOutputPass(
285 QRhiResourceUpdateBatch& res) override
286 {
287 auto& parent = node();
288 auto rt = renderer.renderTargetForOutput(edge);
289 if(rt.renderTarget)
290 {
291 states.push_back(std::make_shared<Node_T>());
292 prepareNewState(states.back(), parent);
293
294 // Graphics nodes that declare prepare(): apply any pending control
295 // input and run prepare() on the freshly-constructed state here, not
296 // in initState, where `states` is still empty. Detection uses
297 // operator-> because states.back() is a shared_ptr<Node_T>.
298 if constexpr(requires { states.back()->prepare(); })
299 {
300 parent.processControlIn(
301 *this, *states.back(), m_last_message, parent.last_message, parent.m_ctx);
302 states.back()->prepare();
303 }
304
305 auto ps = createRenderPipeline(renderer, rt);
306 ps->setShaderResourceBindings(m_srb);
307
308 m_p.emplace_back(&edge, score::gfx::Pass{rt, score::gfx::Pipeline{ps, m_srb}, nullptr});
309 m_passCreated.push_back(false);
310
311 // No update step: we can directly create this pass's pipeline here.
312 // The SRB is shared across all passes (m_srb); creating it is
313 // idempotent for our purposes, and the per-pass flag tracks the
314 // pipeline that is genuinely per-edge.
315 if constexpr(!requires { &Node_T::update; })
316 {
317 SCORE_ASSERT(m_srb->create());
318 SCORE_ASSERT(ps->create());
319 m_passCreated.back() = true;
320 }
321 }
322 }
323
324 bool hasOutputPassForEdge(score::gfx::Edge& edge) const override
325 {
326 return ossia::find_if(m_p, [&](const auto& p) { return p.first == &edge; })
327 != m_p.end();
328 }
329
330 void removeOutputPass(score::gfx::RenderList&, score::gfx::Edge& edge) override
331 {
332 // Mirror addOutputPass: each edge owns one entry in m_p (pipeline +
333 // SRB) and one parallel entry in `states`. Release both. The shared
334 // m_srb pointer is owned by initState; Pass::p.srb refers to the
335 // same pointer (see addOutputPass), so null it out before
336 // Pipeline::release() to avoid double-deleteLater of the shared SRB.
337 auto it
338 = ossia::find_if(m_p, [&](const auto& p) { return p.first == &edge; });
339 if(it == m_p.end())
340 return;
341 const auto idx = std::distance(m_p.begin(), it);
342 it->second.p.srb = nullptr; // shared with siblings, owned by initState
343 it->second.release();
344 m_p.erase(it);
345 if((std::size_t)idx < states.size())
346 states.erase(states.begin() + idx);
347 if((std::size_t)idx < m_passCreated.size())
348 m_passCreated.erase(m_passCreated.begin() + idx);
349 }
350
351 void releaseState(score::gfx::RenderList& r) override
352 {
353 if(!m_initialized)
354 return;
355
356 m_passCreated.clear();
357
358 // Release the object's internal states
359 if constexpr(requires { &Node_T::release; })
360 {
361 for(auto& state : states)
362 {
363 for(auto& promise : state->release())
364 {
365 gpp::qrhi::handle_release handler{*r.state.rhi};
366 visit(handler, promise.current_command);
367 }
368 }
369 }
370 states.clear();
371
372 this->releaseFiles();
373
374 // Release the allocated mesh buffers
375 m_meshBuffer = {};
376
377 // Release the allocated textures
378 for(auto& [id, tex] : this->createdTexs)
379 {
380 const bool ownedByRenderTarget = ossia::any_of(
381 m_rts, [tex](const auto& rt) { return rt.second.texture == tex; });
382 if(!ownedByRenderTarget)
383 tex->deleteLater();
384 }
385 this->createdTexs.clear();
386
387 // Release the allocated samplers
388 for(auto& [id, sampl] : this->createdSamplers)
389 sampl->deleteLater();
390 this->createdSamplers.clear();
391 this->samplerSpecs.clear();
392 this->inputSamplerBindings.clear();
393
394 // Release the allocated ubos
395 for(auto& [id, ubo] : this->createdUbos)
396 ubo->deleteLater();
397 this->createdUbos.clear();
398
399 // Release the allocated rts
400 for(auto [port, rt] : m_rts)
401 rt.release();
402 m_rts.clear();
403
404 // Release the allocated pipelines. Each Pass::p.srb refers to the same
405 // shared m_srb (see addOutputPass), so null it out per-pass before
406 // Pipeline::release() and delete it exactly once below, covering the
407 // m_p-empty case too. Mirrors removeOutputPass.
408 for(auto& pass : m_p)
409 {
410 pass.second.p.srb = nullptr; // shared, owned by initState
411 pass.second.release();
412 }
413 m_p.clear();
414 if(m_srb)
415 m_srb->deleteLater();
416 m_srb = nullptr;
417
418 m_meshBuffer = {};
419
420 m_initialized = false;
421 }
422
423 void init(score::gfx::RenderList& renderer, QRhiResourceUpdateBatch& res) override
424 {
425 initState(renderer, res);
426
427 auto& parent = node();
428 for(score::gfx::Edge* edge : parent.output[0]->edges)
429 addOutputPass(renderer, *edge, res);
430 }
431
432 std::vector<QRhiShaderResourceBinding> tmp;
433 void update(
434 score::gfx::RenderList& renderer, QRhiResourceUpdateBatch& res,
435 score::gfx::Edge* edge) override
436 {
437 // First copy all the "public" uniforms to their space in memory
438 if(states.size() > 0)
439 {
440 auto& state = *states[0];
441
442 avnd::gpu_uniform_introspection<Node_T>::for_all(
443 avnd::get_inputs<Node_T>(state), [&]<avnd::uniform_port F>(const F& t) {
444 //using input_type = std::decay_t<F>;
445 using uniform_type =
446 typename avnd::member_reflection<F::uniform()>::member_type;
447 using ubo_type = typename avnd::member_reflection<F::uniform()>::class_type;
448
449 auto ubo = this->createdUbos.at(ubo_type::binding());
450#if defined(_MSC_VER)
451#define MSVC_BUGGY_STATIC_CONSTEXPR
452#else
453#define MSVC_BUGGY_STATIC_CONSTEXPR static constexpr
454#endif
455 static constexpr int offset = gpp::std140_offset<F::uniform()>();
456 static constexpr int size = sizeof(uniform_type::value);
457 res.updateDynamicBuffer(ubo, offset, size, &t.value);
458 });
459 }
460
461 if constexpr(requires { &Node_T::update; })
462 {
463 // Then run the update loop, which is a bit complicated
464 // as we have to take into account that buffers could be allocated, freed, etc.
465 // and thus updated in the shader resource bindings
466 SCORE_ASSERT(states.size() == m_p.size());
467 SCORE_ASSERT(states.size() == m_passCreated.size());
468 //SCORE_SOFT_ASSERT(state.size() == edges);
469 for(int k = 0; k < states.size(); k++)
470 {
471 auto& state = *states[k];
472 auto& pass = m_p[k].second;
473
474 // Per-pass creation flag: a pass added live (e.g. a new output
475 // edge onto an update()-driven node) starts at false and gets its
476 // srb/pipeline created on the next update; passes already live
477 // keep their pipeline. A single global flag would skip the new
478 // pass entirely (or needlessly destroy the live ones).
479 const bool created = m_passCreated[k];
480
481 bool srb_touched{false};
482 tmp.assign(pass.p.srb->cbeginBindings(), pass.p.srb->cendBindings());
483 for(auto& promise : state.update())
484 {
485 using ret_type = decltype(promise.feedback_value);
487 *this, *renderer.state.rhi, res, tmp, srb_touched};
488 promise.feedback_value = visit(handler, promise.current_command);
489 }
490
491 if(srb_touched)
492 {
493 if(created)
494 pass.p.srb->destroy();
495
496 pass.p.srb->setBindings(tmp.begin(), tmp.end());
497 if(created && !pass.p.srb->create())
498 qWarning("GpuNode: SRB recreation failed");
499 }
500
501 if(!created)
502 {
503 SCORE_ASSERT(pass.p.srb->create());
504 SCORE_ASSERT(pass.p.pipeline->create());
505 m_passCreated[k] = true;
506 }
507 }
508 tmp.clear();
509 }
510 }
511
512 void release(score::gfx::RenderList& r) override { releaseState(r); }
513
514 void runInitialPasses(
515 score::gfx::RenderList& renderer, QRhiCommandBuffer& commands,
516 QRhiResourceUpdateBatch*& res, score::gfx::Edge& edge) override
517 {
518 auto& parent = node();
519 // If we are paused, we don't run the processor implementation.
520 if(parent.last_message.message.token.date == m_last_time)
521 {
522 return;
523 }
524 m_last_time = parent.last_message.message.token.date;
525
526 // Apply the controls
527 for(auto& state : states)
528 {
529 parent.processControlIn(
530 *this, *state, m_last_message, parent.last_message, parent.m_ctx);
531 }
532 parent.drainWorker(renderer.dateFromStepClock);
533 }
534
535 void runRenderPass(
536 score::gfx::RenderList& renderer, QRhiCommandBuffer& commands,
537 score::gfx::Edge& edge) override
538 {
539 auto& parent = node();
540 auto& mesh = renderer.defaultTriangle();
541 score::gfx::defaultRenderPass(renderer, mesh, m_meshBuffer, commands, edge, m_p);
542
543 // Copy the data to the model node
544 if(!this->states.empty())
545 parent.processControlOut(*this->states[0]);
546 for(auto& state : states)
547 parent.clearControlIn(*state);
548 }
549};
550
551template <typename Node_T>
552struct CustomGpuNode final
553 : CustomGpuNodeBase
554 , GpuNodeElements<Node_T>
555{
556 CustomGpuNode(
557 std::weak_ptr<Execution::ExecutionCommandQueue> q, Gfx::exec_controls ctls,
558 int64_t id, const score::DocumentContext& ctx)
559 : CustomGpuNodeBase{std::move(q), std::move(ctls), ctx}
560 {
561 this->instance = id;
562
563 initGfxPorts<Node_T>(this, this->input, this->output);
564
565 using layout = typename Node_T::layout;
566 static constexpr auto lay = layout{};
567
569 if constexpr(requires { &Node_T::vertex; })
570 {
571 vertex = QString::fromStdString(gen.vertex_shader(lay) + Node_T{}.vertex().data());
572 }
573 else
574 {
575 vertex = gpp::qrhi::DefaultPipeline::vertex();
576 }
577
578 fragment
579 = QString::fromStdString(gen.fragment_shader(lay) + Node_T{}.fragment().data());
580 }
581
583 createRenderer(score::gfx::RenderList& r) const noexcept override
584 {
585 return new CustomGpuRenderer<Node_T>{*this};
586 }
587};
588
589}
590#endif
Renderer for a given node.
Definition NodeRenderer.hpp:11
List of nodes to be rendered to an output.
Definition RenderList.hpp:33
RenderState & state
RenderState corresponding to this RenderList.
Definition RenderList.hpp:274
Definition Controls.hpp:30
std::pair< QShader, QShader > makeShaders(const RenderState &v, QString vert, QString frag, int multiViewCount)
Get a pair of compiled vertex / fragment shaders from GLSL 4.5 sources.
Definition score-plugin-gfx/Gfx/Graph/Utils.cpp:1523
TextureRenderTarget createRenderTarget(const RenderState &state, QRhiTexture *tex, int samples, bool depth, bool samplableDepth)
Create a render target from a texture.
Definition score-plugin-gfx/Gfx/Graph/Utils.cpp:86
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:2029
STL namespace.
Definition GppShaders.hpp:10
Definition GppCoroutines.hpp:92
Definition GppCoroutines.hpp:253
Definition DocumentContext.hpp:18
Connection between two score::gfx::Port.
Definition score-plugin-gfx/Gfx/Graph/Utils.hpp:236
Definition Mesh.hpp:95
Definition NodeRenderer.hpp:268
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:61
Useful abstraction for storing all the data related to a render target.
Definition score-plugin-gfx/Gfx/Graph/Utils.hpp:307