fix(cold-eval): sample external inputs

This commit is contained in:
w1n5t0n 2026-09-01 17:00:23 +03:00
parent 67d04bf229
commit d470f1d2c5
3 changed files with 46 additions and 4 deletions

View file

@ -30,10 +30,23 @@ void SignalEngine::init_defaults(Sample bpm, int beats_per_bar,
GraphBuilder::init_symbols(); GraphBuilder::init_symbols();
state = EngineState{}; state = EngineState{};
session_generation = 0; session_generation = 0;
memset(cold_hw_inputs, 0, sizeof(cold_hw_inputs));
reset_session_storage(bpm, beats_per_bar, bars_per_phrase, reset_session_storage(bpm, beats_per_bar, bars_per_phrase,
phrases_per_section, false); 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, void SignalEngine::reset_session_storage(Sample bpm, int beats_per_bar,
int bars_per_phrase, int bars_per_phrase,
int phrases_per_section, int phrases_per_section,
@ -807,7 +820,6 @@ static EvalResult do_set(TokenStream& ts, SignalEngine& engine,
Sample cell_vals[MAX_CELLS]; Sample cell_vals[MAX_CELLS];
engine.cells.snapshot_values(cell_vals, MAX_CELLS); engine.cells.snapshot_values(cell_vals, MAX_CELLS);
Sample hw_inputs[32] = {};
Sample workspace[MAX_TOTAL_NODES] = {}; Sample workspace[MAX_TOTAL_NODES] = {};
Sample outputs[MAX_OUTPUTS] = {}; Sample outputs[MAX_OUTPUTS] = {};
@ -815,7 +827,7 @@ static EvalResult do_set(TokenStream& ts, SignalEngine& engine,
ctx.t = engine.state.current_time; ctx.t = engine.state.current_time;
ctx.dt = engine.state.current_dt; ctx.dt = engine.state.current_dt;
ctx.cell_values = cell_vals; 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_pool = engine.cells.data_pool;
ctx.data_offsets = engine.cells.data_offsets; ctx.data_offsets = engine.cells.data_offsets;
ctx.data_lengths = engine.cells.data_lengths; 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); engine.cells.snapshot_values(cell_values, MAX_CELLS);
// Execute one sample // Execute one sample
Sample hw_inputs[32] = {};
Sample outputs[MAX_OUTPUTS] = {}; Sample outputs[MAX_OUTPUTS] = {};
Sample workspace[MAX_TOTAL_NODES] = {}; 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.t = engine.state.current_time;
ctx.dt = engine.state.current_dt; ctx.dt = engine.state.current_dt;
ctx.cell_values = cell_values; 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_pool = engine.cells.data_pool;
ctx.data_offsets = engine.cells.data_offsets; ctx.data_offsets = engine.cells.data_offsets;
ctx.data_lengths = engine.cells.data_lengths; ctx.data_lengths = engine.cells.data_lengths;

View file

@ -161,6 +161,12 @@ struct SignalEngine {
NodePool scratch_pool; NodePool scratch_pool;
char eval_text_buf[512] = {}; 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 #if USEQ_HAS_SYNTH_ENGINE
// ── Host synth compiler domain (synth-nodes.md) ───────────────────── // ── Host synth compiler domain (synth-nodes.md) ─────────────────────
// Published synth artefacts: identity-keyed declarations + control // Published synth artefacts: identity-keyed declarations + control
@ -197,6 +203,8 @@ struct SignalEngine {
int bars_per_phrase = 4, int bars_per_phrase = 4,
int phrases_per_section = 4, int phrases_per_section = 4,
bool publish_session_clear = true); bool publish_session_clear = true);
void snapshot_cold_hw_inputs(const Sample* values, uint16_t count);
}; };
// ── Cold-Path Evaluation ──────────────────────────────────────────────────── // ── Cold-Path Evaluation ────────────────────────────────────────────────────

View file

@ -192,6 +192,29 @@ TEST_CASE("external inputs resolve as graph leaves", "[ext_registry]")
REQUIRE(h.tick("a1", 1.5) == Approx(0.4)); 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") SECTION("invalid multi-channel selectors return the declared neutral")
{ {
REQUIRE(register_external_input( REQUIRE(register_external_input(