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