rill-core architecture

The rill-core crate is the foundation of the Rill ecosystem — traits, math, buffers, queues, time, and error types.

Core traits

Node

Base trait for all signal graph nodes. No Send or Sync bounds — nodes live on the signal thread exclusively.

#![allow(unused)]
fn main() {
pub trait Node<T: Transcendental, const BUF_SIZE: usize> {
    fn metadata(&self) -> NodeMetadata;
    fn init(&mut self, sample_rate: f32);
    fn reset(&mut self);
    fn id(&self) -> NodeId;
    fn set_id(&mut self, id: NodeId);
    fn get_parameter(&self, id: &ParameterId) -> Option<ParamValue>;
    fn set_parameter(&mut self, id: &ParameterId, value: ParamValue) -> ProcessResult<()>;
}
}

ParameterWrite

ParameterWrite is the polymorphic control interface for DSP engines. It decouples parameter dispatch from the concrete engine type:

#![allow(unused)]
fn main() {
pub trait ParameterWrite {
    fn write_parameter(&mut self, name: &str, value: ParamValue) -> ProcessResult<()>;
    fn read_parameter(&self, name: &str) -> Option<ParamValue> { None }
}
}

Implemented by BasicOscillator<f32>, Ay38910Chip, and any engine that accepts named parameter writes.

MultichannelAlgorithm

Multi-IO signal processing trait (N inputs, M outputs). Unlike Algorithm<T> which is strictly single-input/single-output (SISO), this trait supports N-to-M channel processing in a single call. Used by mixer, EQ, and multi-IO graph engines.

#![allow(unused)]
fn main() {
pub trait MultichannelAlgorithm<T: Transcendental>: Send {
    fn num_inputs(&self) -> usize;
    fn num_outputs(&self) -> usize;
    fn process(&mut self, inputs: &[&[T]], outputs: &mut [&mut [T]]) -> ProcessResult<()>;
    fn reset(&mut self);
}
}

Implemented by CompiledGraphEngine<T, BUF> (with router feature) and by SisoAdapter<A, T> (wraps any Algorithm<T> as a 1-in/1-out MultichannelAlgorithm).

File: rill-core/src/traits/multichannel_algorithm.rs.

BridgeAlgorithm

Bridge backend trait for duplex execution boundaries. A bridge node splits the signal graph into left (recording) and right (playback) sub-graphs.

#![allow(unused)]
fn main() {
pub trait BridgeAlgorithm<T: Transcendental>: Send + Sync {
    fn num_inputs(&self) -> usize;
    fn num_outputs(&self) -> usize;
    fn process_left(&mut self, inputs: &[&[T]]) -> ProcessResult<()>;
    fn process_right(&mut self, outputs: &mut [&mut [T]]) -> ProcessResult<()>;
    fn reset(&mut self);
}
}

The engine runs a 5-phase tick: ReadFeedback, process_left (left sub-graph + bridge.process_left), process_right (bridge.process_right + right sub-graph), WriteFeedback, shadow copy.

File: rill-core/src/traits/bridge.rs.

Source, Processor, Sink

#![allow(unused)]
fn main() {
pub trait Source<T: Transcendental, const BUF_SIZE: usize>: Node<T, BUF_SIZE> {
    fn generate(&mut self, clock: &ClockTick, ctrl: &[T], clk: &[ClockTick]) -> ProcessResult<()>;
}

pub trait Processor<T: Transcendental, const BUF_SIZE: usize>: Node<T, BUF_SIZE> {
    fn process(&mut self, clock: &ClockTick, signal: &[&[T; BUF_SIZE]], ...) -> ProcessResult<()>;
}

pub trait Sink<T: Transcendental, const BUF_SIZE: usize>: Node<T, BUF_SIZE> {
    fn consume(&mut self, clock: &ClockTick, signal: &[&[T; BUF_SIZE]], ...) -> ProcessResult<()>;
}
}

IoDriver, IoCapture, IoPlayback

Backends are created externally by the orchestrator. Three orthogonal traits separate concerns:

#![allow(unused)]
fn main() {
pub trait IoDriver: Send + Sync {
    fn set_process_callback(&self, cb: Box<dyn FnMut(&ClockTick)>);
    fn run(&self, running: Arc<AtomicBool>) -> IoResult<()>;
    fn stop(&self) -> IoResult<()>;
}

pub trait IoCapture: Send + Sync {
    fn read_input(&self, channel: usize, dst: &mut [f32]) -> usize;
    fn num_input_channels(&self) -> usize;
}

pub trait IoPlayback: Send + Sync {
    fn write_output(&self, channel: usize, src: &[f32]) -> usize;
    fn num_output_channels(&self) -> usize;
}
}

A single backend struct (e.g. PipewireBackend) implements IoDriver and optionally IoCapture / IoPlayback. The driver owns the timing loop; capture and playback provide data access.

IoBackend exists as a backward-compatible alias: pub trait IoBackend: IoDriver {}.

ProcessingState

Extracted from Graph via into_processing_state(), this struct is the runtime engine that drives the signal graph inside the I/O callback:

#![allow(unused)]
fn main() {
pub struct ProcessingState<T, const BUF_SIZE: usize> { /* ... */ }

impl ProcessingState<T, BUF_SIZE> {
    pub fn process_block(&mut self, tick: &ClockTick) -> ProcessResult<()>;
    pub fn wire_backends(
        &mut self,
        capture: Option<Arc<dyn IoCapture>>,
        playback: Option<Arc<dyn IoPlayback>>,
    );
    pub fn run_with_driver(
        &mut self,
        driver: Box<dyn IoDriver>,
        running: Arc<AtomicBool>,
    ) -> IoResult<()>;
}
}

process_block() is the per-block entry point called from the I/O callback. It first adopts the tick's sample rate (re-initialising nodes if the backend's hardware rate differs from the built rate — the graph has no clock of its own), drains the actor mailbox, applies any sample-accurate parameter changes due for this block, runs sources/processors/sinks, and triggers port propagation.

ParamValue

#![allow(unused)]
fn main() {
pub enum ParamValue {
    Float(f32),
    Int(i32),
    Bool(bool),
    String(String),
    Choice(String),
    Bytes(Vec<u8>),  // for IoControl::write_data()
}
}

Queues

Non-blocking SPSC queue for dual-thread communication:

#![allow(unused)]
fn main() {
use std::sync::Arc;
use rill_core::queues::{MpscQueue, SetParameter, SignalOrigin};
use rill_core::traits::{ParamValue, ParameterId};

let cmd_queue = Arc::new(MpscQueue::<SetParameter>::with_capacity(64));

// Control thread
cmd_queue.push(SetParameter::new(
    "osc_freq".to_string(),
    ParameterId::new("frequency").unwrap(),
    ParamValue::Float(440.0),
    SignalOrigin::Manual,
));

// Signal thread (in tick closure)
while let Some(cmd) = cmd_queue.pop() {
    if let Some(node) = nodes.get_mut(&cmd.port) {
        node.set_parameter(&cmd.parameter, cmd.value);
    }
}
}

ClockTick

Per-block timing sent from the driver into the graph and to control modules. Carries only timing metadata — I/O access is through IoCapture/IoPlayback traits held by graph nodes.

#![allow(unused)]
fn main() {
pub struct ClockTick {
    pub sample_pos: u64,
    pub samples_since_last: u32,
    pub is_new_block: bool,
    pub sample_rate: f32,
    pub tempo: Option<f32>,
    pub source: String,
    pub speed_ratio: f64,
    pub is_final: bool,
    pub io_quantum: u32,   // frames the backend processes per I/O callback
}
}

io_quantum lets asynchronous control producers schedule sample-accurate parameter changes correctly under backends that batch many block_size chunks into one callback. It defaults to samples_since_last (one chunk per callback) and is set to the full callback size by chunking backends (PipeWire, JACK).

Sample-accurate parameter changes

SetParameter carries an optional sample_pos: Option<u64> and an anchor: String for routing to the correct program in multi-node graphs:

#![allow(unused)]
fn main() {
pub struct SetParameter {
    pub port: String,              // target port name
    pub anchor: String,            // node anchor name for lang-based graphs
    pub parameter: ParameterId,
    pub value: ParamValue,
    pub source: SignalOrigin,
    pub timestamp: u64,            // wall-clock, for ordering/telemetry
    pub sample_pos: Option<u64>,   // absolute sample to apply at; None = ASAP
}
}

When anchor is non-empty, the engine uses the schedule's program_names map for O(1) lookup into the correct RillProgram. When empty, the engine scans all program param maps (backward compat).

  • None — applied immediately when the graph actor drains it (legacy; used by live UI/MIDI writes so there is no added latency).
  • Some(pos) — queued and applied by the graph during the 256-sample block whose range [block_start, block_start + block) contains pos.

Because an async control module reacting to a tick in I/O callback N is only rendered in callback N+1, producers look ahead by one quantum: SetParameter::new(..).with_sample_pos(tick.sample_pos + tick.io_quantum as u64).

Builtin registry

rill-core/src/builtin.rs provides the foreign-function registry for DSP/model built-ins callable from rill-lang:

#![allow(unused)]
fn main() {
pub struct Registry<T: Transcendental> { /* ... */ }
impl<T: Transcendental> Registry<T> {
    pub fn register_sample(&mut self, sig: BuiltinSig, factory: ...);
    pub fn register_block(&mut self, sig: BuiltinSig, factory: ...);
    pub fn get(&self, name: &str) -> Option<&Entry<T>>;
}
}

Key types:

TypePurpose
Registry<T>HashMap-backed collection of built-in definitions
BuiltinSigType-checker-facing signature: name, params (list of ParamType), signal_outs, BuiltinKind
ParamTypeSignal, Float, Int, String, Bool, Record(RecordSchema), Enum(...), Variadic(Box<ParamType>)
RecordSchemaNamed fields with type and optional default
BlockBuiltin<T>Whole-buffer built-in extending Algorithm<T>
SampleBuiltin<T>Per-sample built-in (feedback-legal)
SignatureSourceT-independent trait for type-checker/lowering
#![allow(unused)]
fn main() {
use rill_core::builtin::{Registry, BuiltinSig, BuiltinKind, ParamType};

let mut reg = Registry::<f32>::new();
reg.register_sample(
    BuiltinSig {
        name: "gain",
        params: vec![ParamType::Signal, ParamType::Float],
        signal_outs: 1,
        kind: BuiltinKind::Sample,
    },
    |params, _sr| {
        Box::new(Gain { k: params[0] as f32 }) as Box<dyn SampleBuiltin<f32>>
    },
);
}

Module tree

rill-core/
├── traits/   — Node, Source, Processor, Sink, ParamValue, Port, Algorithm,
│              MultichannelAlgorithm, BridgeAlgorithm, ParameterWrite
├── builtin/  — Registry<T>, BuiltinSig, ParamType, BlockBuiltin<T>, SampleBuiltin<T>
├── math/     — Scalar, Transcendental, Vector, glam re-export (Mat2/3/4, Vec2/3/4)
├── buffer/   — PipeBuffer, FanOutBuffer, FanInBuffer, DelayLine, RingBuffer, TapeLoop, FixedBuffer, ResourceRegistry
├── queues/   — MpscQueue, SetParameter, CommandEnum, Telemetry
├── time/     — ClockTick, RenderContext, SystemClock
├── io/       — IoDriver, IoCapture, IoPlayback, IoControl, IoResult
└── macros/   — source_node!, processor_node!, sink_node!