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