# Bunny Mark (Native)

Bunny Mark lets you switch between GDScript and Odin while the same seeded
bunnies bounce across the screen. It compares per-sprite and whole-buffer
submission paths in both languages. Godot owns drawing, scene composition, the
texture and UI in every mode. Use it to see where you place the native boundary:
one call per bunny or one call for the whole population.

## Controls and scope

- **Mode** changes the transport path and restarts the seeded population.
- **Count** restarts the current mode at the requested capacity.
- **Speed / Gravity** retune all modes consistently without moving them back to
  their seed positions. The UI labels custom history accurately.
- Stats update every 0.25 seconds; eval controls are disabled.

## Build and run

Use a configured copy with its attached SDK; the separate
[project setup guide](../../docs/setup.md) covers the connection. From the
example directory:

```sh
./test.sh          # pure tests + exact SDK doctor/generate/check/debug build
./test.sh eval     # pure measurement, replay and allocation gates; no receipt writes
./test.sh import   # accepted runtime import/parser check; rendering unverified
./test.sh run      # windowed field/node/GDScript campaign, serialized
./test.sh play     # interactive accepted artifact
```

Set `GODODIN_PROFILE=release` for both the build and subsequent run to measure
optimized native code. The runner separates debug/release acceptance caches and
checks the publication profile. Windowed evals disable VSync and the engine FPS
cap, wait for a scene click, and reject samples collected without window focus.
Ordinary play keeps the project's presentation settings.

`test.sh` uses the engine in the attached SDK lock and **keeps `--rendering-driver opengl3`**
for windowed runs. The project also defaults to Compatibility. This bypasses
[Godot GH-123394](https://github.com/godotengine/godot/issues/123394), the dev5
windowed Metal texture-free corruption. Do not drop the flag based on an
assumed future fix. Prepared runtimes come from the accepted publication with
isolated writable caches and a fresh HOME; check exact engine/build identities
in the retained run log. Run one Godot process at a time and close it fully.

## Read the code

[The scene driver](project/main.gd) selects the mode and owns presentation.
Compare [the native per-node class](declarations/bunny.odin) with
[the native field class](declarations/bunny_field.odin) and its
[pure population kernel](subsystem/bunny.odin). The matching GDScript paths are
[the per-sprite loop](project/gd_bunnies.gd) and
[the bulk loop](project/gd_bunnies_bulk.gd). `generated/` is compiler-owned output;
never edit it by hand.

## The three variants

- **GDScript loop** (`project/gd_bunnies.gd`) uses one Sprite2D per bunny and
  one GDScript driver loop over cached sprite references and packed velocities.
- **Native per-node** (`declarations/bunny.odin`) uses one authored Sprite2D
  per bunny. Each step crosses into Odin, reads the engine position, integrates
  it and writes it back through the typed Node2D surface. This deliberately
  demonstrates the cost of putting the native boundary inside the hot loop.
- **Native staged** (`declarations/bunny_field.odin`) advances the pure SoA
  population, packs live state into a retained `Packed_Float32_Staging` owner,
  publishes it, and calls `multimesh_set_buffer` once. Foreign dispatch count
  is independent of the population size; physics, packing, engine copies and
  drawing still scale with the number of bunnies.

All arms use the same integer seed stream, initial positions/velocities/scales,
14-pixel textured footprint and f32 semi-implicit Euler rule. A wall collision
clamps the overshoot and reverses that velocity component. Speed scales both
axes' displacement; gravity changes vertical velocity. This is a discrete
bunnymark rule, not an exact collision solver. Bounds are 1280×720 and fixed dt
is the exact f32 representation of 1/60. Interactive modes all use the scene's
60 Hz physics driver; eval modes step once per rendered frame for a fixed
amount of comparable work regardless of display pacing. There are no hidden
native `_process` callbacks outside the measured driver bracket.

The pure subsystem imports only Odin core packages. One owned block contains
five SoA rows (x/y/vx/vy/scale); create allocates that block and its owner, while
step and packing allocate nothing. Destroy consumes and clears the owning
pointer. Count or mode changes start the same seed at frame zero. A profile
change continues from current state and makes canonical-history replay
inapplicable until the population is reset.

## The authored preset (EX-BUNNY)

The scene owns an authored `BunnyMarkPreset` Resource
(`bunny_mark_preset.tres`): initial count, speed scale, gravity, population
seed, and the scene-owned texture and draw scale. The interactive controls
initialize from it at ready; **Apply preset** re-reads the assigned Resource
into a full three-arm reinitialization (an Inspector edit never reaches the
simulation on its own), and **Reset** restores the last-applied values. The
seed and presentation thread through all three arms, so every arm launches
from the same seeded identities and draws the same scene-owned texture at
the same scale. Canonical evaluation runs never read the Resource — their
mode, count, and frames stay explicit engine arguments over the canonical
seed and texture. The headless `preset` harness mode
(`verify_preset.gd`) proves initialization, Apply/Reset semantics, and a
saved-preset reopen across all three arms.

## The GDScript bulk arm (DX-7)

The comparison keeps four arms: GDScript per-sprite, native per-node, native
staged, and — the maintained optimized GDScript bulk baseline — a GDScript
MultiMesh arm (`gd_bunnies_bulk.gd`) whose state lives in one packed
5-float-per-bunny array and whose frame submits through one MultiMesh buffer
assignment. It shares the identity stream, fixed step, arithmetic order,
texture, scale, and drawable with the other arms, so what it measures against
the native staged arm is exactly the language and the submission call — the
engine's own buffer copy is counted, not hidden. Its packed state feeds the
same `verify_buffer` oracle the other arms use, and a local check
(`verify_bulk.gd`) pins bulk state to the per-sprite arm byte-for-byte after
ten steps. Evaluation mode `gd-bulk` measures it under the same warm/timed
discipline; canonical receipts are historical and every arm is re-measured
before ranking.

## Verification and measurement

This example does not qualify native editor reload or stored-property
restoration. Its controls are ordinary live method calls. It does not claim a
language speedup, a constant total frame cost, one total draw call, cross-platform
bitwise determinism, or GPU throughput. Exact equality is checked on the pinned
compiler/engine/architecture for the named workload.

The pure oracle independently replays one bunny from its seed in O(frames),
without calling the production step, bounce or layout writer. This costs more
than the old circular O(1) check and executes outside timing. Fixed seed vectors,
wall collisions, current-state packing, deliberate corruption, capacity-prefix
identity, bounded inputs, allocation failure and repeated cleanup are tested.

`./test.sh eval` runs the pure workload at 1,000 and 10,000 bunnies, 60 warm
frames and 1,200 measured frames. It repeats both populations, requires exact
state and payload digest equality, checks every final bunny against independent
replay and checks all 1,000 shared capacity-prefix identities. Tracking
allocators cover both context.allocator and context.temp_allocator and require
zero allocations/frees during warm producer work and zero
remaining bytes after cleanup. Invalid arguments and failed checks exit nonzero.
It never rewrites the historical receipt. Timings are informational; equality,
corruption rejection and ownership checks are hard gates.

The windowed campaign uses all three modes at 1,000 bunnies and 600 total
frames: 300 warm and 300 measured (`frames=600`, `measured_frames=300`). `submit_avg_usec` measures only the driver
step loop, including native boundary calls and submission. `frame_avg_usec` and
`frame_p95_usec` measure wall-clock intervals between consecutive measured
process entries, including scene/rendering/pacing work. They are not GPU
execution timings. A single window is not a controlled cross-language ranking;
MultiMesh versus Sprite2D also changes scene and renderer work, so this is not
an isolated measurement of foreign-call overhead.

After measurement, each mode verifies every actual bunny against the same
canonical replay. GD supplies its live positions/velocities/scales in a packed
input; native nodes read real engine positions and compare their velocity;
staged mode checks SoA state, all staged float bits including padding, and every
engine buffer readback. The driver checks actual textured drawable instances
and count. Staged eval additionally resizes twice, submits/verifies each fresh
population, then reconstructs and verifies the measured state before capture.
The gate saves a fresh `user://bunny-<mode>.png`, checks cleanup, and prints:

```text
GODODIN_BUNNY_MARK_PASS workload=bunny-mark-v2 mode=<gd|node|field> ... verified=<count>
GODODIN_BUNNY_MARK_TEARDOWN_PASS staging_live=false count=0
```

A successful workload sentinel alone is insufficient: the runner also requires
successful engine exit, no engine errors, and a Host receipt with zero instances
and owned foreign values. Headless mode cannot establish staged GPU readback or visual
correctness; it does not produce a rendered campaign PASS.

## Engine layout and ownership

The pinned dev5 source archive's
`servers/rendering/renderer_rd/storage_rd/mesh_storage.cpp:1607–1610` establishes
an eight-float TRANSFORM_2D stride without colors/custom data. Lines 1963–1970
write `xx,yx,0,ox,xy,yy,0,oy`; lines 2081–2086 reconstruct the six-float
Transform2D. Runtime bulk-buffer readback checks the submitted eight-float contract; the
six-float getter mapping remains a pure layout test. These are interleaved matrix rows; “column-major” alone is misleading.
The scene constructs a 14px ArrayMesh with top-left UV(0,0), matching Sprite2D;
its decoded vertices, UVs and triangle indices are asserted before the campaign.
Dev5 PlaneMesh FACE_Z (used by QuadMesh) has the opposite V direction in canvas
coordinates (`scene/resources/3d/primitive_meshes.cpp:1469–1478`).

The scene's MultiMesh has a fixed `custom_aabb` around the 1280×720 simulation
volume with an 11-pixel margin. Every center remains clamped inside that volume,
including after interactive speed/gravity changes; the 14px quad's seed-derived
scale stays below 1.5, so its half-width stays below 10.5 pixels. Changing the
bounds, mesh size or scale requires updating this bound. Dev5 GLES3
`drivers/gles3/storage/mesh_storage.cpp:2118` skips the per-instance bounds scan
when this explicit bound exists. Godot still culls the field as one object.

Native verification uses the bulk buffer getter and releases its result before
returning. Dev5 Compatibility individual transform getters create a CPU cache
that later retains submissions (`drivers/gles3/storage/mesh_storage.cpp:1965,2092,2133`);
using them would invalidate a later retained write. Two extra verify/submit cycles
exercise continued streaming before the capacity checks and final reconstruction.

This demo disables physics interpolation on the staged MultiMeshInstance, pins
the Safe rendering thread model and checks
`RenderingServer.is_on_render_thread()` at startup. Direct same-thread
consumption permits reusing the retained store. A separate render thread can
queue the array (`rendering_server_default.h:457` and
`server_wrap_mt_common.h:267–275`) and is outside this staging workload.

The scene owns the MultiMeshInstance2D, MultiMesh, canvas ArrayMesh and texture. Native
setup temporarily owns the reference returned by `get_multimesh` and releases
it on every subsequent return. Staging and population have explicit native
owners, including setup failures. Resizing creates replacement owners before
server allocation; after successful server resize, replacement state is adopted
before old-owner cleanup. A cleanup failure is fatal and does not claim an
engine rollback. Every scene exit calls native shutdown, including interactive
quit and failed evals. CPU-only deinit is a fallback, not a staging owner release.


## Historical comparison

The original implementation did not meet that comparison: the native per-node
arm had no sprites, and the staged arm used different spawn and motion rules.
The [original review](eval/reviews/2026-09-12-original.md) records those defects.
The corrected workload is named **`bunny-mark-v2`**. It retains the original
seed, counts and fixed timestep but replaces the invalid motion contract.
The old [receipt](eval/bunny-mark-evidence.json) and `screenshot.png` are retained
historical artifacts; neither establishes the corrected workload's performance
or appearance. Follow-up evidence belongs in `eval/followups/` after execution.
