Pulp 1.88.0
Cross-platform audio plugin framework
Loading...
Searching...
No Matches
signal_graph_executor_routing.hpp
Go to the documentation of this file.
1#pragma once
2
10
11#include <atomic>
12#include <cstdint>
13#include <functional>
14#include <memory>
15#include <span>
16#include <utility>
17#include <vector>
18
19namespace pulp::audio {
21}
22
23namespace pulp::host {
24
25struct HostParamInfo;
26class PluginSlot;
27
28class SignalGraph;
29struct GraphNode;
30struct Connection;
31
32// Fallback scratch a routed Plugin-node binding hands to PluginSlot::process
33// when the routed call does not need per-node MIDI or automation buffers.
34// MIDI/automation graphs use the executor-owned per-node scratch exposed through
35// GraphRuntimeNodeProcessContext. Serial snapshots may share one fallback
36// scratch; parallel snapshots bind each Plugin node to owned fallback scratch
37// because PluginSlot::process receives a mutable MIDI output buffer.
43
44// Per-Plugin-node binding context (the binding's user_data). Stored in a
45// caller-owned vector reserved to the plugin-node count so its elements keep
46// stable addresses for the snapshot's lifetime. `scratch` points either at the
47// serial snapshot's shared fallback scratch or at `owned_scratch` for a parallel
48// snapshot. Keep owned scratch heap-indirected so `scratch` survives moving this
49// context into `plugin_ctx`.
52 PluginSlot* slot = nullptr;
54 std::unique_ptr<PluginRoutingScratch> owned_scratch;
56 std::uint64_t live = 0;
57 std::uint64_t exact = 0;
58 };
60 void*, state::ParameterEventQueue&) noexcept;
63 std::atomic<std::uint64_t>* parameter_events_live_sequence_seen = nullptr;
64 std::atomic<std::uint64_t>* parameter_events_exact_sequence_seen = nullptr;
66 // Cached, prepare-stable copy of the node's transport-sensitivity capability
67 // (GraphNode::transport_sensitive, resolved once at compile from
68 // PluginSlot::wants_transport()). The binding reads THIS, never a live
69 // slot->wants_transport() per block, so the routed forwarding and the
70 // anticipation partition resolve from the same value and can never disagree.
71 bool wants_transport = false;
72};
73
75 void* user_data = nullptr;
77 std::atomic<std::uint64_t>* live_sequence_seen = nullptr;
78 std::atomic<std::uint64_t>* exact_sequence_seen = nullptr;
79};
80
82 std::span<PluginBindingContext> contexts) noexcept;
84 std::span<PluginBindingContext> contexts) noexcept;
85
86// The custom-node process callback. Declared here (identical to the alias in
87// signal_graph.hpp) so this header carries no dependency on signal_graph.hpp,
88// which itself includes this one — the two are mutually consistent and an
89// identical alias redeclaration is well-formed when both are visible.
90using CustomNodeProcessFn = std::function<void(audio::BufferView<float>& output,
92 int num_samples)>;
93
94// Transport-aware custom callback (identical to the alias in signal_graph.hpp;
95// see the note above on the redeclaration being well-formed). Carries the host
96// transport for a transport-sensitive custom node.
98 std::function<void(audio::BufferView<float>& output,
100 int num_samples,
101 const format::ProcessContext& transport)>;
102
103// Per-Custom-node binding context (the binding's user_data). Stored in a
104// caller-owned vector reserved to the custom-node count so its elements keep
105// stable addresses for the snapshot's lifetime (mirrors PluginBindingContext).
106// `process` is a COPY of the resolved CustomNodeProcessFn: for a stateful custom
107// node the resolved fn is a lambda that captured the node's instance shared_ptr
108// by value, so this copy carries its own keepalive on that instance — exactly as
109// a PluginBindingContext's slot keeps its plugin alive through the snapshot. An
110// empty `process` means the node was unresolved (unregistered type / shape
111// mismatch); the binding then reproduces SignalGraph's pass-through-or-zero.
114 // Transport-aware callback for a transport-sensitive custom node. When set
115 // (and a transport is available for the block) the binding invokes this
116 // instead of `process`; empty == transport-unaware node. A COPY of the
117 // resolved callback (its own keepalive on any captured instance), mirroring
118 // `process`. Its non-empty state mirrors the node's resolved
119 // GraphNode::transport_sensitive, so binding and partition stay consistent.
121 // Event-aware callback for a Custom node whose type reads events. When set,
122 // the binding invokes this instead of `process`/`process_transport` and hands
123 // it the node's gathered inbound MIDI; empty == event-unaware node. A COPY of
124 // the resolved callback, mirroring `process`. Registration refuses a type
125 // that declares both this and a transport callback, so the dispatch ladder
126 // has no ambiguous case.
128};
129
130// A SignalGraph translated into the canonical GraphRuntimeExecutor's routing
131// form — an immutable snapshot plus a pre-sized scratch pool, ready to drive via
132// GraphRuntimeExecutor::process_routed().
133//
134// Lifetime: built from a PREPARED, eligible graph; valid only until that graph
135// re-prepares. The Gain bindings read the live compiled snapshot's gain atomics
136// (so set_node_gain() without a re-prepare is honored on the routed path), the
137// Plugin bindings invoke that snapshot's live PluginSlots, the Custom bindings
138// invoke a COPY of that snapshot's resolved process callbacks (each copy its own
139// instance keepalive), and `snapshot_keepalive` pins that snapshot so all stay
140// valid. `plugin_ctx` and `custom_ctx` (reserved, stable storage that the
141// respective bindings' user_data points into) and `plugin_scratch` (the shared
142// MIDI/param scratch) are owned here. Rebuild the routing after any recompile.
146 std::vector<PluginBindingContext> plugin_ctx; // stable user_data storage
147 std::vector<CustomBindingContext> custom_ctx; // stable user_data storage
149 std::shared_ptr<const void> snapshot_keepalive; // pins gain-atomic + slot lifetime
150 bool valid = false;
151};
152
153// True iff `nodes`/`connections` are in the subset the executor reproduces
154// bit-exactly:
155// - nodes only AudioInput / AudioOutput / Gain / Plugin / MidiInput /
156// MidiOutput / Custom. Gain is the fixed 2-in/2-out built-in utility and
157// always fully writes. A Plugin node with a live slot invokes it; its output
158// region is pinned persistent so a plugin that does not fully write matches
159// SignalGraph's persistent per-node buffer, and a reported latency is fine
160// because its delay compensation is replicated on the routed path. A Plugin
161// node with no live slot (unresolved/placeholder) routes as
162// pass-through-or-zero, exactly as SignalGraph's walk does for a slot-less
163// plugin node. A Custom node is audio-only (no MIDI/automation), and may
164// carry prepare-stable intrinsic latency through its registered type, which
165// participates in PDC;
166// its binding invokes the resolved process callback, or — for an unresolved
167// type / shape mismatch — pass-through-or-zero, so it matches the walk either
168// way;
169// - connections may be audio (feedforward, feedback, or sidechain — a sidechain
170// edge routes as plain audio into a higher input port of the destination
171// plugin), MIDI event edges, sparse automation, or dense audio-rate
172// modulation. Any per-node automated-parameter count routes — the gather's
173// per-node accumulators are sized off-RT to the actual count (no fixed cap).
174// No prepared check.
175bool signal_graph_topology_executor_eligible(std::span<const GraphNode> nodes,
176 std::span<const Connection> connections);
177
178// As above, plus requiring the graph be prepared (a live snapshot exists).
180
181// Calculate host-visible latency for a lowerable audio graph using the exact
182// runtime-plan lane classification and buffer-assignment PDC pass. `custom_latency_for`
183// supplies fixed latency only for Custom nodes that will execute. The result is
184// clamped to INT_MAX; returns 0 if the plan/assignment cannot be built. Callers
185// admitting a graph must validate the topology first so failure cannot be
186// mistaken for genuine zero latency.
188 std::span<const GraphNode> nodes,
189 std::span<const Connection> connections,
190 const std::function<int(NodeId)>& custom_latency_for);
191
192// The per-node value resolvers build_executor_snapshot binds a plan against.
193// One named field per resolver, so two same-shaped resolvers cannot be swapped
194// by argument position and a caller that needs only some of them leaves the rest
195// default-empty instead of counting placeholder arguments.
196//
197// Every field is optional: an empty resolver means "this caller supplies no such
198// value", and the build falls back exactly as documented on
199// build_executor_snapshot below.
201 // Only the graph's closed catalog can supply spectral executable bindings.
202 // Keeping this resolver opaque prevents the raw snapshot builder from
203 // becoming an alternate public native-stage registration path.
205 public:
206 SpectralResolver() = default;
207 explicit operator bool() const noexcept {
208 return bool(resolve_);
209 }
211 return resolve_(id);
212 }
213
214 private:
215 friend class SignalGraph;
216 explicit SpectralResolver(std::function<format::GraphRuntimeNodeBinding(NodeId)> resolve)
217 : resolve_(std::move(resolve)) {}
218 std::function<format::GraphRuntimeNodeBinding(NodeId)> resolve_;
219 };
220 // A Gain node's live atomic. Empty (or a null result) fails the build for a
221 // graph that has Gain nodes.
222 std::function<std::atomic<float>*(NodeId)> gain_for;
223 // A Plugin node's live slot. A null result still routes: the binding
224 // reproduces SignalGraph's pass-through-or-zero, matching the walk.
225 std::function<PluginSlot*(NodeId)> plugin_for;
226 // A per-node persistent CPU-load measurer the executor wraps each node's work
227 // in (node_loads() telemetry parity with the legacy walk). Null result = that
228 // node is not measured; empty = no node is measured (e.g. a baked Processor
229 // that exposes no per-node loads).
231 // A Custom node's live process callback (a COPY is stored in the binding
232 // context as its own keepalive). A null result (unregistered type / shape
233 // mismatch) still routes as pass-through-or-zero, matching the walk.
234 std::function<const CustomNodeProcessFn*(NodeId)> custom_for;
235 // A Custom node's transport-aware callback (null = none). Populated into the
236 // binding context alongside `custom_for`; a node with a non-null result is
237 // transport-sensitive (consistent with GraphNode::transport_sensitive
238 // resolved at compile).
240 // Event-aware custom callback for a Custom node whose type reads events.
241 // Resolved from the same snapshot map the reference walk reads, so the two
242 // execution paths cannot disagree about which nodes consume events.
244 // Prepare-stable intrinsic latency for a resolved Custom node, resolved from
245 // CustomNodeType once at compile/prepare (the caller has already evaluated
246 // it at the snapshot's sample rate). Empty, or a non-positive result, means
247 // zero latency.
248 std::function<int(NodeId)> custom_latency_for;
249 // Cached plugin-metadata accessors so a swap-time build makes NO live
250 // PluginSlot metadata call. Empty → fall back to `plugin_for`'s live slot
251 // (baked / anticipation callers, which are not on the swap path).
252 std::function<int(NodeId)> plugin_latency_for;
253 std::function<const std::vector<HostParamInfo>*(NodeId)> plugin_params_for;
254 // Optional per-Plugin-node ingress binding for the control-to-audio
255 // parameter-event mailbox. Empty means this snapshot has no injected
256 // parameter events (for example baked/external routing callers).
259};
260
261// Build the executor snapshot (plan + bindings) for an eligible topology,
262// resolving each node's live values through `binders`. Plugin binding contexts
263// are appended to `plugin_ctx` (cleared first, then reserved to the plugin count
264// so the bindings' user_data stays valid) and reference `scratch`. Shared by the
265// live-graph translation and by SignalGraph::compile_ (which embeds the snapshot
266// in the compiled graph, resolving from its own runtime/plugins). Returns false
267// (and leaves `out` empty) if the topology is not eligible, a Gain atomic is
268// missing, or the canonical plan/snapshot cannot be built under its limits. Does
269// NOT size a pool or set a keepalive — the caller owns those.
270// `parallel_safe` builds the snapshot's buffer assignment without slot reuse so
271// it can drive GraphRuntimeExecutor::process_parallel (concurrent same-level
272// nodes never alias a recycled slot); default false = the compact serial layout.
273// `custom_ctx`, when non-null, is the caller-owned storage for Custom-node
274// binding contexts (cleared, then reserved to the custom-node count so the
275// bindings' user_data stays valid). When it is null and the topology contains a
276// Custom node, the build fails closed (returns false) so the caller falls back to
277// the legacy walk — callers whose subset never contains Custom nodes (e.g. a
278// baked Processor or the anticipation interior) may omit it.
279bool build_executor_snapshot(std::span<const GraphNode> nodes,
280 std::span<const Connection> connections,
281 const ExecutorSnapshotBinders& binders,
282 std::vector<PluginBindingContext>& plugin_ctx,
283 PluginRoutingScratch& scratch,
285 bool parallel_safe = false,
286 std::vector<CustomBindingContext>* custom_ctx = nullptr);
287
288// Positional-resolver forwarder onto the binder-struct overload above, kept for
289// call sites that have not migrated yet. Each argument maps to the same-named
290// ExecutorSnapshotBinders field, but the semantics are NOT identical: this
291// signature carries no `custom_event_for`, so a Custom node whose type declares
292// the event lane is built event-UNAWARE through here and silently reads no
293// events. It fails open, not closed. Use the binder-struct overload when a
294// snapshot may contain an event-aware Custom node.
295// Prefer the ExecutorSnapshotBinders overload for new code: two same-shaped
296// resolvers cannot be silently swapped there.
297bool build_executor_snapshot(std::span<const GraphNode> nodes,
298 std::span<const Connection> connections,
299 const std::function<std::atomic<float>*(NodeId)>& gain_for,
300 const std::function<PluginSlot*(NodeId)>& plugin_for,
301 std::vector<PluginBindingContext>& plugin_ctx,
302 PluginRoutingScratch& scratch,
304 bool parallel_safe = false,
305 const std::function<audio::AudioProcessLoadMeasurer*(NodeId)>&
306 load_for = {},
307 std::vector<CustomBindingContext>* custom_ctx = nullptr,
308 const std::function<const CustomNodeProcessFn*(NodeId)>&
309 custom_for = {},
310 // Resolves a Custom node's transport-aware callback
311 // (null = none). Populated into the binding context
312 // alongside `custom_for`; a node with a non-null
313 // result is transport-sensitive (consistent with
314 // GraphNode::transport_sensitive resolved at compile).
315 const std::function<const CustomNodeTransportProcessFn*(NodeId)>&
316 custom_transport_for = {},
317 // 2.2b (H2): cached plugin-metadata accessors so a
318 // swap-time build makes NO live PluginSlot metadata
319 // call. Empty → fall back to plugin_for's live slot
320 // (baked / anticipation callers, not on the swap path).
321 const std::function<int(NodeId)>& plugin_latency_for = {},
322 const std::function<const std::vector<HostParamInfo>*(NodeId)>&
323 plugin_params_for = {},
324 const std::function<ParameterEventInjectionBinding(NodeId)>&
325 parameter_events_for = {});
326
327// Translate a prepared, eligible `graph` into `out` (snapshot + sized pool +
328// keepalive). Returns false (out.valid == false) when ineligible/unprepared. On
329// success `out` drives process_routed() with output bit-identical to
330// SignalGraph::process()'s own walk for the eligible subset.
333
334} // namespace pulp::host
Measures the CPU load of an audio processing callback.
Definition load_measurer.hpp:182
Non-owning view over multi-channel audio sample data.
Definition buffer.hpp:30
Pre-allocated scratch buffers backing the routing process() path.
Definition graph_runtime_executor.hpp:297
Control-thread-built graph snapshot for GraphRuntimeExecutor.
Definition graph_runtime_executor.hpp:159
Definition signal_graph_executor_routing.hpp:204
format::GraphRuntimeNodeBinding operator()(NodeId id) const
Definition signal_graph_executor_routing.hpp:210
friend class SignalGraph
Definition signal_graph_executor_routing.hpp:215
Definition plugin_slot.hpp:94
Definition signal_graph_runtime.hpp:83
Collection of timestamped MIDI events within a single audio buffer period.
Definition buffer.hpp:31
Definition parameter_event_queue.hpp:82
The event block a Custom graph node receives, and the callback types that take it.
Definition analyzer_provider.hpp:12
Definition graph_runtime_buffer_assignment.hpp:8
std::uint32_t NodeId
Definition graph_types.hpp:7
Bounded event contracts shared by host ingress and graph-authored nodes.
Definition anticipation_eligibility.hpp:9
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
void reset_plugin_parameter_event_sequences(std::span< PluginBindingContext > contexts) noexcept
int calculate_lowerable_graph_latency_samples(std::span< const GraphNode > nodes, std::span< const Connection > connections, const std::function< int(NodeId)> &custom_latency_for)
bool build_signal_graph_executor_routing(const SignalGraph &graph, SignalGraphExecutorRouting &out)
void commit_plugin_parameter_event_sequences(std::span< PluginBindingContext > contexts) noexcept
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
bool signal_graph_topology_executor_eligible(std::span< const GraphNode > nodes, std::span< const Connection > connections)
bool signal_graph_executor_eligible(const SignalGraph &graph)
std::function< void(audio::BufferView< float > &output, const audio::BufferView< const float > &input, int num_samples)> CustomNodeProcessFn
Definition custom_node_type.hpp:24
bool build_executor_snapshot(std::span< const GraphNode > nodes, std::span< const Connection > connections, const ExecutorSnapshotBinders &binders, std::vector< PluginBindingContext > &plugin_ctx, PluginRoutingScratch &scratch, format::GraphRuntimeSnapshot &out, bool parallel_safe=false, std::vector< CustomBindingContext > *custom_ctx=nullptr)
STL namespace.
Definition graph_runtime_executor.hpp:130
Process context — passed every audio callback with transport state.
Definition process_context.hpp:89
Definition signal_graph_connection.hpp:18
Definition signal_graph_executor_routing.hpp:112
CustomNodeProcessFn process
Definition signal_graph_executor_routing.hpp:113
CustomNodeEventProcessFn process_events
Definition signal_graph_executor_routing.hpp:127
CustomNodeTransportProcessFn process_transport
Definition signal_graph_executor_routing.hpp:120
Definition signal_graph_executor_routing.hpp:200
std::function< ParameterEventInjectionBinding(NodeId)> parameter_events_for
Definition signal_graph_executor_routing.hpp:257
std::function< PluginSlot *(NodeId)> plugin_for
Definition signal_graph_executor_routing.hpp:225
std::function< std::atomic< float > *(NodeId)> gain_for
Definition signal_graph_executor_routing.hpp:222
std::function< audio::AudioProcessLoadMeasurer *(NodeId)> load_for
Definition signal_graph_executor_routing.hpp:230
std::function< const std::vector< HostParamInfo > *(NodeId)> plugin_params_for
Definition signal_graph_executor_routing.hpp:253
std::function< const CustomNodeTransportProcessFn *(NodeId)> custom_transport_for
Definition signal_graph_executor_routing.hpp:239
std::function< const CustomNodeProcessFn *(NodeId)> custom_for
Definition signal_graph_executor_routing.hpp:234
std::function< int(NodeId)> custom_latency_for
Definition signal_graph_executor_routing.hpp:248
std::function< int(NodeId)> plugin_latency_for
Definition signal_graph_executor_routing.hpp:252
SpectralResolver spectral_for
Definition signal_graph_executor_routing.hpp:258
std::function< const CustomNodeEventProcessFn *(NodeId)> custom_event_for
Definition signal_graph_executor_routing.hpp:243
Definition signal_graph_node.hpp:126
Definition plugin_slot.hpp:74
Definition signal_graph_executor_routing.hpp:74
std::atomic< std::uint64_t > * live_sequence_seen
Definition signal_graph_executor_routing.hpp:77
PluginBindingContext::AppendParameterEventsFn append
Definition signal_graph_executor_routing.hpp:76
std::atomic< std::uint64_t > * exact_sequence_seen
Definition signal_graph_executor_routing.hpp:78
void * user_data
Definition signal_graph_executor_routing.hpp:75
Definition signal_graph_executor_routing.hpp:55
std::uint64_t exact
Definition signal_graph_executor_routing.hpp:57
std::uint64_t live
Definition signal_graph_executor_routing.hpp:56
Definition signal_graph_executor_routing.hpp:50
bool wants_transport
Definition signal_graph_executor_routing.hpp:71
std::atomic< std::uint64_t > * parameter_events_live_sequence_seen
Definition signal_graph_executor_routing.hpp:63
NodeId node_id
Definition signal_graph_executor_routing.hpp:51
PendingParameterEventSequences(*)( void *, state::ParameterEventQueue &) noexcept AppendParameterEventsFn
Definition signal_graph_executor_routing.hpp:59
PendingParameterEventSequences parameter_events_pending_sequences
Definition signal_graph_executor_routing.hpp:65
void * parameter_events_user_data
Definition signal_graph_executor_routing.hpp:61
PluginSlot * slot
Definition signal_graph_executor_routing.hpp:52
AppendParameterEventsFn append_parameter_events
Definition signal_graph_executor_routing.hpp:62
std::unique_ptr< PluginRoutingScratch > owned_scratch
Definition signal_graph_executor_routing.hpp:54
PluginRoutingScratch * scratch
Definition signal_graph_executor_routing.hpp:53
std::atomic< std::uint64_t > * parameter_events_exact_sequence_seen
Definition signal_graph_executor_routing.hpp:64
Definition signal_graph_executor_routing.hpp:38
state::ParameterEventQueue param_events
Definition signal_graph_executor_routing.hpp:41
midi::MidiBuffer midi_out
Definition signal_graph_executor_routing.hpp:40
midi::MidiBuffer midi_in
Definition signal_graph_executor_routing.hpp:39
Definition signal_graph_executor_routing.hpp:143
std::shared_ptr< const void > snapshot_keepalive
Definition signal_graph_executor_routing.hpp:149
PluginRoutingScratch plugin_scratch
Definition signal_graph_executor_routing.hpp:148
std::vector< CustomBindingContext > custom_ctx
Definition signal_graph_executor_routing.hpp:147
format::GraphRuntimeSnapshot snapshot
Definition signal_graph_executor_routing.hpp:144
bool valid
Definition signal_graph_executor_routing.hpp:150
std::vector< PluginBindingContext > plugin_ctx
Definition signal_graph_executor_routing.hpp:146
format::GraphRuntimeBufferPool pool
Definition signal_graph_executor_routing.hpp:145