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