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