Example project
Boid Flock (Native)
examples/boid_flock/README.md
Watch thousands of agents steer together as a 3D flock. Odin runs the simulation and produces their transforms; Godot owns the scene, camera, material, lighting and UI. One batch call submits the whole flock's transforms each frame.

Try it
Left-drag orbits, wheel zooms, sliders retune parameters and
count, and the predator selector steers agents away from the saved actor —
mouse mode moves the visible actor by click or drag over the ground, patrol mode
lets its deterministic Godot animation fly the saved trajectory. The scene's
controls initialize from the saved FlockSettings Resource
(flock_settings.tres, editable in the Inspector); interactive edits are
per-run and never write back. The scene starts at 5,000 agents, selected from the
population sweep
for appearance and local frame-time headroom. The run benchmark remains 10,000
and the evaluation campaigns disable the predator actor entirely.
Count changes are debounced for mouse, keyboard and wheel input. Only the selected
flock processes, and stats display its actual population and neighbor count.
GDScript is capped at 500 agents for interactive usability; the count label shows
that cap. Switching modes resets population so the requested count is applied.
Read the code
Start with the scene and controls, the native flock class, and the pure simulation. The scene owns the visible predator; its position is an input to the flock simulation.
Build and run
Project setup explains the machine-local configuration.
Ignored .gododin/authority.toml and .gododin/local.toml select the installed
SDK, pinned Odin and Godot. The harness requires configured tools and a current
accepted build; a missing tool or stale artifact is a failure.
./test.sh all # pure tests, doctor, generate/check, accepted debug build
./test.sh play # interactive flock and controls
./test.sh eval # original offline native producer workload; never overwrites evidence
./test.sh run # windowed 10,000-agent native correctness/capture gate
./test.sh sample 5000 600 # same windowed gate at an explicit native populationFor optimized native profiling, use GODODIN_PROFILE=release ./test.sh pipeline
then the same environment on ./test.sh run. 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.
The runner pins --rendering-driver opengl3, and the scene defaults to
Compatibility. The upstream dev5 Metal texture-free crash is tracked in
Godot GH-123394. A headless
run is a separately labeled limited tier: the dummy renderer returns identity
MultiMesh transforms, so it cannot satisfy the rendered readback/capture gate.
The boundary
subsystem/ imports only Odin core packages. Positions, velocities, facing and
acceleration scratch occupy structure-of-arrays storage in one owned allocation.
A two-pass counting sort rebuilds the uniform spatial hash in retained arrays.
The effective cell size is at least the perception radius, with at most 64 cells
per axis; scanning adjacent cells and filtering by distance covers all neighbors.
Every frame gathers separation, alignment and cohesion steering from the old state, then integrates all agents. Per-rule and total acceleration clamps bound the forces. Wall steering and a final position clamp keep boids inside a 60×30×60 volume (approximately 0.0926 agents/unit³ at 10,000 agents). The fixed 1/60 timestep makes same-configuration replay deterministic on the pinned host; interactive simulation speed follows frame rate. This is not an elapsed-time simulation or a cross-platform floating-point determinism claim. Admission also bounds numerical inputs: half-extents/perception ≤1,000,000, speed/weights ≤10,000, absolute predator coordinates ≤10,000,000 and timestep ≤1 second. These limits prevent finite inputs from overflowing intermediate f32 arithmetic; they exceed the demo controls and leave the original workload unchanged.
declarations/flock.odin authors one Node3D class. The scene owns its MultiMesh
resource and RID. Native code owns the pure allocation and a retained
Packed_Float32_Staging store. Each submit advances the simulation, writes
count * 12 floats, publishes once and calls multimesh_set_buffer once. No
per-agent engine call or intermediate native transform buffer occurs there.
Verification and initial comparison snapshots are separate, untimed operations.
Setup retains every created owner until cleanup. Count changes create replacements before resizing the scene's MultiMesh; once the engine extent changes, native state adopts the matching new owners. Failed old-owner retirement remains reachable for retry. Shutdown handles invalid staging whose underlying foreign array is still owned. Normal scene/window exit and failure paths invoke shutdown; Host zero-owner receipts are the final cleanup evidence.
The transform contract comes from the pinned dev5 engine source:
servers/rendering/renderer_rd/storage_rd/mesh_storage.cpp,
_multimesh_allocate_data and _multimesh_instance_get_transform (stride/offset
caches around 1607–1610; readback around 2050–2061). Transform3D with no colors or
custom data uses 12 interleaved float32 values: each basis row followed by its
origin component. The demo pins the Safe rendering thread model, checks that calls run on the
render thread, and disables physics interpolation on the staged MultiMeshInstance.
In this configuration the engine copies a submission before the call returns;
GPU work need not be complete. Separate-thread queuing and interpolation can
retain arrays and are outside this workload.
The GDScript bulk submission (DX-7)
The GDScript baseline's simulation is packed-SoA bulk already; its drawable
submission now matches the native staged arm's shape too — one MultiMesh
buffer assignment per frame (12 floats per instance: basis rows then origin)
instead of per-instance transform writes. The buffer carries exactly what
set_instance_transform wrote, and the untimed readback check still pins the
stored state, so the remaining difference from the native arm is the language
and the submission call — which is the comparison this example claims.
What the comparison means
The GDScript counterpart has a dictionary spatial hash, Vector3 state, two-pass
steering/integration, the same force bounds and timestep, and per-instance
set_instance_transform submission. It receives one native state snapshot at
initialization, outside measurement, so both start from exactly the same floats.
The corrected counterpart is a new comparison series; the original in-place
GDScript algorithm and its different random initialization were not comparable.
Use the same count (for example 400) and frame count in both modes:
-- --eval --mode=native --count=400 --frames=600 and
-- --eval --mode=gd --count=400 --frames=600 on the prepared runtime. This
compares these complete implementations: storage layout, hash representation,
language and per-instance versus bulk submission. It cannot isolate the effect
of language alone. Interactive mode switching applies the GDScript population
cap, so those differently sized flocks are not a comparison of equal work.
Before measurement the driver checks 12 single-step cases at 64 agents and three perception radii, including predator steering. Each starts from identical old state and compares all position, velocity and facing values with an explicit 0.0001 absolute tolerance. This checks the rules despite floating-point expression differences. It does not assert exact long-running trajectories across runtimes.
Measurement and verification
The original native workload remains 1,000/10,000 agents, seed 20260911, 300 warm
frames then 600 measured frames at fixed 1/60. The immutable
original receipt (repository: examples/boid_flock/eval/boid-flock-evidence.json) retains its original timings and
5c5ad827c334db76 240-step digest. Follow-up records describe their actual host,
source/build identities and measured results; no invocation rewrites history.
The offline gate checks every step and transform write, two independent 240-step same-seed digests, a changed-seed control, and tracked zero allocations/frees in warm frames. Both window allocations must retire to zero live bytes. It measures producer CPU work only; no Godot calls, staging transfer or rendering occurs.
The pure suite also includes a separate all-pairs one-step reference covering cell boundaries, walls and perception changes; neighbor counts match exactly and state matches within f32 tolerance. This checks the spatial-hash result, whereas same-implementation replay alone establishes only determinism. Guarded warm tests cover step, writer and query allocations, and literal u32 comparisons check bitwise layout/readback (including signed zero).
In-engine avg_usec measures one simulation/write/submission callback. Separate
frame_avg_usec and frame_p95_usec use monotonic intervals between frame
callbacks, including scene/renderer/pacing costs. The rendered gate verifies
representative windows from an actual bulk MultiMesh buffer readback, then captures after the measurement
window, checks shutdown, and requires a zero-owner Host receipt. The GDScript
counterpart uses approximate transform/facing readback; native staged readback is
bitwise. Native verification uses the bulk buffer getter and releases its result
before returning. Dev5 Compatibility
drivers/gles3/storage/mesh_storage.cpp:1934,2092,2133 shows why: individual
transform getters create a cache that later retains submissions and invalidates
staging reuse. The rendered preflight interleaves three verify/submit cycles to
check that verification permits continued streaming. Readback can allocate and
stall; it stays outside timing. Neither headless correctness nor callback timing
proves visual quality.
A single MultiMesh submission batches instances, but total draw calls can include
multiple surfaces/passes and shadows. The authored prism has one material and the
scene has a shadow-casting sun. Compatibility does not supply every Forward+
post effect; no glow or GPU time claim is inferred from serialized settings.
Both scene-owned MultiMeshes have the same fixed custom_aabb: the simulation's
clamped volume plus a one-unit margin on every side. The unscaled prism's
half-diagonal is below 0.35 units, so this margin covers every orientation.
Changing the simulation bounds or mesh scale requires updating this bound.
In dev5 GLES3, drivers/gles3/storage/mesh_storage.cpp:2118 and :2316 skip
per-instance AABB rebuilding when this explicit bound exists. Godot still culls
the MultiMesh as one object; the bound does not provide per-agent culling.
At higher densities the maximum neighbor count grows, and clustered worst cases
can approach all-pairs cost. No measured crossover to GPU compute is claimed.
Files and evidence
subsystem/: pure simulation, retained storage and regression tests.declarations/: class lifecycle, coarse staging and verification APIs.project/: Godot scene, UI, camera and GDScript counterpart.eval/: offline harness, immutable original receipt and dated review/follow-ups.generated/: compiler-owned catalog/bridge/docs; never hand-edit.
Original review records defects against the preserved baseline. Follow-up evidence distinguishes compiled fixes, exercised behavior and any unavailable rendered/interactive checks.
Raw in-engine timing samples are saved in the isolated run HOME as
boid-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. Boid captures its screenshot after the measured window. 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
boid-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. An all-zero series is reported unavailable,
not as proof that rendering is free.