#ifndef SIGNAL_ENGINE_EXT_REGISTRY_H #define SIGNAL_ENGINE_EXT_REGISTRY_H #include "types.h" namespace sig { // ── Generic external registers (NISPS-USEQ spec §3) ───────────────────────── // // A firmware profile owns the names and metadata of everything that surrounds // the engine: named external inputs (producers stage latest-value state that // signal graphs read as leaves), named external sinks (live code binds // evaluated signals to transports), and discrete cold commands. The engine // provides only the generic registration and dispatch seam — no nn/* or // midi/* name is compiled in; those arrive via registration at firmware boot. // // Registration is profile state, not livecoding session state: it survives // (useq-clear) and is deliberately NOT touched by SignalEngine session resets. // reset_registry() exists for tests and explicit profile re-initialisation. // Registry capacities sized for the uSEQ+NISPS firmware profile plus headroom: // 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; // Width of the executor-side hw_inputs[] snapshot array (see // ExecutionContext::hw_inputs). Registered descriptors must address channels // inside this span. constexpr uint16_t MAX_HW_INPUT_CHANNELS = 32; // ── External input registers (spec §3.1/§3.2) ─────────────────────────────── struct ExternalInputDesc { const char* name; // static-lifetime spelling, e.g. "nn/out1" uint16_t hw_index; // first hw_inputs[] channel behind the name uint8_t channels; // consecutive channels the register spans float neutral; // profile-neutral value before first update const char* units_or_range; // static metadata for UI/diagnostics }; // ── External sinks (spec §3.3/§3.5/§7.4) ──────────────────────────────────── struct ExternalSinkDesc { const char* name; // static-lifetime spelling, e.g. "midi/cc74" uint8_t arity; // exact argument count of the sink form float min; // accepted range (deadband scaling + metadata) float max; uint32_t max_rate_hz; // profile transport ceiling; enforced firmware-side uint16_t quant_bits; // 0 = unquantised; else step = (max-min)/2^bits // Vector-packing sugar (NISPS-USEQ spec §4.3/§4.4). Both non-zero marks a // sink whose `arity` flat expressions may instead be written as one // [base × vec_base_ch] vector optionally followed by // `:offset [mod × vec_mod_ch]`, or as vec_base_ch channel entries that // are scalars (modulation 0) or [base modulation] pairs — cold_eval // expands these to the same per-channel graphs before publication. // Both zero = plain sink: exactly `arity` flat expressions, unchanged. // Registration validates: either both zero, or both > 0 and summing to // arity (vec_base_ch leading base channels, vec_mod_ch trailing mods). uint8_t vec_base_ch = 0; uint8_t vec_mod_ch = 0; }; struct ExternalCommandDesc { SymbolID cmd; uint8_t arity_min; uint8_t arity_max; }; // ── Registration / lookup ─────────────────────────────────────────────────── // Re-registering a name replaces its descriptor in place (idempotent profile // init). Return false on a full registry or an invalid descriptor. Lookup is // by SymbolID-interned name: registration interns desc->name, and the // tokenizer interns the same spelling to the same ID. bool register_external_input(const ExternalInputDesc& desc); bool register_external_sink(const ExternalSinkDesc& desc); bool register_external_command(SymbolID cmd, uint8_t arity_min, uint8_t arity_max); // Clears inputs, sinks, commands, and the cold-command handler. void reset_registry(); const ExternalInputDesc* find_external_input(SymbolID name); // Hardware-input channel for a registered name, or NODE_NONE. This is the // compiler-facing lookup used by GraphBuilder::resolve_hardware_input. uint16_t external_input_index_for(SymbolID name); const ExternalSinkDesc* find_external_sink(SymbolID name); bool external_sink_registered(SymbolID name); const ExternalCommandDesc* find_external_command(SymbolID cmd); // ── Sink bindings ─────────────────────────────────────────────────────────── // One entry per assigned external sink, owned by SignalEngine. Each channel's // compiled graph is published as a real pool output slot (indices // SINK_SLOT_BASE..MAX_OUTPUTS-1) so execution order, LKG fallback, and GC // reachability are the ordinary output machinery — no second mechanism. // 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; // with the §4.3/§4.4 sugar those arrive as vectors/pairs, still 16 graphs). constexpr uint8_t MAX_SINK_ARITY = 16; constexpr uint8_t MAX_SINK_BINDINGS = 16; // Pool output slots 0..23 are the named a/d/s outputs; sink channel graphs // 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; static_assert(SINK_SLOT_BASE < MAX_OUTPUTS, "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 { SymbolID sink = SymbolIntern::INVALID_ID; uint16_t value_index[MAX_SINK_ARITY] = {}; // pool output slot per channel uint8_t arity = 0; }; // ── Cold command handler (spec §2.3/§6.1 — generic hook) ──────────────────── // A top-level form whose head is a registered command symbol parses its // constant arguments into ColdArg values and calls the installed handler // after every earlier form in the same eval compiled successfully. The // handler must not re-enter the evaluator. constexpr uint8_t MAX_COLD_ARGS = 8; struct ColdArg { enum class Kind : uint8_t { Int, Number, Symbol, Vector }; static constexpr uint8_t MAX_VEC = MAX_SINK_ARITY; 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; ColdArg() : integer(0) {} union { float number; int32_t integer; SymbolID symbol; float vec[MAX_VEC]; }; }; using ColdCommandHandler = bool (*)(SymbolID cmd, const ColdArg* args, uint8_t nargs, void* user); void set_cold_command_handler(ColdCommandHandler handler, void* user); ColdCommandHandler cold_command_handler(); void* cold_command_user(); } // namespace sig #endif // SIGNAL_ENGINE_EXT_REGISTRY_H