ModuLisp/src/signal_engine/ext_registry.h

161 lines
7.5 KiB
C++
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

#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