Loading...
Searching...
No Matches
GpuUtils.hpp
1#pragma once
2
3#include <avnd/introspection/gfx.hpp>
4#if SCORE_PLUGIN_GFX
5#include <Process/ExecutionContext.hpp>
6
7#include <Crousti/File.hpp>
8#include <Crousti/GppCoroutines.hpp>
9#include <Crousti/GppShaders.hpp>
10#include <Crousti/MessageBus.hpp>
11#include <Crousti/SceneConcepts.hpp>
12#include <Crousti/TextureConversion.hpp>
13#include <Crousti/TextureFormat.hpp>
14#include <Gfx/GfxExecNode.hpp>
15#include <Gfx/Graph/Node.hpp>
16#include <Gfx/Graph/OutputNode.hpp>
17#include <Gfx/Graph/RenderList.hpp>
18#include <Gfx/Graph/RenderState.hpp>
19
20#include <score/tools/ThreadPool.hpp>
21
22#include <ossia/detail/hash_map.hpp>
23#include <ossia/detail/small_flat_map.hpp>
24
25#include <ossia-qt/invoke.hpp>
26
27#include <QCoreApplication>
28#include <QTimer>
29#include <QtGui/private/qrhi_p.h>
30
31#include <avnd/binding/ossia/metadatas.hpp>
32#include <avnd/binding/ossia/port_run_postprocess.hpp>
33#include <avnd/binding/ossia/port_run_preprocess.hpp>
34#include <avnd/binding/ossia/soundfiles.hpp>
35#include <avnd/concepts/parameter.hpp>
36#include <avnd/introspection/input.hpp>
37#include <avnd/introspection/output.hpp>
38#include <fmt/format.h>
39#include <gpp/layout.hpp>
40#include <halp/texture.hpp>
41
42#include <score_plugin_avnd_export.h>
43
44namespace oscr
45{
46struct GpuWorker
47{
48 template <typename T>
49 void initWorker(this auto& self, std::shared_ptr<T>& state) noexcept
50 {
51 if constexpr(avnd::has_worker<T>)
52 {
53 auto ptr = QPointer{&self};
54 auto& tq = score::TaskPool::instance();
55 using worker_type = decltype(state->worker);
56
57 auto wk_state = std::weak_ptr{state};
58 state->worker.request = [ptr, &tq, wk_state]<typename... Args>(Args&&... f) {
59 using type_of_result = decltype(worker_type::work(std::forward<Args>(f)...));
60 tq.post([... ff = std::forward<Args>(f), wk_state, ptr]() mutable {
61 if constexpr(std::is_void_v<type_of_result>)
62 {
63 worker_type::work(std::forward<decltype(ff)>(ff)...);
64 }
65 else
66 {
67 // If the worker returns a std::function, it
68 // is to be invoked back in the processor DSP thread
69 auto res = worker_type::work(std::forward<decltype(ff)>(ff)...);
70 if(!res || !ptr)
71 return;
72
73 ossia::qt::run_async(
74 QCoreApplication::instance(),
75 [res = std::move(res), wk_state, ptr]() mutable {
76 if(ptr)
77 if(auto state = wk_state.lock())
78 res(*state);
79 });
80 }
81 });
82 };
83 }
84 }
85};
86
87#if defined(OSCR_HAS_MMAP_FILE_STORAGE)
88// The file ports keep string_views into the raw_file_data, and there is one
89// object instance per renderer (one renderer per RenderList, that is per
90// output) and per instance in CustomGpuRenderer. Storing the handles on the
91// node would thus free, on every load, the memory that the ports of all the
92// other instances still point to.
93template <typename T>
94struct GpuRendererFiles
95{
96 template <std::size_t N, std::size_t NField>
97 void file_loaded(
98 T& state, const std::shared_ptr<oscr::raw_file_data>& hdl,
99 avnd::predicate_index<N> pred, avnd::field_index<NField> field)
100 {
101 m_rawfiles[&state].load(state, hdl, pred, field);
102 }
103
104 void releaseFiles() noexcept { m_rawfiles.clear(); }
105
106private:
107 ossia::hash_map<const T*, oscr::raw_file_storage<T>> m_rawfiles;
108};
109
110template <typename T>
111 requires(avnd::raw_file_input_introspection<T>::size == 0)
112struct GpuRendererFiles<T>
113{
114 void releaseFiles() noexcept { }
115};
116#else
117template <typename T>
118struct GpuRendererFiles
119{
120 void releaseFiles() noexcept { }
121};
122#endif
123
124template <typename GpuNodeRenderer, typename Node>
125struct GpuProcessIns
126{
127 GpuNodeRenderer& gpu;
128 Node& state;
129 const score::gfx::Message& prev_mess;
130 const score::gfx::Message& mess;
131 const score::DocumentContext& ctx;
132
133 bool can_process_message(std::size_t N)
134 {
135 if(mess.input.size() <= N)
136 return false;
137
138 if(prev_mess.input.size() == mess.input.size())
139 {
140 auto& prev = prev_mess.input[N];
141 auto& next = mess.input[N];
142 if(prev.index() == 1 && next.index() == 1)
143 {
144 if(ossia::get<ossia::value>(prev) == ossia::get<ossia::value>(next))
145 {
146 return false;
147 }
148 }
149 }
150 return true;
151 }
152
153 // The same message is handed to the renderer more than once (init, update, every frame between
154 // two execution ticks).
155 bool is_new_message() const noexcept
156 {
157 return prev_mess.node_id != mess.node_id || prev_mess.token.date != mess.token.date
158 || prev_mess.input.size() != mess.input.size();
159 }
160
161 template <avnd::parameter_port Field, std::size_t NField>
162 void operator()(Field& t, avnd::field_index<NField> field_index)
163 {
164 if constexpr(avnd::optional_ish<decltype(Field::value)>)
165 {
166 // Impulses and other one-shot values are events, not states: a value equal to the previous
167 // one is a new event, but the same message must not fire it twice. Mirrors the CPU path
168 // (which sees each port's data stream once per tick).
169 if(mess.input.size() <= NField || !is_new_message())
170 return;
171 }
172 else if(!can_process_message(field_index))
173 {
174 return;
175 }
176
177 if(auto val = ossia::get_if<ossia::value>(&mess.input[field_index]))
178 {
179 oscr::from_ossia_value(t, *val, t.value);
180 if_possible(t.update(state));
181 }
182 }
183
184#if OSCR_HAS_MMAP_FILE_STORAGE
185 template <avnd::raw_file_port Field, std::size_t NField>
186 void operator()(Field& t, avnd::field_index<NField> field_index)
187 {
188 // FIXME we should be loading a file there
189 using file_ports = avnd::raw_file_input_introspection<Node>;
190
191 if(!can_process_message(field_index))
192 return;
193
194 auto val = ossia::get_if<ossia::value>(&mess.input[field_index]);
195 if(!val)
196 return;
197
198 static constexpr bool has_text = requires { decltype(Field::file)::text; };
199 static constexpr bool has_mmap = requires { decltype(Field::file)::mmap; };
200
201 // First we can load it directly since execution hasn't started yet
202 if(auto hdl = loadRawfile(*val, ctx, has_text, has_mmap))
203 {
204 static constexpr auto N = file_ports::field_index_to_index(NField);
205 if constexpr(avnd::port_can_process<Field>)
206 {
207 // FIXME also do it when we get a run-time message from the exec engine,
208 // OSC, etc
209 auto func = executePortPreprocess<Field>(*hdl);
210 gpu.file_loaded(
211 state, hdl, avnd::predicate_index<N>{}, avnd::field_index<NField>{});
212 if(func)
213 func(state);
214 }
215 else
216 {
217 gpu.file_loaded(
218 state, hdl, avnd::predicate_index<N>{}, avnd::field_index<NField>{});
219 }
220 }
221 }
222#endif
223
224 template <avnd::buffer_port Field, std::size_t NField>
225 void operator()(Field& t, avnd::field_index<NField> field_index)
226 {
227 if(!can_process_message(field_index))
228 return;
229
230 using node_type = std::remove_cvref_t<decltype(gpu.node())>;
231 auto& node = const_cast<node_type&>(gpu.node());
232 if(field_index >= mess.input.size())
233 return;
234 auto val = ossia::get_if<ossia::render_target_spec>(&mess.input[field_index]);
235 if(!val)
236 return;
237 node.process(NField, *val);
238 }
239
240 template <avnd::texture_port Field, std::size_t NField>
241 void operator()(Field& t, avnd::field_index<NField> field_index)
242 {
243 if(!can_process_message(field_index))
244 return;
245
246 using node_type = std::remove_cvref_t<decltype(gpu.node())>;
247 auto& node = const_cast<node_type&>(gpu.node());
248 if(field_index >= mess.input.size())
249 return;
250 auto val = ossia::get_if<ossia::render_target_spec>(&mess.input[field_index]);
251 if(!val)
252 return;
253 node.process(NField, *val);
254 }
255
256 template <avnd::geometry_port Field, std::size_t NField>
257 void operator()(Field& t, avnd::field_index<NField> field_index)
258 {
259 // Intentional no-op: geometry is not in the control message, it flows
260 // through geometry_inputs_storage::readInputGeometries. The empty body
261 // keeps GpuProcessIns instantiable for nodes whose input list contains
262 // geometry fields, which would otherwise hit the `= delete` catch-all
263 // below.
264 }
265
266 template <scene_port Field, std::size_t NField>
267 void operator()(Field& t, avnd::field_index<NField> field_index)
268 {
269 // Intentional no-op — same reasoning as the geometry_port overload above.
270 // Scene data flows through scene_inputs_storage / scene_outputs_storage.
271 }
272
273 void operator()(auto& t, auto field_index) = delete;
274};
275
276struct GpuControlIns
277{
278 template <typename Self, typename Node_T>
279 static void processControlIn(
280 Self& self, Node_T& state, score::gfx::Message& renderer_mess,
281 const score::gfx::Message& mess, const score::DocumentContext& ctx) noexcept
282 {
283 // Apply the controls
284 avnd::input_introspection<Node_T>::for_all_n(
285 avnd::get_inputs<Node_T>(state),
286 GpuProcessIns<Self, Node_T>{self, state, renderer_mess, mess, ctx});
287 renderer_mess = mess;
288 }
289
290 // Once the processor has run, the impulses and other one-shot values it received this tick are
291 // consumed, exactly as oscr::process_after_run does on the CPU path. Without this an impulse
292 // would stay set until the next message touches the port.
293 template <typename Node_T>
294 static void clearControlIn(Node_T& state) noexcept
295 {
296 avnd::parameter_input_introspection<Node_T>::for_all(
297 avnd::get_inputs<Node_T>(state), []<typename Field>(Field& t) {
298 if constexpr(avnd::optional_ish<decltype(Field::value)>)
299 t.value.reset();
300 });
301 }
302};
303
304struct GpuControlOuts
305{
306 std::weak_ptr<Execution::ExecutionCommandQueue> queue;
307 Gfx::exec_controls control_outs;
308
309 int64_t instance{};
310
311 template <typename Node_T>
312 void processControlOut(Node_T& state) const noexcept
313 {
314 if(!this->control_outs.empty())
315 {
316 auto q = this->queue.lock();
317 if(!q)
318 return;
319 auto& qq = *q;
320 int parm_k = 0;
321 avnd::parameter_output_introspection<Node_T>::for_all(
322 avnd::get_outputs(state), [&]<avnd::parameter_port T>(const T& t) {
323 qq.enqueue([v = oscr::to_ossia_value(t, t.value),
324 port = control_outs[parm_k]]() mutable {
325 std::swap(port->value, v);
326 port->changed = true;
327 });
328
329 parm_k++;
330 });
331 }
332 }
333};
334
335template <typename T>
336struct SCORE_PLUGIN_AVND_EXPORT GpuNodeElements
337{
338 [[no_unique_address]] oscr::soundfile_storage<T> soundfiles;
339
340 [[no_unique_address]] oscr::midifile_storage<T> midifiles;
341
342 // Raw file handles are stored per-state in the renderers, see GpuRendererFiles
343};
344
345struct SCORE_PLUGIN_AVND_EXPORT CustomGfxNodeBase : score::gfx::NodeModel
346{
347 explicit CustomGfxNodeBase(const score::DocumentContext& ctx)
348 : score::gfx::NodeModel{}
349 , m_ctx{ctx}
350 {
351 }
352 virtual ~CustomGfxNodeBase();
353 const score::DocumentContext& m_ctx;
354 score::gfx::Message last_message;
355 void process(score::gfx::Message&& msg) override;
357};
358struct SCORE_PLUGIN_AVND_EXPORT CustomGfxOutputNodeBase : score::gfx::OutputNode
359{
360 virtual ~CustomGfxOutputNodeBase();
361
362 score::gfx::Message last_message;
363 void process(score::gfx::Message&& msg) override;
364};
365struct SCORE_PLUGIN_AVND_EXPORT CustomGpuNodeBase
367 , GpuWorker
368 , GpuControlIns
369 , GpuControlOuts
370{
371 CustomGpuNodeBase(
372 std::weak_ptr<Execution::ExecutionCommandQueue>&& q, Gfx::exec_controls&& ctls,
373 const score::DocumentContext& ctx)
374 : GpuControlOuts{std::move(q), std::move(ctls)}
375 , m_ctx{ctx}
376 {
377 }
378
379 virtual ~CustomGpuNodeBase() = default;
380
381 const score::DocumentContext& m_ctx;
382 QString vertex, fragment, compute;
383 score::gfx::Message last_message;
384 void process(score::gfx::Message&& msg) override;
385};
386
387struct SCORE_PLUGIN_AVND_EXPORT CustomGpuOutputNodeBase
389 , GpuWorker
390 , GpuControlIns
391 , GpuControlOuts
392{
396 static constexpr QSize defaultRenderSize{200, 200};
397
398 CustomGpuOutputNodeBase(
399 std::weak_ptr<Execution::ExecutionCommandQueue> q, Gfx::exec_controls&& ctls,
400 const score::DocumentContext& ctx);
401 virtual ~CustomGpuOutputNodeBase();
402
403 const score::DocumentContext& m_ctx;
404 std::weak_ptr<score::gfx::RenderList> m_renderer{};
405 std::shared_ptr<score::gfx::RenderState> m_renderState{};
406
407 QString vertex, fragment, compute;
408 score::gfx::Message last_message;
409 void process(score::gfx::Message&& msg) override;
411
412 void setRenderer(std::shared_ptr<score::gfx::RenderList>) override;
413 score::gfx::RenderList* renderer() const override;
414
415 void startRendering() override;
416 void render() override;
417 void stopRendering() override;
418 bool canRender() const override;
419 void onRendererChange() override;
420
421 void createOutput(score::gfx::OutputConfiguration) override;
422
423 void destroyOutput() override;
424 std::shared_ptr<score::gfx::RenderState> renderState() const override;
425
426 Configuration configuration() const noexcept override;
427};
428
429template <typename Node_T, typename Node>
430void prepareNewState(std::shared_ptr<Node_T>& eff, const Node& parent)
431{
432 if constexpr(avnd::has_worker<Node_T>)
433 {
434 parent.initWorker(eff);
435 }
436 if constexpr(avnd::has_processor_to_gui_bus<Node_T>)
437 {
438 auto& process = parent.processModel;
439 eff->send_message = [ptr = QPointer{&process}](auto&& b) mutable {
440 // FIXME right now all the rendering is done in the UI thread, which is very MEH
441 // this->in_edit([&process, bb = std::move(b)]() mutable {
442
443 if(ptr && ptr->to_ui)
444 MessageBusSender{ptr->to_ui}(std::move(b));
445 // });
446 };
447
448 // FIXME GUI -> engine. See executor.hpp
449 }
450
451 avnd::init_controls(*eff);
452
453 if constexpr(avnd::can_prepare<Node_T>)
454 {
455 if constexpr(avnd::function_reflection<&Node_T::prepare>::count == 1)
456 {
457 using prepare_type = avnd::first_argument<&Node_T::prepare>;
458 prepare_type t;
459 if_possible(t.instance = parent.instance);
460 eff->prepare(t);
461 }
462 else
463 {
464 eff->prepare();
465 }
466 }
467}
468
469struct port_to_type_enum
470{
471 template <std::size_t I, avnd::buffer_port F>
472 constexpr auto operator()(avnd::field_reflection<I, F> p)
473 {
474 return score::gfx::Types::Buffer;
475 }
476
477 template <std::size_t I, avnd::cpu_texture_port F>
478 constexpr auto operator()(avnd::field_reflection<I, F> p)
479 {
480 using texture_type = std::remove_cvref_t<decltype(F::texture)>;
481 return (avnd::cpu_fixed_format_texture<texture_type> || avnd::cpu_dynamic_format_texture<texture_type>)
482 ? score::gfx::Types::Image
483 : score::gfx::Types::Buffer;
484 }
485
486 template <std::size_t I, avnd::gpu_texture_port F>
487 constexpr auto operator()(avnd::field_reflection<I, F> p)
488 {
489 return score::gfx::Types::Image;
490 }
491
492 template <std::size_t I, avnd::sampler_port F>
493 constexpr auto operator()(avnd::field_reflection<I, F> p)
494 {
495 return score::gfx::Types::Image;
496 }
497 template <std::size_t I, avnd::image_port F>
498 constexpr auto operator()(avnd::field_reflection<I, F> p)
499 {
500 return score::gfx::Types::Image;
501 }
502 template <std::size_t I, avnd::attachment_port F>
503 constexpr auto operator()(avnd::field_reflection<I, F> p)
504 {
505 return score::gfx::Types::Image;
506 }
507 template <std::size_t I, avnd::gpu_render_target_output_port F>
508 constexpr auto operator()(avnd::field_reflection<I, F> p)
509 {
510 return score::gfx::Types::Image;
511 }
512
513 template <std::size_t I, avnd::geometry_port F>
514 constexpr auto operator()(avnd::field_reflection<I, F> p)
515 {
516 return score::gfx::Types::Geometry;
517 }
518 // Scene ports reuse Types::Geometry — a scene is a richer form of geometry.
519 template <std::size_t I, scene_port F>
520 requires(!avnd::geometry_port<F>)
521 constexpr auto operator()(avnd::field_reflection<I, F> p)
522 {
523 return score::gfx::Types::Geometry;
524 }
525 template <std::size_t I, avnd::mono_audio_port F>
526 constexpr auto operator()(avnd::field_reflection<I, F> p)
527 {
528 return score::gfx::Types::Audio;
529 }
530 template <std::size_t I, avnd::poly_audio_port F>
531 constexpr auto operator()(avnd::field_reflection<I, F> p)
532 {
533 return score::gfx::Types::Audio;
534 }
535 template <std::size_t I, avnd::int_parameter F>
536 constexpr auto operator()(avnd::field_reflection<I, F> p)
537 {
538 return score::gfx::Types::Int;
539 }
540 template <std::size_t I, avnd::enum_parameter F>
541 constexpr auto operator()(avnd::field_reflection<I, F> p)
542 {
543 return score::gfx::Types::Int;
544 }
545 template <std::size_t I, avnd::float_parameter F>
546 constexpr auto operator()(avnd::field_reflection<I, F> p)
547 {
548 return score::gfx::Types::Float;
549 }
550 template <std::size_t I, avnd::parameter_port F>
551 constexpr auto operator()(avnd::field_reflection<I, F> p)
552 {
553 using value_type = std::remove_cvref_t<decltype(F::value)>;
554
555 if constexpr(std::is_array_v<value_type>)
556 {
557 static constexpr int sz = sizeof(value_type) / sizeof(value_type{}[0]);
558 if constexpr(sz == 2)
559 {
560 return score::gfx::Types::Vec2;
561 }
562 else if constexpr(sz == 3)
563 {
564 return score::gfx::Types::Vec3;
565 }
566 else if constexpr(sz == 4)
567 {
568 return score::gfx::Types::Vec4;
569 }
570 }
571 else if constexpr(std::is_aggregate_v<value_type>)
572 {
573 static constexpr int sz = avnd::pfr::tuple_size_v<value_type>;
574 if constexpr(sz == 2)
575 {
576 return score::gfx::Types::Vec2;
577 }
578 else if constexpr(sz == 3)
579 {
580 return score::gfx::Types::Vec3;
581 }
582 else if constexpr(sz == 4)
583 {
584 return score::gfx::Types::Vec4;
585 }
586 }
587 return score::gfx::Types::Empty;
588 }
589 template <std::size_t I, typename F>
590 constexpr auto operator()(avnd::field_reflection<I, F> p)
591 {
592 return score::gfx::Types::Empty;
593 }
594};
595
596// Compile-time port flags derived from a field's declarative metadata.
597// Inspects:
598// - `texture_target` (texture_kind_of) — non-2D textures bypass the
599// local-RT allocation and grab the upstream texture directly.
600// - `samplable_depth` (samplable_depth_of) — opt-in to having the
601// framework allocate a sampleable depth attachment on the producing
602// edge's RT and expose its handle through `texture.depth_handle`,
603// mirroring the semantics CSF/ISF shaders get via "DEPTH": true.
604template <typename Field>
605constexpr score::gfx::Flag port_flags_for_field() noexcept
606{
607 if constexpr(avnd::gpu_texture_port<Field>)
608 {
609 constexpr auto kind = halp::texture_kind_of<Field>();
610 constexpr bool nonD2 = (kind != halp::texture_kind::texture_2d);
611 constexpr bool depth = halp::samplable_depth_of<Field>();
612 if constexpr(nonD2 && depth)
613 return score::gfx::Flag::GrabsFromSource | score::gfx::Flag::SamplableDepth;
614 else if constexpr(nonD2)
615 return score::gfx::Flag::GrabsFromSource;
616 else if constexpr(depth)
617 return score::gfx::Flag::SamplableDepth;
618 }
619 return score::gfx::Flag{};
620}
621
622// Map QRhi's depth-format taxonomy onto halp's depth_format_t.
623// The 4-arg subset matches every depth format score's createRenderTarget
624// can produce (today always D32F, but the API accepts the others).
625inline constexpr halp::gpu_texture::depth_format_t qrhiToHalpDepthFormat(
626 QRhiTexture::Format f) noexcept
627{
628 using D = halp::gpu_texture::depth_format_t;
629 switch(f)
630 {
631 case QRhiTexture::D16: return D::D16;
632 case QRhiTexture::D24: return D::D24;
633 case QRhiTexture::D24S8: return D::D24S8;
634 case QRhiTexture::D32F: return D::D32F;
635 default: break;
636 }
637 return D::D32F;
638}
639
640template <typename Node_T>
641inline void initGfxPorts(auto* self, auto& input, auto& output)
642{
643 avnd::input_introspection<Node_T>::for_all(
644 [self, &input]<typename Field, std::size_t I>(avnd::field_reflection<I, Field> f) {
645 static constexpr auto type = port_to_type_enum{}(f);
646 static constexpr auto flags = port_flags_for_field<Field>();
647 input.push_back(new score::gfx::Port{self, {}, type, flags, {}});
648 });
649 avnd::output_introspection<Node_T>::for_all(
650 [self,
651 &output]<typename Field, std::size_t I>(avnd::field_reflection<I, Field> f) {
652 static constexpr auto type = port_to_type_enum{}(f);
653 // port_flags_for_field encodes INPUT-side sink semantics
654 // (GrabsFromSource → "sample the upstream's texture directly";
655 // SamplableDepth → "ask the producer for a sampleable depth
656 // attachment"). Neither has any meaning on an OUTPUT port — emitting
657 // them here would make the graph treat this node's own output as if it
658 // grabbed from / sampled some upstream source. Outputs carry no such
659 // flags.
660 output.push_back(new score::gfx::Port{self, {}, type, score::gfx::Flag{}, {}});
661 });
662}
663
664static score::gfx::BufferView getInputBuffer(
665 score::gfx::RenderList& renderer, const score::gfx::Node& parent, int port_index)
666{
667 const auto& inputs = parent.input;
668 // SCORE_ASSERT(port_index == 0);
669 {
670 score::gfx::Port* p = inputs[port_index];
671 for(auto& edge : p->edges)
672 {
673 auto src_node = edge->source->node;
674 score::gfx::NodeRenderer* src_renderer = src_node->renderedNodes.at(&renderer);
675 if(src_renderer)
676 {
677 return src_renderer->bufferForOutput(*edge->source);
678 }
679 break;
680 }
681 }
682 return {};
683}
684
685
686static void readbackInputBuffer(
687 score::gfx::RenderList& renderer
688 , QRhiResourceUpdateBatch& res
689 , const score::gfx::Node& parent
690 , QRhiBufferReadbackResult& readback
691 , int port_index
692 )
693{
694 // FIXME: instead of doing this we could do the readback in the
695 // producer node and just read its bytearray once...
696 if(auto buf = getInputBuffer(renderer, parent, port_index))
697 {
698 readback = {};
699 res.readBackBuffer(buf.handle, buf.byte_offset, buf.byte_size, &readback);
700 }
701}
702
703static void recreateOutputBuffer(
704 score::gfx::RenderList& renderer, avnd::cpu_buffer auto& cpu_buf,
705 QRhiResourceUpdateBatch& res, score::gfx::BufferView& buf)
706{
707 const auto bytesize = avnd::get_bytesize(cpu_buf);
708 if(!buf.handle)
709 {
710 if(bytesize > 0)
711 {
712 buf.handle = renderer.state.rhi->newBuffer(
713 QRhiBuffer::Static,
715 *renderer.state.rhi,
716 QRhiBuffer::StorageBuffer | QRhiBuffer::VertexBuffer),
717 bytesize);
718 buf.handle->setName("GpuUtils::recreateOutputBuffer");
719 buf.byte_offset = 0;
720 buf.byte_size = bytesize;
721
722 buf.handle->create();
723 }
724 else
725 {
726 cpu_buf.changed = false;
727 return;
728 }
729 }
730 else if(buf.handle->size() != bytesize)
731 {
732 buf.handle->destroy();
733 buf.handle->setSize(bytesize);
734 buf.handle->create();
735 buf.byte_size = bytesize;
736 }
737}
738
739static void uploadOutputBuffer(
740 score::gfx::RenderList& renderer, avnd::cpu_buffer auto& cpu_buf,
741 QRhiResourceUpdateBatch& res, score::gfx::BufferView& rhi_buf)
742{
743 if(cpu_buf.changed)
744 {
745 recreateOutputBuffer(renderer, cpu_buf, res, rhi_buf);
747 &res, rhi_buf.handle, 0, cpu_buf.byte_size,
748 (const char*)avnd::get_bytes(cpu_buf));
749 cpu_buf.changed = false;
750 }
751}
752
753static void uploadOutputBuffer(
754 score::gfx::RenderList& renderer, avnd::gpu_buffer auto& gpu_buf,
755 QRhiResourceUpdateBatch& res, score::gfx::BufferView& rhi_buf)
756{
757 rhi_buf.handle = reinterpret_cast<QRhiBuffer*>(gpu_buf.handle);
758 rhi_buf.byte_size = gpu_buf.byte_size;
759 rhi_buf.byte_offset = gpu_buf.byte_offset;
760}
761
762template <typename T>
763struct geometry_inputs_storage;
764
765struct mesh_input_storage
766{
767 std::vector<QRhiBufferReadbackResult> readbacks;
768 std::vector<QRhiBuffer*> buffers;
769};
770struct geometry_input_storage
771{
772 ossia::geometry_spec spec;
773 std::vector<mesh_input_storage> meshes;
774};
775
776template <typename T>
777 requires(avnd::geometry_input_introspection<T>::size > 0)
778struct geometry_inputs_storage<T>
779{
780 // FIXME in Gfx/Graph/NodeRenderer.hpp
781 static_assert(avnd::geometry_input_introspection<T>::size == 1);
782
783 geometry_input_storage inputs[avnd::geometry_input_introspection<T>::size];
784 ossia::small_vector<QRhiBuffer*, 4> allocated;
785
786 void readInputGeometries(
787 score::gfx::RenderList& renderer, const ossia::geometry_spec& spec, auto& parent,
788 auto& state)
789 {
790 // Copy the readback output inside the structure
791 // TODO it would be much better to do this inside the readback's
792 // "completed" callback.
793 avnd::geometry_input_introspection<T>::for_all_n(
794 avnd::get_inputs<T>(state),
795 [&]<typename Field, std::size_t N>(Field& t, avnd::predicate_index<N> np) {
796 this->inputs[N].spec = spec; // FIXME multiple geometry input ports
797 this->inputs[N].meshes.resize(1); // FIXME
798
799 // Here we fetch the readbacks results
800 auto& meshes = this->inputs[N].meshes[0];
801
802 oscr::meshes_from_ossia(
803 spec.meshes, t.mesh,
804 [&](auto& write_buf, int buffer_index, void* data, int64_t bytesize) {
805 // CPU input geometry, upload was done before
806 SCORE_ASSERT(buffer_index >= 0);
807 if(buffer_index < meshes.readbacks.size())
808 {
809 QRhiBuffer* handle = meshes.buffers[buffer_index];
810 write_buf.handle = handle;
811 write_buf.byte_size = handle->size();
812 }
813 }, [&](auto& write_buf, int buffer_index, void* handle) {
814 // GPU input buffer, CPU output buffer: need to fetch our readback
815 SCORE_ASSERT(buffer_index >= 0);
816 if(buffer_index < meshes.readbacks.size())
817 {
818 // FIXME investigate why runInitialPasses is called before inputAboutToFinish
819 auto& readback = meshes.readbacks[buffer_index].data;
820 write_buf.raw_data = reinterpret_cast<unsigned char*>(readback.data());
821 write_buf.byte_size = readback.size();
822 }
823 });
824 });
825 }
826
827 void inputAboutToFinish(
828 score::gfx::RenderList& renderer, QRhiResourceUpdateBatch*& res,
829 const ossia::geometry_spec& spec, auto& state, auto& parent)
830 {
831 avnd::geometry_input_introspection<T>::for_all_n2(
832 avnd::get_inputs<T>(state),
833 [&]<typename Field, std::size_t N, std::size_t NField>(
834 Field& t, avnd::predicate_index<N> np, avnd::field_index<NField> nf) {
835 this->inputs[N].spec = spec; // FIXME multiple geometry input ports
836 this->inputs[N].meshes.resize(1); // FIXME
837 // Here we request readbacks if necessary
838
839 auto& meshes = this->inputs[N].meshes[0];
840 oscr::meshes_from_ossia(
841 spec.meshes, t.mesh,
842 [&](auto& write_buf, int buffer_index, void* data, int64_t bytesize) {
843 // cpu -> gpu
844 if(meshes.buffers.size() <= buffer_index)
845 {
846 meshes.buffers.resize(buffer_index + 1);
847 meshes.readbacks.resize(buffer_index + 1);
848
849 auto buf = renderer.state.rhi->newBuffer(
850 QRhiBuffer::Static,
851 score::gfx::compatibleBufferUsage(
852 *renderer.state.rhi,
853 QRhiBuffer::StorageBuffer | QRhiBuffer::VertexBuffer),
854 bytesize);
855 buf->setName(oscr::getUtf8Name<T>() + "::" + oscr::getUtf8Name(t));
856 buf->create();
857 allocated.push_back(buf);
858 meshes.buffers[buffer_index] = buf;
859 }
860 else if(auto* existing = meshes.buffers[buffer_index];
861 existing && existing->size() < bytesize)
862 {
863 // Buffer exists but is too small — resize it.
864 existing->setSize(bytesize);
865 existing->create();
866 }
867
868 res->uploadStaticBuffer(meshes.buffers[buffer_index], 0, bytesize, data);
869 }, [&](auto& write_buf, int buffer_index, void* handle) {
870 // gpu -> cpu
871 if(meshes.readbacks.size() <= buffer_index)
872 {
873 meshes.buffers.resize(buffer_index + 1);
874 meshes.readbacks.resize(buffer_index + 1);
875 }
876
877 meshes.readbacks[buffer_index] = {};
878 if(auto buf = static_cast<QRhiBuffer*>(handle))
879 {
880 meshes.buffers[buffer_index] = buf;
881 res->readBackBuffer(buf, 0, buf->size(), &meshes.readbacks[buffer_index]);
882 }
883 else
884 {
885 meshes.buffers[buffer_index] = {};
886 meshes.readbacks[buffer_index] = {};
887 }
888 });
889 });
890 }
891
892 void release(score::gfx::RenderList& renderer)
893 {
894 for(auto& buf : allocated)
895 renderer.releaseBuffer(buf);
896 allocated.clear();
897 }
898};
899
900template <typename T>
901 requires(avnd::geometry_input_introspection<T>::size == 0)
902struct geometry_inputs_storage<T>
903{
904 static void readInputGeometries(auto&&...) { }
905
906 static void inputAboutToFinish(auto&&...) { }
907
908 static void release(auto&&...) { }
909};
910
911template<typename T>
912struct buffer_inputs_storage;
913
914template<typename T>
915 requires (avnd::buffer_input_introspection<T>::size > 0)
916struct buffer_inputs_storage<T>
917{
918 // +1 because of zero-array-size unsupported
919 QRhiBufferReadbackResult
920 m_readbacks[avnd::cpu_buffer_input_introspection<T>::size + 1];
921 score::gfx::BufferView m_gpubufs[avnd::gpu_buffer_input_introspection<T>::size + 1];
922
923 void readInputBuffers(
924 score::gfx::RenderList& renderer, auto& parent, auto& state)
925 {
926 if constexpr(avnd::cpu_buffer_input_introspection<T>::size > 0)
927 {
928 // Copy the readback output inside the structure
929 // TODO it would be much better to do this inside the readback's
930 // "completed" callback.
931 avnd::cpu_buffer_input_introspection<T>::for_all_n(
932 avnd::get_inputs<T>(state),
933 [&]<typename Field, std::size_t N>
934 (Field& t, avnd::predicate_index<N> np)
935 {
936 auto& readback = m_readbacks[N].data;
937 t.buffer.raw_data = reinterpret_cast<unsigned char*>(readback.data());
938 t.buffer.byte_size = readback.size();
939 t.buffer.byte_offset = 0; // FIXME
940 t.buffer.changed = true;
941 });
942 }
943
944 if constexpr(avnd::gpu_buffer_input_introspection<T>::size > 0)
945 {
946 // Copy the readback output inside the structure
947 // TODO it would be much better to do this inside the readback's
948 // "completed" callback.
949 avnd::gpu_buffer_input_introspection<T>::for_all_n2(
950 avnd::get_inputs<T>(state),
951 [&]<typename Field, std::size_t N, std::size_t NField>(
952 Field& t, avnd::predicate_index<N> np, avnd::field_index<NField> nf) {
953 score::gfx::BufferView& buf = m_gpubufs[N];
954 if(!buf)
955 buf = getInputBuffer(renderer, parent, nf);
956 if(!buf)
957 return;
958 t.buffer.handle = buf.handle;
959 t.buffer.byte_size = buf.byte_size;
960 t.buffer.byte_offset = buf.byte_offset;
961 // t.buffer.changed = true; FIXME
962 });
963 }
964 }
965
966 void inputAboutToFinish(
967 score::gfx::RenderList& renderer,
968 QRhiResourceUpdateBatch*& res,
969 auto& state,
970 auto& parent)
971 {
972 avnd::cpu_buffer_input_introspection<T>::for_all_n2(
973 avnd::get_inputs<T>(state),
974 [&]<typename Field, std::size_t N, std::size_t NField>
975 (Field& port, avnd::predicate_index<N> np, avnd::field_index<NField> nf) {
976 readbackInputBuffer(renderer, *res, parent, m_readbacks[N], nf);
977 });
978 avnd::gpu_buffer_input_introspection<T>::for_all_n2(
979 avnd::get_inputs<T>(state),
980 [&]<typename Field, std::size_t N, std::size_t NField>
981 (Field& port, avnd::predicate_index<N> np, avnd::field_index<NField> nf) {
982 m_gpubufs[N] = getInputBuffer(renderer, parent, nf);
983 });
984 }
985};
986
987template<typename T>
988 requires (avnd::buffer_input_introspection<T>::size == 0)
989struct buffer_inputs_storage<T>
990{
991 static void readInputBuffers(auto&&...)
992 {
993
994 }
995
996 static void inputAboutToFinish(auto&&...)
997 {
998
999 }
1000};
1001
1002struct MaybeOwnedBuffer : score::gfx::BufferView
1003{
1004 bool owned{false};
1005};
1006
1007template<typename T>
1008struct buffer_outputs_storage;
1009
1010template<typename T>
1011 requires (avnd::buffer_output_introspection<T>::size > 0)
1012struct buffer_outputs_storage<T>
1013{
1014 std::pair<const score::gfx::Port*, MaybeOwnedBuffer>
1015 m_buffers[avnd::buffer_output_introspection<T>::size];
1016
1017 QRhiResourceUpdateBatch* currentResourceUpdateBatch{};
1018
1021 score::gfx::RenderList* m_renderer{};
1022
1023 template <typename Field, std::size_t N, std::size_t NField>
1024 requires avnd::cpu_buffer<std::decay_t<decltype(Field::buffer)>>
1025 void createOutput(
1026 score::gfx::RenderList& renderer, auto& parent, Field& port,
1027 avnd::predicate_index<N> np, avnd::field_index<NField> nf)
1028 {
1029 auto& [gfx_port, buf] = m_buffers[N];
1030 gfx_port = parent.output[nf];
1031 buf.handle = renderer.state.rhi->newBuffer(
1032 QRhiBuffer::Static,
1034 *renderer.state.rhi,
1035 QRhiBuffer::StorageBuffer | QRhiBuffer::VertexBuffer), 1);
1036 buf.handle->setName(oscr::getUtf8Name<T>() + "::" + oscr::getUtf8Name(port));
1037 buf.byte_offset = 0;
1038 buf.byte_size = 1;
1039 buf.owned = true;
1040
1041 buf.handle->create();
1042
1043 m_renderer = &renderer;
1044 bindUpload(port, np);
1045 }
1046
1054 template <typename Field, std::size_t N>
1055 void bindUpload(Field& port, avnd::predicate_index<N>)
1056 {
1057 port.buffer.upload
1058 = [this, &port](const char* data, int64_t offset, int64_t bytesize) {
1059 // FIXME is offset and bytesize relative to the input or the output data ?
1060 SCORE_ASSERT(currentResourceUpdateBatch);
1061 SCORE_ASSERT(m_renderer);
1062 auto& rhi = *m_renderer->state.rhi;
1063 auto& [gfx_port, buf] = m_buffers[N];
1064
1065 if(!buf.handle)
1066 {
1067 if(bytesize > 0)
1068 {
1069 buf.handle = rhi.newBuffer(
1070 QRhiBuffer::Static,
1072 rhi, QRhiBuffer::StorageBuffer | QRhiBuffer::VertexBuffer),
1073 bytesize);
1074 buf.handle->setName(oscr::getUtf8Name<T>() + "::" + oscr::getUtf8Name(port));
1075 buf.byte_offset = 0;
1076 buf.byte_size = bytesize;
1077 buf.owned = true;
1078
1079 buf.handle->create();
1080 }
1081 else
1082 {
1083 buf.handle = rhi.newBuffer(
1084 QRhiBuffer::Static,
1086 rhi, QRhiBuffer::StorageBuffer | QRhiBuffer::VertexBuffer),
1087 1);
1088 buf.handle->setName(oscr::getUtf8Name<T>() + "::" + oscr::getUtf8Name(port));
1089 buf.byte_offset = 0;
1090 buf.byte_size = 1;
1091 buf.owned = true;
1092
1093 buf.handle->create();
1094 return;
1095 }
1096 }
1097 else if(buf.handle->size() != bytesize)
1098 {
1099 buf.handle->destroy();
1100 buf.handle->setSize(bytesize);
1101 buf.handle->create();
1102 buf.byte_size = bytesize;
1103 }
1104
1106 currentResourceUpdateBatch, buf.handle, offset, bytesize, data);
1107 };
1108 }
1109
1112 void bindUploads(auto& state)
1113 {
1114 avnd::buffer_output_introspection<T>::for_all_n(
1115 avnd::get_outputs<T>(state),
1116 [this]<typename Field, std::size_t N>(Field& port, avnd::predicate_index<N> np) {
1117 if constexpr(avnd::cpu_buffer<std::decay_t<decltype(Field::buffer)>> && requires {
1118 port.buffer.upload(nullptr, 0, 0);
1119 })
1120 {
1121 bindUpload(port, np);
1122 }
1123 });
1124 }
1125
1126 template <typename Field, std::size_t N, std::size_t NField>
1127 requires avnd::gpu_buffer<std::decay_t<decltype(Field::buffer)>>
1128 void createOutput(
1129 score::gfx::RenderList& renderer, auto& parent, Field& port,
1130 avnd::predicate_index<N> np, avnd::field_index<NField> nf)
1131 {
1132 auto& [gfx_port, buf] = m_buffers[N];
1133 gfx_port = parent.output[nf];
1134 buf.handle = reinterpret_cast<QRhiBuffer*>(port.buffer.handle);
1135 buf.byte_size = port.buffer.byte_size;
1136 buf.byte_offset = port.buffer.byte_offset;
1137 buf.owned = false;
1138 }
1139
1140 void init(score::gfx::RenderList& renderer, auto& state, auto& parent)
1141 {
1142 // Init buffers for the outputs
1143 avnd::buffer_output_introspection<T>::for_all_n2(
1144 avnd::get_outputs<T>(state), [&]<typename Field, std::size_t N, std::size_t NField>
1145 (Field& port, avnd::predicate_index<N> np, avnd::field_index<NField> nf) {
1146 SCORE_ASSERT(parent.output.size() > nf);
1147 SCORE_ASSERT(parent.output[nf]->type == score::gfx::Types::Buffer);
1148 using buffer_type = std::decay_t<decltype(port.buffer)>;
1149
1150 if constexpr(avnd::cpu_raw_buffer<buffer_type> && requires {
1151 port.buffer.upload(nullptr, 0, 0);
1152 })
1153 {
1154 createOutput(renderer, parent, port, np, nf);
1155 }
1156 else if constexpr(avnd::gpu_buffer<buffer_type>)
1157 {
1158 createOutput(renderer, parent, port, np, nf);
1159 }
1160 else
1161 {
1162 // m_buffers[N] = createOutput(renderer, *parent.output[nf], port.buffer);
1163 static_assert(std::is_same_v<T, void>, "unsupported");
1164 }
1165 });
1166 }
1167
1168 void prepareUpload(QRhiResourceUpdateBatch& res)
1169 {
1170 currentResourceUpdateBatch = &res;
1171 }
1172
1173 void upload(score::gfx::RenderList& renderer, auto& state, QRhiResourceUpdateBatch& res)
1174 {
1175 avnd::buffer_output_introspection<T>::for_all_n(
1176 avnd::get_outputs<T>(state), [&]<std::size_t N>(auto& t, avnd::predicate_index<N> idx) {
1177 auto& [port, buf] = m_buffers[N];
1178 uploadOutputBuffer(renderer, t.buffer, res, buf);
1179 });
1180 }
1181
1182 void release(score::gfx::RenderList& renderer)
1183 {
1184 // Free outputs
1185 for(auto& [p, buf] : m_buffers)
1186 {
1187 if(buf.owned)
1188 renderer.releaseBuffer(buf.handle);
1189 buf.handle = nullptr;
1190 buf.owned = false;
1191 }
1192 }
1193};
1194
1195template<typename T>
1196 requires (avnd::buffer_output_introspection<T>::size == 0)
1197struct buffer_outputs_storage<T>
1198{
1199 static void init(auto&&...)
1200 {
1201
1202 }
1203
1204 static void prepareUpload(auto&&...)
1205 {
1206 }
1207
1208 static void bindUploads(auto&&...)
1209 {
1210 }
1211
1212 static void upload(auto&&...)
1213 {
1214 }
1215
1216 static void release(auto&&...)
1217 {
1218 }
1219};
1220
1221
1222template <typename Tex>
1223static auto
1224createOutputTexture(score::gfx::RenderList& renderer, const Tex& texture_spec, QSize size)
1225{
1226 auto& rhi = *renderer.state.rhi;
1227 QRhiTexture* texture = &renderer.emptyTexture();
1228 if(size.width() > 0 && size.height() > 0)
1229 {
1230 texture = rhi.newTexture(
1231 gpp::qrhi::textureFormat(texture_spec), size, 1, QRhiTexture::Flag{});
1232
1233 texture->create();
1234 }
1235
1236 auto sampler = rhi.newSampler(
1237 QRhiSampler::Linear, QRhiSampler::Linear, QRhiSampler::None,
1238 QRhiSampler::ClampToEdge, QRhiSampler::ClampToEdge);
1239
1240 sampler->create();
1241 return score::gfx::Sampler{sampler, texture};
1242}
1243
1244
1245template<typename T>
1246struct texture_inputs_storage;
1247
1248template<typename T>
1249 requires (avnd::texture_input_introspection<T>::size > 0)
1250struct texture_inputs_storage<T>
1251{
1252 ossia::small_flat_map<const score::gfx::Port*, score::gfx::TextureRenderTarget, 2>
1253 m_rts;
1254
1255 QRhiReadbackResult m_readbacks[avnd::texture_input_introspection<T>::size];
1256
1257 template <typename Tex>
1258 QRhiTexture* createInput(
1259 score::gfx::RenderList& renderer, score::gfx::Port* port, Tex& texture_spec,
1260 const score::gfx::RenderTargetSpecs& spec, bool wantsSamplableDepth = false)
1261 {
1262 static constexpr auto flags
1263 = QRhiTexture::RenderTarget | QRhiTexture::UsedAsTransferSource;
1264 QRhiTexture::Format fmt{};
1265 if constexpr(requires (Tex tex) { tex.format = {}; } && !requires (Tex tex) { tex.request_format; })
1266 {
1267 // Format freely assignable: we use what the user sets in the GUI
1268 fmt = spec.format;
1269 gpp::qrhi::toTextureFormat(fmt, texture_spec);
1270 }
1271 else
1272 {
1273 fmt = gpp::qrhi::textureFormat(texture_spec);
1274 }
1275
1276 QRhiTexture* texture = renderer.state.rhi->newTexture(
1277 fmt, spec.size, 1, flags);
1278
1279 SCORE_ASSERT(texture->create());
1280 // wantsSamplableDepth implies wantsDepth: createRenderTarget allocates
1281 // a sampleable single-sample depth texture (with MSAA-resolve when
1282 // available) instead of a renderbuffer / non-resolve depth target.
1283 // Same shape ISF/CSF inputs get when their port has SamplableDepth.
1284 const bool wantsDepth = renderer.requiresDepth(*port) || wantsSamplableDepth;
1285 m_rts[port] = score::gfx::createRenderTarget(
1286 renderer.state, texture, renderer.samples(),
1287 wantsDepth, wantsSamplableDepth);
1288 return texture;
1289 }
1290
1291 void init(auto& self, score::gfx::RenderList& renderer)
1292 {
1293 // Init input render targets
1294 avnd::texture_input_introspection<T>::for_all_n2(
1295 avnd::get_inputs<T>(*self.state),
1296 [&]<typename F, std::size_t K, std::size_t N>(F& t, avnd::predicate_index<K>, avnd::field_index<N>) {
1297 // Non-2D GPU texture inputs (cube / array / 3D) don't get a local
1298 // render target — the port carries Flag::GrabsFromSource (set by
1299 // initGfxPorts via texture_kind_of<F>()), the graph will populate
1300 // t.texture.handle through updateInputTexture when the edge
1301 // resolves. Skipping the allocation here avoids wasting a 2D
1302 // colour attachment that would never be rendered into anyway.
1303 if constexpr(avnd::gpu_texture_port<F>
1304 && halp::texture_kind_of<F>() != halp::texture_kind::texture_2d)
1305 {
1306 t.texture.kind = halp::texture_kind_of<F>();
1307 // Handle + size populated later by updateInputTexture once the
1308 // upstream is resolved.
1309 return;
1310 }
1311
1312 auto& parent = self.node();
1313 auto spec = parent.resolveRenderTargetSpecs(N, renderer);
1314 if constexpr(requires {
1315 t.request_width;
1316 t.request_height;
1317 })
1318 {
1319 spec.size.rwidth() = t.request_width;
1320 spec.size.rheight() = t.request_height;
1321 }
1322
1323 constexpr bool wantsSamplableDepth
1324 = avnd::gpu_texture_port<F> && halp::samplable_depth_of<F>();
1325 auto tex = createInput(
1326 renderer, parent.input[N], t.texture, spec, wantsSamplableDepth);
1327 if constexpr(avnd::cpu_texture_port<F>)
1328 {
1329 t.texture.width = spec.size.width();
1330 t.texture.height = spec.size.height();
1331 }
1332 else if constexpr(avnd::gpu_texture_port<F>)
1333 {
1334 t.texture.handle = tex;
1335 t.texture.width = spec.size.width();
1336 t.texture.height = spec.size.height();
1337 if constexpr(wantsSamplableDepth)
1338 {
1339 // The local RT just allocated owns a sampleable depth texture
1340 // that the upstream renders into when the edge runs — same
1341 // pointer, stable for the RT's lifetime, no per-frame refresh.
1342 const auto& rt = m_rts[parent.input[N]];
1343 t.texture.depth_handle = rt.depthTexture;
1344 if(rt.depthTexture)
1345 t.texture.depth_format = qrhiToHalpDepthFormat(rt.depthTexture->format());
1346 }
1347 }
1348 });
1349 }
1350
1351 bool update(auto& self,
1352 score::gfx::RenderList& renderer, QRhiResourceUpdateBatch& res)
1353 {
1354#if 0
1355 bool need_update = false;
1356 avnd::texture_input_introspection<T>::for_all_n2(
1357 avnd::get_inputs<T>(*self.state),
1358 [&]<typename F, std::size_t K, std::size_t N>(F& t, avnd::predicate_index<K>, avnd::field_index<N>) {
1359 if constexpr(requires {
1360 t.request_width;
1361 t.request_height;
1362 })
1363 {
1364 auto& parent = self.node();
1365 auto port = parent.input[N];
1366 const score::gfx::TextureRenderTarget& texture = m_rts[port];
1367 QSizeF sz{};
1368 if(texture.texture)
1369 sz = texture.texture->pixelSize();
1370 if(sz.width() != t.request_width || sz.height() != t.request_height)
1371 {
1372 // FIXME right now this doesn't work because
1373 // the render target spec is stored in the node.
1374 // Also the RenderList just recomputes everything anyways,
1375 // so we should just emit a "need to change" signal and abort as
1376 // long as things aren't more optimized and actually follow the graph
1377
1378 // m_rts[port].release();
1379
1380 // auto spec = parent.resolveRenderTargetSpecs(N, renderer);
1381 // spec.size.rwidth() = t.request_width;
1382 // spec.size.rheight() = t.request_height;
1383
1384 // createInput(renderer, port, t.texture, sz);
1385
1386 // t.texture.width = spec.size.width();
1387 // t.texture.height = spec.size.height();
1388 // need_update = true;
1389 //
1390 }
1391 }
1392 });
1393 return need_update;
1394#endif
1395 return false;
1396 }
1397
1398 void runInitialPasses(auto& self, QRhi& rhi)
1399 {
1400 // Fetch input textures (if any)
1401 // Copy the readback output inside the structure
1402 // TODO it would be much better to do this inside the readback's
1403 // "completed" callback.
1404 if constexpr(avnd::cpu_texture_input_introspection<T>::size > 0)
1405 {
1406 avnd::texture_input_introspection<T>::for_all_n(
1407 avnd::get_inputs<T>(*self.state), [&]<typename F, std::size_t K>(F& t, avnd::predicate_index<K>) {
1408 if constexpr(avnd::cpu_texture_port<F>)
1409 {
1410 oscr::loadInputTexture(rhi, m_readbacks, t.texture, K);
1411 }
1412 });
1413 }
1414 }
1415
1416 void release()
1417 {
1418 // Free inputs
1419 // TODO investigate why reference does not work here:
1420 for(auto [port, rt] : m_rts)
1421 rt.release();
1422 m_rts.clear();
1423 }
1424
1425 void inputAboutToFinish(auto& parent, const score::gfx::Port& p, QRhiResourceUpdateBatch*& res)
1426 {
1427 if constexpr(avnd::cpu_texture_input_introspection<T>::size > 0)
1428 {
1429 const auto& inputs = parent.input;
1430 auto index_of_port = ossia::find(inputs, &p) - inputs.begin();
1431 {
1432 auto tex = m_rts[&p].texture;
1433 auto& readback = m_readbacks[index_of_port];
1434 readback = {};
1435 res->readBackTexture(QRhiReadbackDescription{tex}, &readback);
1436 }
1437 }
1438 }
1439
1440};
1441template<typename T>
1442 requires (avnd::texture_input_introspection<T>::size == 0)
1443struct texture_inputs_storage<T>
1444{
1445 static void init(auto&&...) { }
1446 static void runInitialPasses(auto&&...) { }
1447 static void release(auto&&...) { }
1448 static void inputAboutToFinish(auto&&...) { }
1449};
1450
1451
1452
1453template <avnd::cpu_texture Tex>
1454static QRhiTexture* updateTexture(auto& self, score::gfx::RenderList& renderer, int k, const Tex& cpu_tex)
1455{
1456 auto& [sampler, texture, fb_] = self.m_samplers[k];
1457 if(texture)
1458 {
1459 auto sz = texture->pixelSize();
1460 if(cpu_tex.width == sz.width() && cpu_tex.height == sz.height())
1461 return texture;
1462 }
1463
1464 // Check the texture size
1465 if(cpu_tex.width > 0 && cpu_tex.height > 0)
1466 {
1467 QRhiTexture* oldtex = texture;
1468 QRhiTexture* newtex = renderer.state.rhi->newTexture(
1469 gpp::qrhi::textureFormat(cpu_tex), QSize{cpu_tex.width, cpu_tex.height}, 1,
1470 QRhiTexture::Flag{});
1471 newtex->create();
1472 for(auto& [edge, pass] : self.m_p)
1473 if(pass.p.srb)
1474 score::gfx::replaceTexture(*pass.p.srb, sampler, newtex);
1475 texture = newtex;
1476
1477 if(oldtex && oldtex != &renderer.emptyTexture())
1478 {
1479 oldtex->deleteLater();
1480 }
1481
1482 return newtex;
1483 }
1484 else
1485 {
1486 for(auto& [edge, pass] : self.m_p)
1487 if(pass.p.srb)
1488 score::gfx::replaceTexture(*pass.p.srb, sampler, &renderer.emptyTexture());
1489
1490 return &renderer.emptyTexture();
1491 }
1492}
1493
1494template <avnd::cpu_texture Tex>
1495static void uploadOutputTexture(auto& self,
1496 score::gfx::RenderList& renderer, int k, Tex& cpu_tex,
1497 QRhiResourceUpdateBatch* res)
1498{
1499 if(cpu_tex.changed)
1500 {
1501 if(auto texture = updateTexture(self, renderer, k, cpu_tex))
1502 {
1503 if(!cpu_tex.bytes || cpu_tex.bytesize() <= 0)
1504 {
1505 cpu_tex.changed = false;
1506 return;
1507 }
1508
1509 QByteArray buf
1510 = QByteArray::fromRawData((const char*)cpu_tex.bytes, cpu_tex.bytesize());
1511 if constexpr(requires { Tex::RGB; })
1512 {
1513 // RGB -> RGBA
1514 // FIXME other conversions
1515 const QByteArray rgb = buf;
1516 QByteArray rgba;
1517 rgba.resize(cpu_tex.width * cpu_tex.height * 4);
1518 auto src = (const unsigned char*)rgb.constData();
1519 auto dst = (unsigned char*)rgba.data();
1520 for(int rgb_byte = 0, rgba_byte = 0, N = rgb.size(); rgb_byte < N;)
1521 {
1522 dst[rgba_byte + 0] = src[rgb_byte + 0];
1523 dst[rgba_byte + 1] = src[rgb_byte + 1];
1524 dst[rgba_byte + 2] = src[rgb_byte + 2];
1525 dst[rgba_byte + 3] = 255;
1526 rgb_byte += 3;
1527 rgba_byte += 4;
1528 }
1529 buf = rgba;
1530 }
1531
1532 // Upload it (mirroring is done in shader generic_texgen_fs if necessary)
1533 {
1534 QRhiTextureSubresourceUploadDescription sd(buf);
1535 QRhiTextureUploadDescription desc{QRhiTextureUploadEntry{0, 0, sd}};
1536
1537 res->uploadTexture(texture, desc);
1538 }
1539
1540 cpu_tex.changed = false;
1541 }
1542 }
1543}
1544
1545static const constexpr auto generic_texgen_vs = R"_(#version 450
1546layout(location = 0) in vec2 position;
1547layout(location = 1) in vec2 texcoord;
1548
1549layout(binding=3) uniform sampler2D y_tex;
1550layout(location = 0) out vec2 v_texcoord;
1551
1552layout(std140, binding = 0) uniform renderer_t {
1553 mat4 clipSpaceCorrMatrix;
1554 vec2 renderSize;
1555} renderer;
1556
1557out gl_PerVertex { vec4 gl_Position; };
1558
1559void main()
1560{
1561#if defined(QSHADER_SPIRV) || defined(QSHADER_GLSL)
1562 v_texcoord = vec2(texcoord.x, 1. - texcoord.y);
1563#else
1564 v_texcoord = texcoord;
1565#endif
1566 gl_Position = renderer.clipSpaceCorrMatrix * vec4(position.xy, 0.0, 1.);
1567}
1568)_";
1569
1570static const constexpr auto generic_texgen_fs = R"_(#version 450
1571layout(location = 0) in vec2 v_texcoord;
1572layout(location = 0) out vec4 fragColor;
1573
1574layout(std140, binding = 0) uniform renderer_t {
1575mat4 clipSpaceCorrMatrix;
1576vec2 renderSize;
1577} renderer;
1578
1579layout(binding=3) uniform sampler2D y_tex;
1580
1581void main ()
1582{
1583 fragColor = texture(y_tex, v_texcoord);
1584}
1585)_";
1586
1587template<typename T>
1588struct texture_outputs_storage;
1589
1590// If we have texture outs we need the whole rendering infrastructure
1591template<typename T>
1592 requires (avnd::texture_output_introspection<T>::size > 0)
1593struct texture_outputs_storage<T>
1594{
1595 void init(auto& self, score::gfx::RenderList& renderer, QRhiResourceUpdateBatch& res)
1596 {
1597 const auto& mesh = renderer.defaultTriangle();
1598 self.defaultMeshInit(renderer, mesh, res);
1599 self.processUBOInit(renderer);
1600 // Not needed here as we do not have a GPU pass:
1601 // this->m_material.init(renderer, this->node.input, this->m_samplers);
1602
1603 std::tie(self.m_vertexS, self.m_fragmentS)
1604 = score::gfx::makeShaders(renderer.state, generic_texgen_vs, generic_texgen_fs);
1605
1606 avnd::cpu_texture_output_introspection<T>::for_all(
1607 avnd::get_outputs<T>(*self.state), [&](auto& t) {
1608 self.m_samplers.push_back(
1609 createOutputTexture(renderer, t.texture, QSize{t.texture.width, t.texture.height}));
1610 });
1611
1612 self.defaultPassesInit(renderer, mesh);
1613 }
1614
1615 void runInitialPasses(auto& self,
1616 score::gfx::RenderList& renderer,
1617 QRhiResourceUpdateBatch*& res)
1618 {
1619 avnd::cpu_texture_output_introspection<T>::for_all_n(
1620 avnd::get_outputs<T>(*self.state), [&]<std::size_t N>(auto& t, avnd::predicate_index<N>) {
1621 uploadOutputTexture(self, renderer, N, t.texture, res);
1622 });
1623 }
1624
1625 void release(auto& self, score::gfx::RenderList& r)
1626 {
1627 // Free outputs
1628 for(auto& [sampl, texture, fb_] : self.m_samplers)
1629 {
1630 if(texture != &r.emptyTexture())
1631 texture->deleteLater();
1632 texture = nullptr;
1633 }
1634 }
1635
1636};
1637
1638template<typename T>
1639 requires (avnd::texture_output_introspection<T>::size == 0)
1640struct texture_outputs_storage<T>
1641{
1642 static void init(auto& self, score::gfx::RenderList& renderer, QRhiResourceUpdateBatch& res)
1643 {
1644 }
1645
1646 static void runInitialPasses(auto& self,
1647 score::gfx::RenderList& renderer,
1648 QRhiResourceUpdateBatch*& res)
1649 {
1650 }
1651
1652 static void release(auto& self, score::gfx::RenderList& r)
1653 {
1654 }
1655};
1656template<typename T>
1657struct geometry_outputs_storage;
1658
1659template<typename T>
1660 requires (avnd::geometry_output_introspection<T>::size > 0)
1661struct geometry_outputs_storage<T>
1662{
1663 ossia::geometry_spec specs[avnd::geometry_output_introspection<T>::size];
1664
1665 template <avnd::geometry_port Field>
1666 void reload_mesh(Field& ctrl, ossia::geometry_spec& spc)
1667 {
1668 spc.meshes = std::make_shared<ossia::mesh_list>();
1669 auto& ossia_meshes = *spc.meshes;
1670 if constexpr(avnd::static_geometry_type<Field> || avnd::dynamic_geometry_type<Field>)
1671 {
1672 ossia_meshes.meshes.resize(1);
1673 load_geometry(ctrl, ossia_meshes.meshes[0]);
1674 }
1675 else if constexpr(
1676 avnd::static_geometry_type<decltype(Field::mesh)>
1677 || avnd::dynamic_geometry_type<decltype(Field::mesh)>)
1678 {
1679 ossia_meshes.meshes.resize(1);
1680 load_geometry(ctrl.mesh, ossia_meshes.meshes[0]);
1681 }
1682 else
1683 {
1684 load_geometry(ctrl, ossia_meshes);
1685 }
1686 }
1687
1688 template <avnd::geometry_port Field, std::size_t N>
1689 void upload(
1690 score::gfx::RenderList& renderer, Field& ctrl, score::gfx::Edge& edge,
1691 avnd::predicate_index<N>)
1692 {
1693 auto edge_sink = edge.sink;
1694 if(auto pnode = edge_sink->node)
1695 {
1696 ossia::geometry_spec& spc = specs[N];
1697
1698 // 1. Reload mesh
1699 {
1700 if(ctrl.dirty_mesh)
1701 {
1702 reload_mesh(ctrl, spc);
1703 }
1704 else
1705 {
1706 if(spc.meshes)
1707 {
1708 auto& ossia_meshes = *spc.meshes;
1709
1710 bool any_need_reload = false;
1711 bool any_need_upload = false;
1712 if constexpr(avnd::static_geometry_type<Field> || avnd::dynamic_geometry_type<Field>)
1713 {
1714 SCORE_ASSERT(ossia_meshes.meshes.size() == 1);
1715 auto [need_reload, need_upload]
1716 = update_geometry(ctrl, ossia_meshes.meshes[0]);
1717 any_need_reload = need_reload;
1718 any_need_upload = need_upload;
1719 }
1720 else if constexpr(
1721 avnd::static_geometry_type<decltype(Field::mesh)>
1722 || avnd::dynamic_geometry_type<decltype(Field::mesh)>)
1723 {
1724 SCORE_ASSERT(ossia_meshes.meshes.size() == 1);
1725 auto [need_reload, need_upload]
1726 = update_geometry(ctrl.mesh, ossia_meshes.meshes[0]);
1727 any_need_reload = need_reload;
1728 any_need_upload = need_upload;
1729 }
1730 else
1731 {
1732 auto [need_reload, need_upload] = update_geometry(ctrl, ossia_meshes);
1733 any_need_reload = need_reload;
1734 any_need_upload = need_upload;
1735 }
1736
1737 if(any_need_reload)
1738 {
1739 reload_mesh(ctrl, spc);
1740 }
1741 }
1742 }
1743 ctrl.dirty_mesh = false;
1744 }
1745
1746 // 2. Push to next node
1747 // FIXME this should be for the renderer of edge, not the node, since
1748 // geometries can have gpu buffers
1749 auto rendered_node = pnode->renderedNodes.find(&renderer);
1750 SCORE_ASSERT(rendered_node != pnode->renderedNodes.end());
1751
1752 auto it = std::find(
1753 edge_sink->node->input.begin(), edge_sink->node->input.end(), edge_sink);
1754 SCORE_ASSERT(it != edge_sink->node->input.end());
1755 int n = it - edge_sink->node->input.begin();
1756
1757 rendered_node->second->process(n, spc, edge.source);
1758
1759 // 3. Same for transform3d
1760
1761 if constexpr(requires { ctrl.transform; })
1762 {
1763 if(ctrl.dirty_transform)
1764 {
1765 ossia::transform3d transform;
1766 std::copy_n(ctrl.transform, std::ssize(ctrl.transform), transform.matrix);
1767 ctrl.dirty_transform = false;
1768
1769 rendered_node->second->process(n, transform);
1770 if(auto pnode = dynamic_cast<score::gfx::ProcessNode*>(edge_sink->node))
1771 pnode->process(n, transform);
1772 }
1773 }
1774 }
1775 }
1776
1777 void upload(score::gfx::RenderList& renderer, auto& state, score::gfx::Edge& edge)
1778 {
1779 // FIXME we need something such as port_run_{pre,post}process for GPU nodes
1780 avnd::geometry_output_introspection<T>::for_all_n(
1781 avnd::get_outputs(state),
1782 [&](auto& field, auto pred) { this->upload(renderer, field, edge, pred); });
1783 }
1784
1785 // Lifecycle parity with the other *_outs storages. The geometry_spec
1786 // wrapper carries non-owning pointers + transform values, so release has
1787 // nothing to do; it exists so RHI handles added later have a hook.
1788 void release(score::gfx::RenderList&) noexcept { }
1789};
1790
1791
1792template<typename T>
1793 requires (avnd::geometry_output_introspection<T>::size == 0)
1794struct geometry_outputs_storage<T>
1795{
1796 static void upload(auto&&...)
1797 {
1798
1799 }
1800 static void release(auto&&...) noexcept { }
1801};
1802
1803// Scene output support (Crousti-side pending promotion to avendish).
1804// The `scene_port` concept and `scene_dirt_flags` live in SceneConcepts.hpp
1805// so the port-creation visitor in ProcessModelPortInit.hpp can reuse them.
1806
1807template <typename Field>
1808using is_scene_port_t = boost::mp11::mp_bool<scene_port<Field>>;
1809
1810template <typename T>
1811using scene_output_introspection =
1812 avnd::predicate_introspection<typename avnd::outputs_type<T>::type, is_scene_port_t>;
1813
1814template <typename T>
1815using scene_input_introspection =
1816 avnd::predicate_introspection<typename avnd::inputs_type<T>::type, is_scene_port_t>;
1817
1818// Scene input transport: NodeRenderer::process(port, scene_spec, source)
1819// already merges multi-producer scenes into `this->scene`, so scene_inputs_storage
1820// only needs to copy that merged scene_spec into each halp scene input field
1821// before operator()() runs. Cheap (shared_ptr assignment), no decode.
1822template <typename T>
1823struct scene_inputs_storage;
1824
1825template <typename T>
1826 requires(scene_input_introspection<T>::size > 0)
1827struct scene_inputs_storage<T>
1828{
1829 void readInputScenes(const ossia::scene_spec& scene, auto& state)
1830 {
1831 scene_input_introspection<T>::for_all(
1832 avnd::get_inputs<T>(state), [&](auto& field) { field.scene = scene; });
1833 }
1834
1835 static void release(score::gfx::RenderList&) { }
1836};
1837
1838template <typename T>
1839 requires(scene_input_introspection<T>::size == 0)
1840struct scene_inputs_storage<T>
1841{
1842 static void readInputScenes(auto&&...) { }
1843 static void release(auto&&...) { }
1844};
1845
1846template <typename T>
1847struct scene_outputs_storage;
1848
1849template <typename T>
1850 requires(scene_output_introspection<T>::size > 0)
1851struct scene_outputs_storage<T>
1852{
1853 template <scene_port Field, std::size_t N>
1854 void upload(
1855 score::gfx::RenderList& renderer, Field& ctrl, score::gfx::Edge& edge,
1856 avnd::predicate_index<N>)
1857 {
1858 // Publish the scene every frame rather than only when `ctrl.dirty` is
1859 // set: a once-only push gets overwritten by any other producer on the
1860 // same downstream inlet. Consumers short-circuit on shared_ptr
1861 // identity + version, so publishing every frame costs only refcount
1862 // bumps.
1863 if(!ctrl.scene.state)
1864 return;
1865
1866 auto* edge_sink = edge.sink;
1867 if(!edge_sink || !edge_sink->node)
1868 return;
1869
1870 auto rendered_node = edge_sink->node->renderedNodes.find(&renderer);
1871 if(rendered_node == edge_sink->node->renderedNodes.end())
1872 return;
1873
1874 auto it = std::find(
1875 edge_sink->node->input.begin(), edge_sink->node->input.end(), edge_sink);
1876 if(it == edge_sink->node->input.end())
1877 return;
1878 int n = it - edge_sink->node->input.begin();
1879
1880 // NodeRenderer::process(port, scene_spec, source_key) handles additive
1881 // merging across multiple producers converging on the same sink port
1882 // (keyed on the source edge's producer Port pointer), extracts a legacy
1883 // geometry_spec for downstream consumers that only understand geometry,
1884 // and sets sceneChanged=true.
1885 rendered_node->second->process(n, ctrl.scene, edge.source);
1886
1887 if constexpr(requires { ctrl.dirty; })
1888 ctrl.dirty = 0;
1889 }
1890
1891 void upload(score::gfx::RenderList& renderer, auto& state, score::gfx::Edge& edge)
1892 {
1893 scene_output_introspection<T>::for_all_n(
1894 avnd::get_outputs(state),
1895 [&](auto& field, auto pred) { this->upload(renderer, field, edge, pred); });
1896 }
1897
1898 // Lifecycle parity with texture_outputs_storage / buffer_outputs_storage:
1899 // the storage owns no QRhi resources (the scene_spec is a value-semantics
1900 // struct plus a shared_ptr to scene_state, both managed by their own
1901 // destructors), so release has nothing to do. It keeps CpuFilterNode /
1902 // CpuAnalysisNode releaseState symmetric across all storages, and gives
1903 // RHI handles added later a hook.
1904 void release(score::gfx::RenderList&) noexcept { }
1905};
1906
1907template <typename T>
1908 requires(scene_output_introspection<T>::size == 0)
1909struct scene_outputs_storage<T>
1910{
1911 static void upload(auto&&...) { }
1912 static void release(auto&&...) noexcept { }
1913};
1914
1915}
1916
1917#endif
Root data model for visual nodes.
Definition score-plugin-gfx/Gfx/Graph/Node.hpp:74
std::vector< Port * > input
Input ports of that node.
Definition score-plugin-gfx/Gfx/Graph/Node.hpp:103
ossia::flat_map< RenderList *, score::gfx::NodeRenderer * > renderedNodes
Map associating each RenderList to a Renderer for this model.
Definition score-plugin-gfx/Gfx/Graph/Node.hpp:114
virtual void process(Message &&msg)
Process a message from the execution engine.
Definition Node.cpp:25
ossia::small_pod_vector< Port *, 1 > output
Output ports of that node.
Definition score-plugin-gfx/Gfx/Graph/Node.hpp:109
Common base class for most single-pass, simple nodes.
Definition score-plugin-gfx/Gfx/Graph/Node.hpp:228
Renderer for a given node.
Definition NodeRenderer.hpp:11
Base class for sink nodes (QWindow, spout, syphon, NDI output, ...)
Definition OutputNode.hpp:54
Common base class for nodes that map to score processes.
Definition score-plugin-gfx/Gfx/Graph/Node.hpp:201
List of nodes to be rendered to an output.
Definition RenderList.hpp:30
bool requiresDepth(const score::gfx::Port &p) const noexcept
Whether this list of rendering actions requires depth testing at all.
Definition RenderList.cpp:945
const score::gfx::Mesh & defaultTriangle() const noexcept
A triangle mesh correct for this API.
Definition RenderList.cpp:984
RenderState & state
RenderState corresponding to this RenderList.
Definition RenderList.hpp:169
QRhiTexture & emptyTexture() const noexcept
Texture to use when a texture is missing (2D)
Definition RenderList.hpp:192
TreeNode< DeviceExplorerNode > Node
Definition DeviceNode.hpp:74
Definition Controls.hpp:27
std::pair< QShader, QShader > makeShaders(const RenderState &v, QString vert, QString frag, int multiViewCount)
Get a pair of compiled vertex / fragment shaders from GLSL 4.5 sources.
Definition score-plugin-gfx/Gfx/Graph/Utils.cpp:1238
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:60
void uploadStaticBufferWithStoredData(QRhiResourceUpdateBatch *ub, QRhiBuffer *buf, int offset, int64_t bytesize, const char *data)
Schedule a Static buffer update when we can guarantee the buffer outlives the frame.
Definition score-plugin-gfx/Gfx/Graph/Utils.hpp:751
QRhiBuffer::UsageFlags compatibleBufferUsage(QRhi &rhi, QRhiBuffer::UsageFlags usage) noexcept
Drop a StorageBuffer usage the backend cannot actually honour.
Definition RenderState.hpp:223
Base toolkit upon which the software is built.
Definition Application.cpp:117
STL namespace.
Definition DocumentContext.hpp:18
Definition Mesh.hpp:18
Connection between two score::gfx::Port.
Definition score-plugin-gfx/Gfx/Graph/Utils.hpp:103
Definition score-plugin-gfx/Gfx/Graph/Node.hpp:49
Definition OutputNode.hpp:15
Port of a score::gfx::Node.
Definition score-plugin-gfx/Gfx/Graph/Utils.hpp:82
score::gfx::Node * node
Parent node of the port.
Definition score-plugin-gfx/Gfx/Graph/Utils.hpp:84
Definition score-plugin-gfx/Gfx/Graph/Node.hpp:56
Stores a sampler and the texture currently associated with it.
Definition score-plugin-gfx/Gfx/Graph/Utils.hpp:47
Useful abstraction for storing all the data related to a render target.
Definition score-plugin-gfx/Gfx/Graph/Utils.hpp:152