From d470f1d2c51d774a0620bdad91f1f72da5351366 Mon Sep 17 00:00:00 2001 From: w1n5t0n Date: Tue, 1 Sep 2026 17:00:23 +0300 Subject: [PATCH] fix(cold-eval): sample external inputs --- src/signal_engine/cold_eval.cpp | 19 +++++++++++++++---- src/signal_engine/cold_eval.h | 8 ++++++++ test/signal_engine/test_ext_registry.cpp | 23 +++++++++++++++++++++++ 3 files changed, 46 insertions(+), 4 deletions(-) diff --git a/src/signal_engine/cold_eval.cpp b/src/signal_engine/cold_eval.cpp index e5ec482..323731e 100644 --- a/src/signal_engine/cold_eval.cpp +++ b/src/signal_engine/cold_eval.cpp @@ -30,10 +30,23 @@ void SignalEngine::init_defaults(Sample bpm, int beats_per_bar, GraphBuilder::init_symbols(); state = EngineState{}; session_generation = 0; + memset(cold_hw_inputs, 0, sizeof(cold_hw_inputs)); reset_session_storage(bpm, beats_per_bar, bars_per_phrase, phrases_per_section, false); } +void SignalEngine::snapshot_cold_hw_inputs(const Sample* values, + uint16_t count) { + if (!values) count = 0; + if (count > MAX_HW_INPUT_CHANNELS) count = MAX_HW_INPUT_CHANNELS; + if (count > 0) + memcpy(cold_hw_inputs, values, count * sizeof(Sample)); + if (count < MAX_HW_INPUT_CHANNELS) { + memset(cold_hw_inputs + count, 0, + (MAX_HW_INPUT_CHANNELS - count) * sizeof(Sample)); + } +} + void SignalEngine::reset_session_storage(Sample bpm, int beats_per_bar, int bars_per_phrase, int phrases_per_section, @@ -807,7 +820,6 @@ static EvalResult do_set(TokenStream& ts, SignalEngine& engine, Sample cell_vals[MAX_CELLS]; engine.cells.snapshot_values(cell_vals, MAX_CELLS); - Sample hw_inputs[32] = {}; Sample workspace[MAX_TOTAL_NODES] = {}; Sample outputs[MAX_OUTPUTS] = {}; @@ -815,7 +827,7 @@ static EvalResult do_set(TokenStream& ts, SignalEngine& engine, ctx.t = engine.state.current_time; ctx.dt = engine.state.current_dt; ctx.cell_values = cell_vals; - ctx.hw_inputs = hw_inputs; + ctx.hw_inputs = engine.cold_hw_inputs; ctx.data_pool = engine.cells.data_pool; ctx.data_offsets = engine.cells.data_offsets; ctx.data_lengths = engine.cells.data_lengths; @@ -2864,7 +2876,6 @@ EvalResult eval_expression(const char* source, uint32_t length, engine.cells.snapshot_values(cell_values, MAX_CELLS); // Execute one sample - Sample hw_inputs[32] = {}; Sample outputs[MAX_OUTPUTS] = {}; Sample workspace[MAX_TOTAL_NODES] = {}; @@ -2872,7 +2883,7 @@ EvalResult eval_expression(const char* source, uint32_t length, ctx.t = engine.state.current_time; ctx.dt = engine.state.current_dt; ctx.cell_values = cell_values; - ctx.hw_inputs = hw_inputs; + ctx.hw_inputs = engine.cold_hw_inputs; ctx.data_pool = engine.cells.data_pool; ctx.data_offsets = engine.cells.data_offsets; ctx.data_lengths = engine.cells.data_lengths; diff --git a/src/signal_engine/cold_eval.h b/src/signal_engine/cold_eval.h index 5caabdd..1917a70 100644 --- a/src/signal_engine/cold_eval.h +++ b/src/signal_engine/cold_eval.h @@ -161,6 +161,12 @@ struct SignalEngine { NodePool scratch_pool; char eval_text_buf[512] = {}; + // Latest host-owned external-input snapshot used only by top-level cold + // expression evaluation. Hot graphs receive their input pointer through + // ExecutionContext on every tick. Keeping a copy here gives queries the + // same last-known values without retaining a host-lifetime pointer. + Sample cold_hw_inputs[MAX_HW_INPUT_CHANNELS] = {}; + #if USEQ_HAS_SYNTH_ENGINE // ── Host synth compiler domain (synth-nodes.md) ───────────────────── // Published synth artefacts: identity-keyed declarations + control @@ -197,6 +203,8 @@ struct SignalEngine { int bars_per_phrase = 4, int phrases_per_section = 4, bool publish_session_clear = true); + + void snapshot_cold_hw_inputs(const Sample* values, uint16_t count); }; // ── Cold-Path Evaluation ──────────────────────────────────────────────────── diff --git a/test/signal_engine/test_ext_registry.cpp b/test/signal_engine/test_ext_registry.cpp index 8d78133..d35160b 100644 --- a/test/signal_engine/test_ext_registry.cpp +++ b/test/signal_engine/test_ext_registry.cpp @@ -192,6 +192,29 @@ TEST_CASE("external inputs resolve as graph leaves", "[ext_registry]") REQUIRE(h.tick("a1", 1.5) == Approx(0.4)); } + SECTION("cold expressions read the latest external-input snapshot") + { + REQUIRE(register_external_input( + {"nn/out", 4, 4, 0.5f, "[0,1]"})); + h.hw_inputs[4] = 0.1; + h.hw_inputs[5] = 0.2; + h.hw_inputs[6] = 0.3; + h.hw_inputs[7] = 0.4; + h.engine.snapshot_cold_hw_inputs(h.hw_inputs, 32); + + EvalResult selected = h.eval("(nn/out 4)"); + REQUIRE(selected.kind == EvalResult::Number); + REQUIRE(selected.number == Approx(0.4)); + + EvalResult composed = h.eval("(* 2 (nn/out 2))"); + REQUIRE(composed.kind == EvalResult::Number); + REQUIRE(composed.number == Approx(0.4)); + + EvalResult invalid = h.eval("(nn/out 9)"); + REQUIRE(invalid.kind == EvalResult::Number); + REQUIRE(invalid.number == Approx(0.5)); + } + SECTION("invalid multi-channel selectors return the declared neutral") { REQUIRE(register_external_input(