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 <atomic>
48
49#include <avnd/binding/ossia/data_node.hpp>
50#include <avnd/binding/ossia/mono_audio_node.hpp>
51#include <avnd/binding/ossia/node.hpp>
52#include <avnd/binding/ossia/ossia_audio_node.hpp>
53#include <avnd/binding/ossia/ossia_dynamic_audio_node.hpp>
54#include <avnd/binding/ossia/poly_audio_node.hpp>
55#include <avnd/concepts/dynamic_items.hpp>
56#include <avnd/concepts/temporality.hpp>
57#include <avnd/concepts/ui.hpp>
58#include <avnd/concepts/worker.hpp>
59#include <Crousti/WorkerBinding.hpp>
60
61#include <QPointer>
62
63#include <atomic>
64
65namespace oscr
66{
73[[nodiscard]] static ossia::value
74gpuControlValue(
75 const Process::ControlInlet& ctrl, const ossia::value& v,
76 const score::DocumentContext& ctx)
77{
78 if(qobject_cast<const Process::FileChooserBase*>(&ctrl)
79 || qobject_cast<const Process::FolderChooser*>(&ctrl))
80 return resolvePathValue(v, ctx);
81 return v;
82}
83
97template <typename Node>
99{
100 using node_with_ui_wakeups<Node>::node_with_ui_wakeups;
101
105
106 void
107 run(const ossia::token_request& tk, ossia::exec_state_facade st) noexcept override
108 {
109 Execution::ExecutionCommand cmd;
110 if(worker_results.try_dequeue(cmd))
111 {
112 const auto [start, frames] = st.timings(tk);
113 this->start_frame_for_this_tick = start;
114 this->frame_count_for_this_tick = frames;
115 do
116 {
117 cmd();
118 } while(worker_results.try_dequeue(cmd));
119 }
120
122 }
123};
124
129template <typename Node>
131{
132 std::weak_ptr<node_with_worker<Node>> node_wp;
133 exec_worker_delivery begin() const noexcept { return *this; }
134
135 template <typename F>
136 void operator()(F&& apply)
137 {
138 ossia::qt::run_async(
139 qApp, [node_wp = std::move(node_wp), apply = std::move(apply)]() mutable {
140 if(std::shared_ptr n = node_wp.lock())
141 n->worker_results.enqueue(std::move(apply));
142 });
143 }
144};
145
146template <typename Node>
147using exec_base_node_t = std::conditional_t<
148 avnd::has_worker<Node>, node_with_worker<Node>, node_with_ui_wakeups<Node>>;
149
155template <typename Node>
156concept stops_with_extra_tick = avnd::tag_process_exec<Node>
157 && avnd::midi_output_introspection<Node>::size > 0
158 && requires(Node& n) { n.stop(); };
159
160template <typename Node>
161struct node_with_deferred_stop : exec_base_node_t<Node>
162{
163 using exec_base_node_t<Node>::exec_base_node_t;
164
166 std::atomic_bool stop_pending{};
167
171 {
172 if(stop_pending.exchange(false))
173 this->impl.effect.stop();
174 }
175
176 void
177 run(const ossia::token_request& tk, ossia::exec_state_facade st) noexcept override
178 {
179 if(tk.date == tk.prev_date && stop_pending.exchange(false))
180 this->impl.effect.stop();
181 exec_base_node_t<Node>::run(tk, st);
182 }
183};
184
187template <typename Node>
188using exec_node_t = std::conditional_t<
190
191template <typename Node>
192class CustomNodeProcess : public ossia::node_process
193{
194 using node_process::node_process;
195 void start() override
196 {
197 node_process::start();
198 auto& n = static_cast<safe_node<Node>&>(*node);
199 if_possible(n.impl.effect.start());
200 }
201 void pause() override
202 {
203 auto& n = static_cast<safe_node<Node>&>(*node);
204 if_possible(n.impl.effect.pause());
205 node_process::pause();
206 }
207
208 void resume() override
209 {
210 node_process::resume();
211 auto& n = static_cast<safe_node<Node>&>(*node);
212 if_possible(n.impl.effect.resume());
213 }
214
215 void stop() override
216 {
217 auto& n = static_cast<safe_node<Node>&>(*node);
218 if constexpr(stops_with_extra_tick<Node>)
219 {
220 // A stopped node is not ticked again: one more, empty tick sends the
221 // note-offs, and stops the object at its start.
222 static_cast<node_with_deferred_stop<Node>&>(*node).stop_pending = true;
223 node_process::stop();
224 n.request(ossia::token_request{});
225 }
226 else
227 {
228 if_possible(n.impl.effect.stop());
229 node_process::stop();
230 }
231 }
232
233 void offset_impl(ossia::time_value date) override
234 {
235 node_process::offset_impl(date);
236 auto& n = static_cast<safe_node<Node>&>(*node);
237 util::flicks f{date.impl};
238 if_possible(n.impl.effect.transport(f));
239 }
240
241 void transport_impl(ossia::time_value date) override
242 {
243 node_process::transport_impl(date);
244 auto& n = static_cast<safe_node<Node>&>(*node);
245
246 util::flicks f{date.impl};
247 if_possible(n.impl.effect.transport(f));
248 }
249};
250
251template <typename Node>
252class Executor final
253 : public Execution::ProcessComponent_T<ProcessModel<Node>, ossia::node_process>
254 , public dynamic_ports_component_data<Node>
255{
256public:
257 static Q_DECL_RELAXED_CONSTEXPR UuidKey<score::Component> static_key() noexcept
258 {
259 return uuid_from_string<Node>();
260 }
261
262 UuidKey<score::Component> key() const noexcept final override { return static_key(); }
263
264 bool key_match(UuidKey<score::Component> other) const noexcept final override
265 {
266 return static_key() == other || Execution::ProcessComponent::base_key_match(other);
267 }
268
269#if defined(SCORE_PLUGIN_GFX)
270 [[no_unique_address]] ossia::type_if<int, is_gpu<Node>> node_id = score::gfx::invalid_node_index;
271#endif
272
274 std::vector<QMetaObject::Connection> m_executionUpdate;
275
278 template <typename F>
280 {
281 auto& settings = score::AppContext().settings<Scenario::Settings::Model>();
282 f(settings.getExecutionUpdate());
283 m_executionUpdate.push_back(QObject::connect(
284 &settings, &Scenario::Settings::Model::ExecutionUpdateChanged, this,
285 std::move(f)));
286 }
287
288 Executor(ProcessModel<Node>& element, const ::Execution::Context& ctx, QObject* p)
289 : Execution::ProcessComponent_T<ProcessModel<Node>, ossia::node_process>{
290 element, ctx, "Executor::ProcessModel<Info>", p}
291 {
292 if constexpr(is_gpu<Node>)
293 {
294#if SCORE_PLUGIN_GFX
295 setup_gpu(element, ctx, p);
296#endif
297 }
298 else
299 {
300 setup_cpu(element, ctx, p);
301 }
302
303 if constexpr(avnd::tag_process_exec<Node>)
304 {
305 this->m_ossia_process = std::make_shared<CustomNodeProcess<Node>>(this->node);
306 }
307 else
308 {
309 this->m_ossia_process = std::make_shared<ossia::node_process>(this->node);
310 }
311 }
312
313 void
314 setup_cpu(ProcessModel<Node>& element, const ::Execution::Context& ctx, QObject* p)
315 {
316 auto& net_ctx
317 = *ctx.doc.findPlugin<Explorer::DeviceDocumentPlugin>()->networkContext();
318 const auto id
319 = std::hash<ObjectPath>{}(Path<Process::ProcessModel>{element}.unsafePath());
320
321 auto st = ossia::exec_state_facade{ctx.execState.get()};
322 std::shared_ptr<safe_node<Node>> ptr;
323 auto node = new exec_node_t<Node>{st.setupBufferSize(), (double)st.sampleRate(), id};
324 node->root_inputs().reserve(element.inlets().size());
325 node->root_outputs().reserve(element.outlets().size());
326
327 node->prepare(*ctx.execState.get()); // Preparation of the ossia side
328
329 if_possible(node->impl.effect.ossia_state = st);
330 if_possible(node->impl.effect.io_context = &net_ctx.context);
331 if_possible(node->impl.effect.ossia_document_context = &ctx.doc);
332 ptr.reset(node);
333 this->node = ptr;
334
335 if constexpr(requires { ptr->impl.effect; })
336 if constexpr(std::is_same_v<std::decay_t<decltype(ptr->impl.effect)>, Node>)
337 {
338 connect_message_bus(element, ctx, ptr->impl.effect, node->wakeups);
339 connect_dynamic_items(element, ptr->impl.effect);
340 // Views follow execution only when the user lets them, as for every
341 // execution-to-UI update: without a monitor they stop being viewers,
342 // and the node stops writing for them
343 if constexpr(requires { element.setMonitor(ptr->impl.effect.monitor); })
345 [&element, monitor = ptr->impl.effect.monitor](bool enabled) {
346 element.setMonitor(enabled ? monitor : nullptr);
347 });
348 }
349 connect_worker(ptr);
350
351 node->dynamic_ports = element.dynamic_ports;
352 node->finish_init();
353
354 connect_controls(element, ctx, ptr);
355 update_controls(ptr);
356
357 // To call prepare() after evertyhing is ready
358 node->audio_configuration_changed(st);
359
360 if constexpr(oscr::has_dynamic_ports<Node>)
361 {
362 QObject::connect(
363 &element, &Process::ProcessModel::inletsChanged, this,
365 QObject::connect(
366 &element, &Process::ProcessModel::outletsChanged, this,
368
369 this->m_oldInlets = element.inlets();
370 this->m_oldOutlets = element.outlets();
371 }
372 }
373
374 void
375 setup_gpu(ProcessModel<Node>& element, const ::Execution::Context& ctx, QObject* p)
376 {
377#if SCORE_PLUGIN_GFX
378 // FIXME net context for gpu node ?
379 const int64_t id
380 = std::hash<ObjectPath>{}(Path<Process::ProcessModel>{element}.unsafePath());
381
382 // The monitor its views share with every renderer of the node: they take
383 // it from the process when they are made, while the user lets execution
384 // update the views
385 if constexpr(requires { Node::monitor; })
386 {
387 using monitor_type = typename decltype(Node::monitor)::element_type;
389 [&element, monitor = std::make_shared<monitor_type>()](bool enabled) {
390 element.setMonitor(enabled ? monitor : nullptr);
391 });
392 }
393
394 auto& gfx_exec = ctx.doc.plugin<Gfx::DocumentPlugin>().exec;
395
396 // Create the executor in the audio thread
397 struct named_exec_node final : Gfx::gfx_exec_node
398 {
399 using Gfx::gfx_exec_node::gfx_exec_node;
400 std::string label() const noexcept override
401 {
402 return std::string(avnd::get_name<Node>());
403 }
404 };
405
406 auto node = std::make_shared<named_exec_node>(gfx_exec);
407 node->prepare(*ctx.execState);
408
409 this->node = node;
410
411 // Create the controls, inputs outputs etc.
412 std::size_t i = 0;
413
414 for(auto& ctl : element.inlets())
415 {
416 if(auto ctrl = qobject_cast<Process::ControlInlet*>(ctl))
417 {
418 auto& p = node->add_control();
419 p->value = gpuControlValue(*ctrl, ctrl->value(), ctx.doc);
420 // An impulse is only ever sent when it is triggered: the object's
421 // one-shot inputs start empty, like setup_control_for_exec leaves them
422 // on the CPU path.
423 p->changed = p->value.get_type() != ossia::val_type::IMPULSE;
424
425 // What the control holds and the range it takes, so that a cable into
426 // it converts and rescales. The ISF executor sets its controls up the
427 // same way.
428 ctrl->setupExecution(*node->root_inputs().back(), this);
429
430 QObject::connect(
431 ctrl, &Process::ControlInlet::valueChanged, this,
432 [con = Gfx::con_unvalidated{ctx, i, 0, node}, ctrl,
433 &doc = ctx.doc](const ossia::value& v) mutable {
434 con(gpuControlValue(*ctrl, v, doc));
435 });
436 i++;
437 }
438 else if(auto ctrl = qobject_cast<Process::ValueInlet*>(ctl))
439 {
440 auto& p = node->add_control();
441 p->changed = false;
442 i++;
443 }
444 else if(auto ctrl = qobject_cast<Process::AudioInlet*>(ctl))
445 {
446 node->add_audio();
447 }
448 else if(auto ctrl = qobject_cast<Gfx::TextureInlet*>(ctl))
449 {
450 ossia::texture_inlet& inl = *node->add_texture();
451 ctrl->setupExecution(inl, this);
452 }
453 else if(auto ctrl = qobject_cast<Gfx::GeometryInlet*>(ctl))
454 {
455 ossia::geometry_inlet& inl = *node->add_geometry();
456 ctrl->setupExecution(inl, this);
457 }
458 }
459
460 // FIXME refactor this with other GFX processes
461 for(auto* outlet : element.outlets())
462 {
463 if(auto ctrl = qobject_cast<Process::ControlOutlet*>(outlet))
464 {
465 node->add_control_out();
466 }
467 else if(auto ctrl = qobject_cast<Process::ValueOutlet*>(outlet))
468 {
469 node->add_control_out();
470 }
471 else if(auto out = qobject_cast<Gfx::TextureOutlet*>(outlet))
472 {
473 node->add_texture_out();
474 }
475 else if(auto out = qobject_cast<Gfx::GeometryOutlet*>(outlet))
476 {
477 node->add_geometry_out();
478 }
479 }
480
481 // Create the GPU node
482 std::weak_ptr qex_ptr = std::shared_ptr<Execution::ExecutionCommandQueue>(
483 ctx.alias.lock(), &ctx.executionQueue);
484 std::unique_ptr<score::gfx::Node> ptr;
485 if constexpr(GpuGraphicsNode2<Node>)
486 {
487 auto gpu_node = new CustomGpuNode<Node>(qex_ptr, node->control_outs, id, ctx.doc);
488 ptr.reset(gpu_node);
489 }
490 else if constexpr(GpuComputeNode2<Node>)
491 {
492 auto gpu_node = new GpuComputeNode<Node>(qex_ptr, node->control_outs, id, ctx.doc);
493 ptr.reset(gpu_node);
494 }
495 else if constexpr(GpuNode<Node>)
496 {
497 auto gpu_node
498 = new GfxNode<Node>(element, qex_ptr, node->control_outs, id, ctx.doc);
499 ptr.reset(gpu_node);
500 }
501
502 i = 0;
503 for(auto& ctl : element.inlets())
504 {
505 if(auto ctrl = qobject_cast<Gfx::TextureInlet*>(ctl))
506 {
507 ossia::texture_inlet& inl
508 = static_cast<ossia::texture_inlet&>(*node->root_inputs()[i]);
509 ptr->process(i, inl.data); // Setup render_target_spec
510 }
511 i++;
512 }
513 node->id = gfx_exec.ui->register_node(std::move(ptr));
514 node_id = node->id;
515
516 // Control outputs reach the model's outlets, for the views and the
517 // inspector, at the UI rate
518 if(!node->control_outs.empty())
519 {
520 std::vector<Process::ControlOutlet*> ui_outlets;
521 for(auto* outlet : element.outlets())
522 if(auto ctl = qobject_cast<Process::ControlOutlet*>(outlet))
523 ui_outlets.push_back(ctl);
524 else if(qobject_cast<Process::ValueOutlet*>(outlet))
525 ui_outlets.push_back(nullptr);
526 followExecutionUpdate([weak = std::weak_ptr{node}](bool enabled) {
527 if(auto n = weak.lock())
528 n->notify_ui.store(enabled, std::memory_order_relaxed);
529 });
530 con(ctx.doc.coarseUpdateTimer, &QTimer::timeout, this,
531 [weak = std::weak_ptr{node}, ui_outlets = std::move(ui_outlets),
532 v = ossia::value{}]() mutable {
533 auto n = weak.lock();
534 if(!n || !n->notify_ui.load(std::memory_order_relaxed))
535 return;
536 const int count = std::min(int(ui_outlets.size()), int(n->ui_control_outs.size()));
537 for(int i = 0; i < count; i++)
538 if(ui_outlets[i] && n->ui_control_outs[i]->latest.consume(v))
539 ui_outlets[i]->setValue(v);
540 }, Qt::QueuedConnection);
541 }
542
543 // Only known once registered: previews of the outlets connect to it
544 for(auto* outlet : element.outlets())
545 if(auto out = qobject_cast<Gfx::TextureOutlet*>(outlet))
546 out->nodeId = node_id;
547#endif
548 }
549
563 {
564 if constexpr(oscr::has_dynamic_ports<Node> && !is_gpu<Node>)
565 {
566 using T = Node;
567 auto n = std::dynamic_pointer_cast<safe_node<Node>>(this->node);
568 if(!n)
569 return;
570
571 auto& element = this->process();
572 const Execution::Context& ctx = this->system();
573 Execution::SetupContext& setup = ctx.setup;
574
575 // The ports the exec node is currently set up for. Those that are not
576 // in the model anymore are about to be deleted (they are still alive
577 // here: ProcessModel deletes them after inletsChanged()).
578 const Process::Inlets old_inlets = std::move(this->m_oldInlets);
579 const Process::Outlets old_outlets = std::move(this->m_oldOutlets);
580 const Process::Inlets& new_inlets = element.inlets();
581 const Process::Outlets& new_outlets = element.outlets();
582
583 for(auto port : old_inlets)
584 if(!ossia::contains(new_inlets, port))
585 this->m_connectedControls.erase(port);
586
587 // The cables at either end of this process that are wired up in the
588 // graph: their edges point to the exec ports that are being replaced.
589 // A cable that is on the ports but not wired yet (restored by a command
590 // that announces it after inletsChanged()) is left to that announcement.
591 ossia::small_vector<Process::Cable*, 8> cables;
592 auto collect_cables = [&](const auto& ports) {
593 for(auto port : ports)
594 for(auto& cbl : port->cables())
595 if(auto c = cbl.try_find(ctx.doc))
596 if(setup.m_cables.find(c->id()) != setup.m_cables.end())
597 if(!ossia::contains(cables, c))
598 cables.push_back(c);
599 };
600 collect_cables(old_inlets);
601 collect_cables(old_outlets);
602 collect_cables(new_inlets);
603 collect_cables(new_outlets);
604
605 // Build the new set of exec ports.
606 struct port_storage
607 {
608 ossia::inlets new_inls_buffer;
609 ossia::outlets new_outls_buffer;
610 inlet_reload_storage<T> reload_inlet;
611 outlet_reload_storage<T> reload_outlet;
612 port_storage(std::size_t ins, std::size_t outs)
613 {
614 new_inls_buffer.reserve(ins);
615 new_outls_buffer.reserve(outs);
616 }
617 };
618 auto port_st = std::make_shared<port_storage>(new_inlets.size(), new_outlets.size());
619 auto& inbuf = port_st->new_inls_buffer;
620 auto& outbuf = port_st->new_outls_buffer;
621
622 // Same order as safe_node_base::reinit() + initialize_all_ports():
623 // first the ports that do not come from a field of the object
624 // (audio / value argument of the processor, message inlets), then the fields.
625 n->audio_ports.init(inbuf, outbuf);
626 n->arg_value_ports.init(inbuf, outbuf);
627 n->message_ports.init(inbuf);
628
629 auto& dp = element.dynamic_ports;
630 if constexpr(avnd::inputs_type<T>::size > 0)
631 {
632 using in_info = avnd::input_introspection<T>;
633 using in_type = typename avnd::inputs_type<T>::type;
634 auto& port_tuple = n->ossia_inlets.ports;
635
636 [&]<typename K, K... Index>(std::integer_sequence<K, Index...>) {
637 reload_inlets<safe_node_base_base<T>> init{*n, inbuf, dp};
638 (init(
639 avnd::field_reflection<Index, avnd::pfr::tuple_element_t<Index, in_type>>{},
640 tuplet::get<Index>(port_tuple),
641 tuplet::get<Index>(port_st->reload_inlet.ports)),
642 ...);
643 }(typename in_info::indices_n{});
644 }
645
646 if constexpr(avnd::outputs_type<T>::size > 0)
647 {
648 using out_info = avnd::output_introspection<T>;
649 using out_type = typename avnd::outputs_type<T>::type;
650 auto& port_tuple = n->ossia_outlets.ports;
651
652 [&]<typename K, K... Index>(std::integer_sequence<K, Index...>) {
653 reload_outlets<safe_node_base_base<T>> init{*n, outbuf, dp};
654 (init(
655 avnd::field_reflection<
656 Index, avnd::pfr::tuple_element_t<Index, out_type>>{},
657 tuplet::get<Index>(port_tuple),
658 tuplet::get<Index>(port_st->reload_outlet.ports)),
659 ...);
660 }(typename out_info::indices_n{});
661 }
662
663 // The exec node must line up with the model port for port, otherwise
664 // cables and addresses would land on the wrong exec port.
665 SCORE_SOFT_ASSERT(inbuf.size() == new_inlets.size());
666 SCORE_SOFT_ASSERT(outbuf.size() == new_outlets.size());
667
668 Execution::Transaction commands{ctx};
669
670 // 1. The edges of the cables, before their ports go away
671 for(auto c : cables)
672 setup.removeCable(*c, commands);
673
674 // 2. Old ports out of the execution state and of the setup maps
675 setup.unregister_node_soft(old_inlets, old_outlets, this->node, commands);
676
677 // 3. Swap the ports in
678 commands.push_back(
679 [node = n, dp, port_st, gcq = ctx.weakGCQueue(),
680 wg = std::weak_ptr{ctx.execGraph}]() mutable {
681 OSSIA_ENSURE_CURRENT_THREAD_KIND(ossia::thread_type::Audio);
682
683 // Nothing may keep an edge to a port that is about to be freed: the
684 // cables were disconnected above, whatever else is still there (the
685 // interval's audio propagation, or an edge nobody tracks) is dropped.
686 // Ports that stay keep their edges.
687 if(auto g = wg.lock())
688 {
689 const auto& inbuf = port_st->new_inls_buffer;
690 const auto& outbuf = port_st->new_outls_buffer;
691 for(auto* p : node->root_inputs())
692 {
693 if(ossia::contains(inbuf, p))
694 continue;
695 const auto edges = p->sources;
696 for(auto e : edges)
697 g->disconnect(e);
698 }
699 for(auto* p : node->root_outputs())
700 {
701 if(ossia::contains(outbuf, p))
702 continue;
703 const auto edges = p->targets;
704 for(auto e : edges)
705 g->disconnect(e);
706 }
707 }
708
709 node->dynamic_ports = dp;
710 node->reload_all_ports(port_st->reload_inlet, port_st->reload_outlet);
711
712 auto& inbuf = port_st->new_inls_buffer;
713 auto& outbuf = port_st->new_outls_buffer;
714 node->root_inputs().assign(inbuf.begin(), inbuf.end());
715 node->root_outputs().assign(outbuf.begin(), outbuf.end());
716
717 // Size the object's own port vectors right away: they are otherwise
718 // only resized on the next tick, and a value from the UI for a new
719 // port may arrive before that.
720 if constexpr(avnd::dynamic_ports_input_introspection<T>::size > 0)
721 {
722 for(auto state : node->impl.full_state())
723 {
724 avnd::dynamic_ports_input_introspection<T>::for_all_n2(
725 state.inputs, [&]<std::size_t Idx>(
726 auto& field, auto, avnd::field_index<Idx> idx) {
727 field.ports.resize(std::max(0, node->dynamic_ports.num_in_ports(idx)));
728 });
729 }
730 }
731 if constexpr(avnd::dynamic_ports_output_introspection<T>::size > 0)
732 {
733 for(auto state : node->impl.full_state())
734 {
735 avnd::dynamic_ports_output_introspection<T>::for_all_n2(
736 state.outputs, [&]<std::size_t Idx>(
737 auto& field, auto, avnd::field_index<Idx> idx) {
738 field.ports.resize(std::max(0, node->dynamic_ports.num_out_ports(idx)));
739 });
740 }
741 }
742
743 // port_st now holds the previous dynamic ports: free them in the main thread
744 if(auto q = gcq.lock())
745 q->enqueue(Execution::gc(std::move(port_st)));
746 });
747
748 // 4. New ports in
749 setup.proc_map[this->node.get()] = &element;
750 const std::size_t n_in = std::min(new_inlets.size(), inbuf.size());
751 for(std::size_t i = 0; i < n_in; i++)
752 setup.register_inlet(*new_inlets[i], inbuf[i], this->node, commands);
753
754 const std::size_t n_out = std::min(new_outlets.size(), outbuf.size());
755 for(std::size_t i = 0; i < n_out; i++)
756 setup.register_outlet(*new_outlets[i], outbuf[i], this->node, commands);
757
758 // 5. Cables against the new ports; then whoever keeps other edges to
759 // this node (the interval's audio propagation) syncs them
760 for(auto c : cables)
761 setup.connectCable(*c, commands);
762 this->portsReplaced(&commands);
763
764 commands.run_all();
765
766 this->m_oldInlets = new_inlets;
767 this->m_oldOutlets = new_outlets;
768
769 // UI -> exec for the control inlets that appeared
770 using dynamic_ports_port_type = avnd::dynamic_ports_input_introspection<Node>;
771 if constexpr(dynamic_ports_port_type::size > 0)
772 {
773 avnd::effect_container<Node>& eff = n->impl;
774 for(auto state : eff.full_state())
775 {
776 dynamic_ports_port_type::for_all_n2(
777 state.inputs,
778 dispatch_control_reconnect<Node>{element, ctx, n, *this, this});
779 }
780 }
781 }
782 }
783
784 void connect_controls(
785 ProcessModel<Node>& element, const ::Execution::Context& ctx,
786 std::shared_ptr<safe_node<Node>>& ptr)
787 {
788 using dynamic_ports_port_type = avnd::dynamic_ports_input_introspection<Node>;
789 using control_inputs_type = avnd::control_input_introspection<Node>;
790 using curve_inputs_type = avnd::curve_input_introspection<Node>;
791 using soundfile_inputs_type = avnd::soundfile_input_introspection<Node>;
792 using midifile_inputs_type = avnd::midifile_input_introspection<Node>;
793 using raw_file_inputs_type = avnd::raw_file_input_introspection<Node>;
794 using control_outputs_type = avnd::control_output_introspection<Node>;
795
796 // UI controls to engine
797 safe_node<Node>& node = *ptr;
798 avnd::effect_container<Node>& eff = node.impl;
799
800 // Initialize all the controls in the node with the current value.
801 // And update the node when the UI changes
802
803 if constexpr(dynamic_ports_port_type::size > 0)
804 {
805 for(auto state : eff.full_state())
806 {
807 dynamic_ports_port_type::for_all_n2(
808 state.inputs, dispatch_control_setup<Node>{element, ctx, ptr, *this, this});
809 }
810 }
811 if constexpr(control_inputs_type::size > 0)
812 {
813 for(auto state : eff.full_state())
814 {
815 control_inputs_type::for_all_n2(
816 state.inputs, dispatch_control_setup<Node>{element, ctx, ptr, *this, this});
817 }
818 }
819 if constexpr(curve_inputs_type::size > 0)
820 {
821 for(auto state : eff.full_state())
822 {
823 curve_inputs_type::for_all_n2(
824 state.inputs, dispatch_control_setup<Node>{element, ctx, ptr, *this, this});
825 }
826 }
827 if constexpr(soundfile_inputs_type::size > 0)
828 {
829 soundfile_inputs_type::for_all_n2(
830 avnd::get_inputs<Node>(eff),
831 dispatch_control_setup<Node>{element, ctx, ptr, *this, this});
832
833 setup_soundfile_task_pool(element, ctx, ptr);
834 }
835 if constexpr(midifile_inputs_type::size > 0)
836 {
837 midifile_inputs_type::for_all_n2(
838 avnd::get_inputs<Node>(eff),
839 dispatch_control_setup<Node>{element, ctx, ptr, *this, this});
840 }
841 if constexpr(raw_file_inputs_type::size > 0)
842 {
843 raw_file_inputs_type::for_all_n2(
844 avnd::get_inputs<Node>(eff),
845 dispatch_control_setup<Node>{element, ctx, ptr, *this, this});
846 }
847
848 // Engine to ui controls
849 if constexpr(control_inputs_type::size > 0 || control_outputs_type::size > 0)
850 {
851 std::weak_ptr<safe_node<Node>> weak_node = ptr;
852 update_control_value_in_ui<Node> timer_action{weak_node, &element};
854 [this, weak_node, timer_action, &ctx,
855 timer = std::make_shared<QMetaObject::Connection>()](bool enabled) {
856 // The node enqueues its control outputs for the UI every tick; with
857 // no reader, that queue grows without bound and allocates in the
858 // audio callback. Tell it not to bother.
859 if constexpr(control_outputs_type::size > 0)
860 if(auto n = weak_node.lock())
861 n->control.notify_ui.store(enabled, std::memory_order_relaxed);
862
863 QObject::disconnect(*timer);
864 if(enabled)
865 {
866 timer_action();
867 *timer = QObject::connect(
868 &ctx.doc.coarseUpdateTimer, &QTimer::timeout, this,
869 [timer_action] { timer_action(); }, Qt::QueuedConnection);
870 }
871 });
872 }
873 }
874
875 void setup_soundfile_task_pool(
876 ProcessModel<Node>& element, const ::Execution::Context& ctx,
877 std::shared_ptr<safe_node<Node>>& ptr)
878 {
879 safe_node<Node>& node = *ptr;
880
881 using soundfile_inputs_type = avnd::soundfile_input_introspection<Node>;
882
883 auto& tq = score::TaskPool::instance();
884 node.soundfiles.load_request
885 = [&tq, p = std::weak_ptr{ptr}, &ctx](std::string& str, int idx) {
886 auto eff_ptr = p.lock();
887 if(!eff_ptr)
888 return;
889 tq.post([eff_ptr = std::move(eff_ptr), filename = str, &ctx, idx]() mutable {
890 if(auto file = loadSoundfile(filename, ctx.doc, ctx.execState))
891 {
892 // Sadly we have to go back to the main thread as execution queue is SPSC
893 QMetaObject::invokeMethod(
894 QCoreApplication::instance(),
895 [&ctx, file = std::move(file), eff_ptr = std::move(eff_ptr), idx = idx] {
896 ctx.executionQueue.enqueue([sf = std::move(file), p = std::weak_ptr{eff_ptr},
897 gcq = ctx.weakGCQueue(), idx]() mutable {
898 if(auto eff_ptr = p.lock())
899 {
900 avnd::effect_container<Node>& eff = eff_ptr->impl;
901 soundfile_inputs_type::for_nth_mapped_n2(
902 avnd::get_inputs<Node>(eff), idx,
903 [&]<std::size_t NField, std::size_t N>(
904 auto& field, avnd::predicate_index<N> p,
905 avnd::field_index<NField> f) {
906 sf = eff_ptr->soundfile_loaded(sf, p, f);
907 });
908 }
909 // sf is now the file the port held before
910 free_on_main_thread(gcq, sf);
911 });
912 });
913 }
914 });
915 };
916 }
917
918 void connect_dynamic_items(ProcessModel<Node>& element, Node& eff)
919 {
920 // type-only inputs: get_inputs would hard-error
921 if constexpr(
922 avnd::inputs_is_type<Node>
923 || avnd::control_input_introspection<Node>::size == 0)
924 return;
925 else
926 avnd::control_input_introspection<Node>::for_all_n2(
927 avnd::get_inputs<Node>(eff),
928 [&element]<std::size_t Idx, typename F>(
929 F& field, auto pred_index, avnd::field_index<Idx>) {
930 if constexpr(avnd::dynamic_items_parameter<F>)
931 {
932 auto ports = element.avnd_input_idx_to_model_ports(Idx);
933 if(ports.size() != 1)
934 return;
935 if(auto combo = qobject_cast<Process::ComboBox*>(ports[0]))
936 {
937 field.update_items = [p = QPointer<Process::ComboBox>{combo}](
938 std::vector<std::string> items) {
939 std::vector<std::pair<QString, ossia::value>> alts;
940 alts.reserve(items.size());
941 for(std::size_t i = 0; i < items.size(); i++)
942 alts.emplace_back(QString::fromStdString(items[i]), (int)i);
943 QMetaObject::invokeMethod(
944 qApp,
945 [p, alts = std::move(alts)]() mutable {
946 if(p)
947 p->setAlternatives(std::move(alts));
948 },
949 Qt::QueuedConnection);
950 };
951 }
952 }
953 });
954 }
955
956 void connect_message_bus(
957 ProcessModel<Node>& element, const ::Execution::Context& ctx, Node& eff,
958 ui_wakeups& wakeups)
959 {
960 // Custom UI messages to engine
961 if constexpr(avnd::has_gui_to_processor_bus<Node>)
962 {
963 element.from_ui = ui_to_processor_bus(eff, weak_exec, weak_gc);
964 }
965
966 // Processor to UI messages never allocate on the processor's thread: they
967 // go in an execution command when they fit, else in a ring of the node's
968 // own. Both have a fixed capacity: when it is full the message is dropped,
969 // and the UI catches up with the next one. The messages a ring holds are
970 // not lost when the queue is full: the node wakes the UI on a later tick.
971 if constexpr(avnd::has_processor_to_gui_bus<Node>)
972 {
973 if constexpr(requires { eff.send_message = [](auto&&) { }; })
974 {
975 using send_type = std::remove_cvref_t<decltype(eff.send_message)>;
976 using message_type
977 = std::remove_cvref_t<typename function_argument<send_type>::type>;
978 if constexpr(
979 sizeof(QPointer<QObject>) + sizeof(message_type)
980 < Execution::ExecutionCommand::max_storage)
981 {
982 eff.send_message = [proc = QPointer{&this->process()},
983 qed_ptr = weak_edit]<typename T>(T&& b) mutable {
984 auto qed = qed_ptr.lock();
985 if(!qed)
986 return;
987 qed->try_enqueue([proc, bb = message_type(std::forward<T>(b))]() mutable {
988 if(proc && proc->to_ui)
989 MessageBusSender{proc->to_ui}(std::move(bb));
990 });
991 };
992 }
993 else
994 {
995 static_assert(
996 std::is_default_constructible_v<message_type>,
997 "a processor to UI message is stored in a preallocated ring");
998 auto ring = std::make_shared<message_ring<message_type>>();
999 // Sends the wake-up the ring requested; false when the queue is full.
1000 auto wake = [proc = QPointer{&this->process()}, qed_ptr = weak_edit, ring] {
1001 auto qed = qed_ptr.lock();
1002 if(!qed)
1003 {
1004 ring->cancel();
1005 return true;
1006 }
1007 if(qed->try_enqueue([proc, ring]() mutable {
1008 ring->drain([&](message_type&& m) {
1009 if(proc && proc->to_ui)
1010 MessageBusSender{proc->to_ui}(std::move(m));
1011 });
1012 }))
1013 return true;
1014 ring->park();
1015 return false;
1016 };
1017 wakeups.add([ring, wake] {
1018 if(!ring->unpark() || !ring->request())
1019 return true;
1020 return wake();
1021 });
1022
1023 eff.send_message = [qed_ptr = weak_edit, ring, wake,
1024 wakeups = &wakeups]<typename T>(T&& b) mutable {
1025 if(qed_ptr.expired())
1026 return;
1027 if(!ring->push(message_type(std::forward<T>(b))))
1028 return;
1029 if(!ring->request())
1030 return;
1031 if(!wake())
1032 wakeups->park();
1033 };
1034 }
1035 }
1036 else if constexpr(requires { eff.send_message = []() { }; })
1037 {
1038 eff.send_message
1039 = [proc = QPointer{&this->process()}, qed_ptr = weak_edit]() mutable {
1040 if(!proc)
1041 return;
1042 auto qed = qed_ptr.lock();
1043 if(!qed)
1044 return;
1045
1046 qed->try_enqueue([proc]() mutable {
1047 if(proc && proc->to_ui)
1048 MessageBusSender{proc->to_ui}();
1049 });
1050 };
1051 }
1052
1053 // A message sent by a worker, from its thread: it may be sent once the
1054 // node is gone, e.g. by a job the score's stop started. It is posted to
1055 // the UI thread, where it reaches the views of the process if the
1056 // process is still there. It allocates: never from the processing
1057 // thread.
1058 if constexpr(requires { eff.send_message_from_worker = [](auto&&) { }; })
1059 {
1060 using send_type = std::remove_cvref_t<decltype(eff.send_message_from_worker)>;
1061 using message_type
1062 = std::remove_cvref_t<typename function_argument<send_type>::type>;
1063 eff.send_message_from_worker = [proc = QPointer{&this->process()}](message_type m) {
1064 ossia::qt::run_async(qApp, [proc, m = std::move(m)]() mutable {
1065 if(proc && proc->to_ui)
1066 MessageBusSender{proc->to_ui}(std::move(m));
1067 });
1068 };
1069 }
1070 }
1071 }
1072
1073 void connect_worker(const std::shared_ptr<safe_node<Node>>& node_ptr)
1074 {
1075 if constexpr(avnd::has_worker<Node>)
1076 {
1077 avnd::effect_container<Node>& eff = node_ptr->impl;
1078
1079 // Initialize the thread pool beforehand
1080 score::TaskPool::instance();
1081
1082 // An object with a worker is always given the node which delivers the
1083 // results at the beginning of its tick (exec_node_t).
1084 const std::shared_ptr<node_with_worker<Node>> self{
1085 node_ptr, static_cast<node_with_worker<Node>*>(node_ptr.get())};
1086
1087 for(auto& e : eff.effects())
1089 e, std::weak_ptr{std::shared_ptr<Node>(node_ptr, &e)},
1090 exec_worker_delivery<Node>{self});
1091 }
1092 }
1093
1094 // Update everything
1095 void update_controls(std::shared_ptr<safe_node<Node>>& ptr)
1096 {
1097 avnd::effect_container<Node>& eff = ptr->impl;
1098 {
1099 for(auto state : eff.full_state())
1100 {
1101 avnd::input_introspection<Node>::for_all(state.inputs, [&](auto& field) {
1102 avnd::update_if_holding_value(state, field);
1103 });
1104 }
1105 }
1106 }
1107
1108 void cleanup() override
1109 {
1110 if constexpr(requires { this->process().from_ui; })
1111 {
1112 this->process().from_ui = [](QByteArray arr) {};
1113 }
1114 // FIXME cleanup eff.effect.send_message too ?
1115 for(auto& c : m_executionUpdate)
1116 QObject::disconnect(c);
1117 m_executionUpdate.clear();
1118 if constexpr(requires { Node::monitor; })
1119 this->process().setMonitor({});
1120
1121#if SCORE_PLUGIN_GFX
1122 if constexpr(is_gpu<Node>)
1123 {
1124 // FIXME this must move in the Node dtor. See video_node
1125 auto& gfx_exec = this->system().doc.template plugin<Gfx::DocumentPlugin>().exec;
1126 if(node_id >= 0)
1127 {
1128 gfx_exec.ui->unregister_node(node_id);
1129 node_id = score::gfx::invalid_node_index;
1130 }
1131 }
1132
1133 // FIXME refactor this with other GFX processes
1134 for(auto* outlet : this->process().outlets())
1135 {
1136 if(auto out = qobject_cast<Gfx::TextureOutlet*>(outlet))
1137 {
1138 out->nodeId = -1;
1139 }
1140 }
1141#endif
1142 ::Execution::ProcessComponent::cleanup();
1143 }
1144
1145 ~Executor() { }
1146};
1147}
Definition GfxApplicationPlugin.hpp:13
Definition GfxExecNode.hpp:46
The Path class is a typesafe wrapper around ObjectPath.
Definition Path.hpp:52
Definition Port.hpp:242
Definition ScenarioSettingsModel.hpp:21
Definition UuidKey.hpp:345
Definition score-plugin-avnd/Crousti/Executor.hpp:193
Definition score-plugin-avnd/Crousti/Executor.hpp:255
void followExecutionUpdate(F f)
Definition score-plugin-avnd/Crousti/Executor.hpp:279
std::vector< QMetaObject::Connection > m_executionUpdate
What follows the execution update setting; cut at cleanup.
Definition score-plugin-avnd/Crousti/Executor.hpp:274
void recompute_ports()
Definition score-plugin-avnd/Crousti/Executor.hpp:562
Definition score-plugin-avnd/Crousti/ProcessModel.hpp:122
Definition Metadatas.hpp:51
Definition score-plugin-avnd/Crousti/Executor.hpp:156
TreeNode< DeviceExplorerNode > Node
Definition DeviceNode.hpp:74
Components used for the execution of a score.
Definition PortTap.cpp:24
Binds the rendering pipeline to ossia processes.
Definition AssetTable.cpp:7
Base classes and tools to implement processes and layers.
Definition JSONVisitor.hpp:1115
Definition Controls.hpp:30
std::function< void(QByteArray)> ui_to_processor_bus(Node &eff, std::weak_ptr< Execution::ExecutionCommandQueue > qex_ptr, std::weak_ptr< Execution::GCCommandQueue > gcq)
Definition MessageBus.hpp:419
void bind_worker(Object &obj, std::weak_ptr< Object > obj_wp, Delivery deliver)
Definition WorkerBinding.hpp:31
std::conditional_t< stops_with_extra_tick< Node >, node_with_deferred_stop< Node >, exec_base_node_t< Node > > exec_node_t
Definition score-plugin-avnd/Crousti/Executor.hpp:189
void free_on_main_thread(const std::weak_ptr< Execution::GCCommandQueue > &gcq, T &... t)
Definition MessageBus.hpp:402
STL namespace.
Definition ExecutionContext.hpp:208
Definition ExecutionContext.hpp:73
Definition Process/Execution/ProcessComponent.hpp:101
Definition ExecutionSetup.hpp:38
Definition ExecutionTransaction.hpp:21
Definition GfxExecNode.hpp:154
Definition PortForward.hpp:23
Definition PortForward.hpp:27
Definition ExecutorPortSetup.hpp:720
Definition ExecutorPortSetup.hpp:672
Definition ExecutorPortSetup.hpp:38
Definition score-plugin-avnd/Crousti/Executor.hpp:131
Definition score-plugin-avnd/Crousti/Executor.hpp:162
std::atomic_bool stop_pending
Set by the process when it stops, from whichever thread stops it.
Definition score-plugin-avnd/Crousti/Executor.hpp:166
~node_with_deferred_stop()
Definition score-plugin-avnd/Crousti/Executor.hpp:170
Definition ExecutorPortSetup.hpp:23
Definition score-plugin-avnd/Crousti/Executor.hpp:99
Execution::ExecutionCommandQueue worker_results
Definition score-plugin-avnd/Crousti/Executor.hpp:104
T & settings() const
Access a specific Settings model instance.
Definition ApplicationContext.hpp:41
Definition DocumentContext.hpp:18