Gododin Gododin Documentation

Example project

Slime Colony (Native)

examples/slime_colony/README.md

View original Markdown · Download · All examples

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
Historical Slime Colony capture
Open image at full size

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:

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. 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:

  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 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.