# Slime Colony (Native)

Watch moving agents leave trails, then follow those trails into branching
patterns. Slime Colony runs a Physarum-inspired simulation in a pure Odin
kernel and displays its field as a Godot texture. A native `SlimeColony` Node
connects them; Godot owns the sprite, shaders, camera and controls.

![Historical Slime Colony capture](screenshot.png)

## Try it

The controls adjust population, evaporation, sensor angle and movement per step;
Randomize starts a new seed. The 0.25-second stats timer reports actual population,
frame number and observed FPS. One simulation step runs per frame, so
simulation speed in wall-clock seconds depends on frame rate.

Start reading with [the scene driver](project/main.gd),
[the native colony class](declarations/slime_colony.odin), and
[the pure kernel](subsystem/slime.odin). The scene supplies settings; the kernel
owns the agents and trail field.

## Project layout

- `subsystem/`: pure kernel, its retained memory owner and tests.
- `declarations/`: authored native Node, coarse calls and explicit engine owners.
- `project/`: scene, frame/error driver, live controls, palette and vignette.
- `eval/`: offline measurement, immutable original review and retained evidence.
- `generated/`: compiler-owned catalog, bridge and class documentation.

The two parameter calls (`set_motion` and `set_field`) keep each method inside
the installed Host's eight-argument variant-call limit. Both validate complete
finite settings before changing the current configuration. `slime.params_valid`
owns the limits: move/sensor distance 0..2048, turn/jitter 0..2π, sensor angle
0..π, sensor size 1 or 3, deposit/gain 0..1,000,000 and decay 0..1. Colony sizes
are 32..2048 per side and populations 1..1,000,000.

## Build and run

[Project setup](../../docs/setup.md) covers the tool and SDK connection.
The example uses the attached immutable SDK and machine-local `.gododin/local.toml`
and `.gododin/authority.toml`. The exact Godot identity comes from `gododin.lock`.
Use the SDK CLI or the example runner:

```sh
/absolute/sdk/bin/gododin doctor --project-root /absolute/gododin/examples/slime_colony
/absolute/sdk/bin/gododin generate --project-root /absolute/gododin/examples/slime_colony
/absolute/sdk/bin/gododin check --project-root /absolute/gododin/examples/slime_colony
/absolute/sdk/bin/gododin build --project-root /absolute/gododin/examples/slime_colony --profile debug
./test.sh          # subsystem tests and SDK pipeline
./test.sh eval     # offline step+render measurement; prints fresh evidence
./test.sh run      # original headless runtime contract workload
./test.sh shot     # original windowed workload, fresh retained screenshot
./test.sh play     # interactive accepted build
```

For optimized native profiling, use `GODODIN_PROFILE=release ./test.sh pipeline`
then the same environment on `./test.sh shot`. Debug remains the default. Each
profile has its own acceptance cache, and the runner checks the publication's
profile before launch. This selects the native Host/image build; the pinned
Godot executable, scene, workload and rendering settings stay the same.

Windowed execution uses GL compatibility and retains the explicit
`--rendering-driver opengl3` flag for the pinned dev5 Metal texture-free defect
[GH-123394](https://github.com/godotengine/godot/issues/123394). The project also
defaults to compatibility for ordinary interactive launches. A headless run
proves native/CPU Image behavior and cleanup, not texture rendering.

The interactive default remains 60,000 agents at 1024², supported by the
[population sweep](../../docs/evaluations/eval-throughput/followups/20260912-population/README.md).
The stress capture remains 120,000. The sweep holds its canonical speed at 1.5;
ordinary play still starts at speed 1.0, so its trajectory is a separate setting.

## Per-frame work and ownership

The kernel keeps one f32 trail grid, diffusion scratch, separate x/y/heading
agent arrays and an R8 display buffer in one arena-backed allocation per colony.
A seeded integer mixer chooses initial positions/headings and per-step jitter.
Movement samples the previous field; all agents then deposit in index order;
a separable 3×3 box blur and multiplicative evaporation finish the step.

The scene calls and checks `submit_frame` once per displayed frame:

1. `colony_step` advances one fixed step.
2. `colony_render` writes saturated bytes into the colony's retained display buffer.
3. `packed_byte_array_from_bytes` constructs a fresh engine array and copies the
   display bytes into it in one CPU bulk copy.
4. `Image.set_data` assigns that array; `ImageTexture.update` submits the image.
   The call-local array owner is then released.

No per-agent calls cross the native boundary. The pure warm step/render path
has allocation-counter assertions in the tests and offline eval. The native
upload source uses no Odin allocation; **the engine array is allocated each
upload**, and renderer conversion/upload work is additional to that CPU copy.
No GPU allocation or total-renderer-copy bound is claimed.

The retained Image shares its array using copy-on-write storage: exact dev5
`core/io/image.cpp` ends `Image::initialize_data` with `data = p_data`.
Reacquiring a shared array through `Packed_Byte_Staging` can detach its pointer,
which that owner's contract rejects. This example therefore uses a fresh array.
That explains this implementation's consume choice; it does not establish that
all alternative Image or RenderingDevice staging designs are impossible.

The native class owns both Image and ImageTexture references. Release goes
through the typed surface's one generic path, `godot.owned_ref_release`: it
unreferences the owner and calls the public object destructor when the last
reference is released, leaves a refused owner live for retry, and treats an
already-released owner as done. This distinction matters: dev5
`core/object/ref_counted.h:208–209` explicitly deletes an object when
`unreference()` returns true. The original zero-reference Image leak was
an example ownership bug, not an engine exit-order exemption. The class
releases its references in its PREDELETE notification branch — the engine's
supported last point with valid call authority — so ordinary teardown needs
no shutdown call; the scene's explicit shutdown method stays as the eager
path the eval teardown probe exercises. Cleanup failures fail runs. Every
gododin method returns a named result — 0, a positive `godot.Call_Error`
pass-through, or a negative `Slime_Error` cause — which the driver's
`_result_text` decodes for its failure logs.
Refused CPU frees preserve their live owners for retry. A failed old-colony
retirement remains reachable alongside the replacement, and another replacement
must retire it before allocating again; two native tests exercise this failure
and retry path.

`get_digest` traverses the current grid and agent arrays on demand, including
frame zero after replacement. Eval checkpoints request it outside measurement.
The ordinary stats timer displays inexpensive live counters; it does not hash
the full simulation merely to refresh a label.

## The nutrient mask (EX-SLIME)

The colony accepts one Godot-authored nutrient mask as a spatial input:
`nutrient_ring.tres` and `nutrient_waves.tres` ship checked in, and the
Nutrient control (None/Ring/Waves plus the attraction slider) imports a
mask on change or clears it explicitly. Each import copies one bounded
whole-mask snapshot; the kernel owns the copy, releases it through one real
cleanup path on replace, resize, clear, and destroy, and maps it
proportionally onto the colony's toroidal world. The `attraction` parameter
weights the mask into the sensor comparisons — zero by default, so the
unmasked canonical workload is byte-identical. Same seed plus different
masks changes attraction; same seed and mask replays exactly; masked
behavior has its own oracle test and the named offline workload
(`slime-colony-masked`) alongside the unchanged canonical eval.

## The GPU field-update variant (DX-9)

The **GPU field update** toggle selects an explicitly labelled variant: the
agents stay on the CPU through `advance_agents` (identical math and order),
while the field update — horizontal 3-tap blur, vertical 3-tap blur, decay —
dispatches to a compute shader (`slime_field_gpu.glsl`) whose per-cell
arithmetic mirrors the CPU's op-for-op. Enabling the toggle validates first:
16 steps each way from the live state, compared bitwise (tolerance 0) —
`GODODIN_SLIME_GPU_VALIDATION_PASS same_model=true` on pass; any mismatch
labels the variant a different model whose FPS is never a language speedup
claim. The variant's frame is upload → dispatch → readback → render, each
boundary measured; the readback is the explicit CPU-results contract (agents
sense the CPU copy next tick). Canonical eval and the offline receipts never
run the variant. The probe (`verify_gpu.gd`) requires a compute-capable
renderer (Forward+/Vulkan); its execution rides the SDK reseat named in the
platform record's fourteenth entry.

## Verification and determinism

There is no native reload journey assertion in this example’s eval.

The guarantee is reproducibility with identical seed, parameters, step count,
native build/profile (including compiler and emitter optimization settings) and
host floating-point behavior. No cross-platform bitwise guarantee
or native-versus-GDScript speedup is claimed. Debug and release are not bitwise
interchangeable: the optimized follow-up observes different frame-16 digests.
Release combines sine/cosine calls that debug issues separately, and the pinned
host's two math paths can differ by one float32 ULP. Each evaluated build still
passes its own same-seed replay. See the
[optimized follow-up](eval/batches/20260912-optimized-profiling.md) for both
digests, code-generation evidence and the comparison bounds.

- The 21 pure tests include replay, independent hand-computed sensing, steering,
  blur, deposition and decay expectations, invalid-input refusal before mutation,
  and warm allocation/teardown accounting.
- `oracle_frame` is explicitly **same-implementation replay**, using a cloned
  pre-step state and comparing output elements. It detects changed observations;
  it is not an independently implemented simulation oracle.
- The offline eval checks every step/render return, compares separate rendered
  byte buffers each measured frame, then requires equal final state digests.
- The runtime eval preserves the original frame-16 digest and compares a fresh
  replay against it. It checks frame-zero state and Image bytes immediately after
  population changes and reseeding, then runs three configure/shutdown cycles.
- Native `verify` checks Image width, height, R8 format, absence of mipmaps and
  exact byte length, then compares the **CPU Image's** bytes from Godot's
  `get_data` to a fresh kernel render. It allocates one scratch buffer outside
  the timing bracket. This does not read GPU texture storage or prove appearance.

Extra agents alter the shared trail field. A larger population shares the
smaller population's initial agent identities, but subsequent trajectories can
and do differ. Reseeding and population changes reset frame metadata and upload
the new colony's empty field before reporting success.

## What the measurements mean

The original inputs are preserved:

| Workload | Grid / agents | Steps | Motion / gain | Scope |
| --- | --- | --- | --- | --- |
| Offline | 512² and 1024² / 100,000 | 30 warm + 300 timed | 1.5 / 6.0 | Optimized Odin step + render; no engine |
| `run` | 1024² / 100,000 | 300, latter half timed | 1.0 / 6.0 | Debug native `submit_frame`, headless |
| `shot` | 1024² / 120,000 | 800, latter half timed | 1.5 / 6.0 | Debug native `submit_frame`, GL windowed |

All use seed 20260911, sensor angle 22.5°, distance 9, size 3, turn 0.15,
deposit 1, decay 0.008 and jitter 0.2. Runtime options are `--eval --size=N
--agents=N --frames=N --seed=N --speed=N --gain=N`; frames must be 32..100000.
The offline command accepts `--size=N --agents=N --frames=N`, validates bounds
before integer narrowing, and always includes the 512² workload alongside the
selected size. `--write` is intentionally refused so it cannot replace receipts.

`avg_usec` is the bracket around `submit_frame`: kernel step, byte render,
array construction/copy, Image assignment and texture-update call. It is **not
whole-frame time or GPU completion time**. Readback, hashing, replacement,
replay and screenshots occur outside that bracket. Headless and windowed costs,
debug and optimized builds, and different motion/population settings are separate
workloads; their quotient is not an integration overhead measurement.

The baseline offline receipt recorded approximately 5.7 ms at 512² and 7.3 ms at
1024² on the development Mac. Those historical timings are informational, not a
threshold or a current-speed claim. Current assertions report per-size state and
display digests, operation success, zero warm pure allocations and zero retained
pure bytes after teardown. Runtime success requires replay and explicit teardown
sentinels plus a clean engine/Host exit.

A GPU compute implementation is a reasonable comparison for this workload, but
one is not shipped or measured here. Neither is a same-algorithm GDScript
baseline. This sample demonstrates an explicit CPU data layout and coarse engine
transport; it cannot establish a CPU/GPU crossover point or a language speedup.

Raw in-engine timing samples are saved in the isolated run HOME as
`slime-profile.csv`. Sample arrival N records the interval from frame entry N−1
to N and the callback for step N; for an even-length run the latter-half frame
intervals cover one step earlier than the callback samples. They include scene
work, rendering/submission waits, pacing and instrumentation, not GPU execution
time in isolation. Timing buffers are allocated before the run and written after
measurement. Measurement protocol 2 captures the final submitted frame after
`frame_post_draw`, outside timing, then starts verification and replay. The older
receipts include two intermediate screenshot stalls; they remain unchanged.
Release selects `-o:speed`
for the native Host/image while debug selects `-debug`.

Windowed evals wait for window focus before their first simulation step, then
require focus at every measured frame entry. The runner adds
`--eval-start-on-click`: click the scene to start once its window is forward. A focus failure preserves raw samples but rejects the result; headless
runs do not qualify frame/rendering performance.

Eval launches disable VSync and the engine FPS cap, then check the requested
state. Ordinary play retains project presentation settings. Driver/compositor
waits may remain even when the engine reports VSync disabled.

Set `GODODIN_EVAL_PROFILE_RENDER=1` on the windowed runner to also save
`slime-render-profile.csv`. This opt-in diagnostic records viewport CPU/GPU
time, render setup time, draw counts and stats callback cost. Dev5's GL timer
results are buffered; their distributions cannot be added row by row to the
callback timings. Viewport timings exclude screen blit/swap, and instrumentation
overhead remains in frame intervals. A separate pre/post-draw wall bracket
includes renderer submission, screen blit/swap and draw callbacks; it is not GPU
execution time. This diagnostic requires synchronous rendering and refuses a
separate render thread, whose deferred callbacks would invalidate that bracket.
An all-zero series is reported unavailable,
not as proof that rendering is free. Stats timing covers the ordinary HUD refresh;
older receipts explicitly include the full-state digest in that refresh.

## Historical captures

The checked-in screenshot and `eval/slime-colony-evidence.json` are historical
baseline evidence. Follow-up runs retain their own logs and captures; they do not
overwrite the original evidence.
