Loading...
Searching...
No Matches
GpuComputeNode.hpp
1#pragma once
2
3#if SCORE_PLUGIN_GFX
4#include <Process/ExecutionContext.hpp>
5
6#include <Crousti/Concepts.hpp>
7#include <Crousti/GpuUtils.hpp>
8#include <Crousti/Metadatas.hpp>
9#include <Gfx/GfxExecNode.hpp>
10#include <Gfx/Graph/NodeRenderer.hpp>
11#include <Gfx/Graph/RenderList.hpp>
12#include <Gfx/Graph/Uniforms.hpp>
13
14#include <avnd/binding/ossia/metadatas.hpp>
15
16namespace oscr
17{
18// Compute nodes that do not have any texture output so that they can get pulled
19// must drive things internally instead
20template <typename Node_T>
21using ComputeNodeBaseType = std::conditional_t<
22 avnd::gpu_image_output_introspection<Node_T>::size != 0, CustomGpuNodeBase,
23 CustomGpuOutputNodeBase>;
24template <typename Node_T>
25using ComputeRendererBaseType = std::conditional_t<
26 avnd::gpu_image_output_introspection<Node_T>::size != 0, score::gfx::NodeRenderer,
28
29template <typename Node_T>
30struct GpuComputeNode final : ComputeNodeBaseType<Node_T>
31{
32 GpuComputeNode(
33 std::weak_ptr<Execution::ExecutionCommandQueue> q, Gfx::exec_controls ctls,
34 int64_t id, const score::DocumentContext& ctx)
35 : ComputeNodeBaseType<Node_T>{std::move(q), std::move(ctls), ctx}
36 {
37 this->instance = id;
38
39 initGfxPorts<Node_T>(this, this->input, this->output);
40 using layout = typename Node_T::layout;
41 static constexpr auto lay = layout{};
42
44 this->compute
45 = QString::fromStdString(gen.compute_shader(lay) + Node_T{}.compute().data());
46 }
47
49 createRenderer(score::gfx::RenderList& r) const noexcept override;
50};
51
52template <typename Node_T>
53struct GpuComputeRenderer final : ComputeRendererBaseType<Node_T>
54{
55 using texture_inputs = avnd::gpu_image_input_introspection<Node_T>;
56 using texture_outputs = avnd::gpu_image_output_introspection<Node_T>;
57 std::shared_ptr<Node_T> state;
58 GpuMessageState m_last_message{};
59 ossia::small_flat_map<const score::gfx::Port*, score::gfx::TextureRenderTarget, 2>
60 m_rts;
61
62 ossia::time_value m_last_time{-1};
63
64 QRhiShaderResourceBindings* m_srb{};
65 QRhiComputePipeline* m_pipeline{};
66
67 bool m_createdPipeline{};
68 bool m_initialized{};
69
70 int ubo_k = 0;
71 ossia::flat_map<int, QRhiBuffer*> createdUbos;
72 ossia::flat_map<int, QRhiTexture*> createdTexs;
73
74#if QT_VERSION < QT_VERSION_CHECK(6, 6, 0)
75 std::vector<QRhiBufferReadbackResult*> bufReadbacks;
76 void addReadback(QRhiBufferReadbackResult* r) { bufReadbacks.push_back(r); }
77#endif
78
79 std::vector<QRhiReadbackResult*> texReadbacks;
80 void addReadback(QRhiReadbackResult* r) { texReadbacks.push_back(r); }
81
82 const GpuComputeNode<Node_T>& node() const noexcept
83 {
84 return static_cast<const GpuComputeNode<Node_T>&>(score::gfx::NodeRenderer::node);
85 }
86
87 GpuComputeRenderer(const GpuComputeNode<Node_T>& p)
88 : ComputeRendererBaseType<Node_T>{p}
89 , state{std::make_shared<Node_T>()}
90 {
91 prepareNewState<Node_T>(state, p);
92 }
93
95 renderTargetForInput(const score::gfx::Port& p) override
96 {
97 auto it = m_rts.find(&p);
98 if(it == m_rts.end())
99 return {};
100 return it->second;
101 }
102
103 QRhiTexture* createInput(
104 score::gfx::RenderList& renderer, int k, QRhiTexture::Format fmt, QSize size)
105 {
106 auto& parent = node();
107 auto port = parent.input[k];
108 static constexpr auto flags
109 = QRhiTexture::RenderTarget | QRhiTexture::UsedWithLoadStore;
110 auto texture = renderer.state.rhi->newTexture(fmt, size, 1, flags);
111 SCORE_ASSERT(texture->create());
112 m_rts[port] = score::gfx::createRenderTarget(
113 renderer.state, texture, renderer.samples(), renderer.requiresDepth(*port));
114 return texture;
115 }
116
117 template <typename F>
118 QRhiShaderResourceBinding initBinding(score::gfx::RenderList& renderer, F field)
119 {
120 static constexpr auto bindingStages = QRhiShaderResourceBinding::ComputeStage;
121 if constexpr(requires { F::ubo; })
122 {
123 auto it = createdUbos.find(F::binding());
124 QRhiBuffer* buffer = it != createdUbos.end() ? it->second : nullptr;
125 return QRhiShaderResourceBinding::uniformBuffer(
126 F::binding(), bindingStages, buffer);
127 }
128 else if constexpr(requires { F::image2D; })
129 {
130 auto tex_it = createdTexs.find(F::binding());
131 QRhiTexture* tex
132 = tex_it != createdTexs.end() ? tex_it->second : &renderer.emptyTexture();
133
134 if constexpr(
135 requires { F::load; } && requires { F::store; })
136 return QRhiShaderResourceBinding::imageLoadStore(
137 F::binding(), bindingStages, tex, 0);
138 else if constexpr(requires { F::readonly; })
139 return QRhiShaderResourceBinding::imageLoad(F::binding(), bindingStages, tex, 0);
140 else if constexpr(requires { F::writeonly; })
141 return QRhiShaderResourceBinding::imageStore(
142 F::binding(), bindingStages, tex, 0);
143 else
144 static_assert(F::load || F::store);
145 }
146 else if constexpr(requires { F::buffer; })
147 {
148 auto it = createdUbos.find(F::binding());
149 QRhiBuffer* buf = it != createdUbos.end() ? it->second : nullptr;
150
151 if constexpr(
152 requires { F::load; } && requires { F::store; })
153 return QRhiShaderResourceBinding::bufferLoadStore(
154 F::binding(), bindingStages, buf);
155 else if constexpr(requires { F::load; })
156 return QRhiShaderResourceBinding::bufferLoad(F::binding(), bindingStages, buf);
157 else if constexpr(requires { F::store; })
158 return QRhiShaderResourceBinding::bufferStore(F::binding(), bindingStages, buf);
159 else
160 static_assert(F::load || F::store);
161 }
162 else
163 {
164 static_assert(F::nope);
165 throw;
166 }
167 }
168
169 auto initBindings(score::gfx::RenderList& renderer)
170 {
171 auto& rhi = *renderer.state.rhi;
172 // Shader resource bindings
173 auto srb = rhi.newShaderResourceBindings();
174 SCORE_ASSERT(srb);
175
176 QVarLengthArray<QRhiShaderResourceBinding, 8> bindings;
177
178 using bindings_type = decltype(Node_T::layout::bindings);
179 avnd::pfr::for_each_field(
180 bindings_type{}, [&](auto f) { bindings.push_back(initBinding(renderer, f)); });
181
182 srb->setBindings(bindings.begin(), bindings.end());
183 return srb;
184 }
185
186 QRhiComputePipeline* createComputePipeline(score::gfx::RenderList& renderer)
187 {
188 auto& parent = node();
189 auto& rhi = *renderer.state.rhi;
190 auto compute = rhi.newComputePipeline();
191 auto cs = score::gfx::makeCompute(renderer.state, parent.compute);
192 compute->setShaderStage(QRhiShaderStage(QRhiShaderStage::Compute, cs));
193
194 return compute;
195 }
196
197 void init_input(score::gfx::RenderList& renderer, auto field)
198 {
199 //using input_type = std::decay_t<F>;
200 }
201
202 template <std::size_t Idx, typename F>
203 requires avnd::image_port<F>
204 void init_input(score::gfx::RenderList& renderer, avnd::field_reflection<Idx, F> field)
205 {
206 using bindings_type = decltype(Node_T::layout::bindings);
207 using image_type = std::decay_t<decltype(bindings_type{}.*F::image())>;
208 auto tex = createInput(
209 renderer, Idx, gpp::qrhi::textureFormat<image_type>(),
210 renderer.resolveInputRenderTargetSpecs(node(), Idx).size);
211
212 using sampler_type = typename avnd::member_reflection<F::image()>::member_type;
213 createdTexs[sampler_type::binding()] = tex;
214 }
215
216 template <std::size_t Idx, typename F>
217 requires avnd::uniform_port<F>
218 void init_input(score::gfx::RenderList& renderer, avnd::field_reflection<Idx, F> field)
219 {
220 using ubo_type = typename avnd::member_reflection<F::uniform()>::class_type;
221
222 // We must mark the UBO to construct.
223 if(createdUbos.find(ubo_type::binding()) != createdUbos.end())
224 return;
225
226 auto ubo = renderer.state.rhi->newBuffer(
227 QRhiBuffer::Dynamic, QRhiBuffer::UniformBuffer, gpp::std140_size<ubo_type>());
228 ubo->setName(oscr::getUtf8Name<F>());
229 ubo->create();
230
231 createdUbos[ubo_type::binding()] = ubo;
232 }
233
234 // Compute renderers own a single shared compute pipeline + SRB; they
235 // don't allocate any per-output-edge state. Edge add/remove is a no-op
236 // for them. These overrides are required because NodeRenderer
237 // ::removeOutputPass is pure-virtual, and Graph.cpp's incremental
238 // path drives renderers through addOutputPass (the per-edge passes a
239 // compute node does not have).
240 void removeOutputPass(score::gfx::RenderList&, score::gfx::Edge&) override { }
241 void addOutputPass(
242 score::gfx::RenderList&, score::gfx::Edge&, QRhiResourceUpdateBatch&) override
243 {
244 }
245
246 // All edge-independent setup lives in initState(), mirroring
247 // CustomGpuRenderer in GpuNode.hpp. The incremental edge-rewire path
248 // (Graph.cpp) only calls initState()/releaseState()/addOutputPass() on
249 // newly-spawned renderers; a compute node inserted live would otherwise
250 // never allocate its pipeline/SRB and run against uninitialised state.
251 void initState(score::gfx::RenderList& renderer, QRhiResourceUpdateBatch& res) override
252 {
253 if(m_initialized)
254 return;
255
256 auto& parent = node();
257 if constexpr(requires { state->prepare(); })
258 {
259 parent.processControlIn(
260 *this, *state, m_last_message, parent.last_message, parent.m_ctx);
261 state->prepare();
262 }
263
264 // Create the global shared inputs
265 avnd::input_introspection<Node_T>::for_all(
266 [this, &renderer](auto f) { init_input(renderer, f); });
267
268 m_srb = initBindings(renderer);
269 m_pipeline = createComputePipeline(renderer);
270 m_pipeline->setShaderResourceBindings(m_srb);
271
272 // No update step: we can directly create the pipeline here
273 if constexpr(!requires { &Node_T::update; })
274 {
275 SCORE_ASSERT(m_srb->create());
276 SCORE_ASSERT(m_pipeline->create());
277 m_createdPipeline = true;
278 }
279
280 m_initialized = true;
281 }
282
283 void init(score::gfx::RenderList& renderer, QRhiResourceUpdateBatch& res) override
284 {
285 initState(renderer, res);
286 }
287
288 std::vector<QRhiShaderResourceBinding> tmp;
289 void update(
290 score::gfx::RenderList& renderer, QRhiResourceUpdateBatch& res,
291 score::gfx::Edge* edge) override
292 {
293 // First copy all the "public" uniforms to their space in memory
294 avnd::gpu_uniform_introspection<Node_T>::for_all(
295 avnd::get_inputs<Node_T>(*state), [&]<avnd::uniform_port F>(const F& t) {
296 using uniform_type =
297 typename avnd::member_reflection<F::uniform()>::member_type;
298 using ubo_type = typename avnd::member_reflection<F::uniform()>::class_type;
299
300 auto ubo = this->createdUbos.at(ubo_type::binding());
301#if defined(_MSC_VER)
302#define MSVC_BUGGY_STATIC_CONSTEXPR
303#else
304#define MSVC_BUGGY_STATIC_CONSTEXPR static constexpr
305#endif
306 MSVC_BUGGY_STATIC_CONSTEXPR int offset = gpp::std140_offset<F::uniform()>();
307 MSVC_BUGGY_STATIC_CONSTEXPR int size = sizeof(uniform_type::value);
308 res.updateDynamicBuffer(ubo, offset, size, &t.value);
309 });
310
311 // Then run the update loop, which is a bit complicated
312 // as we have to take into account that buffers could be allocated, freed, etc.
313 // and thus updated in the shader resource bindings
314
315 if constexpr(requires { &Node_T::update; })
316 {
317 bool srb_touched{false};
318 tmp.assign(m_srb->cbeginBindings(), m_srb->cendBindings());
319 for(auto& promise : state->update())
320 {
321 using ret_type = decltype(promise.feedback_value);
323 *this, *renderer.state.rhi, res, tmp, srb_touched};
324 promise.feedback_value = visit(handler, promise.current_command);
325 }
326
327 if(srb_touched)
328 {
329 if(m_createdPipeline)
330 m_srb->destroy();
331 m_srb->setBindings(tmp.begin(), tmp.end());
332 if(m_createdPipeline && !m_srb->create())
333 qWarning("GpuComputeNode: SRB recreation failed");
334 }
335
336 /*
337 qDebug() << srb_touched << m_createdPipeline;
338 for(auto& b : tmp) {
339 const QRhiShaderResourceBinding::Data& dat = *b.data();
340 switch(dat.type) {
341 case QRhiShaderResourceBinding::UniformBuffer:
342 qDebug() << "ubo: " << dat.binding << (void*) dat.u.ubuf.buf;
343 break;
344 case QRhiShaderResourceBinding::ImageLoad:
345 case QRhiShaderResourceBinding::ImageStore:
346 case QRhiShaderResourceBinding::ImageLoadStore:
347 qDebug() << "image: " << dat.binding << (void*) dat.u.simage.tex;
348 break;
349 case QRhiShaderResourceBinding::BufferLoad:
350 case QRhiShaderResourceBinding::BufferStore:
351 case QRhiShaderResourceBinding::BufferLoadStore:
352 qDebug() << "buffer: " << dat.binding << (void*) dat.u.sbuf.buf;
353 break;
354 default:
355 qDebug() << "WRTF: " << dat.binding;
356 }
357 }
358 */
359
360 if(!m_createdPipeline)
361 {
362 SCORE_ASSERT(m_srb->create());
363 SCORE_ASSERT(m_pipeline->create());
364 m_createdPipeline = true;
365 }
366 tmp.clear();
367 }
368 }
369
370 void releaseState(score::gfx::RenderList& r) override
371 {
372 if(!m_initialized)
373 return;
374
375 m_createdPipeline = false;
376
377 // Release the object's internal states
378 if constexpr(requires { &Node_T::release; })
379 {
380 for(auto& promise : state->release())
381 {
382 gpp::qrhi::handle_release handler{*r.state.rhi};
383 visit(handler, promise.current_command);
384 }
385 state->release();
386 }
387
388 // Allocated textures are all inside RTs which are released a little bit later.
389 // for(auto& [id, tex] : this->createdTexs)
390 // tex->deleteLater();
391 this->createdTexs.clear();
392
393 // Release the allocated ubos
394 for(auto& [id, ubo] : this->createdUbos)
395 ubo->deleteLater();
396 this->createdUbos.clear();
397
398 // Release the allocated rts
399 // TODO investigate why reference does not work here:
400 for(auto& e : m_rts)
401 {
402 e.second.release();
403 }
404 m_rts.clear();
405
406 // Release the allocated pipelines
407 if(m_srb)
408 m_srb->deleteLater();
409 if(m_pipeline)
410 m_pipeline->deleteLater();
411 m_srb = nullptr;
412 m_pipeline = nullptr;
413
414 m_createdPipeline = false;
415
416 ubo_k = 0;
417
418 m_initialized = false;
419 }
420
421 void release(score::gfx::RenderList& r) override
422 {
423 releaseState(r);
424 }
425
426 void runCompute(
427 score::gfx::RenderList& renderer, QRhiCommandBuffer& cb,
428 QRhiResourceUpdateBatch*& res)
429 {
430 auto& parent = node();
431 // If we are paused, we don't run the processor implementation.
432 // if(parent.last_message.message.token.date == m_last_time) {
433 // return;
434 // }
435 // m_last_time = parent.last_message.message.token.date;
436
437 // Apply the controls
438 parent.processControlIn(
439 *this, *this->state, m_last_message, parent.last_message, parent.m_ctx);
440 parent.drainWorker(renderer.dateFromStepClock);
441
442 // Run the compute shader
443 {
444 SCORE_ASSERT(this->m_pipeline);
445 SCORE_ASSERT(this->m_pipeline->shaderResourceBindings());
446 for(auto& promise : this->state->dispatch())
447 {
448 using ret_type = decltype(promise.feedback_value);
450 *this, *renderer.state.rhi, cb, res, *this->m_pipeline};
451 promise.feedback_value = visit(handler, promise.current_command);
452 }
453 }
454
455 // Clear the readbacks
456#if QT_VERSION < QT_VERSION_CHECK(6, 6, 0)
457 for(auto rb : this->bufReadbacks)
458 delete rb;
459 this->bufReadbacks.clear();
460#endif
461 for(auto rb : this->texReadbacks)
462 delete rb;
463 this->texReadbacks.clear();
464
465 // Copy the data to the model node
466 parent.processControlOut(*this->state, renderer, parent);
467 parent.clearControlIn(*this->state);
468 }
469
470 void runInitialPasses(
471 score::gfx::RenderList& renderer, QRhiCommandBuffer& cb,
472 QRhiResourceUpdateBatch*& res, score::gfx::Edge& edge) override
473 {
474 runCompute(renderer, cb, res);
475 }
476
477 void runRenderPass(
478 score::gfx::RenderList& renderer, QRhiCommandBuffer& commands,
479 score::gfx::Edge& edge) override
480 {
481 }
482};
483
484template <typename Node_T>
486GpuComputeNode<Node_T>::createRenderer(score::gfx::RenderList& r) const noexcept
487{
488 return new GpuComputeRenderer<Node_T>{*this};
489};
490
491}
492#endif
Renderer for a given node.
Definition NodeRenderer.hpp:11
Definition OutputNode.hpp:38
List of nodes to be rendered to an output.
Definition RenderList.hpp:36
RenderState & state
RenderState corresponding to this RenderList.
Definition RenderList.hpp:299
Definition Controls.hpp:30
QShader makeCompute(const RenderState &v, QString compute)
Compile a compute shader.
Definition score-plugin-gfx/Gfx/Graph/Utils.cpp:1806
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
Definition GppShaders.hpp:10
Definition GppCoroutines.hpp:129
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
Port of a score::gfx::Node.
Definition score-plugin-gfx/Gfx/Graph/Utils.hpp:215
Useful abstraction for storing all the data related to a render target.
Definition score-plugin-gfx/Gfx/Graph/Utils.hpp:307