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 <Gfx/Graph/Uniforms.hpp>
10 
11 #include <gpp/ports.hpp>
12 
13 namespace oscr
14 {
15 
16 template <typename Node_T>
17 struct CustomGpuRenderer final : score::gfx::NodeRenderer
18 {
19  using texture_inputs = avnd::texture_input_introspection<Node_T>;
20  using texture_outputs = avnd::texture_output_introspection<Node_T>;
21  const CustomGpuNodeBase& parent;
22  std::vector<Node_T> states;
23  ossia::small_flat_map<const score::gfx::Port*, score::gfx::TextureRenderTarget, 2>
24  m_rts;
25 
26  ossia::time_value m_last_time{-1};
27 
28  score::gfx::PassMap m_p;
29 
30  QRhiBuffer* m_meshBuffer{};
31  QRhiBuffer* m_idxBuffer{};
32 
33  bool m_createdPipeline{};
34 
35  int sampler_k = 0;
36  ossia::flat_map<int, QRhiBuffer*> createdUbos;
37  ossia::flat_map<int, QRhiSampler*> createdSamplers;
38  ossia::flat_map<int, QRhiTexture*> createdTexs;
39 
40  CustomGpuRenderer(const CustomGpuNodeBase& p)
41  : NodeRenderer{}
42  , parent{p}
43  {
44  }
45 
47  renderTargetForInput(const score::gfx::Port& p) override
48  {
49  auto it = m_rts.find(&p);
50  SCORE_ASSERT(it != m_rts.end());
51  return it->second;
52  }
53 
54  QRhiTexture* createInput(score::gfx::RenderList& renderer, int k, QSize size)
55  {
56  auto port = parent.input[k];
57  constexpr auto flags = QRhiTexture::RenderTarget;
58  auto texture = renderer.state.rhi->newTexture(QRhiTexture::RGBA8, size, 1, flags);
59  SCORE_ASSERT(texture->create());
60  m_rts[port]
61  = score::gfx::createRenderTarget(renderer.state, texture, renderer.samples());
62  return texture;
63  }
64 
65  template <typename F>
66  QRhiShaderResourceBinding initBinding(score::gfx::RenderList& renderer, F field)
67  {
68  constexpr auto bindingStages = QRhiShaderResourceBinding::VertexStage
69  | QRhiShaderResourceBinding::FragmentStage;
70  if constexpr(requires { F::ubo; })
71  {
72  auto it = createdUbos.find(F::binding());
73  QRhiBuffer* buffer = it != createdUbos.end() ? it->second : nullptr;
74  return QRhiShaderResourceBinding::uniformBuffer(
75  F::binding(), bindingStages, buffer);
76  }
77  else if constexpr(requires { F::sampler2D; })
78  {
79  auto tex_it = createdTexs.find(F::binding());
80  QRhiTexture* tex
81  = tex_it != createdTexs.end() ? tex_it->second : &renderer.emptyTexture();
82 
83  // Samplers are always created by us
84  QRhiSampler* sampler = renderer.state.rhi->newSampler(
85  QRhiSampler::Linear, QRhiSampler::Linear, QRhiSampler::None,
86  QRhiSampler::ClampToEdge, QRhiSampler::ClampToEdge);
87  sampler->create();
88  createdSamplers[F::binding()] = sampler;
89 
90  return QRhiShaderResourceBinding::sampledTexture(
91  F::binding(), bindingStages, tex, sampler);
92  }
93  else
94  {
95  static_assert(F::nope);
96  throw;
97  }
98  }
99 
100  auto initBindings(score::gfx::RenderList& renderer)
101  {
102  auto& rhi = *renderer.state.rhi;
103  // Shader resource bindings
104  auto srb = rhi.newShaderResourceBindings();
105  SCORE_ASSERT(srb);
106 
107  QVarLengthArray<QRhiShaderResourceBinding, 8> bindings;
108 
109  if constexpr(requires { decltype(Node_T::layout::bindings){}; })
110  {
111  using bindings_type = decltype(Node_T::layout::bindings);
112  boost::pfr::for_each_field(bindings_type{}, [&](auto f) {
113  bindings.push_back(initBinding(renderer, f));
114  });
115  }
116  else if constexpr(requires { sizeof(typename Node_T::layout::bindings); })
117  {
118  using bindings_type = typename Node_T::layout::bindings;
119  boost::pfr::for_each_field(bindings_type{}, [&](auto f) {
120  bindings.push_back(initBinding(renderer, f));
121  });
122  }
123 
124  srb->setBindings(bindings.begin(), bindings.end());
125  return srb;
126  }
127 
128  QRhiGraphicsPipeline* createRenderPipeline(
130  {
131  auto& rhi = *renderer.state.rhi;
132  auto& mesh = renderer.defaultTriangle();
133  auto ps = rhi.newGraphicsPipeline();
134  ps->setName("createRenderPipeline");
135  SCORE_ASSERT(ps);
136  QRhiGraphicsPipeline::TargetBlend premulAlphaBlend;
137  premulAlphaBlend.enable = true;
138  premulAlphaBlend.srcColor = QRhiGraphicsPipeline::BlendFactor::SrcAlpha;
139  premulAlphaBlend.dstColor = QRhiGraphicsPipeline::BlendFactor::OneMinusSrcAlpha;
140  premulAlphaBlend.srcAlpha = QRhiGraphicsPipeline::BlendFactor::SrcAlpha;
141  premulAlphaBlend.dstAlpha = QRhiGraphicsPipeline::BlendFactor::OneMinusSrcAlpha;
142  ps->setTargetBlends({premulAlphaBlend});
143 
144  ps->setSampleCount(1);
145 
146  mesh.preparePipeline(*ps);
147 
148  auto [v, f]
149  = score::gfx::makeShaders(renderer.state, parent.vertex, parent.fragment);
150  ps->setShaderStages({{QRhiShaderStage::Vertex, v}, {QRhiShaderStage::Fragment, f}});
151 
152  SCORE_ASSERT(rt.renderPass);
153  ps->setRenderPassDescriptor(rt.renderPass);
154 
155  return ps;
156  }
157 
158  void init_input(score::gfx::RenderList& renderer, auto field)
159  {
160  //using input_type = std::decay_t<F>;
161  }
162 
163  template <std::size_t Idx, typename F>
164  requires avnd::sampler_port<F>
165  void init_input(score::gfx::RenderList& renderer, avnd::field_reflection<Idx, F> field)
166  {
167  auto tex = createInput(renderer, sampler_k++, renderer.state.renderSize);
168 
169  using sampler_type = typename avnd::member_reflection<F::sampler()>::member_type;
170  createdTexs[sampler_type::binding()] = tex;
171  }
172 
173  template <std::size_t Idx, typename F>
174  requires avnd::uniform_port<F>
175  void init_input(score::gfx::RenderList& renderer, avnd::field_reflection<Idx, F> field)
176  {
177  using ubo_type = typename avnd::member_reflection<F::uniform()>::class_type;
178 
179  // We must mark the UBO to construct.
180  if(createdUbos.find(ubo_type::binding()) != createdUbos.end())
181  return;
182 
183  auto ubo = renderer.state.rhi->newBuffer(
184  QRhiBuffer::Dynamic, QRhiBuffer::UniformBuffer, gpp::std140_size<ubo_type>());
185  ubo->create();
186 
187  createdUbos[ubo_type::binding()] = ubo;
188  }
189 
190  void init(score::gfx::RenderList& renderer, QRhiResourceUpdateBatch& res) override
191  {
192  if constexpr(requires { states[0].init(); })
193  {
194  for(auto& state : states)
195  state.init();
196  }
197 
198  if(!m_meshBuffer)
199  {
200  auto& mesh = renderer.defaultTriangle();
201  auto [mbuffer, ibuffer] = renderer.initMeshBuffer(mesh, res);
202  m_meshBuffer = mbuffer;
203  m_idxBuffer = ibuffer;
204  }
205 
206  // Create the global shared inputs
207  avnd::input_introspection<Node_T>::for_all(
208  [this, &renderer](auto f) { init_input(renderer, f); });
209 
210  // Create the initial srbs
211  // TODO when implementing multi-pass, we may have to
212  // move this back inside the loop below as they may depend on the pipelines...
213  auto srb = initBindings(renderer);
214 
215  // Create the states and pipelines
216  for(score::gfx::Edge* edge : this->parent.output[0]->edges)
217  {
218  auto rt = renderer.renderTargetForOutput(*edge);
219  if(rt.renderTarget)
220  {
221  states.push_back({});
222  prepareNewState(states.back(), parent);
223 
224  auto ps = createRenderPipeline(renderer, rt);
225  ps->setShaderResourceBindings(srb);
226 
227  m_p.emplace_back(edge, score::gfx::Pipeline{ps, srb});
228 
229  // No update step: we can directly create the pipeline here
230  if constexpr(!requires { &Node_T::update; })
231  {
232  SCORE_ASSERT(srb->create());
233  SCORE_ASSERT(ps->create());
234  m_createdPipeline = true;
235  }
236  }
237  }
238  }
239 
240  std::vector<QRhiShaderResourceBinding> tmp;
241  void update(score::gfx::RenderList& renderer, QRhiResourceUpdateBatch& res) override
242  {
243  // First copy all the "public" uniforms to their space in memory
244  if(states.size() > 0)
245  {
246  auto& state = states[0];
247 
248  avnd::gpu_uniform_introspection<Node_T>::for_all(
249  avnd::get_inputs<Node_T>(state), [&]<avnd::uniform_port F>(const F& t) {
250  //using input_type = std::decay_t<F>;
251  using uniform_type =
252  typename avnd::member_reflection<F::uniform()>::member_type;
253  using ubo_type = typename avnd::member_reflection<F::uniform()>::class_type;
254 
255  auto ubo = this->createdUbos.at(ubo_type::binding());
256 
257  constexpr int offset = gpp::std140_offset<F::uniform()>();
258  constexpr int size = sizeof(uniform_type::value);
259  res.updateDynamicBuffer(ubo, offset, size, &t.value);
260  });
261  }
262 
263  if constexpr(requires { &Node_T::update; })
264  {
265  // Then run the update loop, which is a bit complicated
266  // as we have to take into account that buffers could be allocated, freed, etc.
267  // and thus updated in the shader resource bindings
268  SCORE_ASSERT(states.size() == m_p.size());
269  //SCORE_SOFT_ASSERT(state.size() == edges);
270  for(int k = 0; k < states.size(); k++)
271  {
272  auto& state = states[k];
273  auto& pass = m_p[k].second;
274 
275  bool srb_touched{false};
276  tmp.assign(pass.srb->cbeginBindings(), pass.srb->cendBindings());
277  for(auto& promise : state.update())
278  {
279  using ret_type = decltype(promise.feedback_value);
280  gpp::qrhi::handle_update<CustomGpuRenderer, ret_type> handler{
281  *this, *renderer.state.rhi, res, tmp, srb_touched};
282  promise.feedback_value = visit(handler, promise.current_command);
283  }
284 
285  if(srb_touched)
286  {
287  if(m_createdPipeline)
288  pass.srb->destroy();
289 
290  pass.srb->setBindings(tmp.begin(), tmp.end());
291  }
292 
293  if(!m_createdPipeline)
294  {
295  SCORE_ASSERT(pass.srb->create());
296  SCORE_ASSERT(pass.pipeline->create());
297  }
298  }
299  m_createdPipeline = true;
300  tmp.clear();
301  }
302  }
303 
304  void release(score::gfx::RenderList& r) override
305  {
306  m_createdPipeline = false;
307 
308  // Release the object's internal states
309  if constexpr(requires { &Node_T::release; })
310  {
311  for(auto& state : states)
312  {
313  for(auto& promise : state.release())
314  {
315  gpp::qrhi::handle_release handler{*r.state.rhi};
316  visit(handler, promise.current_command);
317  }
318  }
319  }
320  states.clear();
321 
322  // Release the allocated mesh buffers
323  m_meshBuffer = nullptr;
324 
325  // Release the allocated textures
326  for(auto& [id, tex] : this->createdTexs)
327  tex->deleteLater();
328  this->createdTexs.clear();
329 
330  // Release the allocated samplers
331  for(auto& [id, sampl] : this->createdSamplers)
332  sampl->deleteLater();
333  this->createdSamplers.clear();
334 
335  // Release the allocated ubos
336  for(auto& [id, ubo] : this->createdUbos)
337  ubo->deleteLater();
338  this->createdUbos.clear();
339 
340  // Release the allocated rts
341  // TODO investigate why reference does not work here:
342  for(auto [port, rt] : m_rts)
343  rt.release();
344  m_rts.clear();
345 
346  // Release the allocated pipelines
347  for(auto& pass : m_p)
348  pass.second.release();
349  m_p.clear();
350 
351  m_meshBuffer = nullptr;
352  m_createdPipeline = false;
353 
354  sampler_k = 0;
355  }
356 
357  void runInitialPasses(
358  score::gfx::RenderList& renderer, QRhiCommandBuffer& commands,
359  QRhiResourceUpdateBatch*& res, score::gfx::Edge& edge) override
360  {
361  // If we are paused, we don't run the processor implementation.
362  if(parent.last_message.token.date == m_last_time)
363  {
364  return;
365  }
366  m_last_time = parent.last_message.token.date;
367 
368  // Apply the controls
369  for(auto& state : states)
370  {
371  this->parent.processControlIn(state, this->parent.last_message);
372  }
373  }
374 
375  void runRenderPass(
376  score::gfx::RenderList& renderer, QRhiCommandBuffer& commands,
377  score::gfx::Edge& edge) override
378  {
379  auto& mesh = renderer.defaultTriangle();
380  score::gfx::defaultRenderPass(
381  renderer, mesh, {m_meshBuffer, m_idxBuffer}, commands, edge, m_p);
382 
383  // Copy the data to the model node
384  if(!this->states.empty())
385  parent.processControlOut(this->states[0]);
386  }
387 };
388 
389 
390 template <typename Node_T>
391 struct CustomGpuNode final : CustomGpuNodeBase
392 {
393  CustomGpuNode(
394  std::weak_ptr<Execution::ExecutionCommandQueue> q, Gfx::exec_controls ctls, int id)
395  : CustomGpuNodeBase{std::move(q), std::move(ctls)}
396  {
397  this->instance = id;
398 
399  using texture_inputs = avnd::gpu_sampler_introspection<Node_T>;
400  using texture_outputs = avnd::gpu_attachment_introspection<Node_T>;
401 
402  for(std::size_t i = 0; i < texture_inputs::size; i++)
403  {
404  input.push_back(new score::gfx::Port{this, {}, score::gfx::Types::Image, {}});
405  }
406  for(std::size_t i = 0; i < texture_outputs::size; i++)
407  {
408  output.push_back(new score::gfx::Port{this, {}, score::gfx::Types::Image, {}});
409  }
410 
411  using layout = typename Node_T::layout;
412  static constexpr auto lay = layout{};
413 
414  gpp::qrhi::generate_shaders gen;
415  if constexpr(requires { &Node_T::vertex; })
416  {
417  vertex = QString::fromStdString(gen.vertex_shader(lay) + Node_T{}.vertex().data());
418  }
419  else
420  {
421  vertex = gpp::qrhi::DefaultPipeline::vertex();
422  }
423 
424  fragment
425  = QString::fromStdString(gen.fragment_shader(lay) + Node_T{}.fragment().data());
426  }
427 
429  createRenderer(score::gfx::RenderList& r) const noexcept override
430  {
431  return new CustomGpuRenderer<Node_T>{*this};
432  }
433 };
434 
435 }
436 #endif
Renderer for a given node.
Definition: NodeRenderer.hpp:11
List of nodes to be rendered to an output.
Definition: RenderList.hpp:19
MeshBuffers initMeshBuffer(const Mesh &mesh, QRhiResourceUpdateBatch &res)
Create buffers for a mesh and mark them for upload.
Definition: RenderList.cpp:39
const score::gfx::Mesh & defaultTriangle() const noexcept
A triangle mesh correct for this API.
Definition: RenderList.cpp:247
RenderState & state
RenderState corresponding to this RenderList.
Definition: RenderList.hpp:89
TextureRenderTarget renderTargetForOutput(const Edge &edge) noexcept
Obtain the texture corresponding to an output port.
Definition: RenderList.cpp:164
QRhiTexture & emptyTexture() const noexcept
Texture to use when a texture is missing.
Definition: RenderList.hpp:111
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
std::pair< QShader, QShader > makeShaders(const RenderState &v, QString vert, QString frag)
Get a pair of compiled vertex / fragment shaders from GLSL 4.5 sources.
Definition: score-plugin-gfx/Gfx/Graph/Utils.cpp:334
Connection between two score::gfx::Port.
Definition: score-plugin-gfx/Gfx/Graph/Utils.hpp:64
Useful abstraction for storing a graphics pipeline and associated resource bindings.
Definition: score-plugin-gfx/Gfx/Graph/Utils.hpp:91
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