# 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 })
```
# FruitFly
An embodied fly driven by a `FruitFlyBrain`. Every frame it writes the world
into the network's sensory neurons and reads a motor command out of named
motor and descending cell types:
| sense | cell types | driven by |
|---|---|---|
| tarsal taste | `claw_tpGRN`, `dorsal_tpGRN` | a front foot on a `FoodPatch` |
| labellar taste | `LB*` | the extended proboscis on a `FoodPatch` |
| looming | `LPLC2`, `LC4` | a player's avatar growing fast in view |
| command | cell types |
|---|---|
| walk | leg motor neurons (`vnc_motor` types ending in ` MN`) |
| turn | `DNa01` + `DNa02`, left minus right |
| proboscis | `MN9` |
| flight | `DLMn` + `DVMn` wing power-muscle motor neurons |
| jump | a `DNp01` burst above `escapeHz` |
Fields: `brain`, `bodyLength`, `walkSpeed`, `turnRate`, `motorFullHz`,
`flightFullHz`, `tasteHz`, `loomHz`, `escapeHz`, `groundY`, `wander`.
Methods: `motor()`, `senses()`, `readouts()`, `sensoryNeurons()`. Events:
`onJump`, `onFeed`.
```luau
local fly = entity.spawn("Fly")
fly.component.add("FruitFly", { brain = entity.find("Brain"), bodyLength = 0.6, wander = 0.15 })
```
On a shared world the entity's owner writes the senses into the brain (a
synced call); every peer poses its own copy from its own network.
# 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)
```
# FruitFlyLab
The bench for a `FruitFlyBrain`: a draggable window with the network's
state (simulated time, mean rate, spikes, dropped), its knobs (run, reset,
clear drive, gain, time scale, connection threshold), every watched group's
live rate with a rolling trace, and a selection line to `stimulate`,
`silence`, `restore`, `watch` or `count` any set of neurons. With `fly` set
it also shows what that fly senses and does.
A selection is space-separated `key:value` terms: `type:MN9`,
`type:DNa01,DNa02`, `prefix:JO-`, `pattern:^LB%d`, `class:gustatory`,
`super:descending_neuron`, `side:L`, `limit:100`.
Fields: `brain`, `fly`, `open`, `refreshHz`. Method: `parseSelection(text)`.
```luau
entity.spawn("Lab").component.add("FruitFlyLab", { brain = entity.find("Brain"), fly = entity.find("Fly") })
```
The `experiments` guide (`guides { path = "experiments" }`) is the manual.
# 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.
# connectome
A whole nervous system's wiring diagram as one asset: every neuron with its
soma position, cell type, class and neurotransmitter, and every
neuron-to-neuron connection with its synapse count, packed into the binary
form a GPU spiking simulation binds directly. The `brain` module of the
`fruitFly` package runs any instance of this type; the type itself carries
no animal.
```
maleCNS.connectome/
manifest.json counts, provenance, byte layouts, vocabularies
neurons.bin 32 bytes per neuron
offsets.bin u32[neurons + 1], CSR row starts into edges.bin
edges.bin u32 per connection: target index | synapse count << 18
README.md the animal, the dataset, the release, the license
.metadata description + tags
```
## The manifest
```json
{
"dataset": "MaleCNS v1.0 ...",
"license": "CC-BY 4.0",
"citation": "...",
"neurons": 164587,
"edges": 25563197,
"edge_format": "...",
"neuron_record": "...",
"flags": "...",
"sign_rule": "...",
"soma_centre_um": [x, y, z],
"vocab": {
"types": [...], "superclasses": [...], "classes": [...],
"neurotransmitters": [...], "sides": [...], "dimorphism": [...], "fruDsx": [...]
}
}
```
## The neuron record (32 bytes, little-endian)
| offset | type | field |
|---|---|---|
| 0 | f32 | soma x, micrometres, centred on `soma_centre_um` |
| 4 | f32 | soma y |
| 8 | f32 | soma z |
| 12 | u32 | index into `vocab.types` |
| 16 | u32 | flags (below) |
| 20 | u32 | source body id, low 32 bits |
| 24 | u32 | source body id, high 32 bits |
| 28 | u32 | out-degree (number of edges in this neuron's row) |
Flags: bits 0-5 superclass, 6-13 class, 14-17 neurotransmitter, 18-19 side,
bit 20 has a soma position, 21-23 dimorphism, 24-26 fruDsx, bit 27 set when the
neuron excites its targets (clear when it inhibits). Every bit field indexes
the vocabulary of the same name, 0 being `unknown`.
## Parts
A payload larger than one blob may carry is stored as numbered parts,
`edges.0.bin`, `edges.1.bin`, ..., and the manifest's `parts` says how many
(`"parts": { "edges": 2 }`). `ref:parts("edges")` lists them with the byte
offset each starts at, so a GPU upload writes each part in place;
`ref:bytes("edges")` concatenates them when a caller wants the whole table.
A payload without an entry in `parts` is the single file `<name>.bin`.
## The edge (4 bytes)
Bits 0-17 hold the postsynaptic neuron's index, bits 18-31 the synapse count
clamped at 16383. Row `i` of the table is `edges[offsets[i] .. offsets[i+1])`,
so a neuron's outgoing connections are one contiguous run, which is what an
event-driven spike propagation reads.
## Reading one from Luau
```luau
local ref = asset.resolve("maleCNS", "connectome")
local m = ref:manifest() -- the decoded manifest
local neurons = ref:bytes("neurons") -- a Luau `buffer` of neurons.bin
local edges = ref:bytes("edges") -- the whole edge table, for a GPU upload
ref:counts() -- { neurons = ..., edges = ... }
```
`ref:bytes(name)` reads the whole file into a Luau `buffer`, so a call for
`edges` on a large connectome holds a hundred megabytes until the caller drops
it; hand it to a GPU buffer's `writeBytes` and let it go.
## Making one
A connectome is written by a packer outside the engine (the `fruitFly`
package's README names the script for MaleCNS). It reads the dataset's own
export, assigns dense indices, sorts edges by presynaptic neuron, and writes the
four files above. `asset.create("connectome", "<name>")` scaffolds the folder
with an empty manifest for a packer to fill.
# experiments
<!-- zero:scaffolded-readme -->
<!-- Replace the line below with what this guide provides and who
consumes it, then delete both comment lines. -->
TODO: describe this guide.
# 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.
·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.
·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()`.
# maleCNS
The complete central nervous system of an adult male *Drosophila melanogaster*:
brain, both optic lobes and the ventral nerve cord, 164,587 traced neurons and
25,563,197 neuron-to-neuron connections carrying 125 million synapses.
Packed from the MaleCNS v1.0 flat-connectome export (HHMI Janelia FlyEM,
Google Research and the Cambridge connectomics group; released 8 June 2026,
published as Berg et al. 2026, *Sexual dimorphism in the complete Drosophila
male central nervous system connectome*, Cell, doi:10.1016/j.cell.2026.08.015).
The export used is the `traced-only`, minimum-confidence 0.5 connectivity table,
with the curated body annotations (cell type, class, superclass, side, sexual
dimorphism, fru/dsx expression) and the per-body consensus neurotransmitter
prediction. Soma positions come from the annotation table's soma location in
8 nm voxels, converted to micrometres and centred; 139,741 neurons carry one,
the rest sit at the origin with the has-soma flag clear.
Data license: CC-BY 4.0, https://male-cns.janelia.org/. The manifest carries
the SHA-256 of each source file.
Sign convention: a neuron whose consensus transmitter is GABA, glutamate or
histamine inhibits every target; every other transmitter excites (Shiu et al.
2024, Nature). Connection strengths are synapse counts; the wiring says which
neurons reach which and how many synapses they make, and nothing about the
efficacy of each synapse in a living fly.
# 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.
# 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.
# 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.