Gododin Gododin Documentation

Example project

Boid Flock (Native)

examples/boid_flock/README.md

View original Markdown · Download · All examples

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.

Historical 10,000-agent capture; retained from the original evaluation
Historical 10,000-agent capture; retained from the original evaluation
Open image at full size

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.

sh
./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 population

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