Skip to main content
Version: Next

Overview

EXEPERT is split into two cooperating layers that share memory:

  • Simulation core: a Rust World struct (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):

  1. Clear the canvas manually (autoClear is off so the GUI-configurable background color stays in effect).
  2. If not paused, read clock.getDelta() and call brain.update(dt).
  3. Brain.update calls world.step_cpu(dt), which advances the simulation and writes signal positions into WASM memory, then flags the geometry attributes as needing a GPU upload.
  4. renderer.render(scene, camera) draws the frame.
src/run.ts
brain.update(deltaTime)
// ...
renderer.render(scene, camera)
src/brain.ts
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:

src/brain.ts
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.

src/brain.ts
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':

src/renderer.ts
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.