64#include <system_error>
84#if !defined(PULP_RELOAD_DEV_WATCHER)
85#define PULP_RELOAD_DEV_WATCHER 1
117 return slot_.with_active([](
Processor& p) {
return p.create_view(); });
129 const bool visited = slot_.with_active([&](
Processor& p) {
130 p.visit_value_channels(visitor);
133 if (!visited && visitor) visitor(
nullptr);
141 const bool visited = slot_.with_active([&](
Processor& p) {
142 p.visit_active_scripted_ui(visitor);
145 if (!visited && visitor) visitor(
nullptr);
150 const bool visited = slot_.with_active([&](
Processor& p) {
154 if (!visited && visitor) visitor(
nullptr);
164 return reload_generation_.load(std::memory_order_acquire);
171 std::lock_guard<std::mutex> g(controller_mutex_);
172 on_reloaded_ = std::move(cb);
182 std::lock_guard<std::mutex> g(controller_mutex_);
183 reload_trust_policy_ = std::move(policy);
185 const auto live = controller_->baseline();
186 controller_.emplace(*session_, logic_path_, std::filesystem::path{},
187 reload_trust_policy_);
189 controller_->set_baseline(*live);
201 if (initial_) initial_->define_parameters(store);
210 prepare_ctx_ = context;
212 initial_->set_state_store(&
state());
213 initial_->prepare(context);
214 (void)slot_.swap(std::move(initial_));
222 slot_.reprepare_active(context);
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
>(
230 const std::size_t max_frames =
232 slot_.prepare_crossfade(max_frames, max_ch);
233 slot_.set_crossfade_samples(
static_cast<std::size_t
>(
kCrossfadeMs * 0.001 * sr));
239 std::lock_guard<std::mutex> g(controller_mutex_);
243#if PULP_RELOAD_DEV_WATCHER
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_);
256 controller_->set_baseline(*live);
271 slot_.process(output, input, midi_in, midi_out, context);
280 std::function<void()> on_reloaded;
282 std::lock_guard<std::mutex> g(controller_mutex_);
284 outcome = controller_->reload_now();
287 reload_generation_.fetch_add(1, std::memory_order_release);
288 on_reloaded = on_reloaded_snapshot();
291 if (on_reloaded) on_reloaded();
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); }
300 double last_reload_ms()
const {
return last_reload_ms_.load(std::memory_order_relaxed); }
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);
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};
326 static_cast<long>(::_getpid());
328 static_cast<long>(::getpid());
332 const fs::path staged =
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);
340 "loading in place — a rebuild that overwrites it while "
341 "live may corrupt the image on Linux", ec.message());
344 return staged.string();
353 void load_initial() {
354 descriptor_ = default_descriptor();
355 if (logic_path_.empty()) {
357 "or pass one); running as passthrough");
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_);
386 loaded_baseline_ = ReloadController::Baseline{loaded_mtime, *hash};
395 if (
auto* rejected = std::get_if<ReloadOutcome>(&gated)) {
397 "a logic with parameters needs a full plugin reload to take effect",
402 for (
const auto& issue :
rejected->issues)
406 GatedImage&
image = std::get<GatedImage>(gated);
407 std::unique_ptr<Processor>
p(
image.create());
409 runtime::log_warn(
"[reload-shell] initial logic create returned null — passthrough");
412 descriptor_ =
p->descriptor();
413 initial_latency_ =
p->latency_samples();
414 initial_ = std::move(p);
415 initial_images_.push_back(std::move(
image.lib));
419 static PluginDescriptor default_descriptor() {
420 return {.name =
"Pulp Reloadable",
421 .manufacturer =
"Pulp",
422 .bundle_id =
"com.pulp.reloadable-shell",
425 .input_buses = {{
"Audio In", 2}},
426 .output_buses = {{
"Audio Out", 2}}};
429 void start_watcher() {
430#if PULP_RELOAD_DEV_WATCHER
431 if (running_.exchange(
true))
return;
437 runtime::log_info(
"[reload-shell] dev watcher running (PULP_RELOAD_DEV_WATCHER_ACTIVE)");
438 watcher_ = std::thread([
this] { watcher_loop(); });
442 void stop_watcher() {
443#if PULP_RELOAD_DEV_WATCHER
444 if (!running_.exchange(
false))
return;
445 if (watcher_.joinable()) watcher_.join();
449#if PULP_RELOAD_DEV_WATCHER
454#if defined(PULP_RELOAD_SHIP_FIXTURE_MARK)
457 void watcher_loop() {
458 while (running_.load(std::memory_order_relaxed)) {
459 std::function<void()> on_reloaded;
461 std::lock_guard<std::mutex>
g(controller_mutex_);
463 if (
auto outcome = controller_->poll()) {
466 reload_generation_.fetch_add(1, std::memory_order_release);
467 on_reloaded = on_reloaded_snapshot();
472 if (on_reloaded) on_reloaded();
477 for (
int i = 0;
i < 10 && running_.load(std::memory_order_relaxed); ++
i)
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);
487 successful_reloads_.fetch_add(1, std::memory_order_relaxed);
488 last_reload_ms_.store(outcome.metrics.total_ms, std::memory_order_relaxed);
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,
498 outcome.metrics.total_ms, outcome.detail);
502 for (
const auto& issue : outcome.issues)
510 std::function<void()> on_reloaded_snapshot()
const {
return on_reloaded_; }
512 std::string logic_path_;
513 ReloadTrustPolicy reload_trust_policy_;
514 PluginDescriptor descriptor_;
515 int initial_latency_ = 0;
516 std::unique_ptr<Processor> initial_;
517 std::vector<ReloadLibrary> initial_images_;
518 std::optional<ReloadController::Baseline>
520 PrepareContext prepare_ctx_;
521 ProcessorHotSwapSlot slot_;
523 std::mutex controller_mutex_;
524 std::optional<ReloadSession> session_;
525 std::optional<ReloadController> controller_;
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};
533 std::atomic<std::uint64_t> reload_generation_{0};
534 std::function<void()> on_reloaded_;
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
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
@ 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
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 ...
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