# fruitFly
A whole fruit fly nervous system running as a spiking network on the GPU, a
fly body it drives, and a bench for experiments on it. Install this package
and a world holds the complete adult male *Drosophila* central nervous system
(MaleCNS v1.0: 164,587 neurons, 25.6 million connections, CC-BY 4.0) as a
leaky integrate-and-fire network you can stimulate, silence, read out and
watch, plus a fly whose taste, looming, walking, turning, feeding, flight and
escape run through that wiring.
## Contents
- `maleCNS.connectome/` — MaleCNS v1.0 packed for the GPU (109 MB, the edge
table in two parts). It is an instance of the `connectome` asset type, which
lives at the world root (`/zero/source/connectome.assetType/`) and travels
with the package as a dependency: any nervous system's wiring as one asset
(`manifest.json`, `neurons.bin`, `offsets.bin`, `edges.bin` or its parts).
`guides { path = "types/connectome" }` is the reference.
- `lif_integrate`, `lif_propagate`, `lif_clear`, `lif_reset` (`.computeShader`)
— the four kernels of the network: membrane step, event-driven spike
propagation over the CSR edge table, step close, reset.
- `brain.module/` — `Brain.load(connectomeRef, params)` binds a connectome to
the kernels and returns the network: `step`, `update`, `select`,
`stimulate`, `silence`, `watch`, `rates`, `activity`, `showCloud`.
- `neuron.shader/` + `neuron.material/` — the instanced neuron cloud, each
neuron lit by its own activity.
- `FruitFlyBrain.component/` — puts a network on an entity: loads the
connectome the `connectome` field names, steps it every frame, draws the
cloud, and exposes `stimulate` / `silence` / `watch` / `rates` / `reset`
(the first three are synced, so every peer's copy receives the same inputs).
- `flyBody.module/` — the fly's body as an entity tree with a tripod gait,
wings, proboscis and a hop.
- `FruitFly.component/` — the embodied fly: reads the world into the brain's
sensory neurons and reads motor commands back out of named cell types.
- `FoodPatch.component/` — a disc of food on the ground the fly can taste.
- `FruitFlyLab.component/` — the bench window: network state and knobs,
live readouts with traces, and a selection line to stimulate, silence,
restore or watch any set of neurons.
- `experiments.guide/` — the experiments the bench was built for, and how to
run them.
## Usage
```luau
-- The brain: one entity, one network. The default connectome is maleCNS.
local brainEnt = entity.spawn("Brain")
brainEnt.position = { 0, 6, -8 }
brainEnt.component.add("FruitFlyBrain", { gain = 0.6, timeScale = 1 })
-- An embodied fly driven by it, and something for it to taste.
local fly = entity.spawn("Fly")
fly.component.add("FruitFly", { brain = brainEnt, bodyLength = 0.6 })
local food = entity.spawn("Food")
food.position = { 3, 0, 0 }
food.component.add("FoodPatch", { radius = 0.6, sugar = 1 })
-- The bench.
entity.spawn("Lab").component.add("FruitFlyLab", { brain = brainEnt, fly = fly })
-- Or from code:
local comp = brainEnt.component.get("FruitFlyBrain")
comp:watch("MN9", { types = { "MN9" } }) -- proboscis motor neurons
comp:stimulate({ types = { "claw_tpGRN" } }, 100) -- tarsal taste pegs at 100 Hz
print(comp:rates().MN9) -- Hz per neuron, updated every frame
```
A selection names neurons by the dataset's own vocabulary: `types` (exact
cell-type names), `typePrefix`, `pattern` (a Lua pattern over the type name),
`classes`, `superclasses`, `sides` (`L`/`R`/`M`), `excitatory`, `indices`,
`limit`. `brain:info(i)` answers what any neuron is.
## The model
Every neuron is a leaky integrate-and-fire unit with the constants Shiu et
al. 2024 (Nature) fitted to the FlyWire brain: rest and reset at -52 mV,
threshold -45 mV, membrane time constant 20 ms, synaptic time constant 5 ms,
refractory period 2.2 ms, transmission delay 1.8 ms, 0.275 mV per synapse. A
presynaptic neuron whose consensus transmitter is GABA, glutamate or histamine
inhibits; every other transmitter excites. A connection carries its synapse
count; connections under `minSynapses` (default 5) carry nothing.
`gain` scales every synapse. MaleCNS traces about 2.5 times the synapses per
connection FlyWire does, so at gain 1 the network runs away under any drive;
at 0.6 (the default) driving the 50 tarsal taste-peg neurons at 100 Hz fires
the proboscis motor neurons MN9 at about 120 Hz while the Kenyon cells stay
under 0.1 Hz, and driving the 595 Johnston's organ neurons instead leaves MN9
silent. At 0.5 the taste drive reaches nothing. The bench's gain slider is
the experiment.
Everything runs on the GPU: the state, the input ring, the spike list and the
edge table. The CPU sees the per-group spike counters each frame and, on
request, the per-neuron activity lane. Integer atomics make a step
order-independent, so two peers stepping the same inputs read the same
network.
## The fly
`FruitFly` reads named cell types out of the brain and writes the world into
others; every name is in `FruitFly.component/init.luau`:
| command | cell types |
|---|---|
| walk | the leg motor neurons (`vnc_motor` types ending in ` MN`) |
| turn | `DNa01`, `DNa02` left minus right |
| proboscis | `MN9` |
| flight | the wing power-muscle motor neurons (`DLMn`, `DVMn`) |
| jump | a `DNp01` (giant fibre) burst |
| sense | cell types | driven by |
|---|---|---|
| tarsal taste | `claw_tpGRN`, `dorsal_tpGRN` | a front foot on a `FoodPatch` |
| labellar taste | `LB*` labellar bristle GRNs | the extended proboscis on a `FoodPatch` |
| looming | `LPLC2`, `LC4` | any player's avatar growing fast in the fly's view |
The body moves kinematically at `walkSpeed` body lengths per second; the fly
faces its entity's -Z. On a shared world the entity's owner writes the senses
into the brain (a synced call) and every peer poses its own copy from its own
network.
## Data and license
MaleCNS v1.0: HHMI Janelia FlyEM, Google Research and the Cambridge
connectomics group, released 8 June 2026 (Berg et al. 2026, Cell,
doi:10.1016/j.cell.2026.08.015), CC-BY 4.0. The pack was written by a script
that reads the public flat-connectome export (`connectome-weights ... traced-only`,
body annotations, consensus neurotransmitters), assigns dense indices, sorts
edges by presynaptic neuron and writes the four files `types/connectome`
describes; the manifest carries each source file's SHA-256.
## Notes
- Loading the network reads the 102 MB edge table into a GPU buffer once per
`FruitFlyBrain`; the VFS copy is released afterwards.
- The neuron cloud is one instanced draw of every neuron with a soma
(139,741). Its transforms are static; the activity lanes are written by the
integrate kernel.
- `maxStepsPerFrame` bounds GPU time per frame; at 32 steps of 1 ms a 60 fps
engine keeps up with real time.
# FruitFlyBrain
A whole connectome running as a spiking network on this entity. It loads the
`connectome` asset the field names (the package's `maleCNS` by default),
steps it every frame the gameplay clock runs and, with `runInEdit`, in edit
mode too; draws every neuron with a soma as a lit point around the entity;
and hands the live network to anything that asks.
Fields: `connectome`, `running`, `runInEdit`, `timeScale`,
`maxStepsPerFrame`, `gain`, `minSynapses`, `showCloud`, `cloudScale`,
`neuronRadius`.
Methods on the ref: `getBrain()`, `stimulate(sel, hz)`, `silence(sel, on)`,
`clearStimulation()`, `reset()`, `watch(name, sel)`, `unwatch(name)`,
`rates()`, `describe()`. `stimulate`, `silence`, `clearStimulation` and
`reset` are synced calls, so every peer's copy of the network receives the
same inputs. `onLoaded` fires with the neuron and edge counts once the
network is on the GPU.
```luau
local e = entity.spawn("Brain")
e.position = { 0, 5, -8 }
e.component.add("FruitFlyBrain", { gain = 0.6 })
local c = e.component.get("FruitFlyBrain")
c:watch("MN9", { types = { "MN9" } })
c:stimulate({ types = { "claw_tpGRN" } }, 100)
```
# flyBody
A fruit fly's body as an entity tree under a root: head with eyes, antennae
and a proboscis, thorax, striped abdomen, six two-segment legs on single-axis
pivots, two wings. `build` stands it; `update` moves the root kinematically
and poses every part from a motor command.
## Exports
- `M.build(root, { length? }) -> Body` — stand a fly under `root`, facing
the root's -Z, `length` world units long (default 0.6).
- `body:update(dt, { speed?, turn?, proboscis?, flight?, jump? })` — one
frame: walk at `speed` (world units per second), turn at `turn` (radians
per second, positive left), extend the proboscis (0 to 1), beat the wings
and hover (0 to 1), hop on `jump`.
- `body:eye() -> pos, dir`, `body:mouthPosition()`, `body:frontFeet()`,
`body:heading()` — where the fly looks from, feeds with, stands on, faces.
- `body:destroy()` — take the parts down.
- `body.groundY` — the floor height the feet rest on (default 0).
## Usage
```luau
local FlyBody = require("~.flyBody")
local body = FlyBody.build(public.entity, { length = 0.6 })
function update(dt)
body:update(dt, { speed = 0.3, turn = 0.1 })
end
```
## Notes
- The gait is a tripod: right front, left mid and right hind step together.
- Every part is a built-in primitive mesh tinted through its `Model`, so the
body needs no asset of its own.
# FoodPatch
A disc of food on the ground, drawn as a flat cylinder tinted by what it
holds. A `FruitFly` whose front foot or extended proboscis is inside the disc
has its taste neurons driven at the fly's `tasteHz` times the patch's
`sugar`. Fields: `radius`, `sugar`, `bitter`, `height`. Method:
`contains(x, y, z)`.
```luau
local food = entity.spawn("Food")
food.position = { 3, 0, 0 }
food.component.add("FoodPatch", { radius = 0.6, sugar = 1 })
```
# brain
A whole connectome running as a leaky integrate-and-fire network on the GPU.
`Brain.load` binds any `connectome` asset to the package's four `lif_*`
kernels and returns a network; the `FruitFlyBrain` component is the usual
owner, and anything holding the network can step, drive, cut, read out and
draw it.
## Exports
- `M.load(connectomeRef, params?) -> Brain` — allocate the network at rest.
`params` overrides any of `dtMs`, `vRest`, `vThresh`, `vReset`, `tauM`,
`tauSyn`, `refracMs`, `delayMs`, `weightMv`, `gain`, `minSynapses`,
`traceMs`, `maxSpikes`, `seed`.
- `brain:update(dt)` — once a frame: queue the steps `dt` covers, issue and
drain the counter readback.
- `brain:step(n)` — queue `n` steps outright.
- `brain:select(sel) -> { index }` — the neurons a selection names.
- `brain:stimulate(sel, hz)` / `brain:clearStimulation()` — Poisson drive.
- `brain:silence(sel, on)` — ablate or restore.
- `brain:watch(name, sel)` / `brain:unwatch(name)` / `brain:rates()` —
read groups out as Hz per neuron.
- `brain:activity()` — network mean rate, total spikes, dropped spikes,
simulated time.
- `brain:info(i)` — what one neuron is.
- `brain:showCloud(opts)` / `brain:hideCloud()` / `brain:worldPosition(i)` —
the instanced neuron cloud.
- `brain:setParams(patch)`, `brain:setRunning(on)`, `brain:setTimeScale(x)`,
`brain:reset()`, `brain:ready()`, `brain:destroy()`, `brain:describe()`.
## Usage
```luau
local Brain = require("~.brain") -- from inside the package
local brain = Brain.load(asset.resolve("fruitFly.maleCNS", "connectome"), { gain = 0.6 })
brain:watch("MN9", { types = { "MN9" } })
brain:stimulate({ types = { "claw_tpGRN" } }, 100)
-- every frame:
brain:update(dt)
print(brain:rates().MN9)
```
A selection is `{ types?, typePrefix?, pattern?, classes?, superclasses?,
sides?, excitatory?, indices?, limit? }`; every given key must hold.
## Notes
- The network is silent until something is driven; a spike always traces
back to a stimulus.
- Every GPU buffer is named `brain.<id>.<name>`; `compute.observe()` lists
them. `destroy` releases them all.
- The kernels' `params` are per shader, so two networks on one engine share
the model constants the last `applyParams` wrote.
# Model
Loads and displays a 3D mesh on an entity. Disabling the component hides the mesh. Supports procedural meshes (`cube`, `sphere`, ...), model files, and remote URLs. Tint and outline are shader effects applied to the mesh.
**Material lives on this component.** There is no standalone `Material` component — a material only renders where there is a mesh to render it on, so the `material` field is part of Model (and SkinnedModel). Set it at add time or assign the field later; properties are registry-wide.
Public fields: `model`, `material` (`AssetRef<material>`), `tintR/G/B`, `tintBlend`, `outlineR/G/B`, `outlineIntensity`.
Methods: `:setTint(color, blend?)` (color: `{r, g, b}` array or `{r=, g=, b=}` map), `:clearTint()`, `:setOutline(color, intensity?)`, `:clearOutline()`, `:setMaterialProperty(prop, value)`, `:getMaterialProperty(prop)`, `:getMaterialPropertyNames()`.
```luau
entity(id).component.add("Model", { model = "cube", material = "gold" })
entity(id).component.get("Model").material = "checkerboard" -- swap material
entity(id).component.get("Model"):setTint({1, 0, 0}, 0.5)
entity(id).component.get("Model"):setMaterialProperty("roughness", 0.2)
```
# Default
The neutral fallback surface — a plain white, non-metallic PBR material with
mid roughness (`0.5`), sampling the built-in white texture. This is what an
object renders as when no material is assigned. Built on
`@builtin::shaders.pbr`.
## Inputs
- `colors.base_color` — the flat tint (white by default).
- `floats.roughness` (`0.5`) / `floats.metallic` (`0.0`) — a neutral matte
dielectric.
- `textures.base_color_texture` — the built-in `default:white` texture.
·computeshader · born here
❒asset # lif_integrate
The membrane step of the `brain` module's leaky integrate-and-fire network,
one thread per neuron. It drains this step's slot of the fixed-point input ring
into the neuron's synaptic conductance, advances the membrane by exponential
Euler on `dv/dt = (vRest - v + g) / tauM` and `dg/dt = -g / tauSyn`, holds a
refractory neuron at reset, draws the neuron's external Poisson event from its
`stim` rate, and on a spike resets it, appends its index to this step's spike
list, and bumps the total and its readout group's counter. Every neuron writes
its activity trace, normalised membrane and spike flag into lane 0 of the
visualisation buffer the neuron cloud draws from. `decayMem` and `decaySyn`
are `exp(-dt / tau)` computed once by the module; the step index comes from the
GPU clock so several steps can be queued in one frame with no CPU write between
them.
·computeshader · born here
❒asset # lif_propagate
The synaptic step of the `brain` module's network. Each workgroup takes one
spike from this step's list, and its 64 threads stride that neuron's outgoing
edge row, adding every connection's synapse count times `unitsPerSynapse`
(signed by the presynaptic neuron's transmitter) into the fixed-point input
ring at the slot `delaySteps` after the current step. A connection carrying fewer than
`minSynapses` synapses is skipped, which is how the module applies the
connection threshold whole-brain models are fitted with (five, in Shiu et al.
2024) without repacking the table. The module dispatches it with `maxSpikes`
workgroups; a workgroup past this step's spike count returns at once. The sum is integer atomics, so the result is the same whatever order
the threads land in.
·computeshader · born here
❒asset # lif_reset
Puts the `brain` module's network at rest in one dispatch: every membrane at
`vRest` with no synaptic conductance, refractory time or activity trace, every
slot of the input ring empty, the visualisation lane dark, the spike list
empty, every spike counter zero and the step clock at zero. The module runs it
once after allocating a network and again on `brain:reset()`.
# json
JSON encode/decode library for Luau. Encodes Lua values to JSON
strings and decodes JSON strings back to Lua values. Used for
communication with the Rust side of the engine, the VFS read/write
bridge, and any wire-format that needs JSON.
Pure Luau, no engine dependencies. Compact and pretty-printed
encoders, plus a hand-rolled decoder that streams the input by position
so it works under WASM as well as native.
## Exports
- `Json.encode(value: any, indent?: string, currentIndent?: string) -> string` — compact encode. Functions / unknown types and NaN/Inf encode as `null`.
- `Json.encodePretty(value: any, indentStr?: string) -> string` — pretty-printed encode with sorted object keys (diff-friendly).
- `Json.encodeArgs(...: any) -> string` — encode varargs as a JSON array.
- `Json.decode(str: string) -> any` — decode a JSON string. Returns the decoded value, or `nil` + error message on failure.
## Usage
```luau
local Json = require("@builtin::modules.json")
local widget = { type = "button", text = "Click Me" }
local compact = Json.encode(widget) -- '{"text":"Click Me","type":"button"}'
local pretty = Json.encodePretty(widget, " ")
local decoded = Json.decode(compact)
local v, err = Json.decode("oops") -- v = nil, err = error message
```
## Notes
- Object keys are sorted alphabetically in both encoders for consistent
output across runs.
- Numeric keys on objects are stringified at encode time (JSON has no
numeric keys). Pure-integer key sets get detected as arrays via
`isArray` and encoded with brackets.
- NaN, +Inf, -Inf encode as `null` — JSON has no representation. Round
trips through `decode` recover `null` (Lua `nil`), so they don't
preserve.
- Unicode `\uXXXX` escapes decode to UTF-8 by hand to stay WASM-safe.
Only the BMP is covered; supplementary planes via surrogate pairs
are not.
- Functions encode as `null`.
- Decode is character-streamed — no regex, no `string.match` patterns
on the whole input — so the line-and-column information needs to be
reconstructed from the position offset.
·computeshader · born here
❒asset # lif_clear
Closes one step of the `brain` module's network: empties the spike list the
integrate kernel filled and the propagate kernel consumed, and advances the GPU
step clock by one. The clock living on the GPU is what lets the module queue
several steps in one frame, each reading its own ring slot, with no CPU write
between dispatches.
# neuron
The surface of one neuron in the brain's instanced neuron cloud. It lights
itself from the instance data the brain's integrate kernel writes each step
(lane 0: activity trace, normalised membrane, spike flag) and the class colour
the module writes once (lane 1): dim at rest, brighter as the membrane
depolarises, flaring to full emission on a spike and fading with the trace.
`rest_brightness`, `membrane_brightness` and `spike_brightness` set those three
levels on the material.