Signal pool
Travelling signals are the part of the simulation that changes every frame.
They are stored in a struct-of-arrays (SoA) SignalPool rather than an array
of structs, which keeps each field contiguous in memory and cache-friendly.
Layout
The pool exposes its fields to JavaScript as raw pointers (the Brain wraps
them in typed arrays):
| Field | Pointer getter | Meaning |
|---|---|---|
positions | particle_positions_ptr | XYZ of each signal (written each frame). |
alive | signal_alive_ptr | Liveness flag per slot. |
axon_idx | signal_axon_idx_ptr | Which axon the signal travels. |
t | signal_t_ptr | Position along the axon, 0..1. |
speed | signal_speed_ptr | Travel speed. |
starting_point | signal_starting_point_ptr | Which end (A or B) it started from. |
The pool is created with capacity limit_signals (default 65_536):
let pool = SignalPool::new(settings.limit_signals as u32);
The number of active signals is bounded separately by current_max_signals
(default 16_000), which is the live cap you can change at runtime. Free slots
are tracked with a free list so acquiring and releasing signals is cheap.
The per-frame step
step_cpu(dt) calls step_inner(dt, write_positions = true), which:
- Fires neurons: neurons that received a signal last frame may fire, releasing new signals onto their connected axons.
- Seeds when empty: if the pool has no active signals, it seeds some so the visualization never goes dark.
- Advances each live signal: increments
tbyspeed * dtalong its axon. - Samples the Bezier curve: converts
tinto an XYZ position and writes it into thepositionsbuffer (this is the per-frame write the renderer reads back). - Detects arrival: when
treaches the end, the signal is released back to the free list and the destination neuron is marked as having received a signal (which can cause it to fire next frame).
step_t_only(dt) runs the same lifecycle but skips step 4, leaving the position
write to a hypothetical GPU pass. The current front end always uses step_cpu.
Speed range
Signal speed is drawn from a configurable range. The Brain defaults are
signalMinSpeed = 2.3 and signalMaxSpeed = 5.0; the GUI enforces
min <= max. Changing these routes through World::set_setting:
this.world.set_setting('signalMinSpeed', this.settings.signalMinSpeed)
this.world.set_setting('signalMaxSpeed', this.settings.signalMaxSpeed)
Active-signal cap
set_setting('currentMaxSignals', value) clamps the live cap to never exceed
the hard limit_signals:
self.settings.current_max_signals = (value as usize).min(self.settings.limit_signals);
This cap also feeds the telemetry "active load" calculation: see Telemetry.