Pulp 1.88.0
Cross-platform audio plugin framework
Loading...
Searching...
No Matches
reloadable_shell.hpp
Go to the documentation of this file.
1#pragma once
2
40
49#include <pulp/runtime/log.hpp>
50#include <pulp/state/store.hpp>
51#include <pulp/view/view.hpp> // complete View for create_view() forwarding
52
53#include <algorithm>
54#include <atomic>
55#include <functional>
56#include <chrono>
57#include <cstdint>
58#include <cstdlib>
59#include <filesystem>
60#include <memory>
61#include <mutex>
62#include <optional>
63#include <string>
64#include <system_error>
65#include <thread>
66#include <utility>
67#include <vector>
68
69#if defined(_WIN32)
70#include <process.h> // _getpid
71#else
72#include <unistd.h> // getpid
73#endif
74
75// Ship-safety gate. The dev filesystem WATCHER — the
76// background thread that polls a source path and dlopen()s whatever compiled
77// artifact appears — is a development affordance, not something a shipped plugin
78// should carry. Define PULP_RELOAD_DEV_WATCHER=0 in a release build and the
79// watcher thread, its poll loop, and the raw-path ReloadController it drives are
80// compiled OUT entirely (no filesystem-watch / auto-dlopen entry point in the
81// binary — asserted by the symbol-absence test). The signed SwapTransaction
82// surface (`session_` / the hot-swap slot) is deliberately NOT gated: that is
83// the path a shipping app-store-style signed content swap would use.
84#if !defined(PULP_RELOAD_DEV_WATCHER)
85#define PULP_RELOAD_DEV_WATCHER 1
86#endif
87
88namespace pulp::format::reload {
89
90class ReloadableShell : public Processor {
91public:
94 static constexpr std::chrono::milliseconds kPollInterval{150};
95
97 static constexpr double kCrossfadeMs = 12.0;
98
101 explicit ReloadableShell(std::string logic_path = {})
102 : logic_path_(resolve_logic_path(std::move(logic_path))) {
103 load_initial();
104 }
105
106 ~ReloadableShell() override { stop_watcher(); }
107
108 // ── Processor interface ──────────────────────────────────────────────
109
110 PluginDescriptor descriptor() const override { return descriptor_; }
111
112 // Forward the editor to the ACTIVE logic, so a hot-reload swaps the UI as well
113 // as the DSP. A host that keeps the editor across reloads should rebuild it on
114 // each swap (see set_on_reloaded) — or the logic should return a self-contained
115 // view; see ProcessorHotSwapSlot::create_active_view() for the lifetime note.
116 std::unique_ptr<view::View> create_view() override {
117 return slot_.with_active([](Processor& p) { return p.create_view(); });
118 }
119
120 // A raw pointer cannot carry the hot-swap slot's lifetime lease beyond this
121 // call. Fail closed for legacy callers; reload-aware consumers use the
122 // visitor below, which keeps the active generation alive through the work.
124 return nullptr;
125 }
126
128 const std::function<void(view::ValueChannelSet*)>& visitor) override {
129 const bool visited = slot_.with_active([&](Processor& p) {
130 p.visit_value_channels(visitor);
131 return true;
132 });
133 if (!visited && visitor) visitor(nullptr);
134 }
135
136 view::ScriptedUiSession* active_scripted_ui() override { return nullptr; }
137 const view::ScriptedUiSession* active_scripted_ui() const override { return nullptr; }
138
140 const std::function<void(view::ScriptedUiSession*)>& visitor) override {
141 const bool visited = slot_.with_active([&](Processor& p) {
142 p.visit_active_scripted_ui(visitor);
143 return true;
144 });
145 if (!visited && visitor) visitor(nullptr);
146 }
147
149 const std::function<void(const view::ScriptedUiSession*)>& visitor) const override {
150 const bool visited = slot_.with_active([&](Processor& p) {
151 static_cast<const Processor&>(p).visit_active_scripted_ui(visitor);
152 return true;
153 });
154 if (!visited && visitor) visitor(nullptr);
155 }
156
157 // Live-swap 1.9: this shell's editor rebuilds IN PLACE on each hot-swap. The
158 // ViewBridge hosts create_view() under a stable root container and rebuilds
159 // its content whenever editor_reload_generation() changes — polled on the
160 // editor idle tick, so the reload notification never crosses threads unsafely
161 // (the counter is bumped on the control/watcher thread, read on the UI thread).
162 bool supports_editor_reload() const override { return true; }
163 std::uint64_t editor_reload_generation() const override {
164 return reload_generation_.load(std::memory_order_acquire);
165 }
166
170 void set_on_reloaded(std::function<void()> cb) {
171 std::lock_guard<std::mutex> g(controller_mutex_);
172 on_reloaded_ = std::move(cb);
173 }
174
182 std::lock_guard<std::mutex> g(controller_mutex_);
183 reload_trust_policy_ = std::move(policy);
184 if (controller_) {
185 const auto live = controller_->baseline();
186 controller_.emplace(*session_, logic_path_, std::filesystem::path{},
187 reload_trust_policy_);
188 if (live)
189 controller_->set_baseline(*live);
190 }
191 }
192
193 // The host caches latency once (for plugin-delay compensation), so it is part
194 // of the frozen contract: report the initial logic's latency, and a reload
195 // that changes latency is rejected (see prepare()/the reload note below).
196 int latency_samples() const override { return initial_latency_; }
197
198 void define_parameters(state::StateStore& store) override {
199 // Mirror the initial logic's parameter contract so the host sees the
200 // automatable surface the DSP expects. Reloads must match this contract.
201 if (initial_) initial_->define_parameters(store);
202 }
203
204 void prepare(const PrepareContext& context) override {
205 // A host may call prepare() again (e.g. a sample-rate change). Join any
206 // running watcher BEFORE touching session_/controller_: the watcher holds
207 // a reference into them via poll(), and controller_ holds a ReloadSession&
208 // — re-emplacing under a live watcher would be a data race / dangling ref.
209 stop_watcher();
210 prepare_ctx_ = context;
211 if (initial_) {
212 initial_->set_state_store(&state());
213 initial_->prepare(context);
214 (void)slot_.swap(std::move(initial_)); // install the starting DSP (first install never fades)
215 initial_.reset();
216 } else {
217 // Re-prepare (e.g. a sample-rate change): the DSP already in the slot
218 // must see the new context, or it keeps running at the stale rate
219 // (tempo-synced LFOs, delay-line sizes). Audio is stopped during
220 // prepare(), and reprepare_active() takes the slot's writer lock, so
221 // this can't race the audio thread.
222 slot_.reprepare_active(context);
223 }
224 // Enable a short click-free crossfade on hot-swap: allocate the
225 // parallel-render scratch here (off the audio thread), sized to the
226 // worst-case block, and set the fade length from the sample rate.
227 const double sr = prepare_ctx_.sample_rate > 0 ? prepare_ctx_.sample_rate : 48000.0;
228 const std::size_t max_ch = static_cast<std::size_t>(
229 std::max({context.input_channels, context.output_channels, 2}));
230 const std::size_t max_frames =
231 static_cast<std::size_t>(std::max(context.max_buffer_size, 1));
232 slot_.prepare_crossfade(max_frames, max_ch);
233 slot_.set_crossfade_samples(static_cast<std::size_t>(kCrossfadeMs * 0.001 * sr));
234
235 // (Re)build the session + controller against the live store. Constructed
236 // here (not in the ctor) because they need the host-provided store,
237 // prepare context, and the running slot.
238 {
239 std::lock_guard<std::mutex> g(controller_mutex_);
240 // session_ is the signed SwapTransaction surface — always present so
241 // a shipping build can still adopt a signed content swap.
242 session_.emplace(slot_, state(), current_build_fingerprint(), prepare_ctx_);
243#if PULP_RELOAD_DEV_WATCHER
244 // The raw-path controller (dev filesystem reload) is compiled out of
245 // a shipping build; without it reload_now() / the watcher have no
246 // path to dlopen an arbitrary on-disk artifact.
247 // Keep the version the previous controller tracked (a re-prepare),
248 // else the one load_initial() loaded: the watcher must compare
249 // against what is live, not adopt whatever the file holds when it
250 // first polls.
251 std::optional<ReloadController::Baseline> live =
252 controller_ ? controller_->baseline() : loaded_baseline_;
253 controller_.emplace(*session_, logic_path_, std::filesystem::path{},
254 reload_trust_policy_);
255 if (live)
256 controller_->set_baseline(*live);
257#endif
258 }
259 start_watcher(); // no-op (compiled out) when the watcher is gated off
260 }
261
262 void release() override { stop_watcher(); }
263
266 midi::MidiBuffer& midi_in,
267 midi::MidiBuffer& midi_out,
268 const ProcessContext& context) override {
269 // Audio thread: forward to the live DSP. On swap contention the slot
270 // passes input through for one block — never blocks/allocates/destroys.
271 slot_.process(output, input, midi_in, midi_out, context);
272 }
273
274 // ── Diagnostics (control-thread / test use) ──────────────────────────
275
279 ReloadOutcome outcome;
280 std::function<void()> on_reloaded;
281 {
282 std::lock_guard<std::mutex> g(controller_mutex_);
283 if (!controller_) return {ReloadOutcome::Status::RejectedLoadFailed, "not prepared"};
284 outcome = controller_->reload_now();
285 record(outcome);
286 if (outcome.ok()) {
287 reload_generation_.fetch_add(1, std::memory_order_release); // editor rebuild (1.9)
288 on_reloaded = on_reloaded_snapshot();
289 }
290 }
291 if (on_reloaded) on_reloaded(); // fire AFTER releasing the lock (re-entrant-safe)
292 return outcome;
293 }
294
295 const std::string& logic_path() const { return logic_path_; }
296 std::uint64_t reload_attempts() const { return reload_attempts_.load(std::memory_order_relaxed); }
297 std::uint64_t successful_reloads() const { return successful_reloads_.load(std::memory_order_relaxed); }
298 ReloadOutcome::Status last_status() const { return last_status_.load(std::memory_order_relaxed); }
300 double last_reload_ms() const { return last_reload_ms_.load(std::memory_order_relaxed); }
301 std::uint64_t contention_blocks() const { return slot_.contention_blocks(); }
302 bool has_active_dsp() const { return slot_.has_active(); }
303
304private:
305 static std::string resolve_logic_path(std::string explicit_path) {
306 if (!explicit_path.empty()) return explicit_path;
307 if (const char* env = std::getenv("PULP_RELOAD_LOGIC_PATH"); env && *env)
308 return std::string(env);
309 return {};
310 }
311
312 // Copy the watched logic to a private, uniquely-named sibling and return that
313 // path; the initial image is loaded from it so the live mapping never sits on
314 // the developer-overwritten watched path (see load_initial). Unique per
315 // process (pid) and per instance (a monotonic counter) so two plugin
316 // instances — even in separate host processes — never stage onto each other's
317 // live mapping. Staged beside the original (dlopen-safe: a world-writable temp
318 // dir trips macOS dyld's unsigned-dylib kill). Best-effort: on any copy
319 // failure, fall back to the original path (the corruption only bites when the
320 // watched file is overwritten while the initial image is still live).
321 static std::string stage_initial(const std::string& logic_path) {
322 namespace fs = std::filesystem;
323 static std::atomic<std::uint64_t> counter{0};
324 const long pid =
325#if defined(_WIN32)
326 static_cast<long>(::_getpid());
327#else
328 static_cast<long>(::getpid());
329#endif
330 std::error_code ec;
331 const fs::path src(logic_path);
332 const fs::path staged =
333 src.parent_path() /
334 (src.stem().string() + ".initial." + std::to_string(pid) + "." +
335 std::to_string(counter.fetch_add(1, std::memory_order_relaxed)) +
336 src.extension().string());
337 fs::copy_file(src, staged, fs::copy_options::overwrite_existing, ec);
338 if (ec) {
339 runtime::log_warn("[reload-shell] could not stage initial logic ({}); "
340 "loading in place — a rebuild that overwrites it while "
341 "live may corrupt the image on Linux", ec.message());
342 return logic_path;
343 }
344 return staged.string();
345 }
346
347 // Load the initial logic image so descriptor()/define_parameters() reflect
348 // the DSP's real contract. Gated like a reload (ABI version + fingerprint)
349 // so a stale/incompatible bundled library degrades to passthrough instead of
350 // crashing. The image is retained for the shell's lifetime because it backs
351 // the initial Processor's code until that Processor is swapped out and
352 // destroyed.
353 void load_initial() {
354 descriptor_ = default_descriptor();
355 if (logic_path_.empty()) {
356 runtime::log_info("[reload-shell] no logic path (set PULP_RELOAD_LOGIC_PATH "
357 "or pass one); running as passthrough");
358 return;
359 }
360 // Load the initial image from a PRIVATE staged copy, never the watched
361 // path directly. The watched path is the developer's build output, which a
362 // rebuild overwrites — often in place (truncate + rewrite of the same
363 // inode, as a `cp`/install step or `std::filesystem::copy_file` does, vs a
364 // temp-file-then-rename that yields a fresh inode). dlopen keeps the file
365 // mapped; on Linux an in-place overwrite of a mapped image bleeds the new
366 // bytes into the still-live mapping (MAP_PRIVATE pages not yet
367 // copy-on-written), corrupting the initial Processor's code and vtable.
368 // That goes unnoticed until the initial Processor is rendered as the
369 // fading-out side of the first hot-swap's crossfade — then a virtual call
370 // jumps through an unrelocated vtable and crashes. Staging puts the live
371 // mapping on a path no rebuild touches, so the watched path can be
372 // overwritten freely. (The reload path already stages every version; see
373 // ReloadController. macOS tolerates the in-place overwrite, so this only
374 // bites on Linux.)
375 // Read the watched file's mtime BEFORE copying it. The baseline pairs this
376 // mtime with the hash of the bytes staged below; a build that lands
377 // after this read leaves the baseline at {old mtime, staged hash}, which
378 // the watcher's first poll sees as a change and reloads. Reading the
379 // mtime after the copy could pair a NEW mtime with the OLD bytes, and
380 // poll() would then treat the new build as already seen.
381 std::error_code mtime_ec;
382 const auto loaded_mtime = std::filesystem::last_write_time(logic_path_, mtime_ec);
383 const std::string load_path = stage_initial(logic_path_);
384 // The version now live is the staged bytes; the watcher's baseline.
385 if (const auto hash = ReloadController::content_hash(load_path); hash && !mtime_ec)
386 loaded_baseline_ = ReloadController::Baseline{loaded_mtime, *hash};
387
388 // Same fail-closed gate sequence the reload transaction uses (single
389 // source of truth — gate_logic_image). On rejection we degrade to
390 // passthrough; note a logic that defines parameters can't be adopted by a
391 // later hot-reload (its contract won't match the empty one the host
392 // cached) — that needs a full plugin reload. A parameter-less logic still
393 // hot-swaps in via the watcher.
395 if (auto* rejected = std::get_if<ReloadOutcome>(&gated)) {
396 runtime::log_warn("[reload-shell] initial logic rejected ({}) — passthrough; "
397 "a logic with parameters needs a full plugin reload to take effect",
398 rejected->detail);
399 // Surface the structured per-field diff (fingerprint/contract) so a
400 // rejection says WHAT differs (e.g. cpp_standard host vs logic), not
401 // just "mismatch" — the difference between a 5-minute fix and an hour.
402 for (const auto& issue : rejected->issues)
403 runtime::log_warn("[reload-shell] diff: {}", issue);
404 return;
405 }
406 GatedImage& image = std::get<GatedImage>(gated);
407 std::unique_ptr<Processor> p(image.create());
408 if (!p) {
409 runtime::log_warn("[reload-shell] initial logic create returned null — passthrough");
410 return;
411 }
412 descriptor_ = p->descriptor();
413 initial_latency_ = p->latency_samples(); // freeze the host-cached latency
414 initial_ = std::move(p);
415 initial_images_.push_back(std::move(image.lib)); // keep the code mapped
416 runtime::log_info("[reload-shell] loaded initial logic: {}", logic_path_);
417 }
418
419 static PluginDescriptor default_descriptor() {
420 return {.name = "Pulp Reloadable",
421 .manufacturer = "Pulp",
422 .bundle_id = "com.pulp.reloadable-shell",
423 .version = "1.0.0",
424 .category = PluginCategory::Effect,
425 .input_buses = {{"Audio In", 2}},
426 .output_buses = {{"Audio Out", 2}}};
427 }
428
429 void start_watcher() {
430#if PULP_RELOAD_DEV_WATCHER
431 if (running_.exchange(true)) return; // already running
432 // The token is the release-artifact scan's string marker for the dev
433 // watcher (tools/cmake/release_artifact_forbidden.json, gate
434 // reload-ship): a stripped shipping build has no symbol table to
435 // show watcher_loop in, so the bytes of this literal are the proof
436 // that the watcher is compiled out.
437 runtime::log_info("[reload-shell] dev watcher running (PULP_RELOAD_DEV_WATCHER_ACTIVE)");
438 watcher_ = std::thread([this] { watcher_loop(); });
439#endif
440 }
441
442 void stop_watcher() {
443#if PULP_RELOAD_DEV_WATCHER
444 if (!running_.exchange(false)) return;
445 if (watcher_.joinable()) watcher_.join();
446#endif
447 }
448
449#if PULP_RELOAD_DEV_WATCHER
450 // PULP_RELOAD_SHIP_FIXTURE_MARK (test-only) force-emits this symbol so the
451 // ship-safety symbol-absence test has a deterministic marker: present in the
452 // dev fixture, gone from the gate-off ship fixture. Normal builds set nothing
453 // here — the method emits only when ODR-used, as usual.
454#if defined(PULP_RELOAD_SHIP_FIXTURE_MARK)
455 [[gnu::used]]
456#endif
457 void watcher_loop() {
458 while (running_.load(std::memory_order_relaxed)) {
459 std::function<void()> on_reloaded;
460 {
461 std::lock_guard<std::mutex> g(controller_mutex_);
462 if (controller_) {
463 if (auto outcome = controller_->poll()) {
464 record(*outcome);
465 if (outcome->ok()) {
466 reload_generation_.fetch_add(1, std::memory_order_release); // editor rebuild (1.9)
467 on_reloaded = on_reloaded_snapshot();
468 }
469 }
470 }
471 }
472 if (on_reloaded) on_reloaded(); // fire outside the lock (re-entrant-safe)
473 // Free a processor whose crossfade has finished (RT-safe reclaim:
474 // the audio thread only marks the fade done; we free it here).
475 slot_.reclaim();
476 // Sleep in small slices so stop_watcher() is responsive.
477 for (int i = 0; i < 10 && running_.load(std::memory_order_relaxed); ++i)
478 std::this_thread::sleep_for(kPollInterval / 10);
479 }
480 }
481#endif
482
483 void record(const ReloadOutcome& outcome) {
484 reload_attempts_.fetch_add(1, std::memory_order_relaxed);
485 last_status_.store(outcome.status, std::memory_order_relaxed);
486 if (outcome.ok()) {
487 successful_reloads_.fetch_add(1, std::memory_order_relaxed);
488 last_reload_ms_.store(outcome.metrics.total_ms, std::memory_order_relaxed);
489 // `swapped in NNN ms` dev diagnostic + per-phase breakdown.
490 const auto& m = outcome.metrics;
492 "[reload-shell] swapped DSP in {:.2f} ms "
493 "(load+gate {:.2f} / construct {:.2f} / prepare {:.2f} / swap {:.2f}): {}",
494 m.total_ms, m.load_gate_ms, m.construct_ms, m.prepare_ms, m.swap_ms,
495 outcome.detail);
496 } else {
497 runtime::log_warn("[reload-shell] reload rejected after {:.2f} ms: {}",
498 outcome.metrics.total_ms, outcome.detail);
499 // Surface the structured per-field diff so a rejection says WHAT
500 // differs (e.g. which parameter's contract changed), not just the
501 // category — the difference between a 5-minute fix and an hour.
502 for (const auto& issue : outcome.issues)
503 runtime::log_warn("[reload-shell] reject-diff: {}", issue);
504 }
505 }
506
507 // Snapshot the on-reloaded callback (caller holds controller_mutex_). The
508 // caller fires it AFTER releasing the lock, so a callback that re-enters the
509 // shell (reload_now / create_view) can't self-deadlock on controller_mutex_.
510 std::function<void()> on_reloaded_snapshot() const { return on_reloaded_; }
511
512 std::string logic_path_;
513 ReloadTrustPolicy reload_trust_policy_; // opt-in require_signed (default OFF)
514 PluginDescriptor descriptor_;
515 int initial_latency_ = 0; // host-cached latency of the initial logic
516 std::unique_ptr<Processor> initial_; // pre-prepare; moved into slot_
517 std::vector<ReloadLibrary> initial_images_; // retains the initial logic's code
518 std::optional<ReloadController::Baseline>
519 loaded_baseline_; // file version load_initial() loaded
520 PrepareContext prepare_ctx_;
521 ProcessorHotSwapSlot slot_;
522
523 std::mutex controller_mutex_; // serializes poll() vs reload_now()
524 std::optional<ReloadSession> session_;
525 std::optional<ReloadController> controller_;
526
527 std::thread watcher_;
528 std::atomic<bool> running_{false};
529 std::atomic<std::uint64_t> reload_attempts_{0};
530 std::atomic<std::uint64_t> successful_reloads_{0};
531 std::atomic<double> last_reload_ms_{0.0}; // wall-clock of the last swap
532 std::atomic<ReloadOutcome::Status> last_status_{ReloadOutcome::Status::RejectedLoadFailed};
533 std::atomic<std::uint64_t> reload_generation_{0}; // editor rebuild counter (1.9; UI-thread read)
534 std::function<void()> on_reloaded_; // host editor-rebuild hook (control thread)
535};
536
537} // namespace pulp::format::reload
Strict build-compatibility fingerprint — the PRIMARY safety gate for DSP hot reload.
Non-owning view over multi-channel audio sample data.
Definition buffer.hpp:30
A processor's declared channels.
Definition value_channel_set.hpp:61
state::StateStore & state()
Access the parameter state store.
Definition processor.hpp:955
Processor()
Definition processor.hpp:176
static std::optional< std::uint64_t > content_hash(const std::filesystem::path &p)
Content fingerprint used for change detection (see hash_file).
Definition reload_controller.hpp:198
const std::string & logic_path() const
Definition reloadable_shell.hpp:295
void release() override
Release resources.
Definition reloadable_shell.hpp:262
void set_on_reloaded(std::function< void()> cb)
Register a callback fired (on the CONTROL/watcher thread) after each successful hot-swap — e....
Definition reloadable_shell.hpp:170
static constexpr double kCrossfadeMs
Crossfade length applied on each hot-swap so the DSP change is click-free.
Definition reloadable_shell.hpp:97
ReloadOutcome reload_now()
Force a reload now (e.g.
Definition reloadable_shell.hpp:278
std::uint64_t contention_blocks() const
Definition reloadable_shell.hpp:301
void process(audio::BufferView< float > &output, const audio::BufferView< const float > &input, midi::MidiBuffer &midi_in, midi::MidiBuffer &midi_out, const ProcessContext &context) override
Process one buffer of audio.
Definition reloadable_shell.hpp:264
double last_reload_ms() const
Total wall-clock of the last successful DSP reload, ms.
Definition reloadable_shell.hpp:300
void set_reload_trust_policy(ReloadTrustPolicy policy)
Opt-in "require signed reloads" for this shell's dev watcher (default OFF = the frictionless unsigned...
Definition reloadable_shell.hpp:181
PluginDescriptor descriptor() const override
Return the plugin's metadata. Called once during initialization.
Definition reloadable_shell.hpp:110
ReloadOutcome::Status last_status() const
Definition reloadable_shell.hpp:298
ReloadableShell(std::string logic_path={})
Definition reloadable_shell.hpp:101
const view::ScriptedUiSession * active_scripted_ui() const override
Definition reloadable_shell.hpp:137
std::unique_ptr< view::View > create_view() override
Create a custom view for this processor.
Definition reloadable_shell.hpp:116
void define_parameters(state::StateStore &store) override
Register parameters with the state store.
Definition reloadable_shell.hpp:198
view::ValueChannelSet * value_channels() override
Named value channels this processor publishes for its UI — gain reduction, an envelope follower,...
Definition reloadable_shell.hpp:123
bool has_active_dsp() const
Definition reloadable_shell.hpp:302
void prepare(const PrepareContext &context) override
Prepare for processing.
Definition reloadable_shell.hpp:204
~ReloadableShell() override
Definition reloadable_shell.hpp:106
std::uint64_t reload_attempts() const
Definition reloadable_shell.hpp:296
void visit_value_channels(const std::function< void(view::ValueChannelSet *)> &visitor) override
Synchronously visit the current value-channel set while the processor keeps it alive.
Definition reloadable_shell.hpp:127
static constexpr std::chrono::milliseconds kPollInterval
Poll interval for the watcher thread.
Definition reloadable_shell.hpp:94
std::uint64_t successful_reloads() const
Definition reloadable_shell.hpp:297
std::uint64_t editor_reload_generation() const override
Monotonic counter that increments each time this processor's editor content should be rebuilt (after ...
Definition reloadable_shell.hpp:163
bool supports_editor_reload() const override
Editor live-reload support (live-swap 1.9).
Definition reloadable_shell.hpp:162
int latency_samples() const override
Latency in samples introduced by this processor (default 0).
Definition reloadable_shell.hpp:196
void visit_active_scripted_ui(const std::function< void(const view::ScriptedUiSession *)> &visitor) const override
Const counterpart to visit_active_scripted_ui().
Definition reloadable_shell.hpp:148
void visit_active_scripted_ui(const std::function< void(view::ScriptedUiSession *)> &visitor) override
Synchronously visit the active scripted-UI session while the processor keeps it alive.
Definition reloadable_shell.hpp:139
view::ScriptedUiSession * active_scripted_ui() override
Return the active scripted UI session when a custom create_view() path owns one.
Definition reloadable_shell.hpp:136
Collection of timestamped MIDI events within a single audio buffer period.
Definition buffer.hpp:31
Centralized parameter storage with lock-free value access.
Definition store.hpp:164
Definition scripted_ui.hpp:125
@ image
Definition scene_node.hpp:95
Definition build_fingerprint.hpp:23
std::variant< GatedImage, ReloadOutcome > gate_logic_image(const std::string &library_path, const BuildFingerprint &host_fingerprint, const SwapPackTrust *trust=nullptr)
Load library_path and run the pre-construction gates — reload-ABI version, then build fingerprint — a...
Definition reload_transaction.hpp:200
BuildFingerprint current_build_fingerprint()
Capture THIS translation unit's build fingerprint.
Definition build_fingerprint.hpp:50
@ Effect
Audio effect (takes input, produces output).
Definition plugin_descriptor.hpp:186
@ g
Definition pitch.hpp:27
void log_warn(std::format_string< Args... > fmt, Args &&... args)
Definition log.hpp:190
void log_info(std::format_string< Args... > fmt, Args &&... args)
Definition log.hpp:185
@ i
Definition formant_filter_bank.hpp:20
@ gated
Flat body, hard cut.
Definition nonlin_ambience_design.hpp:245
@ rejected
Definition rmlui_host_bridge.hpp:21
@ m
Definition input_events.hpp:40
@ p
Definition input_events.hpp:40
STL namespace.
RT-safe hot-swap slot for a Processor.
The C-ABI contract a hot-reloadable "logic" library exports, and the macro a plugin uses to export it...
Watch a logic library on disk and hot-reload it when it changes (the standalone dev loop).
Dynamic-library handle for the DSP hot-reload path, with an explicit and deliberately conservative le...
The verify-before-commit reload transaction — ties the Phase-0 primitives into one operation: load a ...
Plugin metadata — declared once, immutable.
Definition plugin_descriptor.hpp:245
Definition prepare_resources.hpp:102
double sample_rate
Definition prepare_resources.hpp:103
int input_channels
Definition prepare_resources.hpp:105
int max_buffer_size
Definition prepare_resources.hpp:104
int output_channels
Definition prepare_resources.hpp:106
Process context — passed every audio callback with transport state.
Definition process_context.hpp:89
Definition reload_transaction.hpp:63
bool ok() const
Definition reload_transaction.hpp:84
Status
Definition reload_transaction.hpp:64
@ RejectedLoadFailed
dlopen / LoadLibrary failed.
Definition reload_transaction.hpp:72
Definition reload_trust_policy.hpp:26