feat(signal-engine): 16-channel sink arity, pool capacity 280, ColdArg::vec_len, Arduino-safe kTwoPi rename
This commit is contained in:
parent
db2031ae9e
commit
1b87f89121
13 changed files with 95 additions and 47 deletions
|
|
@ -160,7 +160,7 @@ static void publish_output_graph_plan(SignalEngine& engine,
|
||||||
const GraphBuildResult& result) {
|
const GraphBuildResult& result) {
|
||||||
engine.pool.outputs[output_index].root_node = result.root_node;
|
engine.pool.outputs[output_index].root_node = result.root_node;
|
||||||
engine.pool.outputs[output_index].valid = true;
|
engine.pool.outputs[output_index].valid = true;
|
||||||
engine.pool.runtime_fallback_mask &= ~((uint64_t)1 << output_index);
|
engine.pool.runtime_fallback[output_index] = 0;
|
||||||
engine.output_compile_diagnostics[output_index] =
|
engine.output_compile_diagnostics[output_index] =
|
||||||
ActiveCompileDiagnostic{};
|
ActiveCompileDiagnostic{};
|
||||||
engine.pool.output_deps[output_index].clear();
|
engine.pool.output_deps[output_index].clear();
|
||||||
|
|
@ -2231,8 +2231,7 @@ static EvalResult do_unassign(TokenStream& ts, SignalEngine& engine) {
|
||||||
engine.pool.prev_output_values[output_index] = 0.0;
|
engine.pool.prev_output_values[output_index] = 0.0;
|
||||||
engine.pool.output_deps[output_index].clear();
|
engine.pool.output_deps[output_index].clear();
|
||||||
engine.output_sources[output_index] = OutputSource{};
|
engine.output_sources[output_index] = OutputSource{};
|
||||||
engine.pool.runtime_fallback_mask &=
|
engine.pool.runtime_fallback[output_index] = 0;
|
||||||
~((uint64_t)1 << output_index);
|
|
||||||
engine.output_compile_diagnostics[output_index] =
|
engine.output_compile_diagnostics[output_index] =
|
||||||
ActiveCompileDiagnostic{};
|
ActiveCompileDiagnostic{};
|
||||||
|
|
||||||
|
|
@ -2362,7 +2361,7 @@ static void release_output_slot(SignalEngine& engine, uint16_t slot) {
|
||||||
engine.pool.outputs[slot] = OutputSlot{};
|
engine.pool.outputs[slot] = OutputSlot{};
|
||||||
engine.pool.prev_output_values[slot] = 0.0;
|
engine.pool.prev_output_values[slot] = 0.0;
|
||||||
engine.pool.output_deps[slot].clear();
|
engine.pool.output_deps[slot].clear();
|
||||||
engine.pool.runtime_fallback_mask &= ~((uint64_t)1 << slot);
|
engine.pool.runtime_fallback[slot] = 0;
|
||||||
engine.output_compile_diagnostics[slot] = ActiveCompileDiagnostic{};
|
engine.output_compile_diagnostics[slot] = ActiveCompileDiagnostic{};
|
||||||
engine.registry.begin_context(slot);
|
engine.registry.begin_context(slot);
|
||||||
engine.registry.commit_context(slot,
|
engine.registry.commit_context(slot,
|
||||||
|
|
@ -2550,10 +2549,11 @@ static EvalResult do_cold_command(const ExternalCommandDesc* desc,
|
||||||
if (!parse_numeric_vector(ts, values, count, error)) return error;
|
if (!parse_numeric_vector(ts, values, count, error)) return error;
|
||||||
if (count > ColdArg::MAX_VEC) {
|
if (count > ColdArg::MAX_VEC) {
|
||||||
return make_cold_arity_error(
|
return make_cold_arity_error(
|
||||||
"Vector command arguments support at most 8 values",
|
"Vector command arguments support at most 16 values",
|
||||||
"Shorten the vector");
|
"Shorten the vector");
|
||||||
}
|
}
|
||||||
arg.kind = ColdArg::Kind::Vector;
|
arg.kind = ColdArg::Kind::Vector;
|
||||||
|
arg.vec_len = (uint8_t)count;
|
||||||
for (uint16_t v = 0; v < count; v++)
|
for (uint16_t v = 0; v < count; v++)
|
||||||
arg.vec[v] = (float)values[v];
|
arg.vec[v] = (float)values[v];
|
||||||
} else {
|
} else {
|
||||||
|
|
|
||||||
|
|
@ -18,7 +18,7 @@ OutputHealth output_health(const NodePool& pool, uint16_t output_index) {
|
||||||
if (output_index >= MAX_OUTPUTS) return OutputHealth::Idle;
|
if (output_index >= MAX_OUTPUTS) return OutputHealth::Idle;
|
||||||
const OutputSlot& slot = pool.outputs[output_index];
|
const OutputSlot& slot = pool.outputs[output_index];
|
||||||
if (!slot.valid || slot.root_node == NODE_NONE) return OutputHealth::Idle;
|
if (!slot.valid || slot.root_node == NODE_NONE) return OutputHealth::Idle;
|
||||||
if (((pool.runtime_fallback_mask >> output_index) & 1u) == 0)
|
if (!pool.runtime_fallback_bit(output_index))
|
||||||
return OutputHealth::Running;
|
return OutputHealth::Running;
|
||||||
return slot.has_lkg ? OutputHealth::Fallback : OutputHealth::Error;
|
return slot.has_lkg ? OutputHealth::Fallback : OutputHealth::Error;
|
||||||
}
|
}
|
||||||
|
|
@ -176,9 +176,10 @@ void execute_all_outputs(const NodePool& pool, ExecutionContext& ctx) {
|
||||||
ctx.workspace[idx] = result;
|
ctx.workspace[idx] = result;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Read output values with LKG fallback
|
// Read output values with LKG fallback. runtime_fallback[] is recomputed
|
||||||
uint64_t fallback_mask = 0;
|
// wholesale on every pass.
|
||||||
for (uint16_t i = 0; i < MAX_OUTPUTS; i++) {
|
for (uint16_t i = 0; i < MAX_OUTPUTS; i++) {
|
||||||
|
pool.runtime_fallback[i] = 0;
|
||||||
if (pool.outputs[i].root_node != NODE_NONE) {
|
if (pool.outputs[i].root_node != NODE_NONE) {
|
||||||
Sample v = ctx.workspace[pool.outputs[i].root_node];
|
Sample v = ctx.workspace[pool.outputs[i].root_node];
|
||||||
if (g_failure_mode == FailureMode::LkgFallback &&
|
if (g_failure_mode == FailureMode::LkgFallback &&
|
||||||
|
|
@ -187,7 +188,7 @@ void execute_all_outputs(const NodePool& pool, ExecutionContext& ctx) {
|
||||||
// value (or the neutral default when no LKG exists —
|
// value (or the neutral default when no LKG exists —
|
||||||
// failure-model.md §2.4) and record the fallback.
|
// failure-model.md §2.4) and record the fallback.
|
||||||
v = pool.outputs[i].has_lkg ? pool.outputs[i].lkg_value : 0.0;
|
v = pool.outputs[i].has_lkg ? pool.outputs[i].lkg_value : 0.0;
|
||||||
fallback_mask |= (uint64_t)1 << i;
|
pool.runtime_fallback[i] = 1;
|
||||||
}
|
}
|
||||||
ctx.output_values[i] = v;
|
ctx.output_values[i] = v;
|
||||||
} else if (pool.outputs[i].valid) {
|
} else if (pool.outputs[i].valid) {
|
||||||
|
|
@ -199,7 +200,6 @@ void execute_all_outputs(const NodePool& pool, ExecutionContext& ctx) {
|
||||||
ctx.output_values[i] = 0.0;
|
ctx.output_values[i] = 0.0;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
pool.runtime_fallback_mask = fallback_mask;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// ── External Sink Publication ───────────────────────────────────────────────
|
// ── External Sink Publication ───────────────────────────────────────────────
|
||||||
|
|
@ -237,8 +237,7 @@ void publish_sink_values(SignalEngine& engine, const Sample* output_values) {
|
||||||
void commit_outputs(NodePool& pool, const Sample* output_values) {
|
void commit_outputs(NodePool& pool, const Sample* output_values) {
|
||||||
for (uint16_t i = 0; i < MAX_OUTPUTS; i++) {
|
for (uint16_t i = 0; i < MAX_OUTPUTS; i++) {
|
||||||
pool.prev_output_values[i] = output_values[i];
|
pool.prev_output_values[i] = output_values[i];
|
||||||
bool substituted =
|
bool substituted = pool.runtime_fallback_bit(i);
|
||||||
((pool.runtime_fallback_mask >> i) & 1u) != 0;
|
|
||||||
if (pool.outputs[i].root_node != NODE_NONE && !substituted &&
|
if (pool.outputs[i].root_node != NODE_NONE && !substituted &&
|
||||||
std::isfinite(output_values[i])) {
|
std::isfinite(output_values[i])) {
|
||||||
pool.outputs[i].lkg_value = output_values[i];
|
pool.outputs[i].lkg_value = output_values[i];
|
||||||
|
|
@ -257,7 +256,7 @@ void commit_state(NodePool& pool, const Sample* workspace,
|
||||||
if (pool.state_update_roots[s] != NODE_NONE) {
|
if (pool.state_update_roots[s] != NODE_NONE) {
|
||||||
uint16_t owner = pool.state_owner_context[s];
|
uint16_t owner = pool.state_owner_context[s];
|
||||||
if (owner < MAX_OUTPUTS &&
|
if (owner < MAX_OUTPUTS &&
|
||||||
((pool.runtime_fallback_mask >> owner) & 1u) != 0) {
|
pool.runtime_fallback_bit(owner)) {
|
||||||
// Stateful nodes owned by an output advance only when that
|
// Stateful nodes owned by an output advance only when that
|
||||||
// output publishes a healthy sample. Otherwise an oscillator
|
// output publishes a healthy sample. Otherwise an oscillator
|
||||||
// or integrator could run invisibly behind scalar fallback,
|
// or integrator could run invisibly behind scalar fallback,
|
||||||
|
|
@ -311,7 +310,7 @@ void execute_batch(
|
||||||
|
|
||||||
const size_t CHUNK = pool.batch_chunk_size;
|
const size_t CHUNK = pool.batch_chunk_size;
|
||||||
Sample* regs = pool.batch_workspace.get();
|
Sample* regs = pool.batch_workspace.get();
|
||||||
uint64_t fallback_mask = 0;
|
memset(pool.runtime_fallback, 0, sizeof(pool.runtime_fallback));
|
||||||
|
|
||||||
for (size_t chunk_start = 0; chunk_start < sample_count; chunk_start += CHUNK) {
|
for (size_t chunk_start = 0; chunk_start < sample_count; chunk_start += CHUNK) {
|
||||||
size_t chunk_size = std::min(CHUNK, sample_count - chunk_start);
|
size_t chunk_size = std::min(CHUNK, sample_count - chunk_start);
|
||||||
|
|
@ -385,7 +384,7 @@ void execute_batch(
|
||||||
Sample v = src[s];
|
Sample v = src[s];
|
||||||
if (!std::isfinite(v)) {
|
if (!std::isfinite(v)) {
|
||||||
v = lkg;
|
v = lkg;
|
||||||
fallback_mask |= (uint64_t)1 << o;
|
pool.runtime_fallback[o] = 1;
|
||||||
}
|
}
|
||||||
dst[s] = v;
|
dst[s] = v;
|
||||||
}
|
}
|
||||||
|
|
@ -400,7 +399,6 @@ void execute_batch(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
pool.runtime_fallback_mask = fallback_mask;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// ── Output Classification ───────────────────────────────────────────────────
|
// ── Output Classification ───────────────────────────────────────────────────
|
||||||
|
|
|
||||||
|
|
@ -12,7 +12,7 @@ namespace sig {
|
||||||
// LkgFallback (default, spec-mandated): values propagate freely through
|
// LkgFallback (default, spec-mandated): values propagate freely through
|
||||||
// the node graph; a non-finite value reaching an OUTPUT ROOT makes that
|
// the node graph; a non-finite value reaching an OUTPUT ROOT makes that
|
||||||
// output substitute its last-known-good value (or 0 if none) and marks
|
// output substitute its last-known-good value (or 0 if none) and marks
|
||||||
// it as being in fallback (see NodePool::runtime_fallback_mask).
|
// it as being in fallback (see NodePool::runtime_fallback).
|
||||||
// ZeroSquash (legacy): every node's result is clamped to 0.0 when
|
// ZeroSquash (legacy): every node's result is clamped to 0.0 when
|
||||||
// non-finite. No fallback, no diagnostic — pre-v1.2 behaviour.
|
// non-finite. No fallback, no diagnostic — pre-v1.2 behaviour.
|
||||||
//
|
//
|
||||||
|
|
|
||||||
|
|
@ -18,8 +18,12 @@ namespace sig {
|
||||||
// (useq-clear) and is deliberately NOT touched by SignalEngine session resets.
|
// (useq-clear) and is deliberately NOT touched by SignalEngine session resets.
|
||||||
// reset_registry() exists for tests and explicit profile re-initialisation.
|
// reset_registry() exists for tests and explicit profile re-initialisation.
|
||||||
|
|
||||||
constexpr uint8_t MAX_EXTERNAL_INPUTS = 24;
|
// Registry capacities sized for the uSEQ+NISPS firmware profile plus headroom:
|
||||||
constexpr uint8_t MAX_EXTERNAL_SINKS = 16;
|
// 19 meml/* control registers + 8 nn/out* neural outputs = 27 inputs (cap 32,
|
||||||
|
// aligned with MAX_HW_INPUT_CHANNELS); 32 midi/cc* transports + nn/in = 33
|
||||||
|
// sinks (cap 40); 14 nn/* commands (cap 16).
|
||||||
|
constexpr uint8_t MAX_EXTERNAL_INPUTS = 32;
|
||||||
|
constexpr uint8_t MAX_EXTERNAL_SINKS = 40;
|
||||||
constexpr uint8_t MAX_EXTERNAL_COMMANDS = 16;
|
constexpr uint8_t MAX_EXTERNAL_COMMANDS = 16;
|
||||||
|
|
||||||
// Width of the executor-side hw_inputs[] snapshot array (see
|
// Width of the executor-side hw_inputs[] snapshot array (see
|
||||||
|
|
@ -83,14 +87,20 @@ const ExternalCommandDesc* find_external_command(SymbolID cmd);
|
||||||
// SINK_SLOT_BASE..MAX_OUTPUTS-1) so execution order, LKG fallback, and GC
|
// SINK_SLOT_BASE..MAX_OUTPUTS-1) so execution order, LKG fallback, and GC
|
||||||
// reachability are the ordinary output machinery — no second mechanism.
|
// reachability are the ordinary output machinery — no second mechanism.
|
||||||
|
|
||||||
constexpr uint8_t MAX_SINK_ARITY = 8;
|
// 16 channel expressions per binding: the nn/in neural-input sink binds
|
||||||
|
// 8 base + 8 modulation expressions in one form (NISPS-USEQ spec §4.2/§4.3).
|
||||||
|
constexpr uint8_t MAX_SINK_ARITY = 16;
|
||||||
constexpr uint8_t MAX_SINK_BINDINGS = 16;
|
constexpr uint8_t MAX_SINK_BINDINGS = 16;
|
||||||
|
|
||||||
// Pool output slots 0..23 are the named a/d/s outputs; sink channel graphs
|
// Pool output slots 0..23 are the named a/d/s outputs; sink channel graphs
|
||||||
// occupy the remainder.
|
// occupy the remainder. The pool must hold every binding row at full arity
|
||||||
|
// simultaneously: 24 + 16 × 16 = 280 = MAX_OUTPUTS.
|
||||||
constexpr uint16_t SINK_SLOT_BASE = 24;
|
constexpr uint16_t SINK_SLOT_BASE = 24;
|
||||||
static_assert(SINK_SLOT_BASE < MAX_OUTPUTS,
|
static_assert(SINK_SLOT_BASE < MAX_OUTPUTS,
|
||||||
"sink channel slots must fit inside the pool output table");
|
"sink channel slots must fit inside the pool output table");
|
||||||
|
static_assert(SINK_SLOT_BASE + (uint32_t)MAX_SINK_BINDINGS * MAX_SINK_ARITY
|
||||||
|
<= MAX_OUTPUTS,
|
||||||
|
"pool outputs must fit every sink binding at full arity");
|
||||||
|
|
||||||
struct SinkBinding {
|
struct SinkBinding {
|
||||||
SymbolID sink = SymbolIntern::INVALID_ID;
|
SymbolID sink = SymbolIntern::INVALID_ID;
|
||||||
|
|
@ -111,10 +121,12 @@ struct ColdArg {
|
||||||
static constexpr uint8_t MAX_VEC = MAX_SINK_ARITY;
|
static constexpr uint8_t MAX_VEC = MAX_SINK_ARITY;
|
||||||
|
|
||||||
Kind kind = Kind::Int;
|
Kind kind = Kind::Int;
|
||||||
|
// Number of entries in vec[] when kind == Vector (0..MAX_VEC). Without
|
||||||
|
// it a handler could not validate an exact-width vector argument such as
|
||||||
|
// nn/add-example's Vector[8] pair, nor tell a short vector from padding.
|
||||||
|
uint8_t vec_len = 0;
|
||||||
|
|
||||||
// Number tokens without a fractional part that fit in int32 parse as
|
ColdArg() : integer(0) {}
|
||||||
// Int; everything numeric else is Number. Names parse as their interned
|
|
||||||
// SymbolID; bracketed numeric literals parse as Vector.
|
|
||||||
union {
|
union {
|
||||||
float number;
|
float number;
|
||||||
int32_t integer;
|
int32_t integer;
|
||||||
|
|
@ -122,7 +134,6 @@ struct ColdArg {
|
||||||
float vec[MAX_VEC];
|
float vec[MAX_VEC];
|
||||||
};
|
};
|
||||||
|
|
||||||
ColdArg() : integer(0) {}
|
|
||||||
};
|
};
|
||||||
|
|
||||||
using ColdCommandHandler = bool (*)(SymbolID cmd, const ColdArg* args,
|
using ColdCommandHandler = bool (*)(SymbolID cmd, const ColdArg* args,
|
||||||
|
|
|
||||||
|
|
@ -1076,7 +1076,7 @@ uint16_t GraphBuilder::compile_waveform(
|
||||||
"Remove the extra arguments");
|
"Remove the extra arguments");
|
||||||
}
|
}
|
||||||
|
|
||||||
constexpr Sample TWO_PI = 6.28318530717958647692;
|
constexpr Sample kTwoPi = 6.28318530717958647692;
|
||||||
const bool is_trig = waveform == sym.sin_ || waveform == sym.cos_ ||
|
const bool is_trig = waveform == sym.sin_ || waveform == sym.cos_ ||
|
||||||
waveform == sym.bsin || waveform == sym.bcos ||
|
waveform == sym.bsin || waveform == sym.bcos ||
|
||||||
waveform == sym.tan_;
|
waveform == sym.tan_;
|
||||||
|
|
@ -1088,11 +1088,11 @@ uint16_t GraphBuilder::compile_waveform(
|
||||||
if (name_space != OperatorNamespace::Radians &&
|
if (name_space != OperatorNamespace::Radians &&
|
||||||
name_space != OperatorNamespace::Raw) {
|
name_space != OperatorNamespace::Raw) {
|
||||||
phase_or_angle = pool.make_binop(
|
phase_or_angle = pool.make_binop(
|
||||||
NodeOp::Mul, input, pool.make_const(TWO_PI));
|
NodeOp::Mul, input, pool.make_const(kTwoPi));
|
||||||
}
|
}
|
||||||
} else if (name_space == OperatorNamespace::Radians) {
|
} else if (name_space == OperatorNamespace::Radians) {
|
||||||
phase_or_angle = pool.make_binop(
|
phase_or_angle = pool.make_binop(
|
||||||
NodeOp::Div, input, pool.make_const(TWO_PI));
|
NodeOp::Div, input, pool.make_const(kTwoPi));
|
||||||
}
|
}
|
||||||
|
|
||||||
uint16_t result = NODE_NONE;
|
uint16_t result = NODE_NONE;
|
||||||
|
|
|
||||||
|
|
@ -487,7 +487,7 @@ void NodePool::reset() {
|
||||||
memset(output_class, 0, sizeof(output_class));
|
memset(output_class, 0, sizeof(output_class));
|
||||||
memset(output_input_mask, 0, sizeof(output_input_mask));
|
memset(output_input_mask, 0, sizeof(output_input_mask));
|
||||||
memset(prev_output_values, 0, sizeof(prev_output_values));
|
memset(prev_output_values, 0, sizeof(prev_output_values));
|
||||||
runtime_fallback_mask = 0;
|
memset(runtime_fallback, 0, sizeof(runtime_fallback));
|
||||||
state_update_failure_mask = 0;
|
state_update_failure_mask = 0;
|
||||||
memset(state_values, 0, sizeof(state_values));
|
memset(state_values, 0, sizeof(state_values));
|
||||||
for (uint16_t s = 0; s < MAX_STATE_SLOTS; s++) {
|
for (uint16_t s = 0; s < MAX_STATE_SLOTS; s++) {
|
||||||
|
|
|
||||||
|
|
@ -145,13 +145,18 @@ struct NodePool {
|
||||||
// Cross-output reads use previous-tick values
|
// Cross-output reads use previous-tick values
|
||||||
Sample prev_output_values[MAX_OUTPUTS] = {};
|
Sample prev_output_values[MAX_OUTPUTS] = {};
|
||||||
|
|
||||||
// Runtime fallback tracking (failure-model.md §2.1/§5): bit i is set when
|
// Runtime fallback tracking (failure-model.md §2.1/§5): entry i is set
|
||||||
// output i substituted its LKG value on the most recent execution pass
|
// when output i substituted its LKG value on the most recent execution
|
||||||
// because a non-finite value reached its root (FailureMode::LkgFallback
|
// pass because a non-finite value reached its root (FailureMode::LkgFallback
|
||||||
// only). Recomputed on every pass; mutable because execution paths take
|
// only). Recomputed on every pass; mutable because execution paths take
|
||||||
// `const NodePool&` — this is diagnostic bookkeeping, not graph state.
|
// `const NodePool&` — this is diagnostic bookkeeping, not graph state.
|
||||||
mutable uint64_t runtime_fallback_mask = 0;
|
// One byte per output rather than a uint64_t bitmask: the pool output
|
||||||
static_assert(MAX_OUTPUTS <= 64, "runtime_fallback_mask is 64-bit");
|
// table grew past 64 entries when sink channel slots were sized for
|
||||||
|
// 16 bindings × 16 channels (ext_registry.h).
|
||||||
|
mutable uint8_t runtime_fallback[MAX_OUTPUTS] = {};
|
||||||
|
bool runtime_fallback_bit(uint16_t output_index) const {
|
||||||
|
return output_index < MAX_OUTPUTS && runtime_fallback[output_index] != 0;
|
||||||
|
}
|
||||||
|
|
||||||
// Bit s is active when state slot s most recently produced a non-finite
|
// Bit s is active when state slot s most recently produced a non-finite
|
||||||
// update candidate. The previous finite state remains installed until
|
// update candidate. The previous finite state remains installed until
|
||||||
|
|
|
||||||
|
|
@ -67,7 +67,13 @@ constexpr size_t MAX_STATE_SLOTS = 32;
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
constexpr size_t MAX_CALLABLE_PARAMS = 8;
|
constexpr size_t MAX_CALLABLE_PARAMS = 8;
|
||||||
constexpr size_t MAX_OUTPUTS = 42;
|
// Named a/d/s outputs occupy 0..SINK_SLOT_BASE-1; bound external-sink channel
|
||||||
|
// graphs take pool slots above that (ext_registry.h). Sized so every binding
|
||||||
|
// row can hold a full-arity sink at once: SINK_SLOT_BASE(24) +
|
||||||
|
// MAX_SINK_BINDINGS(16) × MAX_SINK_ARITY(16) = 280 (static_assert beside the
|
||||||
|
// constants in ext_registry.h). The nn/in neural-input sink needs 16 channels
|
||||||
|
// (8 bases + 8 modulations, NISPS-USEQ spec §4.2/§4.3).
|
||||||
|
constexpr size_t MAX_OUTPUTS = 280;
|
||||||
constexpr size_t MAX_SCOPE_DEPTH = 32;
|
constexpr size_t MAX_SCOPE_DEPTH = 32;
|
||||||
constexpr size_t MAX_LOCAL_BINDINGS = 32;
|
constexpr size_t MAX_LOCAL_BINDINGS = 32;
|
||||||
constexpr size_t MAX_DIAGNOSTICS = 16;
|
constexpr size_t MAX_DIAGNOSTICS = 16;
|
||||||
|
|
|
||||||
|
|
@ -259,6 +259,35 @@ TEST_CASE("external sinks publish evaluated values", "[ext_registry]")
|
||||||
REQUIRE(h.engine.sink_values[0][2] == Approx(0.6));
|
REQUIRE(h.engine.sink_values[0][2] == Approx(0.6));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
SECTION("full 16-channel sink binds and publishes every channel")
|
||||||
|
{
|
||||||
|
// The nn/in neural-input shape (NISPS-USEQ spec §4.2/§4.3): 8 base +
|
||||||
|
// 8 modulation expressions in one binding. MAX_SINK_ARITY and the
|
||||||
|
// pool arithmetic in ext_registry.h are sized for this.
|
||||||
|
REQUIRE(register_external_sink({"nn/in", 16, 0.0f, 1.0f, 200, 7}));
|
||||||
|
h.eval_ok("(nn/in 0.00 0.05 0.10 0.15 0.20 0.25 0.30 0.35 "
|
||||||
|
"0.5 0.4 0.3 0.2 0.1 0.0 -0.1 -0.2)");
|
||||||
|
h.tick_sinks(0.0);
|
||||||
|
const SinkBinding* binding = h.binding_for("nn/in");
|
||||||
|
REQUIRE(binding != nullptr);
|
||||||
|
REQUIRE(binding->arity == 16);
|
||||||
|
for (uint8_t ch = 0; ch < 16; ch++) {
|
||||||
|
const double expected = ch < 8 ? 0.05 * ch : 0.5 - 0.1 * (ch - 8);
|
||||||
|
REQUIRE(h.engine.sink_values[0][ch] == Approx(expected));
|
||||||
|
}
|
||||||
|
// Slot plan: 16 pool output slots at/above SINK_SLOT_BASE.
|
||||||
|
for (uint8_t ch = 0; ch < 16; ch++) {
|
||||||
|
REQUIRE(binding->value_index[ch] >= SINK_SLOT_BASE);
|
||||||
|
REQUIRE(binding->value_index[ch] < MAX_OUTPUTS);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Rebinding swaps all 16 channels as one transaction.
|
||||||
|
h.eval_ok("(nn/in 1 1 1 1 1 1 1 1 0 0 0 0 0 0 0 0)");
|
||||||
|
h.tick_sinks(1.0);
|
||||||
|
REQUIRE(h.engine.sink_values[0][0] == Approx(1.0));
|
||||||
|
REQUIRE(h.engine.sink_values[0][15] == Approx(0.0));
|
||||||
|
}
|
||||||
|
|
||||||
SECTION("wrong arity is a compile error and binds nothing")
|
SECTION("wrong arity is a compile error and binds nothing")
|
||||||
{
|
{
|
||||||
h.expect_error("(midi/cc74 0.1 0.2)", DiagnosticCategory::Arity);
|
h.expect_error("(midi/cc74 0.1 0.2)", DiagnosticCategory::Arity);
|
||||||
|
|
@ -338,7 +367,6 @@ TEST_CASE("registered cold commands dispatch to the handler", "[ext_registry]")
|
||||||
REQUIRE(register_external_command(internSymbol("meml/cmd"), 1, 2));
|
REQUIRE(register_external_command(internSymbol("meml/cmd"), 1, 2));
|
||||||
set_cold_command_handler(&test_command_handler, &cookie);
|
set_cold_command_handler(&test_command_handler, &cookie);
|
||||||
const SymbolID cmd_id = internSymbol("meml/cmd");
|
const SymbolID cmd_id = internSymbol("meml/cmd");
|
||||||
|
|
||||||
SECTION("integer and float arguments parse by shape")
|
SECTION("integer and float arguments parse by shape")
|
||||||
{
|
{
|
||||||
h.eval_ok("(meml/cmd 3)");
|
h.eval_ok("(meml/cmd 3)");
|
||||||
|
|
@ -359,8 +387,8 @@ TEST_CASE("registered cold commands dispatch to the handler", "[ext_registry]")
|
||||||
h.eval_ok("(meml/cmd level [1 2 3])");
|
h.eval_ok("(meml/cmd level [1 2 3])");
|
||||||
REQUIRE(g_capture.nargs == 2);
|
REQUIRE(g_capture.nargs == 2);
|
||||||
REQUIRE(g_capture.args[0].kind == ColdArg::Kind::Symbol);
|
REQUIRE(g_capture.args[0].kind == ColdArg::Kind::Symbol);
|
||||||
REQUIRE(g_capture.args[0].symbol == internSymbol("level"));
|
|
||||||
REQUIRE(g_capture.args[1].kind == ColdArg::Kind::Vector);
|
REQUIRE(g_capture.args[1].kind == ColdArg::Kind::Vector);
|
||||||
|
REQUIRE(g_capture.args[1].vec_len == 3);
|
||||||
REQUIRE(g_capture.args[1].vec[0] == Approx(1.0f));
|
REQUIRE(g_capture.args[1].vec[0] == Approx(1.0f));
|
||||||
REQUIRE(g_capture.args[1].vec[1] == Approx(2.0f));
|
REQUIRE(g_capture.args[1].vec[1] == Approx(2.0f));
|
||||||
REQUIRE(g_capture.args[1].vec[2] == Approx(3.0f));
|
REQUIRE(g_capture.args[1].vec[2] == Approx(3.0f));
|
||||||
|
|
|
||||||
|
|
@ -2,7 +2,7 @@
|
||||||
//
|
//
|
||||||
// Mode A (FailureMode::LkgFallback, DEFAULT): a non-finite value reaching an
|
// Mode A (FailureMode::LkgFallback, DEFAULT): a non-finite value reaching an
|
||||||
// output root substitutes the last-known-good value (or 0 with no LKG),
|
// output root substitutes the last-known-good value (or 0 with no LKG),
|
||||||
// sets the pool's runtime_fallback_mask bit, and never zeroes per node.
|
// sets the pool's runtime_fallback[] entry, and never zeroes per node.
|
||||||
// Mode B (FailureMode::ZeroSquash, legacy): every non-finite node result is
|
// Mode B (FailureMode::ZeroSquash, legacy): every non-finite node result is
|
||||||
// clamped to 0.0; no fallback, no diagnostic.
|
// clamped to 0.0; no fallback, no diagnostic.
|
||||||
|
|
||||||
|
|
@ -71,7 +71,7 @@ struct Harness {
|
||||||
}
|
}
|
||||||
|
|
||||||
bool in_fallback(int output_index) const {
|
bool in_fallback(int output_index) const {
|
||||||
return (engine.pool.runtime_fallback_mask >> output_index) & 1;
|
return engine.pool.runtime_fallback_bit(output_index);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -85,7 +85,7 @@ TEST_CASE("First failure has Error health until a finite root establishes LKG",
|
||||||
// immediately while retaining its finite LKG as a safety net.
|
// immediately while retaining its finite LKG as a safety net.
|
||||||
h.eval_ok("(a1 7)");
|
h.eval_ok("(a1 7)");
|
||||||
REQUIRE(output_health(h.engine.pool, 0) == OutputHealth::Running);
|
REQUIRE(output_health(h.engine.pool, 0) == OutputHealth::Running);
|
||||||
REQUIRE((h.engine.pool.runtime_fallback_mask & 1u) == 0);
|
REQUIRE_FALSE(h.engine.pool.runtime_fallback_bit(0));
|
||||||
REQUIRE(h.engine.pool.outputs[0].has_lkg);
|
REQUIRE(h.engine.pool.outputs[0].has_lkg);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -181,14 +181,14 @@ TEST_CASE("Unassign clears runtime and reactive health with the program",
|
||||||
h.eval_ok("(define health-unassign-dep 1)");
|
h.eval_ok("(define health-unassign-dep 1)");
|
||||||
h.eval_ok("(a1 (* health-unassign-dep (* t 1e308)))");
|
h.eval_ok("(a1 (* health-unassign-dep (* t 1e308)))");
|
||||||
REQUIRE(h.tick(0, 2.0) == Approx(0.0));
|
REQUIRE(h.tick(0, 2.0) == Approx(0.0));
|
||||||
REQUIRE((h.engine.pool.runtime_fallback_mask & 1u) != 0);
|
REQUIRE(h.engine.pool.runtime_fallback_bit(0));
|
||||||
|
|
||||||
h.eval_ok("(defn health-unassign-dep [x] x)");
|
h.eval_ok("(defn health-unassign-dep [x] x)");
|
||||||
REQUIRE(h.engine.output_compile_diagnostics[0].active);
|
REQUIRE(h.engine.output_compile_diagnostics[0].active);
|
||||||
|
|
||||||
h.eval_ok("(unassign a1)");
|
h.eval_ok("(unassign a1)");
|
||||||
REQUIRE(output_health(h.engine.pool, 0) == OutputHealth::Idle);
|
REQUIRE(output_health(h.engine.pool, 0) == OutputHealth::Idle);
|
||||||
REQUIRE((h.engine.pool.runtime_fallback_mask & 1u) == 0);
|
REQUIRE_FALSE(h.engine.pool.runtime_fallback_bit(0));
|
||||||
REQUIRE_FALSE(h.engine.output_compile_diagnostics[0].active);
|
REQUIRE_FALSE(h.engine.output_compile_diagnostics[0].active);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -308,7 +308,7 @@ TEST_CASE("Phase 4: non-finite value handling", "[phase4][numerical]") {
|
||||||
double val = h.sample("a1", 0.0);
|
double val = h.sample("a1", 0.0);
|
||||||
REQUIRE(std::isfinite(val));
|
REQUIRE(std::isfinite(val));
|
||||||
REQUIRE(val == Approx(0.0));
|
REQUIRE(val == Approx(0.0));
|
||||||
REQUIRE((h.engine.pool.runtime_fallback_mask & 1u) != 0);
|
REQUIRE(h.engine.pool.runtime_fallback_bit(0));
|
||||||
}
|
}
|
||||||
|
|
||||||
SECTION("legitimate large values stay finite") {
|
SECTION("legitimate large values stay finite") {
|
||||||
|
|
@ -342,7 +342,7 @@ TEST_CASE("Phase 4: non-finite value handling", "[phase4][numerical]") {
|
||||||
double val = h.sample("a1", 0.0);
|
double val = h.sample("a1", 0.0);
|
||||||
REQUIRE(std::isfinite(val));
|
REQUIRE(std::isfinite(val));
|
||||||
REQUIRE(val == Approx(0.0));
|
REQUIRE(val == Approx(0.0));
|
||||||
REQUIRE((h.engine.pool.runtime_fallback_mask & 1u) != 0);
|
REQUIRE(h.engine.pool.runtime_fallback_bit(0));
|
||||||
}
|
}
|
||||||
|
|
||||||
SECTION("chained operations producing intermediate infinities stay finite") {
|
SECTION("chained operations producing intermediate infinities stay finite") {
|
||||||
|
|
@ -351,7 +351,7 @@ TEST_CASE("Phase 4: non-finite value handling", "[phase4][numerical]") {
|
||||||
h.assign_ok("a1", "(* (/ 1 0) 5)");
|
h.assign_ok("a1", "(* (/ 1 0) 5)");
|
||||||
double val = h.sample("a1", 0.0);
|
double val = h.sample("a1", 0.0);
|
||||||
REQUIRE(std::isfinite(val));
|
REQUIRE(std::isfinite(val));
|
||||||
REQUIRE((h.engine.pool.runtime_fallback_mask & 1u) != 0);
|
REQUIRE(h.engine.pool.runtime_fallback_bit(0));
|
||||||
}
|
}
|
||||||
|
|
||||||
SECTION("NaN guard on executor output") {
|
SECTION("NaN guard on executor output") {
|
||||||
|
|
|
||||||
|
|
@ -906,7 +906,7 @@ TEST_CASE("Arithmetic edge cases produce finite outputs", "[robustness][edge]")
|
||||||
double v = h.sample("a1", 0.0);
|
double v = h.sample("a1", 0.0);
|
||||||
REQUIRE(std::isfinite(v));
|
REQUIRE(std::isfinite(v));
|
||||||
REQUIRE(v == Approx(0.0));
|
REQUIRE(v == Approx(0.0));
|
||||||
REQUIRE((h.engine.pool.runtime_fallback_mask & 1u) != 0);
|
REQUIRE(h.engine.pool.runtime_fallback_bit(0));
|
||||||
}
|
}
|
||||||
|
|
||||||
SECTION("Modulo by zero activates bootstrap LKG")
|
SECTION("Modulo by zero activates bootstrap LKG")
|
||||||
|
|
@ -916,7 +916,7 @@ TEST_CASE("Arithmetic edge cases produce finite outputs", "[robustness][edge]")
|
||||||
double v = h.sample("a1", 0.0);
|
double v = h.sample("a1", 0.0);
|
||||||
REQUIRE(std::isfinite(v));
|
REQUIRE(std::isfinite(v));
|
||||||
REQUIRE(v == Approx(0.0));
|
REQUIRE(v == Approx(0.0));
|
||||||
REQUIRE((h.engine.pool.runtime_fallback_mask & 1u) != 0);
|
REQUIRE(h.engine.pool.runtime_fallback_bit(0));
|
||||||
}
|
}
|
||||||
|
|
||||||
SECTION("sqrt of negative is finite")
|
SECTION("sqrt of negative is finite")
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue