Overview
EXEPERT is split into two cooperating layers that share memory:
- Simulation core: a Rust
Worldstruct (rust/brain_sim) compiled to WebAssembly. It owns every piece of mutable state: neuron positions, axon geometry, the signal pool, and telemetry. - Front end: TypeScript modules under
src/that set up the Three.js renderer, build the meshes, run the animation loop, and host the UI.
The connection between them is the key design choice: Three.js geometry
attributes are Float32Array/Uint32Array views that point directly into the
WASM linear memory. The simulation writes new positions into that memory each
frame, and the renderer reads the same bytes: no per-frame copy.
Per-frame data flow
Each animation frame (src/run.ts):
- Clear the canvas manually (
autoClearis off so the GUI-configurable background color stays in effect). - If not paused, read
clock.getDelta()and callbrain.update(dt). Brain.updatecallsworld.step_cpu(dt), which advances the simulation and writes signal positions into WASM memory, then flags the geometry attributes as needing a GPU upload.renderer.render(scene, camera)draws the frame.
brain.update(deltaTime)
// ...
renderer.render(scene, camera)
update(deltaTime: number): void {
this.refreshIfMemoryGrew()
this.world.step_cpu(deltaTime)
this.particlePool.update()
}
Zero-copy buffers and memory growth
When the Brain builds its geometry, it wraps WASM pointers in typed-array
views:
private viewF32(ptr: number, len: number): Float32Array {
return new Float32Array(this.wasm.memory.buffer, ptr, len)
}
WebAssembly memory can grow at runtime, which replaces the underlying
ArrayBuffer and detaches any existing views. Brain.refreshIfMemoryGrew()
guards against this: it compares wasm.memory.buffer to the cached buffer each
frame and rebuilds the views when they differ.
private refreshIfMemoryGrew(): void {
if (this.wasm.memory.buffer === this.currentMemoryBuffer) return
this.currentMemoryBuffer = this.wasm.memory.buffer
// rebuild the neuron position view; ParticlePoolBridge checks its own
}
Neuron positions are static after construction, and axon attributes are uploaded once, so only the moving signal/particle buffer needs frequent re-binding.
The "Path B" rendering choice
renderer.ts constructs a classic WebGLRenderer and reports its backend as
'webgl2':
export type RendererBackend = 'webgl2'
const renderer = new WebGLRenderer({ antialias, alpha })
return { renderer, backend: 'webgl2' }
The simulation is fully CPU-driven; Brain.travelBackend is hardcoded to
'cpu'. Earlier designs explored a WebGPU + TSL compute path (see the Plans/
folder), but the shipped code uses classic WebGL2 with GLSL ShaderMaterial
shaders. The Rust core still exposes a step_t_only(dt) entry point (lifecycle
without position writes) for a future GPU travel pass, but the current front end
does not call it.
Continue with the Boot sequence and the Rendering pipeline.