ModuLisp/src/signal_engine/ext_registry.h

149 lines
6.7 KiB
C
Raw Normal View History

#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
};
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).
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