Loading...
Searching...
No Matches
score-plugin-avnd/Crousti/Executor.hpp
1#pragma once
2
3#include <Process/Execution/ProcessComponent.hpp>
4#include <Process/ExecutionContext.hpp>
5#include <Process/ExecutionSetup.hpp>
6
7#include <Explorer/DocumentPlugin/DeviceDocumentPlugin.hpp>
8
9#include <Crousti/CpuAnalysisNode.hpp>
10#include <Crousti/CpuFilterNode.hpp>
11#include <Crousti/ExecutorPortSetup.hpp>
12#include <Crousti/ExecutorUpdateControlValueInUi.hpp>
13#include <Crousti/File.hpp>
14#include <Crousti/GpuComputeNode.hpp>
15#include <Crousti/GpuNode.hpp>
16#include <Crousti/MessageBus.hpp>
17#include <Crousti/Metadatas.hpp>
18#include <Crousti/ProcessModel.hpp>
19
20#include <score/tools/Bind.hpp>
21#include <ossia/detail/flat_set.hpp>
22#include <ossia/dataflow/exec_state_facade.hpp>
23#include <ossia/dataflow/graph/graph_interface.hpp>
24#include <ossia/dataflow/graph_edge.hpp>
25#include <ossia/dataflow/node_process.hpp>
26#include <ossia/network/context.hpp>
27
28#include <ossia-qt/invoke.hpp>
29
30#include <QGuiApplication>
31
32#include <flicks.h>
33
34#if SCORE_PLUGIN_GFX
35#include <Crousti/GpuNode.hpp>
36#include <Gfx/GfxApplicationPlugin.hpp>
37#endif
38
39#include <Scenario/Settings/ScenarioSettingsModel.hpp>
40
41#include <score/tools/ThreadPool.hpp>
42
43#include <ossia/detail/type_if.hpp>
44
45#include <QTimer>
46
47#include <avnd/binding/ossia/data_node.hpp>
48#include <avnd/binding/ossia/mono_audio_node.hpp>
49#include <avnd/binding/ossia/node.hpp>
50#include <avnd/binding/ossia/ossia_audio_node.hpp>
51#include <avnd/binding/ossia/ossia_dynamic_audio_node.hpp>
52#include <avnd/binding/ossia/poly_audio_node.hpp>
53#include <avnd/concepts/dynamic_items.hpp>
54#include <avnd/concepts/temporality.hpp>
55#include <avnd/concepts/ui.hpp>
56#include <avnd/concepts/worker.hpp>
57
58#include <QPointer>
59
60namespace oscr
61{
62
63template <typename Node>
64class CustomNodeProcess : public ossia::node_process
65{
66 using node_process::node_process;
67 void start() override
68 {
69 node_process::start();
70 auto& n = static_cast<safe_node<Node>&>(*node);
71 if_possible(n.impl.effect.start());
72 }
73 void pause() override
74 {
75 auto& n = static_cast<safe_node<Node>&>(*node);
76 if_possible(n.impl.effect.pause());
77 node_process::pause();
78 }
79
80 void resume() override
81 {
82 node_process::resume();
83 auto& n = static_cast<safe_node<Node>&>(*node);
84 if_possible(n.impl.effect.resume());
85 }
86
87 void stop() override
88 {
89 auto& n = static_cast<safe_node<Node>&>(*node);
90 if_possible(n.impl.effect.stop());
91 node_process::stop();
92 }
93
94 void offset_impl(ossia::time_value date) override
95 {
96 node_process::offset_impl(date);
97 auto& n = static_cast<safe_node<Node>&>(*node);
98 util::flicks f{date.impl};
99 if_possible(n.impl.effect.transport(f));
100 }
101
102 void transport_impl(ossia::time_value date) override
103 {
104 node_process::transport_impl(date);
105 auto& n = static_cast<safe_node<Node>&>(*node);
106
107 util::flicks f{date.impl};
108 if_possible(n.impl.effect.transport(f));
109 }
110};
111
112template <typename Node>
113class Executor final
114 : public Execution::ProcessComponent_T<ProcessModel<Node>, ossia::node_process>
115 , public dynamic_ports_component_data<Node>
116{
117public:
118 static Q_DECL_RELAXED_CONSTEXPR UuidKey<score::Component> static_key() noexcept
119 {
120 return uuid_from_string<Node>();
121 }
122
123 UuidKey<score::Component> key() const noexcept final override { return static_key(); }
124
125 bool key_match(UuidKey<score::Component> other) const noexcept final override
126 {
127 return static_key() == other || Execution::ProcessComponent::base_key_match(other);
128 }
129
130#if defined(SCORE_PLUGIN_GFX)
131 [[no_unique_address]] ossia::type_if<int, is_gpu<Node>> node_id = score::gfx::invalid_node_index;
132#endif
133
134 Executor(ProcessModel<Node>& element, const ::Execution::Context& ctx, QObject* p)
136 element, ctx, "Executor::ProcessModel<Info>", p}
137 {
138 if constexpr(is_gpu<Node>)
139 {
140#if SCORE_PLUGIN_GFX
141 setup_gpu(element, ctx, p);
142#endif
143 }
144 else
145 {
146 setup_cpu(element, ctx, p);
147 }
148
149 if constexpr(avnd::tag_process_exec<Node>)
150 {
151 this->m_ossia_process = std::make_shared<CustomNodeProcess<Node>>(this->node);
152 }
153 else
154 {
155 this->m_ossia_process = std::make_shared<ossia::node_process>(this->node);
156 }
157 }
158
159 void
160 setup_cpu(ProcessModel<Node>& element, const ::Execution::Context& ctx, QObject* p)
161 {
162 auto& net_ctx
163 = *ctx.doc.findPlugin<Explorer::DeviceDocumentPlugin>()->networkContext();
164 const auto id
165 = std::hash<ObjectPath>{}(Path<Process::ProcessModel>{element}.unsafePath());
166
167 auto st = ossia::exec_state_facade{ctx.execState.get()};
168 std::shared_ptr<safe_node<Node>> ptr;
169 auto node = new safe_node<Node>{st.bufferSize(), (double)st.sampleRate(), id};
170 node->root_inputs().reserve(element.inlets().size());
171 node->root_outputs().reserve(element.outlets().size());
172
173 node->prepare(*ctx.execState.get()); // Preparation of the ossia side
174
175 if_possible(node->impl.effect.ossia_state = st);
176 if_possible(node->impl.effect.io_context = &net_ctx.context);
177 if_possible(node->impl.effect.ossia_document_context = &ctx.doc);
178 ptr.reset(node);
179 this->node = ptr;
180
181 if constexpr(requires { ptr->impl.effect; })
182 if constexpr(std::is_same_v<std::decay_t<decltype(ptr->impl.effect)>, Node>)
183 {
184 connect_message_bus(element, ctx, ptr->impl.effect);
185 connect_dynamic_items(element, ptr->impl.effect);
186 }
187 connect_worker(ctx, ptr->impl);
188
189 node->dynamic_ports = element.dynamic_ports;
190 node->finish_init();
191
192 connect_controls(element, ctx, ptr);
193 update_controls(ptr);
194
195 // To call prepare() after evertyhing is ready
196 node->audio_configuration_changed(st);
197
198 if constexpr(oscr::has_dynamic_ports<Node>)
199 {
200 QObject::connect(
201 &element, &Process::ProcessModel::inletsChanged, this,
203 QObject::connect(
204 &element, &Process::ProcessModel::outletsChanged, this,
206
207 this->m_oldInlets = element.inlets();
208 this->m_oldOutlets = element.outlets();
209 }
210 }
211
212 void
213 setup_gpu(ProcessModel<Node>& element, const ::Execution::Context& ctx, QObject* p)
214 {
215#if SCORE_PLUGIN_GFX
216 // FIXME net context for gpu node ?
217 const int64_t id
218 = std::hash<ObjectPath>{}(Path<Process::ProcessModel>{element}.unsafePath());
219
220 auto& gfx_exec = ctx.doc.plugin<Gfx::DocumentPlugin>().exec;
221
222 // Create the executor in the audio thread
223 struct named_exec_node final : Gfx::gfx_exec_node
224 {
225 using Gfx::gfx_exec_node::gfx_exec_node;
226 std::string label() const noexcept override
227 {
228 return std::string(avnd::get_name<Node>());
229 }
230 };
231
232 auto node = std::make_shared<named_exec_node>(gfx_exec);
233 node->prepare(*ctx.execState);
234
235 this->node = node;
236
237 // Create the controls, inputs outputs etc.
238 std::size_t i = 0;
239
240 for(auto& ctl : element.inlets())
241 {
242 if(auto ctrl = qobject_cast<Process::ControlInlet*>(ctl))
243 {
244 auto& p = node->add_control();
245 p->value = ctrl->value();
246 p->changed = true;
247
248 QObject::connect(
249 ctrl, &Process::ControlInlet::valueChanged, this,
250 Gfx::con_unvalidated{ctx, i, 0, node});
251 i++;
252 }
253 else if(auto ctrl = qobject_cast<Process::ValueInlet*>(ctl))
254 {
255 auto& p = node->add_control();
256 p->changed = true;
257 i++;
258 }
259 else if(auto ctrl = qobject_cast<Process::AudioInlet*>(ctl))
260 {
261 node->add_audio();
262 }
263 else if(auto ctrl = qobject_cast<Gfx::TextureInlet*>(ctl))
264 {
265 ossia::texture_inlet& inl = *node->add_texture();
266 ctrl->setupExecution(inl, this);
267 }
268 else if(auto ctrl = qobject_cast<Gfx::GeometryInlet*>(ctl))
269 {
270 ossia::geometry_inlet& inl = *node->add_geometry();
271 ctrl->setupExecution(inl, this);
272 }
273 }
274
275 // FIXME refactor this with other GFX processes
276 for(auto* outlet : element.outlets())
277 {
278 if(auto ctrl = qobject_cast<Process::ControlOutlet*>(outlet))
279 {
280 node->add_control_out();
281 }
282 else if(auto ctrl = qobject_cast<Process::ValueOutlet*>(outlet))
283 {
284 node->add_control_out();
285 }
286 else if(auto out = qobject_cast<Gfx::TextureOutlet*>(outlet))
287 {
288 node->add_texture_out();
289 out->nodeId = node_id;
290 }
291 else if(auto out = qobject_cast<Gfx::GeometryOutlet*>(outlet))
292 {
293 node->add_geometry_out();
294 }
295 }
296
297 // Create the GPU node
298 std::weak_ptr qex_ptr = std::shared_ptr<Execution::ExecutionCommandQueue>(
299 ctx.alias.lock(), &ctx.executionQueue);
300 std::unique_ptr<score::gfx::Node> ptr;
301 if constexpr(GpuGraphicsNode2<Node>)
302 {
303 auto gpu_node = new CustomGpuNode<Node>(qex_ptr, node->control_outs, id, ctx.doc);
304 ptr.reset(gpu_node);
305 }
306 else if constexpr(GpuComputeNode2<Node>)
307 {
308 auto gpu_node = new GpuComputeNode<Node>(qex_ptr, node->control_outs, id, ctx.doc);
309 ptr.reset(gpu_node);
310 }
311 else if constexpr(GpuNode<Node>)
312 {
313 auto gpu_node
314 = new GfxNode<Node>(element, qex_ptr, node->control_outs, id, ctx.doc);
315 ptr.reset(gpu_node);
316 }
317
318 i = 0;
319 for(auto& ctl : element.inlets())
320 {
321 if(auto ctrl = qobject_cast<Gfx::TextureInlet*>(ctl))
322 {
323 ossia::texture_inlet& inl
324 = static_cast<ossia::texture_inlet&>(*node->root_inputs()[i]);
325 ptr->process(i, inl.data); // Setup render_target_spec
326 }
327 i++;
328 }
329 node->id = gfx_exec.ui->register_node(std::move(ptr));
330 node_id = node->id;
331#endif
332 }
333
347 {
348 if constexpr(oscr::has_dynamic_ports<Node> && !is_gpu<Node>)
349 {
350 using T = Node;
351 auto n = std::dynamic_pointer_cast<safe_node<Node>>(this->node);
352 if(!n)
353 return;
354
355 auto& element = this->process();
356 const Execution::Context& ctx = this->system();
357 Execution::SetupContext& setup = ctx.setup;
358
359 // The ports the exec node is currently set up for. Those that are not
360 // in the model anymore are about to be deleted (they are still alive
361 // here: ProcessModel deletes them after inletsChanged()).
362 const Process::Inlets old_inlets = std::move(this->m_oldInlets);
363 const Process::Outlets old_outlets = std::move(this->m_oldOutlets);
364 const Process::Inlets& new_inlets = element.inlets();
365 const Process::Outlets& new_outlets = element.outlets();
366
367 for(auto port : old_inlets)
368 if(!ossia::contains(new_inlets, port))
369 this->m_connectedControls.erase(port);
370
371 // The cables at either end of this process that are wired up in the
372 // graph: their edges point to the exec ports that are being replaced.
373 // A cable that is on the ports but not wired yet (restored by a command
374 // that announces it after inletsChanged()) is left to that announcement.
375 ossia::small_vector<Process::Cable*, 8> cables;
376 auto collect_cables = [&](const auto& ports) {
377 for(auto port : ports)
378 for(auto& cbl : port->cables())
379 if(auto c = cbl.try_find(ctx.doc))
380 if(setup.m_cables.find(c->id()) != setup.m_cables.end())
381 if(!ossia::contains(cables, c))
382 cables.push_back(c);
383 };
384 collect_cables(old_inlets);
385 collect_cables(old_outlets);
386 collect_cables(new_inlets);
387 collect_cables(new_outlets);
388
389 // Build the new set of exec ports.
390 struct port_storage
391 {
392 ossia::inlets new_inls_buffer;
393 ossia::outlets new_outls_buffer;
394 inlet_reload_storage<T> reload_inlet;
395 outlet_reload_storage<T> reload_outlet;
396 port_storage(std::size_t ins, std::size_t outs)
397 {
398 new_inls_buffer.reserve(ins);
399 new_outls_buffer.reserve(outs);
400 }
401 };
402 auto port_st = std::make_shared<port_storage>(new_inlets.size(), new_outlets.size());
403 auto& inbuf = port_st->new_inls_buffer;
404 auto& outbuf = port_st->new_outls_buffer;
405
406 // Same order as safe_node_base::reinit() + initialize_all_ports():
407 // first the ports that do not come from a field of the object
408 // (audio / value argument of the processor, message inlets), then the fields.
409 n->audio_ports.init(inbuf, outbuf);
410 n->arg_value_ports.init(inbuf, outbuf);
411 n->message_ports.init(inbuf);
412
413 auto& dp = element.dynamic_ports;
414 if constexpr(avnd::inputs_type<T>::size > 0)
415 {
416 using in_info = avnd::input_introspection<T>;
417 using in_type = typename avnd::inputs_type<T>::type;
418 auto& port_tuple = n->ossia_inlets.ports;
419
420 [&]<typename K, K... Index>(std::integer_sequence<K, Index...>) {
421 reload_inlets<safe_node_base_base<T>> init{*n, inbuf, dp};
422 (init(
423 avnd::field_reflection<Index, avnd::pfr::tuple_element_t<Index, in_type>>{},
424 tuplet::get<Index>(port_tuple),
425 tuplet::get<Index>(port_st->reload_inlet.ports)),
426 ...);
427 }(typename in_info::indices_n{});
428 }
429
430 if constexpr(avnd::outputs_type<T>::size > 0)
431 {
432 using out_info = avnd::output_introspection<T>;
433 using out_type = typename avnd::outputs_type<T>::type;
434 auto& port_tuple = n->ossia_outlets.ports;
435
436 [&]<typename K, K... Index>(std::integer_sequence<K, Index...>) {
437 reload_outlets<safe_node_base_base<T>> init{*n, outbuf, dp};
438 (init(
439 avnd::field_reflection<
440 Index, avnd::pfr::tuple_element_t<Index, out_type>>{},
441 tuplet::get<Index>(port_tuple),
442 tuplet::get<Index>(port_st->reload_outlet.ports)),
443 ...);
444 }(typename out_info::indices_n{});
445 }
446
447 // The exec node must line up with the model port for port, otherwise
448 // cables and addresses would land on the wrong exec port.
449 SCORE_SOFT_ASSERT(inbuf.size() == new_inlets.size());
450 SCORE_SOFT_ASSERT(outbuf.size() == new_outlets.size());
451
452 Execution::Transaction commands{ctx};
453
454 // 1. The edges of the cables, before their ports go away
455 for(auto c : cables)
456 setup.removeCable(*c, commands);
457
458 // 2. Old ports out of the execution state and of the setup maps
459 setup.unregister_node_soft(old_inlets, old_outlets, this->node, commands);
460
461 // 3. Swap the ports in
462 commands.push_back(
463 [node = n, dp, port_st, gcq = ctx.weakGCQueue(),
464 wg = std::weak_ptr{ctx.execGraph}]() mutable {
465 OSSIA_ENSURE_CURRENT_THREAD_KIND(ossia::thread_type::Audio);
466
467 // Nothing may keep an edge to a port that is about to be freed: the
468 // cables were disconnected above, whatever else is still there (the
469 // interval's audio propagation, or an edge nobody tracks) is dropped.
470 // Ports that stay keep their edges.
471 if(auto g = wg.lock())
472 {
473 const auto& inbuf = port_st->new_inls_buffer;
474 const auto& outbuf = port_st->new_outls_buffer;
475 for(auto* p : node->root_inputs())
476 {
477 if(ossia::contains(inbuf, p))
478 continue;
479 const auto edges = p->sources;
480 for(auto e : edges)
481 g->disconnect(e);
482 }
483 for(auto* p : node->root_outputs())
484 {
485 if(ossia::contains(outbuf, p))
486 continue;
487 const auto edges = p->targets;
488 for(auto e : edges)
489 g->disconnect(e);
490 }
491 }
492
493 node->dynamic_ports = dp;
494 node->reload_all_ports(port_st->reload_inlet, port_st->reload_outlet);
495
496 auto& inbuf = port_st->new_inls_buffer;
497 auto& outbuf = port_st->new_outls_buffer;
498 node->root_inputs().assign(inbuf.begin(), inbuf.end());
499 node->root_outputs().assign(outbuf.begin(), outbuf.end());
500
501 // Size the object's own port vectors right away: they are otherwise
502 // only resized on the next tick, and a value from the UI for a new
503 // port may arrive before that.
504 if constexpr(avnd::dynamic_ports_input_introspection<T>::size > 0)
505 {
506 for(auto state : node->impl.full_state())
507 {
508 avnd::dynamic_ports_input_introspection<T>::for_all_n2(
509 state.inputs, [&]<std::size_t Idx>(
510 auto& field, auto, avnd::field_index<Idx> idx) {
511 field.ports.resize(std::max(0, node->dynamic_ports.num_in_ports(idx)));
512 });
513 }
514 }
515 if constexpr(avnd::dynamic_ports_output_introspection<T>::size > 0)
516 {
517 for(auto state : node->impl.full_state())
518 {
519 avnd::dynamic_ports_output_introspection<T>::for_all_n2(
520 state.outputs, [&]<std::size_t Idx>(
521 auto& field, auto, avnd::field_index<Idx> idx) {
522 field.ports.resize(std::max(0, node->dynamic_ports.num_out_ports(idx)));
523 });
524 }
525 }
526
527 // port_st now holds the previous dynamic ports: free them in the main thread
528 if(auto q = gcq.lock())
529 q->enqueue(Execution::gc(std::move(port_st)));
530 });
531
532 // 4. New ports in
533 setup.proc_map[this->node.get()] = &element;
534 const std::size_t n_in = std::min(new_inlets.size(), inbuf.size());
535 for(std::size_t i = 0; i < n_in; i++)
536 setup.register_inlet(*new_inlets[i], inbuf[i], this->node, commands);
537
538 const std::size_t n_out = std::min(new_outlets.size(), outbuf.size());
539 for(std::size_t i = 0; i < n_out; i++)
540 setup.register_outlet(*new_outlets[i], outbuf[i], this->node, commands);
541
542 // 5. Cables against the new ports; then whoever keeps other edges to
543 // this node (the interval's audio propagation) syncs them
544 for(auto c : cables)
545 setup.connectCable(*c, commands);
546 this->portsReplaced(&commands);
547
548 commands.run_all();
549
550 this->m_oldInlets = new_inlets;
551 this->m_oldOutlets = new_outlets;
552
553 // UI -> exec for the control inlets that appeared
554 using dynamic_ports_port_type = avnd::dynamic_ports_input_introspection<Node>;
555 if constexpr(dynamic_ports_port_type::size > 0)
556 {
557 avnd::effect_container<Node>& eff = n->impl;
558 for(auto state : eff.full_state())
559 {
560 dynamic_ports_port_type::for_all_n2(
561 state.inputs,
562 dispatch_control_reconnect<Node>{element, ctx, n, *this, this});
563 }
564 }
565 }
566 }
567
568 void connect_controls(
569 ProcessModel<Node>& element, const ::Execution::Context& ctx,
570 std::shared_ptr<safe_node<Node>>& ptr)
571 {
572 using dynamic_ports_port_type = avnd::dynamic_ports_input_introspection<Node>;
573 using control_inputs_type = avnd::control_input_introspection<Node>;
574 using curve_inputs_type = avnd::curve_input_introspection<Node>;
575 using soundfile_inputs_type = avnd::soundfile_input_introspection<Node>;
576 using midifile_inputs_type = avnd::midifile_input_introspection<Node>;
577 using raw_file_inputs_type = avnd::raw_file_input_introspection<Node>;
578 using control_outputs_type = avnd::control_output_introspection<Node>;
579
580 // UI controls to engine
581 safe_node<Node>& node = *ptr;
582 avnd::effect_container<Node>& eff = node.impl;
583
584 // Initialize all the controls in the node with the current value.
585 // And update the node when the UI changes
586
587 if constexpr(dynamic_ports_port_type::size > 0)
588 {
589 for(auto state : eff.full_state())
590 {
591 dynamic_ports_port_type::for_all_n2(
592 state.inputs, dispatch_control_setup<Node>{element, ctx, ptr, *this, this});
593 }
594 }
595 if constexpr(control_inputs_type::size > 0)
596 {
597 for(auto state : eff.full_state())
598 {
599 control_inputs_type::for_all_n2(
600 state.inputs, dispatch_control_setup<Node>{element, ctx, ptr, *this, this});
601 }
602 }
603 if constexpr(curve_inputs_type::size > 0)
604 {
605 for(auto state : eff.full_state())
606 {
607 curve_inputs_type::for_all_n2(
608 state.inputs, dispatch_control_setup<Node>{element, ctx, ptr, *this, this});
609 }
610 }
611 if constexpr(soundfile_inputs_type::size > 0)
612 {
613 soundfile_inputs_type::for_all_n2(
614 avnd::get_inputs<Node>(eff),
615 dispatch_control_setup<Node>{element, ctx, ptr, *this, this});
616
617 setup_soundfile_task_pool(element, ctx, ptr);
618 }
619 if constexpr(midifile_inputs_type::size > 0)
620 {
621 midifile_inputs_type::for_all_n2(
622 avnd::get_inputs<Node>(eff),
623 dispatch_control_setup<Node>{element, ctx, ptr, *this, this});
624 }
625 if constexpr(raw_file_inputs_type::size > 0)
626 {
627 raw_file_inputs_type::for_all_n2(
628 avnd::get_inputs<Node>(eff),
629 dispatch_control_setup<Node>{element, ctx, ptr, *this, this});
630 }
631
632 // Engine to ui controls
633 if constexpr(control_inputs_type::size > 0 || control_outputs_type::size > 0)
634 {
635 auto& settings = score::AppContext().settings<Scenario::Settings::Model>();
636 if(settings.getExecutionUpdate())
637 {
638 // Update the value in the UI
639 std::weak_ptr<safe_node<Node>> weak_node = ptr;
640 update_control_value_in_ui<Node> timer_action{weak_node, &element};
641 timer_action();
642
643 con(ctx.doc.coarseUpdateTimer, &QTimer::timeout, this,
644 [timer_action = std::move(timer_action)] { timer_action(); },
645 Qt::QueuedConnection);
646 }
647 }
648 }
649
650 void setup_soundfile_task_pool(
651 ProcessModel<Node>& element, const ::Execution::Context& ctx,
652 std::shared_ptr<safe_node<Node>>& ptr)
653 {
654 safe_node<Node>& node = *ptr;
655
656 using soundfile_inputs_type = avnd::soundfile_input_introspection<Node>;
657
658 auto& tq = score::TaskPool::instance();
659 node.soundfiles.load_request
660 = [&tq, p = std::weak_ptr{ptr}, &ctx](std::string& str, int idx) {
661 auto eff_ptr = p.lock();
662 if(!eff_ptr)
663 return;
664 tq.post([eff_ptr = std::move(eff_ptr), filename = str, &ctx, idx]() mutable {
665 if(auto file = loadSoundfile(filename, ctx.doc, ctx.execState))
666 {
667 // Sadly we have to go back to the main thread as execution queue is SPSC
668 QMetaObject::invokeMethod(
669 QCoreApplication::instance(),
670 [&ctx, file = std::move(file), eff_ptr = std::move(eff_ptr), idx = idx] {
671 ctx.executionQueue.enqueue(
672 [sf = std::move(file), p = std::weak_ptr{eff_ptr}, idx]() mutable {
673 auto eff_ptr = p.lock();
674 if(!eff_ptr)
675 return;
676
677 avnd::effect_container<Node>& eff = eff_ptr->impl;
678 soundfile_inputs_type::for_nth_mapped_n2(
679 avnd::get_inputs<Node>(eff), idx,
680 [&]<std::size_t NField, std::size_t N>(
681 auto& field, avnd::predicate_index<N> p,
682 avnd::field_index<NField> f) {
683 sf = eff_ptr->soundfile_loaded(sf, p, f);
684 });
685 });
686 });
687 }
688 });
689 };
690 }
691
692 void connect_dynamic_items(ProcessModel<Node>& element, Node& eff)
693 {
694 // type-only inputs: get_inputs would hard-error
695 if constexpr(
696 avnd::inputs_is_type<Node>
697 || avnd::control_input_introspection<Node>::size == 0)
698 return;
699 else
700 avnd::control_input_introspection<Node>::for_all_n2(
701 avnd::get_inputs<Node>(eff),
702 [&element]<std::size_t Idx, typename F>(
703 F& field, auto pred_index, avnd::field_index<Idx>) {
704 if constexpr(avnd::dynamic_items_parameter<F>)
705 {
706 auto ports = element.avnd_input_idx_to_model_ports(Idx);
707 if(ports.size() != 1)
708 return;
709 if(auto combo = qobject_cast<Process::ComboBox*>(ports[0]))
710 {
711 field.update_items = [p = QPointer<Process::ComboBox>{combo}](
712 std::vector<std::string> items) {
713 std::vector<std::pair<QString, ossia::value>> alts;
714 alts.reserve(items.size());
715 for(std::size_t i = 0; i < items.size(); i++)
716 alts.emplace_back(QString::fromStdString(items[i]), (int)i);
717 QMetaObject::invokeMethod(
718 qApp,
719 [p, alts = std::move(alts)]() mutable {
720 if(p)
721 p->setAlternatives(std::move(alts));
722 },
723 Qt::QueuedConnection);
724 };
725 }
726 }
727 });
728 }
729
730 void connect_message_bus(
731 ProcessModel<Node>& element, const ::Execution::Context& ctx, Node& eff)
732 {
733 // Custom UI messages to engine
734 if constexpr(avnd::has_gui_to_processor_bus<Node>)
735 {
736 element.from_ui = [qex_ptr = weak_exec, &eff](QByteArray b) {
737 auto qex = qex_ptr.lock();
738 if(!qex)
739 return;
740
741 qex->enqueue([mess = std::move(b), &eff]() mutable {
742 using refl = avnd::function_reflection<&Node::process_message>;
743 static_assert(refl::count <= 1);
744
745 if constexpr(refl::count == 0)
746 {
747 // no arguments, just call it
748 eff.process_message();
749 }
750 else if constexpr(refl::count == 1)
751 {
752 using arg_type = avnd::first_argument<&Node::process_message>;
753 std::decay_t<arg_type> arg;
754 MessageBusReader reader{mess};
755 reader(arg);
756 eff.process_message(std::move(arg));
757 }
758 });
759 };
760 }
761
762 if constexpr(avnd::has_processor_to_gui_bus<Node>)
763 {
764 if constexpr(requires { eff.send_message = [](auto&&) { }; })
765 {
766 eff.send_message = [proc = QPointer{&this->process()},
767 qed_ptr = weak_edit]<typename T>(T&& b) mutable {
768 auto qed = qed_ptr.lock();
769 if(!qed)
770 return;
771 if constexpr(
772 sizeof(QPointer<QObject>) + sizeof(b)
773 < Execution::ExecutionCommand::max_storage)
774 {
775 qed->enqueue([proc, bb = std::move(b)]() mutable {
776 if(proc && proc->to_ui)
777 MessageBusSender{proc->to_ui}(std::move(bb));
778 });
779 }
780 else
781 {
782 qed->enqueue(
783 [proc, bb = std::make_unique<std::decay_t<T>>(std::move(b))]() mutable {
784 if(proc && proc->to_ui)
785 MessageBusSender{proc->to_ui}(*std::move(bb));
786 });
787 }
788 };
789 }
790 else if constexpr(requires { eff.send_message = []() { }; })
791 {
792 eff.send_message
793 = [proc = QPointer{&this->process()}, qed_ptr = weak_edit]() mutable {
794 if(!proc)
795 return;
796 auto qed = qed_ptr.lock();
797 if(!qed)
798 return;
799
800 qed->enqueue([proc]() mutable {
801 if(proc && proc->to_ui)
802 MessageBusSender{proc->to_ui}();
803 });
804 };
805 }
806 }
807 }
808
809 void connect_worker(const ::Execution::Context& ctx, avnd::effect_container<Node>& eff)
810 {
811 if constexpr(avnd::has_worker<Node>)
812 {
813 // Initialize the thread pool beforehand
814 auto& tq = score::TaskPool::instance();
815 using worker_type = decltype(eff.effect.worker);
816 for(auto& eff : eff.effects())
817 {
818 std::weak_ptr eff_ptr = std::shared_ptr<Node>(this->node, &eff);
819 std::weak_ptr qex_ptr = std::shared_ptr<Execution::ExecutionCommandQueue>(
820 ctx.alias.lock(), &ctx.executionQueue);
821
822 eff.worker.request
823 = [&tq, qex_ptr = std::move(qex_ptr),
824 eff_ptr = std::move(eff_ptr)]<typename... Args>(Args&&... f) mutable {
825 // request() is invoked in the DSP / processor thread
826 // and just posts the task to the thread pool
827 tq.post([eff_ptr, qex_ptr, ... ff = std::forward<Args>(f)]() mutable {
828 // This happens in the worker thread
829 // If for some reason the object has already been removed, not much
830 // reason to perform the work
831 if(!eff_ptr.lock())
832 return;
833
834 using type_of_result
835 = decltype(worker_type::work(std::forward<decltype(ff)>(ff)...));
836 if constexpr(std::is_void_v<type_of_result>)
837 {
838 worker_type::work(std::forward<decltype(ff)>(ff)...);
839 }
840 else
841 {
842 // If the worker returns a std::function, it
843 // is to be invoked back in the processor DSP thread
844 auto res = worker_type::work(std::forward<decltype(ff)>(ff)...);
845 if(!res)
846 return;
847
848 // Execution queue is currently spsc from main thread to an exec thread,
849 // we cannot just yeet the result back from the thread-pool
850 ossia::qt::run_async(
851 qApp, [eff_ptr = std::move(eff_ptr), qex_ptr = std::move(qex_ptr),
852 res = std::move(res)]() mutable {
853 // Main thread
854 std::shared_ptr qex = qex_ptr.lock();
855 if(!qex)
856 return;
857
858 qex->enqueue(
859 [eff_ptr = std::move(eff_ptr), res = std::move(res)]() mutable {
860 // DSP / processor thread
861 // We need res to be mutable so that the worker can use it to e.g. store
862 // old data which will be freed back in the main thread
863 if(auto p = eff_ptr.lock())
864 res(*p);
865 });
866 });
867 }
868 });
869 };
870 }
871 }
872 }
873
874 // Update everything
875 void update_controls(std::shared_ptr<safe_node<Node>>& ptr)
876 {
877 avnd::effect_container<Node>& eff = ptr->impl;
878 {
879 for(auto state : eff.full_state())
880 {
881 avnd::input_introspection<Node>::for_all(
882 state.inputs, [&](auto& field) { if_possible(field.update(state.effect)); });
883 }
884 }
885 }
886
887 void cleanup() override
888 {
889 if constexpr(requires { this->process().from_ui; })
890 {
891 this->process().from_ui = [](QByteArray arr) {};
892 }
893 // FIXME cleanup eff.effect.send_message too ?
894
895#if SCORE_PLUGIN_GFX
896 if constexpr(is_gpu<Node>)
897 {
898 // FIXME this must move in the Node dtor. See video_node
899 auto& gfx_exec = this->system().doc.template plugin<Gfx::DocumentPlugin>().exec;
900 if(node_id >= 0)
901 {
902 gfx_exec.ui->unregister_node(node_id);
903 node_id = score::gfx::invalid_node_index;
904 }
905 }
906
907 // FIXME refactor this with other GFX processes
908 for(auto* outlet : this->process().outlets())
909 {
910 if(auto out = qobject_cast<Gfx::TextureOutlet*>(outlet))
911 {
912 out->nodeId = -1;
913 }
914 }
915#endif
916 ::Execution::ProcessComponent::cleanup();
917 }
918
919 ~Executor() { }
920};
921}
Definition GfxApplicationPlugin.hpp:13
Definition GfxExecNode.hpp:40
The Path class is a typesafe wrapper around ObjectPath.
Definition Path.hpp:52
Definition ScenarioSettingsModel.hpp:19
Definition UuidKey.hpp:345
Definition score-plugin-avnd/Crousti/Executor.hpp:65
Definition score-plugin-avnd/Crousti/Executor.hpp:116
void recompute_ports()
Definition score-plugin-avnd/Crousti/Executor.hpp:346
Definition score-plugin-avnd/Crousti/ProcessModel.hpp:88
Definition Metadatas.hpp:28
Definition Metadatas.hpp:22
Definition Metadatas.hpp:14
Definition Metadatas.hpp:34
Definition Controls.hpp:27
Definition ExecutionContext.hpp:196
Definition Process/Execution/ProcessComponent.hpp:101
Definition ExecutionSetup.hpp:38
Definition ExecutionTransaction.hpp:21
Definition GfxExecNode.hpp:134
Definition PortForward.hpp:23
Definition PortForward.hpp:27
Definition ExecutorPortSetup.hpp:545
Definition ExecutorPortSetup.hpp:497
Definition ExecutorPortSetup.hpp:15
T & settings() const
Access a specific Settings model instance.
Definition ApplicationContext.hpp:41