Pulp 1.88.0
Cross-platform audio plugin framework
Loading...
Searching...
No Matches
signal_graph_runtime.hpp
Go to the documentation of this file.
1#pragma once
2
3// Runtime and mutation surface for the Pulp host signal graph. Include the
4// focused node, connection, or custom-node header when runtime access is not
5// required.
6
7#include <atomic>
8#include <cassert>
9#include <cmath>
10#include <cstddef>
11#include <cstdint>
12#include <functional>
13#include <memory>
14#include <mutex>
15#include <optional>
16#include <pulp/audio/buffer.hpp>
36#include <pulp/midi/buffer.hpp>
40#include <pulp/runtime/slot.hpp>
44#include <string>
45#include <string_view>
46#include <thread>
47#include <type_traits>
48#include <unordered_map>
49#include <unordered_set>
50#include <vector>
51
52namespace pulp::format {
53class Processor;
55} // namespace pulp::format
56
57namespace pulp::host {
58
59class GraphSerializer;
60class SignalGraph;
61class SpectralRuntimeBank;
62
63namespace detail {
64struct ExactParameterIngressOwner;
65// Mints an exact-parameter ingress ownership token. The type stays defined in
66// the signal-graph translation unit so its identity is owned here, not by the
67// timeline binding that claims nodes with it.
68std::shared_ptr<ExactParameterIngressOwner> make_exact_parameter_ingress_owner();
69
70// Returns the frozen host-facing parameter catalog for an authored in-process
71// ProcessorNode, or nullopt when `id` is not one of this graph's ProcessorNodes.
72// The helper is kept in the timeline delivery translation unit so the graph's
73// private lifetime map remains the sole owner of ProcessorNode reachability.
74std::optional<std::vector<HostParamInfo>> processor_node_parameters(const SignalGraph& graph,
75 NodeId id);
76} // namespace detail
77
80
81// ── Signal Graph ────────────────────────────────────────────────────────
82
84 public:
87
88 struct GraphLimits {
89 std::size_t max_nodes = 4096;
90 std::size_t max_connections = 16384;
91 std::size_t max_ports = 32768;
92 int max_block_size = 16384;
93 // Deterministic generated-graph work-unit budget. This is not a
94 // hardware CPU-cycle estimate; it is a stable shape/block-size score
95 // for importers that need to reject expensive generated graphs before
96 // prepare(). Zero disables the budget.
97 std::size_t max_estimated_work_units = 0;
98 };
99
109
116
118 std::size_t node_count = 0;
119 std::size_t ordered_node_count = 0;
120 std::size_t connection_count = 0;
121 std::size_t total_ports = 0;
123 std::size_t node_audio_buffer_bytes = 0;
124 std::size_t automation_buffer_bytes = 0;
125 std::size_t delay_buffer_bytes = 0;
127 std::size_t spectral_buffer_bytes = 0;
128 std::uint64_t snapshot_generation = 0;
129 };
130
131 // Immutable, control-thread view of the latency analysis captured by a
132 // prepared graph snapshot. Entries are emitted in snapshot order (the
133 // deterministic processing order for nodes, and authored connection order
134 // for edges). This is an introspection value only; it is not an audio
135 // callback or realtime API.
138 std::int64_t input_latency_samples = 0;
139 std::int64_t output_latency_samples = 0;
140 };
141
146
148 std::int64_t total_latency_samples = 0;
149 std::vector<PreparedNodeLatency> nodes;
150 std::vector<PreparedConnectionDelay> connections;
151
152 // Value lookups deliberately return nullopt for unknown identities;
153 // zero is a valid latency and must not be used as a miss sentinel.
154 std::optional<PreparedNodeLatency> node(NodeId id) const;
155 std::optional<std::int64_t> connection_delay(const Connection& connection) const;
156 };
157
161 std::uint64_t estimated_cost = 0;
162 bool prepared = false;
163
164 bool should_run_optional_work() const noexcept {
165 return decision.should_run();
166 }
167 };
168
170 std::uint64_t value = 0;
171 explicit operator bool() const noexcept {
172 return value != 0;
173 }
175 return a.value == b.value;
176 }
177 };
178
179 SignalGraph() = default;
180
182 const std::string& name);
185 const std::string& name);
187 const std::string& name);
189 return connect(source.handle, dest.handle);
190 }
202 SpectralError spectral_error() const noexcept {
203 return spectral_error_.load(std::memory_order_relaxed);
204 }
205
206 // Add nodes — returns the node ID
207 NodeId add_input_node(int channels, const std::string& name = "Input");
208 NodeId add_output_node(int channels, const std::string& name = "Output");
210 NodeId add_unresolved_plugin_node(const PluginInfo& info, int num_inputs, int num_outputs,
211 const std::string& name);
212
213 // Add a plugin node wrapping a caller-provided slot. Useful for tests
214 // (mock latency, mock processing) and for hosts that build their own
215 // PluginSlot implementations outside of PluginSlot::load().
216 NodeId add_plugin_node(std::unique_ptr<PluginSlot> slot, int num_inputs, int num_outputs,
217 const std::string& name = "Plugin");
221 NodeId add_processor_node(std::shared_ptr<format::ProcessorNodeInstance> processor,
222 const std::string& name = {});
223 NodeId add_processor_node(std::unique_ptr<format::Processor> processor,
224 const std::string& name = {});
225 bool is_processor_node(NodeId id) const;
228 std::shared_ptr<format::ProcessorNodeInstance> processor_node_instance(NodeId id) const;
229
236 format::AudioInputBusProvider provider) noexcept;
237 NodeId add_gain_node(const std::string& name = "Gain");
241 NodeId add_trigger_lowpass(float coefficient = 0.5f);
243 std::vector<TriggerEdge> trigger_edges() const;
244 TriggerPreparation prepare_trigger_plan(double sample_rate, int max_frames);
246 try_install_prepared_trigger_plan(std::unique_ptr<PreparedTriggerPlan>& candidate);
248 TriggerGraphResult process_checked(audio::BufferView<float>& output,
249 const audio::BufferView<const float>& input, int frames);
252 // View expires at the next render/reset/publication boundary. Read while stopped.
253 std::span<const TriggerRecord> trigger_trace() const noexcept;
254
255 NodeId add_midi_input_node(const std::string& name = "MIDI In");
256 NodeId add_midi_output_node(const std::string& name = "MIDI Out");
257 // Registers or replaces a custom type. If existing nodes use the same
258 // `(type_id, version)`, the live snapshot is invalidated so prepare()
259 // can rebuild with the matching process callback. Shape mismatches keep
260 // placeholder passthrough semantics instead of attaching the callback.
264 // Try-lock reads fail Busy instead of waiting for prepare/release. A graph
265 // generation change invalidates handles; obtain a fresh one after prepare.
269 std::span<std::byte> output,
272 template <typename Report>
273 requires(!std::is_same_v<std::remove_cvref_t<Report>, std::span<std::byte>>)
276 Report& output,
279 static_assert(std::is_trivially_copyable_v<Report>);
281 handle, schema, std::as_writable_bytes(std::span<Report>(&output, 1)), consistency);
282 }
283
284 // Returns a callback-free, value-owned snapshot sorted lexicographically by
285 // (type_id, version). The snapshot remains valid across later registrations
286 // and after this graph is destroyed.
287 std::vector<CustomNodeTypeMetadata> custom_node_types() const;
288 const CustomNodeType* custom_node_type(std::string_view type_id) const;
289 const CustomNodeType* custom_node_type(std::string_view type_id, int version) const;
290 const SampleKernelDescriptor* sample_kernel_type(std::string_view type_id, int version) const;
292 std::optional<SampleRegionDescriptor> sample_region(SampleRegionId id) const;
293 std::vector<SampleRegionDescriptor> sample_regions() const;
295 return sample_region_parameter_binding_;
296 }
297 NodeId add_custom_node(std::string_view type_id, const std::string& name = {});
298 NodeId add_custom_node(std::string_view type_id, int version, const std::string& name = {});
299 NodeId add_unresolved_custom_node(std::string_view type_id, int version, int num_inputs,
300 int num_outputs, const std::string& name);
301
302 // Opaque per-node state for stateful custom nodes.
303 // custom_node_state() returns the live instance's save_state() when the node
304 // is resolved + stateful, else the last-loaded blob (preserved for
305 // unresolved nodes). Empty when `id` is not a custom node or has no state.
306 // set_custom_node_state() stores the blob to apply to the instance on the
307 // next prepare(); returns false when `id` is not a custom node. Both run on
308 // the UI/main thread — never the audio thread.
309 std::vector<uint8_t> custom_node_state(NodeId id) const;
310 bool set_custom_node_state(NodeId id, const std::vector<uint8_t>& bytes);
311
312 // Remove a node and all its connections
314
315 // Connect two nodes (port-to-port)
316 bool connect(NodeId source, PortIndex source_port, NodeId dest, PortIndex dest_port);
317
320 GraphResult connect(const PortHandle& source, const PortHandle& dest);
321
322 // Connect with an explicit one-block delay. Permitted to close a cycle
323 // (the back-edge the user is intentionally introducing) and invisible to
324 // topological sort. The destination reads the source's previous-block
325 // output, giving the feedback loop a block-sized delay.
326 bool connect_feedback(NodeId source, PortIndex source_port, NodeId dest, PortIndex dest_port);
327
328 // MIDI connection: routes events from source's MIDI output into dest's
329 // MIDI input. Ports are ignored (MIDI is node-scoped, not port-scoped).
330 // Participates in cycle detection and topological sort the same way as
331 // audio connections.
332 bool connect_midi(NodeId source, NodeId dest);
333
334 // Sidechain connection: routes a source's audio output port into the
335 // destination plugin's sidechain bus port. The destination
336 // port is `dest_sidechain_port` — the caller supplies the absolute
337 // port index on the destination node (the host knows how many main
338 // input ports a plugin exposes via PluginInfo::num_inputs; sidechain
339 // ports follow main inputs). The flag does NOT change topological
340 // ordering — sidechain participates in cycle detection and PDC the
341 // same as a normal audio edge — but it is tagged so UIs, serializers,
342 // and per-format adapters can recognize the role.
343 //
344 // Tagging is metadata: the actual routing still uses (source, source
345 // port) → (dest, dest sidechain port). Calling this is equivalent to
346 // connect() with an extra flag, plus a guard that the destination is
347 // a Plugin node (sidechain only makes sense on plugins).
348 bool connect_sidechain(NodeId source, PortIndex source_port, NodeId dest,
349 PortIndex dest_sidechain_port);
350
351 // Automation connection: the audio samples on `src`'s output port drive
352 // `dest`'s parameter `dest_param_id`. Two control points per block (first
353 // + last sample) are delivered to the plugin via
354 // PluginSlot::process()'s ParameterEventQueue so plugins can interpolate
355 // sample-accurately.
356 //
357 // Source values are clamped to [0,1] then mapped linearly to
358 // [range_lo, range_hi] in the plugin's plain parameter domain.
359 //
360 // smoothing_ms applies a per-source linear slew before mix/clamp.
361 // MixMode::Replace is the default; a second Replace edge targeting the
362 // same (dest, param) is rejected. MixMode::Add sums multiple edges,
363 // then clamps to the param's range.
364 bool connect_automation(NodeId src, PortIndex src_audio_port, NodeId dest,
365 uint32_t dest_param_id, float range_lo, float range_hi,
366 float smoothing_ms = 0.0f, AutomationMix mix = AutomationMix::Replace);
367
368 // Audio-rate modulation connection: source audio samples drive every
369 // sample of an AudioRate destination parameter. This edge is distinct
370 // from sparse automation above; it is accepted only when the edge can be
371 // represented as a valid GraphNode-scoped state::ModulationLane targeting
372 // a continuous AudioRate destination parameter.
373 bool connect_audio_rate_modulation(NodeId src, PortIndex src_audio_port, NodeId dest,
374 uint32_t dest_param_id, float range_lo, float range_hi,
375 float smoothing_ms = 0.0f,
377
381 bool disconnect_modulation(NodeId src, PortIndex src_audio_port, NodeId dest,
382 uint32_t dest_param_id, bool audio_rate);
383
384 // Project an accepted graph audio-rate modulation edge into the typed
385 // modulation-lane contract used by instruments, adapters, and generated
386 // graphs. Returns false for non-modulation edges or unresolved metadata.
388 state::ModulationLane& lane) const;
389
390 // Inject a MIDI buffer into a MidiInput source node. Call before process();
391 // the latest published events become that node's MIDI output exactly once.
392 // Audio-callback-safe after prepare(): the mailbox is fixed-capacity and the
393 // call never allocates or locks. Exactly one thread may inject into a given
394 // MidiInput node; it may be the audio thread immediately before process(), or
395 // a control-side producer, but never concurrent writers. A false return means
396 // the live node is unavailable or the bounded mailbox retained only a prefix;
397 // that retained prefix is still published. A preserved MidiInput NodeId keeps
398 // its unconsumed publication across a gap-free prepare_swap().
399 bool inject_midi(NodeId midi_input_node, const midi::MidiBuffer& events) noexcept;
400
401 // Inject a block-local MIDI stream together with its prepared MPE
402 // expression sidecar. The two payloads share one mailbox sequence and are
403 // consumed exactly once, so a bounded sidecar refusal cannot publish a
404 // newer expression stream against an older MIDI block.
405 bool inject_midi(NodeId midi_input_node, const midi::MidiBuffer& events,
406 const midi::MpeBuffer& mpe) noexcept;
407
408 // Inject sample-accurate parameter events into a Plugin node. Call before
409 // process(); the latest published batch is consumed exactly once by that
410 // node on the next block it processes. Events use block-relative sample
411 // offsets and are delivered after graph-generated automation, so an
412 // injected event wins an equal-offset tie while existing automation keeps
413 // capacity priority. Returns false for an unprepared/non-Plugin/unresolved
414 // node or when `events` already reports source-side overflow; the retained
415 // prefix is still published in the overflow case.
417
418 // Drain queued nonempty MIDI blocks from a MidiOutput sink node in process
419 // order. Appends to `out`; returns false if bounded egress overflowed, a
420 // queued block was already incomplete, or `out` lacks capacity/UMP sidecar
421 // storage. A destination-copy failure retains the undelivered suffix;
422 // provide more storage and call again to resume without replaying the
423 // delivered prefix.
424 bool extract_midi(NodeId midi_output_node, midi::MidiBuffer& out) const;
425
426 // Disconnect
427 bool disconnect(NodeId source, PortIndex source_port, NodeId dest, PortIndex dest_port);
428
429 // Query
430 const GraphNode* node(NodeId id) const;
431 const std::vector<GraphNode>& nodes() const {
432 return nodes_;
433 }
434 const std::vector<Connection>& connections() const {
435 return connections_;
436 }
437
438 // Capture and validate control-side authoring lineage. These operations
439 // take the same mutation lock as graph edits, so a receipt is never a
440 // mixed read of graph identity and generation. A receipt is observational
441 // only; PreparedTopologyEdit remains the publication boundary and performs
442 // its own stale-base check at commit().
445
446 // Check if connecting would create a cycle
447 bool would_create_cycle(NodeId source, NodeId dest) const;
448
449 // Compute processing order (topological sort)
450 std::vector<NodeId> processing_order() const;
451
452 // Lifecycle
453 bool prepare(double sample_rate, int max_block_size);
454 // Exact Custom node whose create/load_state callback most recently caused
455 // prepare() to fail, or 0 when the last prepare had no such failure.
456 // Control-thread diagnostic; useful for signed-artifact rejection messages.
458
459 // Begin an isolated topology transaction. The returned edit owns a private
460 // authoring graph and custom-type registry; its mutations never invalidate
461 // this graph's live snapshot. prepare() compiles that candidate off-side and
462 // commit() installs the authoring graph and compiled snapshot together.
463 // Destroying an uncommitted edit is a complete rollback.
464 std::unique_ptr<PreparedTopologyEdit> begin_prepared_topology_edit();
465
466 // Live swap transaction outcomes.
467 enum class SwapResult {
468 Swapped, // the new topology was published with no silent block.
469 Staged, // a replacement instance is staged for prepare_swap().
470 NeedsEagerPrepare, // not reinit-free (or a latency change) — caller must
471 // call prepare() under the usual no-process/no-pump
472 // contract. Ordinary failures invalidate the live
473 // snapshot; the MidiOutput pending-egress refusal keeps
474 // it live for extraction until that eager prepare.
475 NotInSwapEdit, // prepare_swap called without a matching begin_swap_edit.
476 };
477 // Begin a transactional topology edit that MAY publish with no silence. Between
478 // begin_swap_edit() and prepare_swap()/abort_swap_edit(), the CALLING thread's
479 // graph mutations (connect/disconnect/gain/add/remove) do NOT invalidate the
480 // live snapshot — it keeps playing the old compiled graph until prepare_swap
481 // atomically publishes the new one. A mutation from any OTHER thread, or a
482 // lifecycle/limits/registry mutation from any thread, cancels the no-silence
483 // attempt (the live snapshot is invalidated as usual). Single-owner: nesting is
484 // refused. Caller must pair with prepare_swap or abort_swap_edit.
486 // Publish the staged topology with no silent block if it is reinit-free (same
487 // instances, no re-init, unchanged latency, no per-snapshot state to glitch);
488 // otherwise return NeedsEagerPrepare so the caller eager-prepares. Ordinary
489 // refusals invalidate the live snapshot. The MidiOutput pending-egress refusal
490 // deliberately leaves it live so the caller can drain extract_midi() first.
491 SwapResult prepare_swap(double sample_rate, int max_block_size);
492 // Abandon the no-silence attempt: invalidate the live snapshot (the staged
493 // edits remain in the graph and take effect on the next prepare()).
495 void release();
496
501 // Stage a live instance replacement for a node, resolved through the scanned
502 // catalog (never an arbitrary path). To capture the outgoing instance's state
503 // without a silent block, this reads the OLD, still-live plugin's save_state()
504 // and get_parameter() on the control thread while the audio thread may be inside
505 // its process(). CONTRACT: a hosted plugin that opts into live swap must make
506 // save_state()/get_parameter() safe to call concurrently with process() (they are
507 // read-only snapshots for most plugins). Parameter capture goes through the cached
508 // contract + atomic get_parameter; full-state capture is best-effort — a plugin
509 // whose getState is strictly main-thread-only may capture a torn state, so keep it
510 // concurrency-tolerant or rely on parameter re-sync alone.
513
514 using PluginLoaderForTest = std::function<std::unique_ptr<PluginSlot>(const PluginInfo&)>;
516
517 // Test seam for the non-zero MIDI-ingress publication sequence. The value
518 // becomes the predecessor consumed by the next inject_midi() call.
520 std::uint64_t predecessor) noexcept;
521 std::uint64_t midi_input_sequence_for_test(NodeId midi_input_node) const noexcept;
522
523 // Prepare-time topology bounds. Hosts that accept generated or user-built
524 // graphs can lower these before prepare() so oversized graphs fail before
525 // snapshot allocation or plugin prepare.
528 return limits_;
529 }
530 std::size_t estimate_generated_graph_work_units(int max_block_size) const;
533 std::uint64_t sample_region_binding_generation() const noexcept;
535
536 // Per-node CPU-load telemetry, accumulated by process() and read from the
537 // control/UI thread. process() wraps each node's work in an
538 // AudioProcessLoadMeasurer begin()/end() (relaxed-atomic, RT-safe); these
539 // measurers persist across re-prepare() so a node's load history survives
540 // topology recompiles. Returns one entry per currently-present node that
541 // has been prepared (removed nodes' lingering measurers are filtered out),
542 // each a latest-value snapshot (may mix adjacent callbacks). Safe to poll
543 // while the audio thread runs.
544 // Nodes are timed only once read (two clock reads per node per block): the
545 // first call turns monitoring on, returning current (zero or stale) values.
568 // One ordinary audio edge, observed at its source before PDC/mixing. Attach
569 // and detach are control-thread lifecycle operations: stop process() and
570 // any pinned-snapshot rendering first, as for prepare(). Reattachment starts
571 // a fresh 256-record ring and a zero-based sample clock. Reprepare/topology
572 // publication retires the attachment; transport reset detaches it. Serial
573 // routing only; execution-mode changes detach at the next process call.
575 const PortHandle& destination);
577 // One control-thread consumer may read concurrently with process(). These
578 // methods serialize with other control-thread lifecycle operations.
580 std::uint64_t audio_edge_probe_dropped() const;
583 std::vector<NodeLoadReport> node_loads() const;
584 // true times nodes before any read; false stops until the next node_loads().
585 void set_node_load_monitoring(bool enabled) noexcept;
586 bool node_load_monitoring() const noexcept;
587
588 // Whole-graph process-load snapshot (load == callback-budget fullness; 1.0 == a
589 // full buffer). callback_count == 0 means no measurement yet. Safe to poll while
590 // the audio thread runs. Feeds the live-swap admission gate.
591 audio::AudioProcessLoadSnapshot graph_load() const;
592
593 // Per-node live-DSP telemetry (fixed-slot p50/p95/p99 + jitter + over-budget
594 // attribution; see audio/live_dsp_telemetry.hpp). Disabled by default; the
595 // audio path is a single predicted-not-taken branch when off. The store lives
596 // per-CompiledGraph so it rides the RCU snapshot lifetime — telemetry resets on
597 // a topology recompile (a new topology is a new timing baseline). Per-node
598 // timing is recorded on the legacy reference-walk path (the default when routed
599 // dispatch is off); routed-executor blocks still contribute graph-level timing.
600 // Control/UI thread only; serialized with snapshot publication.
603 // Drain the live snapshot's ring and return a copy of the latest summary.
604 // Non-real-time; single poller (control/UI thread). Pins the live snapshot for
605 // the duration of the drain. Returns an empty (available == false) snapshot when
606 // no graph is prepared.
607 audio::LiveDspTelemetrySnapshot poll_live_dsp_telemetry();
608
609 // Test-only: run compile_() once and DISCARD the result, WITHOUT prepare()'s
610 // null-first prologue. Exists so a TSan/ASan test can run compile_() on one
611 // thread while process() runs the live snapshot on another — proving compile_()
612 // never mutates state the audio thread reads (the 2.2a no-silence-swap
613 // contract; planning/2026-07-02-signalgraph-prepared-swap-design.md). Publishes
614 // nothing, so it has no audible effect and no production caller; it only
615 // exercises the compilation path for the race test. Control-thread only.
616 void compile_snapshot_for_test(double sample_rate, int max_block_size);
617
618 // ── Canonical-executor migration ─────────────────────────────────────
619 // Hooks the SignalGraph→GraphRuntimeExecutor translation needs without
620 // exposing the private CompiledGraph. The translation lives in
621 // signal_graph_executor_routing.{hpp,cpp}; these are control-thread only.
622
623 // True once prepare() has published a live compiled snapshot.
624 bool is_prepared() const noexcept {
625 return spectral_mode_.load(std::memory_order_acquire) || live_slot_.has_value();
626 }
627 // Max block size the live snapshot was prepared for (0 if not prepared).
628 int prepared_max_block_size() const noexcept;
629 // The live compiled snapshot's per-node gain atomic (Gain nodes only), or
630 // nullptr. The pointer stays valid only while the snapshot returned by
631 // live_snapshot_handle() is retained AND no re-prepare has occurred; a
632 // routing built from it must be rebuilt after the graph recompiles.
633 std::atomic<float>* live_gain_atomic(NodeId id) const noexcept;
634 // The live compiled snapshot's PluginSlot for a Plugin node, or nullptr.
635 // Same lifetime contract as live_gain_atomic: valid only while
636 // live_snapshot_handle() is retained and no re-prepare has occurred.
637 PluginSlot* live_plugin_slot(NodeId id) const noexcept;
638 // The live compiled snapshot's resolved process callback for a Custom node,
639 // or nullptr when the node is unresolved (unregistered type / shape
640 // mismatch). The pointee is the snapshot's own callback (which keeps any
641 // stateful instance alive); same lifetime contract as live_plugin_slot.
643 // The live compiled snapshot's transport-aware custom callback for a Custom
644 // node, or nullptr when the node's type registered none. Same lifetime
645 // contract as live_custom_processor; non-null iff that node's
646 // GraphNode::transport_sensitive was resolved true.
648 // The live compiled snapshot's event-aware custom callback for a Custom
649 // node, or nullptr when the node's type registered none. Same lifetime
650 // contract as live_custom_processor.
652 // Prepare-stable intrinsic latency cached in the live compiled snapshot.
653 // Returns zero for an unresolved/zero-latency/non-Custom node, or without a
654 // snapshot.
655 int live_custom_latency_samples(NodeId id) const noexcept;
656 // The live compiled snapshot's bound param-aware custom callback for a Custom
657 // node whose type declared baked_params + process_instance_baked_param, or
658 // nullptr otherwise. The pointee captured the stateful instance shared_ptr by
659 // value (keepalive), so a bake() that copies it stays self-contained. Same
660 // lifetime contract as live_custom_processor. Used only by the bake-layer
661 // parameter-injection path — never by live-graph processing.
663 // Opaque keepalive for the live compiled snapshot so a translated routing
664 // can pin the lifetime of the gain atomics + plugin slots it references.
665 // Takes the graph mutation lock to copy the snapshot pointer, so it blocks
666 // for the length of a concurrent prepare()/compile. Control thread only:
667 // never call it from the audio thread, nor while holding that lock.
668 std::shared_ptr<const void> live_snapshot_handle() const noexcept;
669
675 RoutedExecutionStatus routed_execution_status(int block_size) const noexcept;
680 std::uint64_t routed_only_execution_failures() const noexcept {
681 return routed_only_execution_failures_.load(std::memory_order_relaxed);
682 }
683
688 routed_only_execution_owners_.fetch_add(1, std::memory_order_relaxed);
689 }
691 auto owners = routed_only_execution_owners_.load(std::memory_order_relaxed);
692 while (owners != 0 && !routed_only_execution_owners_.compare_exchange_weak(
693 owners, owners - 1, std::memory_order_relaxed)) {
694 }
695 assert(owners != 0 && "unbalanced routed-only execution lease");
696 }
697
698 // Controls whether the audio callback drives the canonical
699 // GraphRuntimeExecutor when the live snapshot is executor-eligible. Default
700 // ON: the routed executor is the primary inter-node backend. Set it OFF to
701 // force the legacy walk — the parity tests do this to keep the walk as an
702 // independent reference oracle. Ineligible graphs always fall back to the
703 // legacy walk regardless of this flag. The flag is a control-thread toggle read
704 // relaxed on the audio thread. The two paths produce bit-identical output
705 // per block for eligible zero-PDC graphs, so toggling mid-stream is RT-safe
706 // and seamless there. A prepared snapshot with active feed-forward PDC pins
707 // its execution domain until the next prepare: the legacy, serial, and
708 // parallel paths own independent delay history, so switching between them
709 // mid-stream would select a stale ring. For graphs with FEEDBACK, the legacy walk
710 // and the executor keep INDEPENDENT one-block-delay state (the legacy
711 // ConnectionDelay::feedback_prev vs the executor's per-edge prev slot), so a
712 // mid-stream switch resets feedback history to whatever the destination path
713 // last wrote — a one-block transient, not a crash or a permanent divergence.
714 // Pick a path before starting a feedback graph and keep it for the stream.
715 void set_canonical_executor_routing_enabled(bool enabled) noexcept {
716 canonical_executor_routing_enabled_.store(enabled, std::memory_order_relaxed);
717 }
718 // Returns the requested opt-in flag, not a promise that the current block
719 // is taking the executor path. Actual dispatch also requires an eligible
720 // prepared snapshot and a scratch pool sized for the block.
722 return canonical_executor_routing_enabled_.load(std::memory_order_relaxed);
723 }
724
725 // Opt into the LEVELIZED PARALLEL executor for eligible graphs (default OFF,
726 // independent of the serial executor opt-in). Enable BEFORE prepare(): the
727 // parallel-safe routing snapshot + levelization are built at compile time and
728 // the worker pool is started there, so flipping this on after prepare() has
729 // no effect until the next prepare(). When enabled + eligible + the pool is
730 // running + the parallel pool fits the block, process() routes through
731 // GraphRuntimeExecutor::process_parallel; otherwise it falls back to the
732 // serial executor (if its opt-in is set) and then the legacy walk. Output is
733 // bit-identical across all three paths.
734 void set_parallel_routing_enabled(bool enabled) noexcept {
735 parallel_routing_enabled_.store(enabled, std::memory_order_relaxed);
736 }
737 bool parallel_routing_enabled() const noexcept {
738 return parallel_routing_enabled_.load(std::memory_order_relaxed);
739 }
740 // Publish the native device or AU render workgroup to the persistent
741 // parallel executor. Safe before prepare and on render-context changes.
742 void set_audio_workgroup(void* workgroup) noexcept override {
743 worker_pool_.set_audio_workgroup(workgroup);
744 }
745 void set_audio_workgroup_from_render_context(void* workgroup) noexcept override {
746 worker_pool_.set_audio_workgroup_from_render_context(workgroup);
747 }
748 bool prepare_audio_workgroup_for_render() noexcept override {
749 return worker_pool_.prepare_audio_workgroup_for_render();
750 }
751 void wait_for_audio_workgroup_update() noexcept override {
752 worker_pool_.wait_for_audio_workgroup_update();
753 }
754 void* configured_audio_workgroup() const noexcept {
755 return worker_pool_.configured_audio_workgroup();
756 }
757 // Break-even threshold for the parallel executor. A level runs across the
758 // worker pool only when its static work-weight x frame count reaches this
759 // many channel-samples; lower-cost levels stay serial to avoid fork/join
760 // overhead. Default 0 preserves the original "parallelize every eligible
761 // level" behavior. This is an RT-safe atomic setting and can be changed
762 // before or after prepare().
763 void set_parallel_min_work_units(std::uint64_t channel_samples) noexcept {
764 executor_.set_parallel_min_work_units(channel_samples);
765 }
766 std::uint64_t parallel_min_work_units() const noexcept {
767 return executor_.parallel_min_work_units();
768 }
769 // Diagnostic executor counters for verifying whether routed processing
770 // actually took the parallel/serial executor paths. This is telemetry, not a
771 // synchronization primitive; values may mix adjacent audio blocks.
773 return executor_.stats();
774 }
775
776 // Count of blocks where a routed path was ELIGIBLE (canonical/parallel routing
777 // enabled, a valid routed snapshot, and the pool fit the block) yet routed
778 // dispatch returned failure, so process() silently fell back to the legacy
779 // walk. The walk is BOTH the reference oracle and the fallback, so this
780 // degradation is invisible to the routed-vs-walk parity test — this counter is
781 // the only signal that an eligible graph stopped routing. Stays 0 in healthy
782 // operation; a non-zero value means the routed path is failing for graphs it
783 // should handle. Relaxed-atomic, written only by the audio thread.
784 std::uint64_t routed_walk_fallbacks() const noexcept {
785 return routed_walk_fallbacks_.load(std::memory_order_relaxed);
786 }
787
788 // Opt into ANTICIPATIVE RENDERING for eligible graphs (default OFF). When
789 // enabled + the canonical-executor routing is eligible + the graph has an
790 // anticipation-eligible latent interior (no live input / feedback / sidechain
791 // dependency — see analyze_anticipation_eligibility), compile_ carves that
792 // interior into an AnticipationLane that is pre-rendered AHEAD of the deadline
793 // off the audio thread; process() then consumes the pre-rendered boundary
794 // signals and runs the rest of the graph with the interior masked, absorbing
795 // the interior's CPU cost off the critical block. Requires the canonical
796 // executor path (set_canonical_executor_routing_enabled). Enable BEFORE prepare().
797 //
798 // Output is bit-identical to the canonical (interior-live) render WHEN the host
799 // pumps enough and uses a fixed block size equal to the prepared max block. Two
800 // intrinsic exceptions: (1) a block whose size differs from the prepared max, or
801 // a ring underrun, silences the interior for that block (it is never re-rendered
802 // live — see the producer-ownership note below); (2) a parameter/gain change on
803 // an interior node takes effect at render-ahead time, i.e. up to a lead's-worth
804 // of blocks earlier than in a live render.
805 //
806 // The interior's plugin state is advanced ONLY by the anticipation producer
807 // (pump_anticipation), never by process() — so the interior is always masked
808 // on the live path; an underrun yields silence for that block, never a live
809 // re-render (which would double-advance the producer-owned state).
810 //
811 // SAFETY NOTE: a host-clock-sensitive node (one whose output depends on the
812 // transport playhead) is NOT detectable from topology alone, so it opts in
813 // via PluginSlot::wants_transport() / a transport-aware custom callback. That
814 // capability is resolved once at compile into GraphNode::transport_sensitive
815 // and SEEDS AnticipationExclusion::TransportSensitive in
816 // analyze_anticipation_eligibility, which excludes the node AND its whole
817 // downstream cone from the ahead-rendered interior. A transport-sensitive
818 // node therefore always runs live/exterior, never ahead — so the live
819 // transport can stay populated on every block (the masked interior nodes are
820 // transport-insensitive by construction and ignore it). This per-node
821 // exclusion replaces the former blanket transport suppression under
822 // anticipation; transport_suppressed_for_anticipation() now counts the
823 // transport-sensitive nodes that anticipation forced exterior.
824 void set_anticipation_enabled(bool enabled) noexcept {
825 anticipation_enabled_.store(enabled, std::memory_order_relaxed);
826 }
827 bool anticipation_enabled() const noexcept {
828 return anticipation_enabled_.load(std::memory_order_relaxed);
829 }
830 // Producer pump (OFF the audio thread): render the live graph's anticipation
831 // interior ahead into its ring, up to `max_blocks` blocks. The host calls this
832 // from a single background/idle thread (it is the lane's sole producer; a
833 // concurrent/reentrant call is a guarded no-op). A no-op when anticipation is
834 // disabled or the live graph has no eligible interior. Returns the number of
835 // blocks rendered ahead this call.
836 //
837 // CONTRACT: the host MUST stop calling pump_anticipation and JOIN its producer
838 // thread before any prepare()/graph mutation. The pump renders the interior's
839 // plugin instances, which prepare() reinitializes and re-binds; running them
840 // concurrently is a data race (the snapshot reader-pin protects object lifetime,
841 // not plugin-state exclusivity). Same discipline as "no process() during
842 // prepare()". Re-prepare also resets the lead buffer — expect a brief transient.
843 int pump_anticipation(int max_blocks = 8);
844
848 bool required = false) const noexcept;
849
850 // Process one block of audio through the graph
851 void process(audio::BufferView<float>& output, const audio::BufferView<const float>& input,
852 int num_samples);
853
854 // Process one block with host transport/timeline context. Additive overload
855 // of the no-transport process() above: the supplied transport is delivered to
856 // the nodes that consume it (e.g. ProcessorNode, via the routed ProcessBlock's
857 // transport pointer), so a Processor sees the host playhead, mode, and
858 // render-speed hint for the block. Nodes that do not consume transport are
859 // bit-identical to the no-transport overload. Under active anticipation the
860 // transport stays live: transport-sensitive nodes are excluded from the
861 // ahead-rendered interior (AnticipationExclusion::TransportSensitive) and run
862 // exterior, so every interior node that IS ahead-rendered is
863 // transport-insensitive by construction and ignores the populated transport
864 // (see set_anticipation_enabled / transport_suppressed_for_anticipation).
865 void process(audio::BufferView<float>& output, const audio::BufferView<const float>& input,
866 int num_samples, const format::ProcessContext& transport);
867
868 // Count of transport-sensitive nodes that anticipation forced to run
869 // exterior (excluded from the ahead-rendered interior so they observe the
870 // live host transport). Resolved at compile when anticipation is active; the
871 // former "transport suppressed per block" meaning is retired — the per-node
872 // exclusion (AnticipationExclusion::TransportSensitive) replaced the blanket
873 // suppression. Stays 0 unless a transport-sensitive node coexists with active
874 // anticipation. Relaxed-atomic; written by compile_() on the control thread.
875 std::uint64_t transport_suppressed_for_anticipation() const noexcept {
876 return transport_suppressed_for_anticipation_.load(std::memory_order_relaxed);
877 }
878
879 // Clear all nodes and connections
880 void clear();
881
882 // Gain for a Gain node (linear, not dB). Defaults to 1.0. The setter is
883 // a UI/control-thread API; when prepared, it also updates the live
884 // snapshot's atomic gain so process() observes the change without
885 // re-prepare. The getter reads UI-owned graph state and is UI-thread-only.
886 bool set_node_gain(NodeId id, float linear_gain);
887 float node_gain(NodeId id) const;
888
889 // Latency in samples from any AudioInput to the graph's AudioOutput, as
890 // computed by prepare(). Reflects plugin-reported latencies plus any
891 // delay inserted by PDC. Returns 0 when not prepared.
892 int latency_samples() const {
893 return (int)total_latency_samples_.load(std::memory_order_relaxed);
894 }
895
896 // Latency arriving at a specific node's input (samples). Returns 0 when
897 // the node is unknown or the graph is not prepared.
899
900 // Copy the deterministic latency/PDC analysis from the currently prepared
901 // snapshot. Returns nullopt when topology has been mutated or prepare()
902 // has not yet published a snapshot. Control-thread only: copying the
903 // vectors is intentionally outside the audio callback surface.
904 std::optional<PreparedLatencySnapshot> prepared_latency_snapshot() const;
905
906 // Set a single parameter value on a Plugin node at the graph level. The
907 // call is forwarded to PluginSlot::set_parameter(). Returns false if the
908 // node is not a Plugin node or has no loaded slot. For block-rate routing
909 // from graph audio into parameters, use connect_automation() or
910 // connect_audio_rate_modulation().
911 bool set_node_parameter(NodeId id, uint32_t param_id, float value);
912
913 // Read a parameter's current value from a Plugin node (returns 0.0f if
914 // the node is not a Plugin or has no slot).
915 float get_node_parameter(NodeId id, uint32_t param_id) const;
916
917 // Control-thread diagnostic for transactional registry-GC tests and hosts
918 // that generate short-lived custom types.
919 std::size_t custom_node_type_count() const;
920
921 private:
922 // The standalone executor-routing builder must resolve every pointer and
923 // capture the matching snapshot keepalive as one publication-serialized
924 // operation. It is a friend rather than a new public locking API so the
925 // existing source/ABI surface stays unchanged.
928
929 friend std::optional<std::vector<HostParamInfo>>
931
932 friend class GraphSerializer;
934 friend class ExecutionSnapshot;
935 struct PrepareLifecycleObserver {
936 void* context = nullptr;
937 void (*plugin_will_prepare)(void*, PluginSlot*) noexcept = nullptr;
938 void (*custom_will_prepare)(void*, void*) noexcept = nullptr;
939 };
940 struct MidiBlockSnapshot {
941 MidiBlockSnapshot();
942 explicit MidiBlockSnapshot(bool reserve_storage);
943 MidiBlockSnapshot(const MidiBlockSnapshot& other);
944 MidiBlockSnapshot& operator=(const MidiBlockSnapshot& other) noexcept;
945 bool set_from_midi(const midi::MidiBuffer& src, uint64_t new_sequence,
946 bool source_incomplete = false) noexcept;
947 bool set_from_midi(const midi::MidiBuffer& src, const midi::MpeBuffer& mpe,
948 uint64_t new_sequence, bool source_incomplete = false) noexcept;
949 bool copy_to_midi(midi::MidiBuffer& dst) const noexcept;
950 bool append_to_midi(midi::MidiBuffer& dst, std::size_t& event_index,
951 std::size_t& sysex_index, std::size_t& ump_index) const noexcept;
952 bool has_payload() const noexcept;
953
954 midi::MidiBuffer events;
955 midi::UmpBuffer ump;
956 midi::MpeBuffer mpe;
957 bool incomplete = false;
958 uint64_t sequence = 0;
959 };
960
961 struct MidiInputMailbox {
962 static_assert(std::atomic<uint64_t>::is_always_lock_free,
963 "MIDI ingress sequence atomics must be lock-free");
964
966 MidiBlockSnapshot writer_scratch;
967 std::atomic<uint64_t> next_sequence{0};
968 std::atomic<uint64_t> sequence_seen{0};
969 };
970
971 struct MidiOutputMailbox {
972 static constexpr std::size_t kBlockCapacity = 4;
973 static_assert(std::atomic<bool>::is_always_lock_free,
974 "MIDI egress overflow state must be lock-free");
975
976 runtime::SpscQueue<MidiBlockSnapshot, kBlockCapacity> pending;
977 std::atomic<bool> incomplete{false};
978 MidiBlockSnapshot consumer_scratch;
979 bool consumer_has_retry = false;
980 bool consumer_incomplete = false;
981 std::size_t consumer_event_index = 0;
982 std::size_t consumer_sysex_index = 0;
983 std::size_t consumer_ump_index = 0;
984 };
985
986 struct ParameterBlockSnapshot {
987 std::array<state::ParameterEvent, state::ParameterEventQueue::kCapacity> events{};
988 std::size_t size = 0;
989 std::uint64_t sequence = 0;
990 bool source_incomplete = false;
991
992 bool set_from_queue(const state::ParameterEventQueue& src,
993 std::uint64_t new_sequence) noexcept;
994 bool append_to(state::ParameterEventQueue& dst) const noexcept;
995 };
996
997 struct ParameterInputMailbox {
998 runtime::TripleBuffer<ParameterBlockSnapshot> published;
999 ParameterBlockSnapshot writer_scratch;
1000 std::atomic<std::uint64_t> next_sequence{0};
1001 std::atomic<std::uint64_t> sequence_seen{0};
1002 };
1003
1004 struct NodeRuntime {
1005 NodeRuntime() : mpe_in(false), mpe_out(false) {}
1006
1007 // Per-node output-port channel storage (interleaved per-port, flat).
1008 // data_ has size num_output_ports * max_block_size_; channel_ptrs_[p]
1009 // points at data_[p * max_block_size_].
1010 std::vector<float> output_data;
1011 std::vector<float*> output_ptrs;
1012 // Per-node input-port scratch — callers write into these before the
1013 // node processes, then zero before the next block.
1014 std::vector<float> input_data;
1015 std::vector<float*> input_ptrs;
1016 std::vector<const float*> input_const_ptrs;
1017 struct EdgeRef {
1019 NodeRuntime* source_runtime = nullptr;
1020 };
1021 std::unique_ptr<std::atomic<float>> gain;
1022 std::vector<EdgeRef> inbound_midi_edges;
1023 std::vector<EdgeRef> inbound_audio_edges;
1024 std::vector<EdgeRef> sparse_automation_edges;
1025 std::vector<EdgeRef> audio_rate_modulation_edges;
1026
1027 // PDC: cumulative samples of latency from AudioInput to this node's
1028 // input ports (input_latency) and output ports (output_latency).
1029 // output_latency = input_latency + the node's prepare-stable intrinsic
1030 // latency (Plugin metadata or CustomNodeType::latency_samples).
1031 int64_t input_latency = 0;
1032 int64_t output_latency = 0;
1033
1034 // Per-node CPU-load telemetry. Points at a SignalGraph-owned
1035 // AudioProcessLoadMeasurer that PERSISTS across CompiledGraph snapshots
1036 // (keyed by NodeId in node_load_), so re-prepare() doesn't reset a
1037 // node's accumulated load. process() wraps this node's work in
1038 // begin()/end(); the measurer is relaxed-atomic (RT-safe). Null until
1039 // resolved in compile_(); never owned by NodeRuntime.
1041
1043 uint32_t id = 0;
1044 float min_value = 0.0f;
1045 float max_value = 1.0f;
1046 };
1047 std::vector<ParamBounds> param_bounds;
1048
1050 float v0 = 0.0f;
1051 float vN = 0.0f;
1052 float lo = 0.0f;
1053 float hi = 1.0f;
1054 bool has_add = false;
1055 bool touched = false;
1056 };
1057 std::vector<uint32_t> sparse_automation_param_ids;
1058 std::vector<SparseAutomationAccum> sparse_automation_accum;
1059
1060 // MIDI scratch is audio-thread-owned. Single-writer ingress and
1061 // control-thread egress use the mailboxes below so inject_midi()/
1062 // extract_midi() do not race process() scratch mutation. The shared
1063 // ingress mailbox preserves an unconsumed publication when a stable
1064 // MidiInput node crosses a gap-free snapshot swap. Egress is an ordered
1065 // bounded drain queue so later blocks cannot overwrite a pending
1066 // note-off before the control thread extracts it.
1067 midi::MidiBuffer midi_in;
1068 midi::MidiBuffer midi_out;
1069 midi::UmpBuffer midi_in_ump;
1070 midi::UmpBuffer midi_out_ump;
1071 midi::MpeBuffer mpe_in;
1072 midi::MpeBuffer mpe_out;
1073 bool midi_in_incomplete = false;
1074 bool midi_out_incomplete = false;
1075 std::shared_ptr<MidiInputMailbox> midi_input_mailbox;
1076 std::unique_ptr<MidiOutputMailbox> midi_output_mailbox;
1077
1078 // Control/audio-thread parameter-event ingress for Plugin nodes. The
1079 // mailbox is single-writer/latest-wins like MIDI ingress; the sequence
1080 // is committed only after the selected processing path succeeds.
1081 std::shared_ptr<ParameterInputMailbox> parameter_input_mailbox;
1082 // ExecutionSnapshot injection is generation-local. Unlike the live
1083 // mailbox above, this mailbox is never carried across recompiles.
1084 std::unique_ptr<ParameterInputMailbox> exact_parameter_input_mailbox;
1085 std::weak_ptr<detail::ExactParameterIngressOwner> exact_parameter_event_owner;
1086
1087 // Audio-rate modulation scratch. Each listed param gets one
1088 // max-block-sized region in audio_rate_param_data, filled immediately
1089 // before the destination plugin processes.
1091 float lo = 0.0f;
1092 float hi = 1.0f;
1093 bool has_replace = false;
1094 bool has_add = false;
1095 };
1096 std::vector<uint32_t> audio_rate_param_ids;
1097 std::vector<float> audio_rate_param_data;
1098 std::vector<DenseAutomationAccum> audio_rate_accum;
1099 };
1100
1101 // One delay line per graph connection, parallel to connections_. Used to
1102 // align branch latencies so a node receives all its inbound audio with a
1103 // common alignment at input_latency samples.
1104 struct ConnectionDelay {
1105 int delay_samples = 0;
1106 struct State {
1107 // Ring buffer: delay_samples + max_block_size_ frames per source
1108 // channel, plus the audio-thread-owned cursor. Shared only between
1109 // identity-matched live snapshots with equal delay structure.
1110 std::vector<float> ring;
1111 int write_pos = 0;
1112 };
1113 std::shared_ptr<State> state;
1114 // Feedback edges hold the previous block's source-port audio so the
1115 // destination can read it before the source writes the current block.
1116 std::vector<float> feedback_prev; // size = max_block_size_
1117
1118 // Per-source automation slew state. Holds the value
1119 // we last delivered to the destination (post-slew, post range-
1120 // map) so the next block can resume ramping toward a new target
1121 // instead of snapping. `primed` tracks whether `last_value` has
1122 // been seeded yet; on the first block of a freshly-prepared graph
1123 // we snap to the source value to avoid a glide from zero.
1124 float slew_last_value = 0.0f;
1125 bool slew_primed = false;
1126 };
1127
1128 // CompiledGraph — immutable audio-thread-safe snapshot of the graph
1129 // after prepare(). Published via atomic<shared_ptr> so topology mutations
1130 // on the UI thread never tear state the audio thread is reading.
1131 //
1132 // Mutation protocol:
1133 // 1. UI thread changes nodes_ / connections_ / etc. (NOT snapshot state).
1134 // 2. Snapshot is atomically reset to nullptr, making process() return
1135 // silence until the caller invokes prepare() again.
1136 // 3. prepare() rebuilds a fresh CompiledGraph and atomic-swaps it in.
1137 //
1138 // Because the snapshot owns its own copies of connections / delays / per
1139 // node runtime AND holds shared_ptr<PluginSlot>, it's safe to read even
1140 // if GraphNode owners are mutated before the audio thread releases its
1141 // reference. The old snapshot is destroyed only when both threads let go.
1142 enum class PdcExecutionDomain : std::uint8_t {
1143 Dynamic, // no PDC: runtime routing toggles remain live
1144 Legacy,
1145 RoutedSerial,
1146 RoutedParallel,
1147 };
1148
1149 // Private graph-ownership token. Public shared ProcessorNodeInstance handles
1150 // may outlive node removal, but they do not extend its prepared lifecycle.
1151 // Authoring state and every executable snapshot share this token, so its
1152 // final destruction releases only after both ownership domains are gone.
1153 struct ProcessorNodeLifetime {
1154 explicit ProcessorNodeLifetime(
1155 std::shared_ptr<format::ProcessorNodeInstance> value) noexcept;
1156
1157 ~ProcessorNodeLifetime();
1158
1159 std::shared_ptr<format::ProcessorNodeInstance> instance;
1160 };
1161
1162 struct CompiledGraph {
1163 explicit CompiledGraph(bool reserve_midi = true) : midi_publish_scratch(reserve_midi) {}
1164 std::shared_ptr<detail::TriggerState> trigger_state;
1165 // Keeps candidate-owned per-node measurers alive after publication.
1166 std::shared_ptr<SignalGraph> trigger_preparation_owner;
1167
1168 std::shared_ptr<SpectralRuntimeBank> spectral;
1169 std::uint64_t execution_generation = 0;
1170 std::shared_ptr<int> legacy_processing_lifetime;
1171 // Private executable topology. For ordinary graphs this is a copy of
1172 // nodes_; region-bearing graphs replace each admitted region with one
1173 // synthetic Custom anchor while leaving public authoring state intact.
1174 std::vector<GraphNode> executable_nodes;
1175 std::vector<NodeId> order;
1176 std::vector<Connection> connections;
1177 std::vector<std::uint64_t> connection_identities;
1178 std::vector<NodeRuntime::EdgeRef> feedback_edges;
1179 std::vector<ConnectionDelay> connection_delays; // parallel to connections
1180 // Runtime + plugin + node-info keyed by NodeId so we don't rely on
1181 // pointers into an outer container.
1182 std::unordered_map<NodeId, NodeRuntime> runtime;
1183 std::unordered_map<NodeId, std::shared_ptr<PluginSlot>> plugins;
1184 // Runtime-only authored processors. The shared ownership is part of the
1185 // compiled snapshot so a retired graph keeps every binding alive until
1186 // its last audio-thread reader leaves.
1187 std::unordered_map<NodeId, std::shared_ptr<ProcessorNodeLifetime>> processors;
1188 std::unordered_map<NodeId, CustomNodeProcessFn> custom_processors;
1189 // Prepare-stable intrinsic latency for each resolved, shape-matched
1190 // Custom node, resolved once from the registered type at THIS snapshot's
1191 // sample rate and clamped into the declared range. Shared by the legacy
1192 // PDC pass and the routed snapshot builder.
1193 std::unordered_map<NodeId, int> custom_latency_samples;
1194 // Bound param-aware custom callbacks (bake-layer injection). Populated for
1195 // any Custom node whose type declared baked_params +
1196 // process_instance_baked_param. Consumed only by bake(); the live routed
1197 // path never reads this map.
1198 std::unordered_map<NodeId, CustomNodeParamProcessFn> custom_param_processors;
1199 // Transport-aware custom callbacks, populated alongside custom_processors
1200 // for any Custom node whose type registered a transport-aware variant.
1201 // The presence of an entry mirrors GraphNode::transport_sensitive for
1202 // that node (both resolved from the same compile-time condition), so the
1203 // routed binding and the anticipation analysis stay consistent.
1204 std::unordered_map<NodeId, CustomNodeTransportProcessFn> custom_transport_processors;
1205 // Event-aware custom callbacks, populated alongside custom_processors for
1206 // any Custom node whose type declared an event-aware variant. Always the
1207 // stateless shape: a stateful type is resolved into a lambda that holds
1208 // its instance, exactly as custom_processors does. Both the routed
1209 // binding and the reference walk resolve from THIS map, so the two paths
1210 // cannot disagree about which nodes read events.
1211 std::unordered_map<NodeId, CustomNodeEventProcessFn> custom_event_processors;
1218 NodeId id = 0;
1220 NodeRuntime* runtime = nullptr;
1221 };
1222 // The authored node identities captured before sample-region quotienting.
1223 // Live-swap eligibility compares against this map; `shapes` below is the
1224 // executable topology and intentionally omits region interior nodes.
1225 std::unordered_map<NodeId, NodeShape> authored_shapes;
1226 std::unordered_map<NodeId, NodeShape> shapes;
1227 std::vector<OrderedRuntime> ordered_runtime;
1228 // Per-node live-DSP timing, prepared at compile in ordered_runtime order
1229 // (slot i == ordered_runtime[i]). Non-movable (holds atomics); CompiledGraph
1230 // is only ever make_shared'd and referenced, never copied, so an in-place
1231 // member is safe. Rides this snapshot's RCU lifetime.
1232 audio::LiveDspTelemetryStore live_dsp_telemetry;
1233 int max_block_size = 0;
1234 double sample_rate = 0.0; // needed to convert automation_smoothing_ms
1235 // into samples.
1236 int64_t total_latency_samples = 0;
1237 // Non-Dynamic when this snapshot owns feed-forward delay history. The
1238 // selected path is captured at prepare time so a relaxed runtime toggle
1239 // cannot jump to another domain's stale ring mid-stream.
1240 PdcExecutionDomain pdc_execution_domain = PdcExecutionDomain::Dynamic;
1241 MidiBlockSnapshot midi_publish_scratch;
1242 // 2.2b reinit-free-swap predicate inputs (captured at compile so a
1243 // prepare_swap candidate can be compared against this live snapshot):
1244 // - custom_instances: NodeId → raw custom-instance identity for each
1245 // Custom node (set-equality vs the current nodes_ detects an added /
1246 // removed / re-instantiated custom node → not reinit-free).
1247 // - custom_registry_generation: the SignalGraph custom-type registry
1248 // generation at compile time (a re-register would rebind callbacks to
1249 // instances built by the old factory → not reinit-free).
1250 std::unordered_map<NodeId, const void*> custom_instances;
1251 std::uint64_t custom_registry_generation = 0;
1252
1253 // Sample-region state and callbacks ride the same RCU lifetime as the
1254 // executable topology. Snapshots produced by a reinit-free edit own a
1255 // distinct bank view whose exact-key cells and execution domain are
1256 // shared with the preceding generation.
1257 std::shared_ptr<SampleRegionStateBank> sample_region_bank;
1258 std::vector<std::shared_ptr<PreparedSampleRegion>> sample_regions;
1259
1260 // ONE routed-executor path: the plan snapshot, the scratch pool sized for
1261 // exactly that snapshot, the stable binding storage its bindings' user_data
1262 // points into, and whether the build succeeded. They are grouped because
1263 // they are only ever valid TOGETHER — driving one path's snapshot against
1264 // the other path's pool is a bug that field naming alone could not prevent.
1265 //
1266 // `pool` is sized in compile_() to `snapshot`'s slot count × max_block_size
1267 // and owned by THIS snapshot (like the legacy per-node runtime scratch), so
1268 // a re-prepare builds a fresh pool on a fresh cg and never mutates the
1269 // buffers an in-flight audio-thread reader is using on a retired snapshot.
1270 // Feedback previous-block state lives there and resets with the snapshot.
1271 // `plugin_ctx` / `custom_ctx` are reserved in compile_() to the plugin /
1272 // custom-node count so each binding's user_data points at a stable element;
1273 // each CustomBindingContext holds a COPY of this snapshot's resolved process
1274 // callback (its own keepalive on any captured instance, same cg lifetime as
1275 // `custom_processors`). The plugin slots themselves live in `plugins` above.
1276 // `levelization` is the static level schedule the parallel path runs; it
1277 // stays empty on the serial path.
1286
1287 // `pending_seq` is audio-thread scratch: the mailbox sequence a MidiInput
1288 // would consume this block, captured during the ingress pre-fill and
1289 // committed to the shared mailbox's sequence_seen only after routed dispatch
1290 // succeeds (so a fallback to the legacy walk re-consumes the same block).
1292 std::uint32_t plan_index;
1294 std::uint64_t pending_seq = 0;
1295 };
1297 std::uint32_t plan_index;
1299 std::uint64_t pending_seq = 0;
1300 };
1301
1302 // Immutable canonical-executor routing for this snapshot, built in
1303 // compile_() when the topology is executor-eligible (see
1304 // signal_graph_topology_executor_eligible). Empty/invalid otherwise. The
1305 // Gain bindings reference this snapshot's own `runtime[id].gain` atomics
1306 // and the Plugin/Custom bindings invoke this snapshot's own slots /
1307 // callbacks, so this carries no keepalive — it lives and dies exactly with
1308 // this CompiledGraph, published atomically with it via live_slot_. The
1309 // legacy walk remains the reference/fallback path for ineligible or
1310 // routing-failed graphs, not a retired one.
1312 // The default path: compact buffer assignment, slots reused.
1314 // Levelized PARALLEL routing (built only when parallel routing is
1315 // enabled at compile time). Same plan as `serial` but with a reuse-free
1316 // buffer assignment (parallel_safe) so concurrent same-level nodes never
1317 // alias a slot, plus its own (larger) pool and the level schedule. Each
1318 // of its Plugin bindings owns its fallback scratch so same-level Plugin
1319 // nodes do not share mutable MIDI output state.
1321 // The serial path's shared MIDI/param fallback scratch, used when no
1322 // per-node executor scratch is supplied.
1324 // Per-node MIDI buffers for the routed path's event edges. Empty
1325 // (node_count 0) for graphs with no MIDI. The routed dispatch bridges
1326 // the SignalGraph MIDI mailboxes to these buffers around
1327 // process_routed: a MidiInput node's mailbox is read into its scratch
1328 // output before the walk, and a MidiOutput node's gathered scratch
1329 // input is published to its mailbox after. `midi_inputs` / `midi_outputs`
1330 // list those system nodes by (dense plan index, NodeId) so the bridge
1331 // avoids a per-block NodeId lookup.
1333 std::vector<RoutedMidiNode> midi_inputs;
1334 std::vector<RoutedMidiNode> midi_outputs;
1335 std::vector<RoutedProcessorNode> processor_parameter_inputs;
1336 // Per-node parameter-event queues + per-connection slew state for
1337 // routed sparse automation. Empty (node_count 0) for graphs with no
1338 // sparse automation.
1340 format::BusBufferSet buses; // the block's main buses, rebuilt per block
1341 // The MIDI/automation scratch and the MidiInput/Output node lists are
1342 // deliberately SHARED across the two paths: the plans are identical and
1343 // only ONE path runs per block.
1344 };
1345 RoutedPaths routed;
1346 std::unique_ptr<format::GraphRuntimeAudioEdgeProbe> audio_edge_probe;
1347 EdgeProbeIdentity audio_edge_probe_identity;
1348
1349 // Anticipative rendering for this snapshot (built only when anticipation is
1350 // enabled at compile time AND the routed plan has an eligible latent
1351 // interior). The lane pre-renders the interior off the audio thread; the
1352 // live path masks the interior in routed.serial's walk and feeds the lane's
1353 // pre-rendered boundary signals into the interior boundary-source output
1354 // slots of routed.serial.pool. `skip_mask` is indexed by
1355 // routed.serial.snapshot's dense node order; `prefill` maps each lane output
1356 // channel to the routed.serial.pool slot carrying that boundary signal;
1357 // `consume_scratch` is the per-snapshot capture buffer consume() pops a
1358 // block into before the prefill copy. The lane owns the interior plugin
1359 // instances' state advancement — process() never runs them.
1362 std::vector<std::uint8_t> skip_mask;
1363 struct Prefill {
1364 std::uint32_t out_channel = 0; // lane output channel
1365 std::uint32_t slot = 0; // routed.serial.pool mono slot to fill
1366 };
1367 std::vector<Prefill> prefill;
1368 std::vector<std::vector<float>> consume_scratch;
1369 std::vector<float*> consume_ptrs;
1370 bool valid = false;
1371 };
1372 AnticipationSplice anticipation;
1373 };
1374
1375 std::vector<GraphNode> nodes_;
1376 // Processor nodes deliberately reuse the existing Plugin node topology kind
1377 // so the graph/runtime plan ABI does not grow. Presence in this map is the
1378 // unambiguous runtime-only discriminator used by routing and serialization.
1379 std::unordered_map<NodeId, std::shared_ptr<ProcessorNodeLifetime>> processor_nodes_;
1380 std::vector<Connection> connections_;
1381 // Private authoring identity parallel to connections_. Public Connection
1382 // stays a value-only routing description; disconnect+reconnect mints a new
1383 // identity so stale PDC history can never attach to a logically new edge.
1384 std::vector<std::uint64_t> connection_identities_;
1385 std::uint64_t next_connection_identity_{1};
1386 std::unordered_map<std::string, CustomNodeType> custom_node_types_;
1387 std::unordered_map<std::string, SampleKernelDescriptor> sample_kernel_types_;
1388 std::unordered_map<std::string, CustomNodeDiagnosticsDescriptor> custom_node_diagnostics_;
1389 static std::uint64_t next_diagnostic_graph_identity_() noexcept;
1390 const std::uint64_t diagnostic_graph_identity_ = next_diagnostic_graph_identity_();
1391 std::uint32_t spectral_transition_serial_ = 0;
1392 SpectralRuntimeBank* spectral_render_target_ = nullptr;
1393
1394 std::vector<SampleRegionDefinition> sample_region_definitions_;
1395 const SampleRegionParameterBinding* sample_region_parameter_binding_ = nullptr;
1396 std::uint32_t sample_region_proof_block_size_ = 16384;
1397 // Control-thread staging consumed by compile_(). These objects are built
1398 // only after the complete region proof succeeds and are copied into the
1399 // resulting CompiledGraph snapshot.
1400 std::shared_ptr<SampleRegionStateBank> prepared_sample_region_bank_;
1401 std::vector<std::shared_ptr<PreparedSampleRegion>> prepared_sample_regions_;
1402 // Bumped on every register_custom_node_type; captured into each CompiledGraph
1403 // so the 2.2b reinit-free-swap predicate can reject a candidate compiled after
1404 // the custom registry changed (M6 — prevents binding new callbacks to
1405 // instances the old factory built). Guarded by graph_mutation_mutex_.
1406 std::uint64_t custom_registry_generation_{0};
1407 // Invalidates an outstanding PreparedTopologyEdit when any direct authoring
1408 // or lifecycle operation touches this graph. Guarded by graph_mutation_mutex_.
1409 std::uint64_t authoring_generation_{0};
1410 NodeId next_id_ = 1;
1411 NodeId last_prepare_custom_failure_node_ = 0;
1412 GraphLimits limits_;
1413
1414 // 2.2b prerequisite (planning/2026-07-07-signalgraph-swap-and-bake-implementation-plan.md,
1415 // H2): plugin metadata captured ONCE at prepare() time so compile_() and the
1416 // executor-routing build never call live PluginSlot metadata methods
1417 // (parameters()/latency_samples()/wants_transport()). Those reach into the
1418 // live plugin object (e.g. VST3 getLatencySamples()) and are unsafe to call
1419 // concurrent with process() — which a swap-time recompile (prepare_swap, 2.2b)
1420 // would do. Captured after n.plugin->prepare() succeeds (null-first prepare,
1421 // graph_mutation_mutex_ held, no concurrent process on these instances).
1422 // NodeId-keyed; cleared + rebuilt on every prepare().
1423 struct PreparedPluginMetadata {
1424 std::vector<HostParamInfo> parameters;
1425 int latency_samples = 0;
1426 bool wants_transport = false;
1427 };
1428 // Captured once per prepare() (cleared + rebuilt); read by compile_ / the
1429 // routing build / edit-time param validation instead of the live PluginSlot.
1430 // STALENESS INVARIANT: an entry is valid ONLY while the node set and per-node
1431 // slot identities are unchanged since the last prepare(). A 2.2b swap never
1432 // recaptures it — the reinit-free predicate (snapshot_is_plugin_reinit_free_locked_)
1433 // enforces exactly that invariant, and a non-reinit-free edit falls back to a
1434 // full prepare() which rebuilds this map. Do NOT read it after a mutation that
1435 // could add/remove/re-instantiate a node without a matching re-prepare.
1436 std::unordered_map<NodeId, PreparedPluginMetadata> prepared_plugin_meta_;
1437 std::unordered_map<NodeId, std::weak_ptr<detail::ExactParameterIngressOwner>>
1438 exact_parameter_event_claims_;
1439
1440 struct StagedReplacement {
1441 NodeId id = 0;
1442 PluginInfo info;
1443 std::shared_ptr<PluginSlot> slot;
1444 PreparedPluginMetadata metadata;
1445 pulp::audio::AudioProcessLoadSnapshot warmed_load;
1446 bool same_identity = false;
1447 };
1448 std::unordered_map<NodeId, StagedReplacement> staged_replacements_;
1449
1450 std::unordered_map<std::uint64_t, PluginInfo> scanned_plugin_catalog_;
1451 std::unordered_map<std::string, std::uint64_t> scanned_plugin_identity_to_token_;
1452 std::uint64_t next_scanned_plugin_token_{1};
1453 LiveSwapDiagnostics last_swap_diagnostics_;
1454 PluginLoaderForTest live_swap_plugin_loader_for_test_;
1455
1456 // Audio-thread snapshot, published by prepare() / mutators through the
1457 // shared reader-pinned RCU primitive. `runtime::Slot` owns the atomic
1458 // pointer the audio thread loads, the reader count, and the retire list;
1459 // the pointed-to CompiledGraph stays alive until every pinned reader
1460 // leaves. See runtime/slot.hpp — a pin guarantees LIFETIME, not constness:
1461 // the audio thread writes this snapshot's scratch buffers every block.
1463 friend struct detail::TriggerTestAccess;
1464 void trigger_test_clock_(std::uint64_t cursor, std::uint64_t sequence, std::uint64_t epoch,
1465 std::uint8_t phase, bool saturate);
1466 TriggerEmitResult trigger_test_emit_(std::int64_t target, std::uint64_t epoch,
1467 std::uint8_t port);
1468 TriggerEmitResult trigger_test_schedule_(std::int64_t target, std::uint64_t epoch);
1469 bool trigger_test_budget_(std::size_t total);
1470 void trigger_test_gate_(bool held);
1471 NodeId add_trigger_audio_(TriggerAudioConfig config);
1472
1473 std::unordered_map<NodeId, TriggerProducerConfig> trigger_producers_;
1474 std::unordered_map<NodeId, TriggerEnvelopeConfig> trigger_envelopes_;
1475 std::unordered_map<NodeId, TriggerAudioConfig> trigger_audio_;
1476 std::vector<TriggerEdge> trigger_edges_;
1477 std::atomic<bool> typed_trigger_authoring_{false};
1478 std::shared_ptr<detail::TriggerController> trigger_controller_;
1479 std::shared_ptr<detail::TriggerState> compiling_trigger_state_;
1480 bool ensure_trigger_controller_();
1481 bool compile_trigger_pair_(CompiledGraph& graph);
1482 void bind_trigger_pair_(CompiledGraph& graph);
1483 void invalidate_trigger_binding_() noexcept;
1484 TriggerGraphResult process_trigger_snapshot_(audio::BufferView<float>& output,
1485 const audio::BufferView<const float>& input,
1486 int frames,
1487 const format::ProcessContext* transport,
1488 CompiledGraph* graph);
1489 static void reset_trigger_audio_(CompiledGraph& graph) noexcept;
1490 runtime::Slot<CompiledGraph> live_slot_;
1491 std::shared_ptr<int> processing_handle_lifetime_ = std::make_shared<int>(0);
1492 std::array<std::shared_ptr<CompiledGraph>, 3> spectral_slots_;
1493 std::unique_ptr<SignalGraph> spectral_candidate_authoring_;
1494 std::atomic<bool> spectral_mode_{false};
1495 std::atomic<int> spectral_active_{0}, spectral_pending_{-1}, spectral_ack_{-1};
1496 std::atomic<bool> spectral_access_closed_{false};
1497 mutable std::atomic<bool> spectral_reclaiming_{false};
1498 mutable std::atomic<unsigned> spectral_readers_{0}, spectral_accesses_{0};
1499 mutable std::atomic<SpectralError> spectral_error_{SpectralError::None};
1500 std::uint64_t spectral_offer_generation_ = 0;
1501 std::uint64_t spectral_render_generation_ = 0;
1502 SpectralError spectral_ack_error_ = SpectralError::None;
1503 int spectral_retired_ = -1;
1504 bool spectral_terminal_ = false;
1505
1506 class SnapshotAccess {
1507 public:
1508 explicit SnapshotAccess(runtime::Slot<CompiledGraph>::ReadGuard guard)
1509 : legacy_(std::move(guard)), value_(legacy_.get()) {}
1510 SnapshotAccess(const SignalGraph* owner, CompiledGraph* value, bool access)
1511 : owner_(owner), value_(value), access_(access) {}
1512 ~SnapshotAccess() {
1513 if (owner_) {
1514 if (access_)
1515 owner_->spectral_accesses_.fetch_sub(1, std::memory_order_seq_cst);
1516 owner_->spectral_readers_.fetch_sub(1, std::memory_order_seq_cst);
1517 }
1518 }
1519 SnapshotAccess(const SnapshotAccess&) = delete;
1520 CompiledGraph* get() const noexcept {
1521 return value_;
1522 }
1523 CompiledGraph* operator->() const noexcept {
1524 return value_;
1525 }
1526 CompiledGraph& operator*() const noexcept {
1527 return *value_;
1528 }
1529 explicit operator bool() const noexcept {
1530 return value_ != nullptr;
1531 }
1532
1533 private:
1534 runtime::Slot<CompiledGraph>::ReadGuard legacy_;
1535 const SignalGraph* owner_ = nullptr;
1536 CompiledGraph* value_ = nullptr;
1537 bool access_ = false;
1538 };
1539 SnapshotAccess read_snapshot_(bool access = false) const noexcept;
1540 bool refuse_spectral_mutation_() const noexcept {
1541 if (!spectral_mode_.load(std::memory_order_acquire))
1542 return false;
1543 spectral_error_.store(spectral_access_closed_.load(std::memory_order_seq_cst)
1544 ? SpectralError::TransitionPending
1545 : SpectralError::UnsupportedOperation,
1546 std::memory_order_relaxed);
1547 return true;
1548 }
1549 bool spectral_mailboxes_pending_(const CompiledGraph&) const noexcept;
1550 SpectralError offer_spectral_candidate_locked_(std::unique_ptr<SignalGraph>&,
1551 std::shared_ptr<CompiledGraph>, std::uint64_t);
1552 void adopt_candidate_authoring_locked_(SignalGraph&);
1554 struct SpectralBoundary {
1555 int active;
1556 int offered;
1557 };
1558 SpectralBoundary capture_spectral_boundary_() noexcept;
1559 void process_spectral_boundary_(SpectralBoundary, audio::BufferView<float>&,
1560 const audio::BufferView<const float>&, int,
1561 const format::ProcessContext*);
1562 void process_spectral_(audio::BufferView<float>&, const audio::BufferView<const float>&, int,
1563 const format::ProcessContext*);
1564
1565 // Canonical-executor routing (control toggle, read relaxed on the audio
1566 // thread). DEFAULT ON: the routed executor is the primary inter-node backend
1567 // for every eligible graph and is bit-identical to the legacy walk for that
1568 // subset (proven by the routed-vs-walk parity suite) AND now reports the same
1569 // per-node node_loads() telemetry, so the default-ON flip is behavior-
1570 // preserving where it takes effect. Ineligible graphs (per-node automation
1571 // past the executor's fixed capacity, or a Plugin node with no live slot)
1572 // still fall back to the legacy walk, which remains the reference oracle the
1573 // parity tests pin OFF explicitly. One long-lived executor whose telemetry survives re-prepare;
1574 // it is stateless w.r.t. topology (it takes the snapshot + the snapshot's own
1575 // pool as arguments) and prepare() never mutates it, so the single audio
1576 // thread is its only writer (relaxed stat counters). The mutable scratch pool
1577 // is owned per-snapshot by CompiledGraph (see CompiledGraph::RoutedPath::pool) so it
1578 // rides the existing RCU lifetime and is never resized under a reader.
1579 std::atomic<bool> canonical_executor_routing_enabled_{true};
1581 // Levelized parallel routing opt-in + its persistent worker pool. The pool is
1582 // a long-lived SignalGraph member (NOT per-RCU-snapshot): started off-RT in
1583 // compile_() when parallel routing is enabled, joined in the destructor. The
1584 // parallel-safe snapshot + levelization + scratch pool ride the CompiledGraph
1585 // (see CompiledGraph::routing_*_parallel).
1586 std::atomic<bool> parallel_routing_enabled_{false};
1587 std::atomic<bool> anticipation_enabled_{false};
1588
1589 // Non-null only on the private shadow graph owned by PreparedTopologyEdit.
1590 // compile_ uses the origin's persistent load measurers for unchanged nodes
1591 // and keeps newly allocated measurers local until commit transfers them.
1592 SignalGraph* prepared_edit_origin_{nullptr};
1593
1594 // 2.2b swap-edit transaction state (guarded by graph_mutation_mutex_).
1595 // in_swap_edit_ is set between begin_swap_edit() and prepare_swap()/
1596 // abort_swap_edit(); swap_edit_owner_ is the thread that opened it. While set,
1597 // invalidate_live_locked_() no-ops for the OWNER thread's allow-set mutations (so
1598 // the live snapshot keeps playing); any direct invalidate_live_locked_() from a non-owner
1599 // thread, and the lifecycle/limits/registry mutators which force-abort at their
1600 // top, cancel the transaction.
1601 bool in_swap_edit_{false};
1602 std::thread::id swap_edit_owner_{};
1603 // Guards the single-producer contract on pump_anticipation: a concurrent or
1604 // reentrant call degrades to a no-op instead of corrupting the lane's
1605 // unsynchronized executor/pool/scratch + interior plugin state.
1606 std::atomic<bool> anticipation_pump_busy_{false};
1607 // See routed_walk_fallbacks(): incremented when an eligible routed path failed
1608 // dispatch and process() fell back to the walk. Audio-thread writer only.
1609 std::atomic<std::uint64_t> routed_walk_fallbacks_{0};
1610 std::atomic<std::uint32_t> routed_only_execution_owners_{0};
1611 // Audio-thread writer only: terminal routed processing failures and routed-only
1612 // lease failures that produce a deterministic silent block.
1613 std::atomic<std::uint64_t> routed_only_execution_failures_{0};
1614 // See transport_suppressed_for_anticipation(): set by compile_() to the count
1615 // of transport-sensitive nodes that active anticipation forced exterior
1616 // (excluded from the ahead-rendered interior so they observe the live
1617 // transport). Control-thread writer (compile_) only.
1618 std::atomic<std::uint64_t> transport_suppressed_for_anticipation_{0};
1619 format::GraphRuntimeWorkerPool worker_pool_;
1620 std::atomic<int64_t> total_latency_samples_{0}; // reflected for const-query access
1621 std::atomic<std::size_t> prepared_node_count_{0};
1622 std::atomic<std::size_t> prepared_ordered_node_count_{0};
1623 std::atomic<std::size_t> prepared_connection_count_{0};
1624 std::atomic<std::size_t> prepared_total_ports_{0};
1625 std::atomic<int> prepared_max_block_size_{0};
1626 std::atomic<std::size_t> prepared_node_audio_buffer_bytes_{0};
1627 std::atomic<std::size_t> prepared_automation_buffer_bytes_{0};
1628 std::atomic<std::size_t> prepared_delay_buffer_bytes_{0};
1629 std::atomic<std::size_t> prepared_total_buffer_bytes_{0};
1630
1631 // Persistent per-node load measurers, keyed by NodeId. Control-thread
1632 // owned; compile_() resolves each NodeRuntime::load to one of these (only
1633 // ever ADDS entries while a snapshot may be live, never erases, so the
1634 // audio thread's raw measurer pointers stay valid across snapshot swaps —
1635 // the measurers are heap-stable behind unique_ptr regardless of map
1636 // rehash). node_loads() reads their snapshots.
1637 std::unordered_map<NodeId, std::unique_ptr<audio::AudioProcessLoadMeasurer>> node_load_;
1638 // Guards node_load_ MAP structure (insert in compile_ vs iterate in
1639 // node_loads()) — those can run on different control threads (host
1640 // prepare() vs UI poll). Control-side only; the audio thread never takes
1641 // it (it touches measurer OBJECTS via NodeRuntime::load, not the map).
1642 mutable std::mutex node_load_mu_;
1643
1644 // Whole-graph process load — one measurer that PERSISTS across snapshots (like
1645 // node_load_). process_impl brackets each block with begin()/end() via an RAII
1646 // scope; the control thread reads graph_load() for the live-swap admission gate.
1647 // Heap-stable behind unique_ptr; snapshot() is concurrency-safe.
1648 mutable std::atomic<bool> node_load_monitoring_{false}; // node_loads() sets it
1649 std::unique_ptr<audio::AudioProcessLoadMeasurer> graph_load_ =
1650 std::make_unique<audio::AudioProcessLoadMeasurer>();
1651
1652 // Desired per-node live-DSP telemetry enable state. Read by compile_ to seed a
1653 // new snapshot's store and written by set_live_dsp_telemetry_enabled(); both are
1654 // control-thread but may be different control threads (host prepare vs UI
1655 // toggle), so it is atomic.
1656 std::atomic<bool> desired_live_dsp_telemetry_enabled_{false};
1657
1658 // Serializes CONTROL-THREAD access to the source-of-truth topology — the
1659 // nodes_ / connections_ vectors and the plain (non-atomic) GraphNode fields
1660 // they own (gain, ports, plugin/custom identity, custom state) — AND the
1661 // snapshot-publication state (live_slot_) that a
1662 // re-prepare and a concurrent invalidate_live_locked_() both mutate. The control
1663 // surface is multi-threaded: a host thread calls prepare()/compile_() (which
1664 // iterates nodes_/connections_ and reads GraphNode::gain) while a UI thread
1665 // adds/removes nodes or connections, edits a GraphNode field
1666 // (set_node_gain / set_node_parameter / set_custom_node_state), or reads one
1667 // (node_gain / get_node_parameter / custom_node_state / node_loads). Without
1668 // this lock those vector mutations + plain-field reads/writes race.
1669 //
1670 // Contract — EVERY public control-thread method that reads or writes
1671 // nodes_ / connections_ / a mutable GraphNode field, or that publishes/retires
1672 // a snapshot, holds this mutex for the duration of that access:
1673 // mutators : add_input_node, add_output_node, add_plugin_node(×2),
1674 // add_unresolved_plugin_node, add_gain_node,
1675 // add_midi_input_node, add_midi_output_node,
1676 // add_unresolved_custom_node, register_custom_node_type,
1677 // remove_node, connect, connect_midi, connect_sidechain,
1678 // connect_automation, connect_audio_rate_modulation,
1679 // connect_feedback, disconnect, clear, set_node_gain,
1680 // set_node_parameter, set_custom_node_state, prepare, release.
1681 // readers : node_gain, get_node_parameter, custom_node_state, node_loads.
1682 // The low-level helpers node() / has_path_locked_() / would_create_cycle() /
1683 // processing_order() / custom_node_type() / audio_rate_modulation_lane() /
1684 // validate_generated_graph() / total_declared_ports_locked_() do NOT lock and
1685 // assume the caller already holds this mutex; the public methods above are
1686 // their only callers on the mutating path. Public methods that delegate to
1687 // another public mutator (add_custom_node -> add_unresolved_custom_node) do
1688 // NOT take the lock themselves — the leaf takes it once — to avoid
1689 // self-deadlock on this non-recursive mutex.
1690 //
1691 // ENFORCEMENT, not just prose: a helper that assumes the lock carries a
1692 // `_locked_` suffix, and every one whose call graph is entirely internal
1693 // opens with assert_graph_mutation_locked_() — which checks the debug-only
1694 // owner record GraphMutationLock maintains, so a missed lock fails loudly in
1695 // a debug/CI build instead of racing silently. Acquire the mutex through
1696 // GraphMutationLock (never a bare lock_guard) so that record stays accurate.
1697 // The two helpers reachable from a public method that does NOT lock —
1698 // has_path_locked_ (via would_create_cycle) and total_declared_ports_locked_
1699 // (via validate_generated_graph / estimate_generated_graph_work_units) —
1700 // carry the suffix for their internal contract but cannot assert until those
1701 // public entry points become locking wrappers over `_locked_` cores.
1702 //
1703 // The nodes() / connections() accessors hand out a reference to the live
1704 // vector and the live_slot_.live()*() snapshot getters read live_slot_.live(); both are governed by
1705 // the same caller-side keepalive/serialization contract they always were and
1706 // are intentionally NOT locked here (locking a reference-returning accessor
1707 // can't protect the caller's later iteration anyway).
1708 //
1709 // Deadlock-free by construction:
1710 // - Taken ONLY on the control thread. The audio render path
1711 // (process_impl / the executor / the reference walk) reads the immutable
1712 // CompiledGraph snapshot — never nodes_ or GraphNode fields — so it must
1713 // never take this lock. Taking a mutex on the audio thread would be an
1714 // RT-safety violation.
1715 // - NEVER held while acquiring a Slot reader-pin. The methods
1716 // that both edit source state AND reflect into the live snapshot
1717 // (set_node_gain) take this mutex for the source write, RELEASE it, then
1718 // separately pin the snapshot — so this mutex can never invert lock order
1719 // with the RCU drain (Slot::reclaim_if_quiescent / Slot::wait_and_clear,
1720 // the latter busy-waited under this mutex by release()).
1721 mutable std::mutex graph_mutation_mutex_;
1722
1723#ifndef NDEBUG
1724 // The thread currently holding graph_mutation_mutex_, recorded by
1725 // GraphMutationLock and read by assert_graph_mutation_locked_(). Debug-only:
1726 // it exists to catch a missed lock, so release builds carry neither the field
1727 // nor the check.
1728 mutable std::atomic<std::thread::id> graph_mutation_owner_{};
1729#endif
1730
1731 // Scoped acquisition of graph_mutation_mutex_. Records the owning thread in a
1732 // debug build so assert_graph_mutation_locked_() can prove a `_locked_`
1733 // helper's caller really holds the lock; in a release build it is a plain
1734 // unique_lock over the same mutex. unlock() releases early for the one caller
1735 // (prepare_swap) that drops the lock before invoking user callbacks.
1736 class GraphMutationLock {
1737 public:
1738 explicit GraphMutationLock(const SignalGraph& graph)
1739 : lock_(graph.graph_mutation_mutex_)
1740#ifndef NDEBUG
1741 ,
1742 owner_(&graph.graph_mutation_owner_)
1743#endif
1744 {
1745 note_acquired_();
1746 }
1747 GraphMutationLock(const SignalGraph& graph, std::try_to_lock_t)
1748 : lock_(graph.graph_mutation_mutex_, std::try_to_lock)
1749#ifndef NDEBUG
1750 ,
1751 owner_(&graph.graph_mutation_owner_)
1752#endif
1753 {
1754 if (lock_.owns_lock())
1755 note_acquired_();
1756 }
1757 bool owns_lock() const noexcept {
1758 return lock_.owns_lock();
1759 }
1760 ~GraphMutationLock() {
1761 if (lock_.owns_lock())
1762 note_released_();
1763 }
1764 GraphMutationLock(const GraphMutationLock&) = delete;
1765 GraphMutationLock& operator=(const GraphMutationLock&) = delete;
1766 void unlock() {
1767 note_released_();
1768 lock_.unlock();
1769 }
1770
1771 private:
1772 void note_acquired_() noexcept {
1773#ifndef NDEBUG
1774 owner_->store(std::this_thread::get_id(), std::memory_order_relaxed);
1775#endif
1776 }
1777 void note_released_() noexcept {
1778#ifndef NDEBUG
1779 owner_->store(std::thread::id{}, std::memory_order_relaxed);
1780#endif
1781 }
1782
1783 std::unique_lock<std::mutex> lock_;
1784#ifndef NDEBUG
1785 std::atomic<std::thread::id>* owner_ = nullptr;
1786#endif
1787 };
1788
1789 // Fails a debug build when a `_locked_` helper is reached without this thread
1790 // holding graph_mutation_mutex_. Compiles to nothing in release.
1791 void assert_graph_mutation_locked_() const noexcept {
1792#ifndef NDEBUG
1793 assert(graph_mutation_owner_.load(std::memory_order_relaxed) ==
1794 std::this_thread::get_id() &&
1795 "graph_mutation_mutex_ must be held by the calling thread");
1796#endif
1797 }
1798
1799 // Writable counterpart to the const node() lookup — the ONE place the source
1800 // topology's constness is shed, so the mutators do not each hand-roll a
1801 // const_cast. Caller holds graph_mutation_mutex_ (same contract as node()).
1802 GraphNode* node_mut_locked_(NodeId id);
1803 SampleRegionId sample_region_for_node_locked_(NodeId id) const;
1804 SampleRegionCandidate
1805 sample_region_candidate_locked_(const SampleRegionDefinition& definition) const;
1806 SampleRegionProof sample_region_proof_locked_(SampleRegionId id) const;
1807 SampleRegionProof sample_region_metadata_proof_locked_(const SampleRegionDefinition& definition,
1808 bool complete = true) const;
1809 SampleRegionProof sample_region_exterior_proof_locked_() const;
1810 bool has_sample_kernel_nodes_locked_() const;
1811 void append_connection_locked_(Connection connection);
1812 void erase_connection_at_locked_(std::size_t index);
1813
1814 bool has_path_locked_(NodeId from, NodeId to) const;
1815 std::size_t total_declared_ports_locked_() const;
1816 // Core of audio_rate_modulation_lane() — scans nodes_ via node() and
1817 // assumes the caller already holds graph_mutation_mutex_. The public
1818 // audio_rate_modulation_lane() locks and forwards here; connect_audio_rate_
1819 // modulation() (already holding the mutex) calls this core directly to avoid
1820 // re-locking the non-recursive mutex.
1821 bool audio_rate_modulation_lane_locked_(const Connection& connection,
1822 state::ModulationLane& lane) const;
1823 // Shared body of both process() overloads. `transport` is the host
1824 // transport context for the block, or nullptr for the no-transport path.
1825 // RT-safe (no allocation per block).
1826 void process_impl(audio::BufferView<float>& output, const audio::BufferView<const float>& input,
1827 int num_samples, const format::ProcessContext* transport);
1828 void process_snapshot_impl(audio::BufferView<float>& output,
1829 const audio::BufferView<const float>& input, int num_samples,
1830 const format::ProcessContext* transport, CompiledGraph* snapshot,
1831 bool trigger_admitted = false);
1832 static bool inject_midi_into_snapshot_(CompiledGraph& snapshot, NodeId id,
1833 const midi::MidiBuffer& events,
1834 const midi::MpeBuffer* mpe = nullptr) noexcept;
1835 static bool
1836 inject_parameter_events_into_snapshot_(CompiledGraph& snapshot, NodeId id,
1837 const state::ParameterEventQueue& events) noexcept;
1838 // Exact-generation injection into a claimed node's exact mailbox. Fails for
1839 // any node that is not currently owner-claimed for exact ingress.
1840 static bool
1841 inject_exact_parameter_events_into_snapshot_(CompiledGraph& snapshot, NodeId id,
1842 const state::ParameterEventQueue& events) noexcept;
1843 static PluginBindingContext::PendingParameterEventSequences
1844 append_parameter_mailbox_events_(void* runtime,
1845 state::ParameterEventQueue& destination) noexcept;
1846 // Legacy serial reference walk — the hand-maintained, bit-exact inter-node
1847 // DSP oracle and fallback for process_impl(). Lives in its own translation
1848 // unit (signal_graph_reference_walk.cpp) but stays a member so it can name
1849 // the private nested CompiledGraph / NodeRuntime types without promotion.
1850 // process_impl() invokes it when no routed path takes a block.
1851 void run_reference_walk_(audio::BufferView<float>& output,
1852 const audio::BufferView<const float>& input, int num_samples,
1853 CompiledGraph* cg, bool time_nodes);
1854 // compile_ mode. Normal is the full build (prepare()). SwapNoAnticipation is
1855 // used by (2.2b) prepare_swap: it HARD-skips the anticipation-lane build so a
1856 // swap-time recompile can never construct an AnticipationLane over the live
1857 // interior instances (M5 — the reinit-free predicate already requires
1858 // anticipation off, but this closes the TOCTOU where the host flips the atomic
1859 // between the predicate check and compile).
1860 enum class CompileMode { Normal, SwapNoAnticipation };
1861 std::shared_ptr<CompiledGraph> compile_(double sample_rate, int max_block_size,
1862 CompileMode mode = CompileMode::Normal);
1863 bool prepare_impl_(double sample_rate, int max_block_size,
1864 const PrepareLifecycleObserver* lifecycle_observer);
1865 // Build one routed-executor snapshot for compile_, defining the live-value
1866 // resolver set (gain / plugin slot / custom process / custom transport / load
1867 // measurer / plugin latency / plugin params) ONCE. compile_ builds two
1868 // snapshots — the serial (parallel_safe=false) and the parallel-safe
1869 // (parallel_safe=true) path — and both go through here, so the resolvers can
1870 // never drift between them. The plugin-metadata resolvers read
1871 // prepared_plugin_meta_, never the live PluginSlot, so a swap-time recompile
1872 // makes no live metadata call. Called from compile_ (caller holds
1873 // graph_mutation_mutex_); load resolution takes node_load_mu_ internally.
1874 bool build_routing_snapshot_locked_(CompiledGraph& cg, bool parallel_safe,
1875 std::vector<PluginBindingContext>& plugin_ctx,
1876 std::vector<CustomBindingContext>& custom_ctx,
1877 format::GraphRuntimeSnapshot& out);
1878 std::shared_ptr<const void> live_snapshot_handle_locked_() const noexcept;
1879 // Shared preflight (generated-graph limits + audio-rate automation
1880 // event-capacity gate) for prepare() and prepare_swap(). PURE — mutates no
1881 // live state. Caller holds graph_mutation_mutex_. Returns false (logged) on
1882 // any rejection.
1883 bool preflight_locked_(int max_block_size);
1884 // Reinit-free-swap predicate (PRE-compile half). True iff a live topology
1885 // swap to the current nodes_/connections_ needs NO plugin/custom re-init and
1886 // carries no per-snapshot mutable state a fresh snapshot would glitch: same
1887 // sr/block; unchanged custom registry; anticipation off both sides; no
1888 // MidiOutput in either the candidate or live snapshot; no smoothed sparse-
1889 // automation edge; identical node set + plugin/custom instance identity; every
1890 // resolved plugin node has a cached-metadata entry. Ingress-only MIDI is
1891 // eligible because a stable MidiInput shares its mailbox and consumed-sequence
1892 // state across snapshots.
1893 // The latency-equality gate is checked POST-compile in prepare_swap(). Pure
1894 // const read; caller holds the mutation mutex.
1895 bool snapshot_is_plugin_reinit_free_locked_(const CompiledGraph& old_cg, double sr,
1896 int bs) const;
1897 // Edit-time plugin parameter list for connection validation. Prefers the
1898 // prepare-time cache (prepared_plugin_meta_) so a connect during a swap-edit
1899 // does NOT call the live PluginSlot::parameters() concurrently with process()
1900 // on that slot; falls back to the live slot for a not-yet-prepared node.
1901 // Edit-path only (not real-time), so the by-value copy is fine.
1902 std::vector<HostParamInfo> cached_or_live_params_locked_(const GraphNode& n) const;
1903 std::unique_ptr<PluginSlot> load_live_swap_plugin_locked_(const PluginInfo& info) const;
1904 pulp::audio::AudioProcessLoadSnapshot warm_staged_slot_locked_(PluginSlot& slot,
1905 NodeId id) const;
1906 bool node_has_feedback_edge_locked_(NodeId id) const;
1907 bool node_has_parameter_or_automation_contract_locked_(NodeId id) const;
1908 bool staged_replacement_relaxes_identity_locked_(NodeId id) const;
1909 void set_live_swap_diagnostics_locked_(LiveSwapFallbackReason reason, NodeId node,
1910 std::string message);
1911 SwapResult fail_swap_edit_locked_(LiveSwapFallbackReason reason, NodeId node,
1912 std::string message);
1913 void cancel_swap_edit_locked_();
1914 std::vector<audio::AudioProcessLoadSnapshot> staged_node_loads_locked_() const;
1915 void publish_prepared_stats_locked_(const CompiledGraph& cg);
1916 static PreparedStats stats_for_snapshot_(const CompiledGraph& cg);
1917 void clear_prepared_stats_locked_();
1918 void invalidate_live_locked_();
1919 static void
1920 compute_latencies_for_(CompiledGraph& cg, const std::vector<Connection>& connections,
1921 const std::unordered_map<NodeId, PreparedPluginMetadata>& plugin_meta);
1922};
1923
1924// Drag-add helper.
1925//
1926// `add_plugin_node_from_drop` is the host-side companion to
1927// `pulp::view::PluginManagerPanel::on_row_drag_start`. It attempts to
1928// load the plugin in the supplied `PluginInfo`; if PluginSlot::load
1929// returns null (plugin missing on this machine, ABI mismatch, scanner
1930// gave us a stale path, etc.) it falls back to
1931// `add_unresolved_plugin_node` so the topology survives a serialize +
1932// reload pass and the user can resolve the plugin later.
1933//
1934// `loaded_out`, if non-null, is set to true when a live PluginSlot
1935// attached or false when the node landed unresolved — useful for
1936// driving the drop-target UI (e.g. ghost-vs-solid node rendering).
1937//
1938// The function takes the graph by reference; it is the caller's
1939// responsibility to call `graph.prepare(...)` afterwards before the
1940// next audio block. We do NOT do that here because real hosts batch
1941// drops and prepare once at the end of the UI tick.
1943 bool* loaded_out = nullptr);
1944
1945// Live-swap admission decision: whether a live plugin-instance swap can proceed
1946// without risking an xrun, plus a stable reason for diagnostics.
1948 bool admit = false;
1949 const char* reason = "no history";
1950};
1951
1952// Decide whether a live plugin-instance swap is admissible under the current audio
1953// load. CONSERVATIVE (deny on uncertainty): if the whole-graph OR any staged node has
1954// fewer than min_callbacks of measured history, deny with "no history" — an unmeasured
1955// path could xrun and the eager-silence fallback is the safe outcome. Otherwise admit
1956// iff max(graph.load, graph.last_load) + the sum over staged nodes of
1957// max(node.load, node.last_load) (the fade's added second-render cost) is within
1958// headroom_threshold. Pure — no graph state, so it is unit-testable in isolation.
1961 const std::vector<audio::AudioProcessLoadSnapshot>& staged_nodes,
1962 float headroom_threshold, std::uint64_t min_callbacks);
1963
1964// Explicitly registers the closed framework-owned v1 scalar cohort. There are
1965// no process-global registries or static-initializer side effects.
1968
1969} // namespace pulp::host
Small runtime policy helper for budgeted work and graceful degradation.
Measures the CPU load of an audio processing callback.
Definition load_measurer.hpp:182
The store. One producer (audio thread) and one consumer (drain/owner thread).
Definition live_dsp_telemetry.hpp:178
Optional capability for processors and graph runtimes which own auxiliary realtime rendering threads.
Definition audio_workgroup_client.hpp:11
Fixed-capacity, non-owning set of block-local bus views.
Definition process_block.hpp:146
Pre-allocated scratch backing the routing path's parameter-automation edges.
Definition graph_runtime_executor.hpp:566
Pre-allocated scratch buffers backing the routing process() path.
Definition graph_runtime_executor.hpp:297
Definition graph_runtime_executor.hpp:738
Pre-allocated per-node MIDI buffers backing the routing path's event edges.
Definition graph_runtime_executor.hpp:486
Control-thread-built graph snapshot for GraphRuntimeExecutor.
Definition graph_runtime_executor.hpp:159
Retained, self-contained lifetime for an authored in-process graph node.
Definition processor_node_adapter.hpp:114
Definition processor.hpp:174
Closed catalog value.
Definition spectral_frame.hpp:174
Profile-one indexed channels: symmetric float Hann, local-window phase, forward scale one,...
Definition spectral_frame.hpp:126
Definition anticipation_lane.hpp:33
Definition graph_serializer.hpp:97
Definition plugin_slot.hpp:94
Definition sample_region_runtime.hpp:181
Definition sample_region_parameters.hpp:80
Definition sample_region_runtime.hpp:121
Strong, typed handle to one exact compiled SignalGraph generation.
Definition signal_graph_execution_snapshot.hpp:23
Definition signal_graph_prepared_topology_edit.hpp:27
Definition signal_graph_runtime.hpp:83
GeneratedGraphValidationRejectReason
Definition signal_graph_runtime.hpp:100
@ MaxBlockSizeExceeded
Definition signal_graph_runtime.hpp:103
@ None
Definition signal_graph_runtime.hpp:101
@ ConnectionLimitExceeded
Definition signal_graph_runtime.hpp:105
@ InvalidBlockSize
Definition signal_graph_runtime.hpp:102
@ EstimatedWorkExceeded
Definition signal_graph_runtime.hpp:107
@ NodeLimitExceeded
Definition signal_graph_runtime.hpp:104
@ PortLimitExceeded
Definition signal_graph_runtime.hpp:106
bool parallel_routing_enabled() const noexcept
Definition signal_graph_runtime.hpp:737
bool prepare(double sample_rate, int max_block_size)
void set_parallel_min_work_units(std::uint64_t channel_samples) noexcept
Definition signal_graph_runtime.hpp:763
bool set_node_hosted_editor_open(NodeId id, bool open)
std::uint64_t routed_walk_fallbacks() const noexcept
Definition signal_graph_runtime.hpp:784
bool connect(NodeId source, PortIndex source_port, NodeId dest, PortIndex dest_port)
void acquire_routed_only_execution() noexcept
Require compiled routed execution while at least one owner holds a lease.
Definition signal_graph_runtime.hpp:687
NodeId add_trigger_producer(const TriggerProducerConfig &config)
SpectralResult< SpectralSynthesisPorts > add_spectral_synthesis(SpectralIdentity identity, const std::string &name)
SpectralResult< SpectralStagePorts > add_spectral_stage(SpectralIdentity identity, QualifiedSpectralStage stage, const std::string &name)
const GraphNode * node(NodeId id) const
bool set_node_parameter(NodeId id, uint32_t param_id, float value)
audio::AudioProcessLoadSnapshot graph_load() const
bool is_processor_node(NodeId id) const
std::optional< PreparedLatencySnapshot > prepared_latency_snapshot() const
const CustomNodeType * custom_node_type(std::string_view type_id, int version) const
bool inject_midi(NodeId midi_input_node, const midi::MidiBuffer &events, const midi::MpeBuffer &mpe) noexcept
int pump_anticipation(int max_blocks=8)
bool remove_node(NodeId id)
SignalGraphExecutionStatus RoutedExecutionStatus
Control-thread view of the actual routed paths embedded in the live compiled snapshot.
Definition signal_graph_runtime.hpp:674
GeneratedGraphValidation validate_generated_graph(int max_block_size) const
void compile_snapshot_for_test(double sample_rate, int max_block_size)
bool connect_sidechain(NodeId source, PortIndex source_port, NodeId dest, PortIndex dest_sidechain_port)
bool audio_rate_modulation_lane(const Connection &connection, state::ModulationLane &lane) const
SpectralError stop_spectral_controller()
Control thread only, after the host has stopped calling process().
const CustomNodeType * custom_node_type(std::string_view type_id) const
SpectralResult< SpectralAnalysisPorts > add_spectral_analysis(SpectralIdentity identity, const std::string &name)
void set_limits(GraphLimits limits)
void set_live_dsp_telemetry_enabled(bool enabled)
NodeId add_trigger_envelope(const TriggerEnvelopeConfig &config={})
TriggerPreparation prepare_trigger_plan(double sample_rate, int max_frames)
std::atomic< float > * live_gain_atomic(NodeId id) const noexcept
bool would_create_cycle(NodeId source, NodeId dest) const
SwapResult stage_plugin_replacement(NodeId id, PluginCatalogToken token)
float node_gain(NodeId id) const
const std::vector< GraphNode > & nodes() const
Definition signal_graph_runtime.hpp:431
SpectralTransitionState spectral_transition_state() const noexcept
bool is_prepared() const noexcept
Definition signal_graph_runtime.hpp:624
std::optional< SampleRegionDescriptor > sample_region(SampleRegionId id) const
SpectralError finalize_spectral_transition()
void set_audio_workgroup(void *workgroup) noexcept override
Publish the device-owned workgroup for subsequent renders, or null when the standalone device has no ...
Definition signal_graph_runtime.hpp:742
std::vector< SampleRegionRuntimeReceipt > sample_region_runtime_receipts() const
friend class GraphSerializer
Definition signal_graph_runtime.hpp:932
bool node_load_monitoring() const noexcept
std::vector< SampleRegionDescriptor > sample_regions() const
void set_node_load_monitoring(bool enabled) noexcept
bool prepare_audio_workgroup_for_render() noexcept override
RT-safe render-cycle preparation.
Definition signal_graph_runtime.hpp:748
bool connect_feedback(NodeId source, PortIndex source_port, NodeId dest, PortIndex dest_port)
audio::LiveDspTelemetrySnapshot poll_live_dsp_telemetry()
NodeId add_custom_node(std::string_view type_id, int version, const std::string &name={})
bool live_dsp_telemetry_enabled() const
bool set_node_gain(NodeId id, float linear_gain)
PreparedStats prepared_stats() const
std::vector< uint8_t > custom_node_state(NodeId id) const
EdgeProbeIdentity audio_edge_probe_identity() const
NodeId add_gain_node(const std::string &name="Gain")
NodeId add_plugin_node(const PluginInfo &info)
bool connect_automation(NodeId src, PortIndex src_audio_port, NodeId dest, uint32_t dest_param_id, float range_lo, float range_hi, float smoothing_ms=0.0f, AutomationMix mix=AutomationMix::Replace)
void set_parallel_routing_enabled(bool enabled) noexcept
Definition signal_graph_runtime.hpp:734
std::size_t estimate_generated_graph_work_units(int max_block_size) const
SpectralError spectral_error() const noexcept
Definition signal_graph_runtime.hpp:202
bool register_custom_node_diagnostics(CustomNodeDiagnosticsDescriptor descriptor)
GraphResult connect(SpectralOutputPort source, SpectralInputPort dest)
Definition signal_graph_runtime.hpp:188
bool seed_midi_input_sequence_for_test(NodeId midi_input_node, std::uint64_t predecessor) noexcept
CustomNodeDiagnosticResult query_custom_node_diagnostics(CustomNodeDiagnosticHandle handle, std::uint64_t schema, std::span< std::byte > output, CustomNodeDiagnosticConsistency consistency=CustomNodeDiagnosticConsistency::LiveApproximate) const
std::shared_ptr< const void > live_snapshot_handle() const noexcept
int node_latency_samples(NodeId id) const
void release_routed_only_execution() noexcept
Definition signal_graph_runtime.hpp:690
const CustomNodeEventProcessFn * live_custom_event_processor(NodeId id) const noexcept
bool anticipation_enabled() const noexcept
Definition signal_graph_runtime.hpp:827
bool set_processor_audio_input_provider(NodeId id, format::AudioInputBusProvider provider) noexcept
Install a host-owned realtime input-bus provider on an in-process Processor node.
GraphResult connect(const PortHandle &source, const PortHandle &dest)
Connect named typed ports.
bool extract_midi(NodeId midi_output_node, midi::MidiBuffer &out) const
PluginSlot * live_plugin_slot(NodeId id) const noexcept
friend bool register_builtin_sample_region_types(SignalGraph &graph)
std::span< const TriggerRecord > trigger_trace() const noexcept
TriggerGraphResult connect_trigger(TriggerEdge edge)
const CustomNodeParamProcessFn * live_custom_param_processor(NodeId id) const noexcept
NodeId add_plugin_node(std::unique_ptr< PluginSlot > slot, int num_inputs, int num_outputs, const std::string &name="Plugin")
bool register_custom_node_type(CustomNodeType type)
bool read_audio_edge_probe(graph::GraphRuntimeProbeSample &sample)
std::vector< NodeLoadReport > node_loads() const
void wait_for_audio_workgroup_update() noexcept override
Off-RT barrier for borrowed-handle teardown.
Definition signal_graph_runtime.hpp:751
void set_canonical_executor_routing_enabled(bool enabled) noexcept
Definition signal_graph_runtime.hpp:715
bool connect_midi(NodeId source, NodeId dest)
bool inject_parameter_events(NodeId plugin_node, const state::ParameterEventQueue &events)
RoutedExecutionStatus routed_execution_status(int block_size) const noexcept
std::vector< NodeId > processing_order() const
TriggerGraphResult process_checked(audio::BufferView< float > &output, const audio::BufferView< const float > &input, int frames)
const CustomNodeTransportProcessFn * live_custom_transport_processor(NodeId id) const noexcept
int live_custom_latency_samples(NodeId id) const noexcept
bool inject_midi(NodeId midi_input_node, const midi::MidiBuffer &events) noexcept
RuntimeBudgetReport evaluate_optional_runtime_budget(runtime::RuntimeBudgetFrame &frame, runtime::RuntimeWorkLane lane=runtime::RuntimeWorkLane::Background, bool required=false) const noexcept
GraphLimits limits() const
Definition signal_graph_runtime.hpp:527
NodeId add_processor_node(std::unique_ptr< format::Processor > processor, const std::string &name={})
int latency_samples() const
Definition signal_graph_runtime.hpp:892
NodeId add_custom_node(std::string_view type_id, const std::string &name={})
TriggerResourceReport trigger_resources() const
SwapResult prepare_swap(double sample_rate, int max_block_size)
bool set_node_live_swap_policy(NodeId id, NodeLiveSwapPolicy policy)
std::uint64_t midi_input_sequence_for_test(NodeId midi_input_node) const noexcept
EdgeProbeStatus attach_audio_edge_probe(const PortHandle &source, const PortHandle &destination)
int prepared_max_block_size() const noexcept
EdgeProbeStatus audio_edge_probe_status() const
friend struct SpectralControllerTestAccess
Definition signal_graph_runtime.hpp:1553
SpectralTransitionState
Definition signal_graph_runtime.hpp:191
@ Staged
Definition signal_graph_runtime.hpp:193
@ Finalized
Definition signal_graph_runtime.hpp:196
@ Activated
Definition signal_graph_runtime.hpp:194
@ Rejected
Definition signal_graph_runtime.hpp:195
@ Idle
Definition signal_graph_runtime.hpp:192
NodeId add_midi_output_node(const std::string &name="MIDI Out")
std::shared_ptr< format::ProcessorNodeInstance > processor_node_instance(NodeId id) const
Returns the retained Processor instance for a runtime-owned node.
friend class PreparedTriggerPlan
Definition signal_graph_runtime.hpp:1462
const std::vector< Connection > & connections() const
Definition signal_graph_runtime.hpp:434
TriggerGraphResult try_reset_events() noexcept
TriggerGraphResult trigger_counters(TriggerCounters &counters) const noexcept
SampleRegionProof prove_sample_region(SampleRegionId id) const
void * configured_audio_workgroup() const noexcept
Definition signal_graph_runtime.hpp:754
NodeId add_processor_node(std::shared_ptr< format::ProcessorNodeInstance > processor, const std::string &name={})
Add an owned in-process Processor.
void set_anticipation_enabled(bool enabled) noexcept
Definition signal_graph_runtime.hpp:824
bool set_custom_node_state(NodeId id, const std::vector< uint8_t > &bytes)
format::GraphRuntimeExecutorStats routing_executor_stats() const noexcept
Definition signal_graph_runtime.hpp:772
bool disconnect_modulation(NodeId src, PortIndex src_audio_port, NodeId dest, uint32_t dest_param_id, bool audio_rate)
Remove one automation or audio-rate modulation edge by its typed target.
std::size_t custom_node_type_count() const
NodeId add_input_node(int channels, const std::string &name="Input")
TriggerGraphResult try_install_prepared_trigger_plan(std::unique_ptr< PreparedTriggerPlan > &candidate)
std::uint64_t sample_region_binding_generation() const noexcept
void set_live_swap_plugin_loader_for_test(PluginLoaderForTest loader)
float get_node_parameter(NodeId id, uint32_t param_id) const
const CustomNodeProcessFn * live_custom_processor(NodeId id) const noexcept
NodeId add_unresolved_plugin_node(const PluginInfo &info, int num_inputs, int num_outputs, const std::string &name)
void process(audio::BufferView< float > &output, const audio::BufferView< const float > &input, int num_samples)
NodeId last_prepare_custom_failure_node() const
SwapResult
Definition signal_graph_runtime.hpp:467
@ NotInSwapEdit
Definition signal_graph_runtime.hpp:475
@ NeedsEagerPrepare
Definition signal_graph_runtime.hpp:470
@ Swapped
Definition signal_graph_runtime.hpp:468
std::uint64_t parallel_min_work_units() const noexcept
Definition signal_graph_runtime.hpp:766
NodeId add_output_node(int channels, const std::string &name="Output")
NodeId add_midi_input_node(const std::string &name="MIDI In")
PluginCatalogToken register_scanned_plugin(const PluginInfo &info)
LiveSwapDiagnostics last_swap_diagnostics() const
bool connect_audio_rate_modulation(NodeId src, PortIndex src_audio_port, NodeId dest, uint32_t dest_param_id, float range_lo, float range_hi, float smoothing_ms=0.0f, AutomationMix mix=AutomationMix::Replace)
NodeId add_unresolved_custom_node(std::string_view type_id, int version, int num_inputs, int num_outputs, const std::string &name)
std::function< std::unique_ptr< PluginSlot >(const PluginInfo &)> PluginLoaderForTest
Definition signal_graph_runtime.hpp:514
std::uint64_t transport_suppressed_for_anticipation() const noexcept
Definition signal_graph_runtime.hpp:875
std::uint64_t audio_edge_probe_dropped() const
const SampleKernelDescriptor * sample_kernel_type(std::string_view type_id, int version) const
std::uint64_t routed_only_execution_failures() const noexcept
Number of blocks made silent because routed processing failed, including a hosted node rejection,...
Definition signal_graph_runtime.hpp:680
bool disconnect(NodeId source, PortIndex source_port, NodeId dest, PortIndex dest_port)
const SampleRegionParameterBinding * sample_region_parameter_binding() const noexcept
Definition signal_graph_runtime.hpp:294
bool canonical_executor_routing_enabled() const noexcept
Definition signal_graph_runtime.hpp:721
GraphAuthoringReceiptStatus validate_authoring_receipt(GraphAuthoringReceipt receipt) const
GraphAuthoringReceipt authoring_receipt() const
NodeId add_trigger_lowpass(float coefficient=0.5f)
std::unique_ptr< PreparedTopologyEdit > begin_prepared_topology_edit()
std::vector< CustomNodeTypeMetadata > custom_node_types() const
std::vector< TriggerEdge > trigger_edges() const
void set_audio_workgroup_from_render_context(void *workgroup) noexcept override
Publish an AU render-context workgroup.
Definition signal_graph_runtime.hpp:745
friend bool build_signal_graph_executor_routing(const SignalGraph &, SignalGraphExecutorRouting &)
CustomNodeDiagnosticResult custom_node_diagnostic_handle(NodeId id) const
EdgeProbeStatus
Definition signal_graph_runtime.hpp:550
@ UnsupportedKind
Definition signal_graph_runtime.hpp:556
@ UnsupportedExecution
Definition signal_graph_runtime.hpp:557
@ Detached
Definition signal_graph_runtime.hpp:552
@ Attached
Definition signal_graph_runtime.hpp:551
@ UnknownPort
Definition signal_graph_runtime.hpp:554
@ MissingEdge
Definition signal_graph_runtime.hpp:555
@ Unprepared
Definition signal_graph_runtime.hpp:553
@ Busy
Definition signal_graph_runtime.hpp:558
Desktop adapter that lowers phase-1 timeline tracks into stable SignalGraph nodes.
Definition timeline_graph_binding.hpp:184
Collection of timestamped MIDI events within a single audio buffer period.
Definition buffer.hpp:31
A time-ordered list of per-note expression events for one block.
Definition mpe_buffer.hpp:51
A time-ordered list of UMP events for one audio block.
Definition ump_buffer.hpp:41
Definition budget_policy.hpp:101
Lock-free triple buffer for publishing the latest value from a writer thread to a reader thread witho...
Definition triple_buffer.hpp:45
Definition parameter_event_queue.hpp:82
Fixed-slot per-node live-DSP timing telemetry.
Real-time CPU load measurement for audio callbacks.
Sample-accurate MPE expression event stream.
Definition analyzer_provider.hpp:12
@ Normal
Definition font_options.hpp:38
@ events
a per-block list of ValueEvent occurrences
Definition value_channel_telemetry.hpp:25
Definition child_process_manager.hpp:18
What the product grants a device: the quota table indexed by capability tier and live thermal state.
Definition aax_editor.hpp:31
Definition graph_runtime_buffer_assignment.hpp:8
std::uint32_t NodeId
Definition graph_types.hpp:7
SpectralError
Definition spectral_frame.hpp:15
std::uint32_t PortIndex
Definition graph_types.hpp:8
Definition baked_codec.hpp:158
std::shared_ptr< ExactParameterIngressOwner > make_exact_parameter_ingress_owner()
std::optional< std::vector< HostParamInfo > > processor_node_parameters(const SignalGraph &graph, NodeId id)
Bounded event contracts shared by host ingress and graph-authored nodes.
Definition anticipation_eligibility.hpp:9
TriggerGraphResult
Definition trigger_graph.hpp:87
AutomationMix
Definition signal_graph_connection.hpp:13
@ Replace
Definition signal_graph_connection.hpp:14
std::function< void(audio::BufferView< float > &output, const audio::BufferView< const float > &input, int num_samples, const CustomNodeEventBlock &events)> CustomNodeEventProcessFn
Stateless event-aware process callback.
Definition custom_node_events.hpp:62
std::function< void(audio::BufferView< float > &output, const audio::BufferView< const float > &input, int num_samples, const BakedParamView &params)> CustomNodeParamProcessFn
Definition custom_node_type.hpp:61
std::function< void(audio::BufferView< float > &output, const audio::BufferView< const float > &input, int num_samples, const format::ProcessContext &transport)> CustomNodeTransportProcessFn
Definition custom_node_type.hpp:84
TriggerEmitResult
Definition trigger_graph.hpp:34
::pulp::state::ParameterEventQueue ParameterEventQueue
Definition parameter_event_queue.hpp:8
std::function< void(audio::BufferView< float > &output, const audio::BufferView< const float > &input, int num_samples)> CustomNodeProcessFn
Definition custom_node_type.hpp:24
LiveSwapFallbackReason
Definition signal_graph_node.hpp:99
CustomNodeDiagnosticConsistency
Definition custom_node_diagnostics.hpp:21
@ LiveApproximate
Definition custom_node_diagnostics.hpp:22
graph::SpectralResult< T > SpectralResult
Definition spectral_graph.hpp:11
LiveSwapAdmission evaluate_live_swap_admission(const audio::AudioProcessLoadSnapshot &graph, const std::vector< audio::AudioProcessLoadSnapshot > &staged_nodes, float headroom_threshold, std::uint64_t min_callbacks)
GraphAuthoringReceiptStatus
Definition signal_graph_authoring.hpp:25
NodeType
Definition signal_graph_node.hpp:21
NodeId add_plugin_node_from_drop(SignalGraph &graph, const PluginInfo &info, bool *loaded_out=nullptr)
std::uint32_t SampleRegionId
Definition sample_region_proof.hpp:13
Definition custom_node_events.hpp:26
@ info
Definition gpu_diagnostics.hpp:56
@ pending
Definition gpu_pass.hpp:556
Definition generated_event_source.hpp:33
RuntimeWorkLane
Definition budget_policy.hpp:11
@ Background
Definition budget_policy.hpp:14
Definition host_transport_projector.hpp:8
@ size
Definition config.hpp:216
@ GraphNode
Definition modulation_lane.hpp:16
@ incomplete
Definition health_read_result.hpp:20
@ move
Definition web_event_translate.hpp:110
@ value
the widget's primary value (Knob, Fader, RangeSlider, Toggle, ProgressBar)
Definition binding_diagnostics.hpp:19
STL namespace.
Publishing a value built off the audio thread, and reclaiming the old one without ever calling a dest...
Snapshot published by AudioProcessLoadMeasurer.
Definition load_measurer.hpp:25
A host-owned, realtime-safe view provider for declared processor input buses.
Definition audio_input_provider.hpp:17
Definition graph_runtime_executor.hpp:255
Definition graph_runtime_levelization.hpp:25
Definition graph_runtime_probe.hpp:9
Definition signal_graph_connection.hpp:18
Definition custom_node_diagnostics.hpp:27
Definition custom_node_diagnostics.hpp:32
Definition custom_node_diagnostics.hpp:46
Definition custom_node_type.hpp:90
Definition signal_graph_authoring.hpp:14
Definition signal_graph_node.hpp:126
Definition signal_graph_node.hpp:46
Definition signal_graph_runtime.hpp:1947
const char * reason
Definition signal_graph_runtime.hpp:1949
bool admit
Definition signal_graph_runtime.hpp:1948
Definition signal_graph_node.hpp:118
Definition signal_graph_node.hpp:81
Definition scanner.hpp:35
Definition signal_graph_executor_routing.hpp:38
Definition signal_graph_node.hpp:36
Definition custom_node_type.hpp:386
Definition sample_region_authoring.hpp:24
Definition sample_region_proof.hpp:134
Definition sample_region_runtime.hpp:171
Definition signal_graph_execution_status.hpp:12
Definition signal_graph_executor_routing.hpp:143
std::uint32_t slot
Definition signal_graph_runtime.hpp:1365
std::uint32_t out_channel
Definition signal_graph_runtime.hpp:1364
Definition signal_graph_runtime.hpp:1360
std::vector< std::uint8_t > skip_mask
Definition signal_graph_runtime.hpp:1362
AnticipationLane lane
Definition signal_graph_runtime.hpp:1361
std::vector< Prefill > prefill
Definition signal_graph_runtime.hpp:1367
std::vector< std::vector< float > > consume_scratch
Definition signal_graph_runtime.hpp:1368
bool valid
Definition signal_graph_runtime.hpp:1370
std::vector< float * > consume_ptrs
Definition signal_graph_runtime.hpp:1369
Definition signal_graph_runtime.hpp:1212
NodeType type
Definition signal_graph_runtime.hpp:1213
int num_input_ports
Definition signal_graph_runtime.hpp:1214
int num_output_ports
Definition signal_graph_runtime.hpp:1215
Definition signal_graph_runtime.hpp:1217
NodeShape shape
Definition signal_graph_runtime.hpp:1219
NodeRuntime * runtime
Definition signal_graph_runtime.hpp:1220
Definition signal_graph_runtime.hpp:1291
std::uint64_t pending_seq
Definition signal_graph_runtime.hpp:1294
std::uint32_t plan_index
Definition signal_graph_runtime.hpp:1292
NodeId id
Definition signal_graph_runtime.hpp:1293
Definition signal_graph_runtime.hpp:1278
format::GraphRuntimeBufferPool pool
Definition signal_graph_runtime.hpp:1280
std::vector< CustomBindingContext > custom_ctx
Definition signal_graph_runtime.hpp:1282
graph::GraphRuntimeLevelization levelization
Definition signal_graph_runtime.hpp:1283
std::vector< PluginBindingContext > plugin_ctx
Definition signal_graph_runtime.hpp:1281
format::GraphRuntimeSnapshot snapshot
Definition signal_graph_runtime.hpp:1279
bool valid
Definition signal_graph_runtime.hpp:1284
Definition signal_graph_runtime.hpp:1311
std::vector< RoutedMidiNode > midi_inputs
Definition signal_graph_runtime.hpp:1333
format::BusBufferSet buses
Definition signal_graph_runtime.hpp:1340
PluginRoutingScratch plugin_scratch
Definition signal_graph_runtime.hpp:1323
std::vector< RoutedProcessorNode > processor_parameter_inputs
Definition signal_graph_runtime.hpp:1335
RoutedPath parallel
Definition signal_graph_runtime.hpp:1320
RoutedPath serial
Definition signal_graph_runtime.hpp:1313
std::vector< RoutedMidiNode > midi_outputs
Definition signal_graph_runtime.hpp:1334
format::GraphRuntimeMidiScratch midi
Definition signal_graph_runtime.hpp:1332
format::GraphRuntimeAutomationScratch automation
Definition signal_graph_runtime.hpp:1339
Definition signal_graph_runtime.hpp:1296
std::uint64_t pending_seq
Definition signal_graph_runtime.hpp:1299
std::uint32_t plan_index
Definition signal_graph_runtime.hpp:1297
NodeId id
Definition signal_graph_runtime.hpp:1298
Definition signal_graph_runtime.hpp:1106
std::vector< float > ring
Definition signal_graph_runtime.hpp:1110
int write_pos
Definition signal_graph_runtime.hpp:1111
Definition signal_graph_runtime.hpp:560
std::uint64_t connection
Definition signal_graph_runtime.hpp:562
PortIndex source_port
Definition signal_graph_runtime.hpp:565
PortIndex destination_port
Definition signal_graph_runtime.hpp:566
NodeId source
Definition signal_graph_runtime.hpp:563
GraphAuthoringReceipt authoring
Definition signal_graph_runtime.hpp:561
NodeId destination
Definition signal_graph_runtime.hpp:564
Definition signal_graph_runtime.hpp:110
bool accepted
Definition signal_graph_runtime.hpp:111
std::size_t actual
Definition signal_graph_runtime.hpp:113
GeneratedGraphValidationRejectReason reason
Definition signal_graph_runtime.hpp:112
std::size_t limit
Definition signal_graph_runtime.hpp:114
Definition signal_graph_runtime.hpp:88
std::size_t max_ports
Definition signal_graph_runtime.hpp:91
std::size_t max_connections
Definition signal_graph_runtime.hpp:90
std::size_t max_estimated_work_units
Definition signal_graph_runtime.hpp:97
int max_block_size
Definition signal_graph_runtime.hpp:92
std::size_t max_nodes
Definition signal_graph_runtime.hpp:89
Definition signal_graph_runtime.hpp:546
NodeId node_id
Definition signal_graph_runtime.hpp:547
audio::AudioProcessLoadSnapshot load
Definition signal_graph_runtime.hpp:548
Definition signal_graph_runtime.hpp:1090
bool has_add
Definition signal_graph_runtime.hpp:1094
float lo
Definition signal_graph_runtime.hpp:1091
float hi
Definition signal_graph_runtime.hpp:1092
bool has_replace
Definition signal_graph_runtime.hpp:1093
Definition signal_graph_runtime.hpp:1017
size_t connection_index
Definition signal_graph_runtime.hpp:1018
NodeRuntime * source_runtime
Definition signal_graph_runtime.hpp:1019
Definition signal_graph_runtime.hpp:1042
float min_value
Definition signal_graph_runtime.hpp:1044
float max_value
Definition signal_graph_runtime.hpp:1045
Definition signal_graph_runtime.hpp:1049
bool touched
Definition signal_graph_runtime.hpp:1055
float lo
Definition signal_graph_runtime.hpp:1052
float v0
Definition signal_graph_runtime.hpp:1050
float vN
Definition signal_graph_runtime.hpp:1051
float hi
Definition signal_graph_runtime.hpp:1053
bool has_add
Definition signal_graph_runtime.hpp:1054
Definition signal_graph_runtime.hpp:169
std::uint64_t value
Definition signal_graph_runtime.hpp:170
friend bool operator==(PluginCatalogToken a, PluginCatalogToken b) noexcept
Definition signal_graph_runtime.hpp:174
Definition signal_graph_runtime.hpp:142
std::int64_t delay_samples
Definition signal_graph_runtime.hpp:144
Connection connection
Definition signal_graph_runtime.hpp:143
Definition signal_graph_runtime.hpp:147
std::vector< PreparedNodeLatency > nodes
Definition signal_graph_runtime.hpp:149
std::optional< std::int64_t > connection_delay(const Connection &connection) const
std::vector< PreparedConnectionDelay > connections
Definition signal_graph_runtime.hpp:150
std::int64_t total_latency_samples
Definition signal_graph_runtime.hpp:148
std::optional< PreparedNodeLatency > node(NodeId id) const
Definition signal_graph_runtime.hpp:136
std::int64_t output_latency_samples
Definition signal_graph_runtime.hpp:139
std::int64_t input_latency_samples
Definition signal_graph_runtime.hpp:138
NodeId node_id
Definition signal_graph_runtime.hpp:137
Definition signal_graph_runtime.hpp:117
std::size_t spectral_buffer_bytes
Definition signal_graph_runtime.hpp:127
std::size_t connection_count
Definition signal_graph_runtime.hpp:120
std::size_t ordered_node_count
Definition signal_graph_runtime.hpp:119
int max_block_size
Definition signal_graph_runtime.hpp:122
std::size_t total_prepared_buffer_bytes
Definition signal_graph_runtime.hpp:126
std::size_t node_count
Definition signal_graph_runtime.hpp:118
std::uint64_t snapshot_generation
Definition signal_graph_runtime.hpp:128
std::size_t total_ports
Definition signal_graph_runtime.hpp:121
std::size_t node_audio_buffer_bytes
Definition signal_graph_runtime.hpp:123
std::size_t delay_buffer_bytes
Definition signal_graph_runtime.hpp:125
std::size_t automation_buffer_bytes
Definition signal_graph_runtime.hpp:124
Definition signal_graph_runtime.hpp:158
bool should_run_optional_work() const noexcept
Definition signal_graph_runtime.hpp:164
std::uint64_t estimated_cost
Definition signal_graph_runtime.hpp:161
runtime::RuntimeBudgetFrameStats frame_stats
Definition signal_graph_runtime.hpp:160
runtime::RuntimeBudgetDecision decision
Definition signal_graph_runtime.hpp:159
bool prepared
Definition signal_graph_runtime.hpp:162
Definition spectral_graph.hpp:13
PortHandle handle
Definition spectral_graph.hpp:14
Definition spectral_graph.hpp:16
PortHandle handle
Definition spectral_graph.hpp:17
Definition trigger_graph.hpp:76
Definition trigger_graph.hpp:104
Definition trigger_graph.hpp:80
Definition trigger_graph.hpp:71
Definition trigger_graph.hpp:136
Definition trigger_graph.hpp:66
Definition trigger_graph.hpp:23
Definition trigger_graph.hpp:113
Definition budget_policy.hpp:39
Definition budget_policy.hpp:46
Definition modulation_lane.hpp:46
Lock-free latest-value publication between two threads.
Sample-accurate MIDI 2.0 UMP packet stream.