Example project
Slime Colony (Native)
examples/slime_colony/README.md
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.

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, the native colony class, and the pure kernel. 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 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:
/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 buildFor 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. 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. 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:
colony_stepadvances one fixed step.colony_renderwrites saturated bytes into the colony's retained display buffer.packed_byte_array_from_bytesconstructs a fresh engine array and copies the display bytes into it in one CPU bulk copy.Image.set_dataassigns that array;ImageTexture.updatesubmits 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 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_frameis 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
verifychecks Image width, height, R8 format, absence of mipmaps and exact byte length, then compares the CPU Image's bytes from Godot'sget_datato 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.