Linked repository source
examples/bunny_mark/subsystem/bunny.odin
This read-only view comes from the file in the Gododin repository. It retains its design, example or evidence context.
Download the original file · Return to the guide
// Package bunny owns the CPU bunny population and its retained SoA storage.
// No Godot/Gododin/Raylib imports; stepping and packing allocate nothing.
//
// RenderingServer's TRANSFORM_2D buffer uses eight floats without color/custom
// data: xx,yx,0,ox,xy,yy,0,oy. The pinned dev5 mesh_storage.cpp lines
// 1607–1610, 1963–1970 and 2081–2086 establish the stride, writer and getter.
package bunny
import "core:math"
import "core:mem"
STRIDE :: 8
TRANSFORM_2D_FLOATS :: 6
MAX_COUNT :: 1_000_000
CANONICAL_BOUNDS_X :: f32(1280)
CANONICAL_BOUNDS_Y :: f32(720)
CANONICAL_SPEED_SCALE :: f32(1)
CANONICAL_GRAVITY :: f32(900)
CANONICAL_DT :: f32(1.0 / 60.0)
Step_Profile :: struct {dt, bounds_x, bounds_y, speed_scale, gravity: f32}
canonical_profile :: proc() -> Step_Profile {
return {CANONICAL_DT, CANONICAL_BOUNDS_X, CANONICAL_BOUNDS_Y, CANONICAL_SPEED_SCALE, CANONICAL_GRAVITY}
}
profile_equal :: proc(a, b: Step_Profile) -> bool {
return a.dt == b.dt && a.bounds_x == b.bounds_x && a.bounds_y == b.bounds_y && a.speed_scale == b.speed_scale && a.gravity == b.gravity
}
// Admit dt in (0,1], bounds in (0,1000000], speed in [0,10], gravity in
// [0,10000]. These bounds prevent finite-but-overflowing arithmetic.
valid_profile :: proc(p: Step_Profile) -> bool {
for v in ([5]f32{p.dt, p.bounds_x, p.bounds_y, p.speed_scale, p.gravity}) {
if math.is_nan(v) || math.is_inf(v) do return false
}
return p.dt > 0 && p.dt <= 1 && p.bounds_x > 0 && p.bounds_x <= 1000000 && p.bounds_y > 0 && p.bounds_y <= 1000000 && p.speed_scale >= 0 && p.speed_scale <= 10 && p.gravity >= 0 && p.gravity <= 10000
}
Bunny_Parameter :: struct {x0, y0, vx0, vy0, scale: f32}
// bunny_parameter is mirrored by the GDScript baseline and checked by runtime
// replay. Integer coordinates/velocities and dyadic scale avoid trig/PRNG drift
// across languages. All products fit signed i64 before the GDScript u32 mask.
bunny_parameter :: proc(index: i64, population_seed: u32) -> Bunny_Parameter {
x := population_seed ~ (u32(index) * 2654435761)
x = x * 1664525 + 1013904223
px := 40 + (x >> 16) % 1200
x = x * 1664525 + 1013904223
py := 40 + (x >> 16) % 640
x = x * 1664525 + 1013904223
vx := i32((x >> 16) % 481) - 240
x = x * 1664525 + 1013904223
vy := i32((x >> 16) % 481) - 240
x = x * 1664525 + 1013904223
return {f32(px), f32(py), f32(vx), f32(vy), 0.5 + f32(x >> 24)/256.0}
}
// Field owns one allocation for five SoA rows and one for the owner itself.
// No pointers into the arena survive destroy. Canonical history records whether
// the seed/frame replay oracle applies; custom profiles still simulate normally.
Field :: struct {
allocator: mem.Allocator,
count: i64,
population_seed: u32,
frame: i64,
arena, x, y, vx, vy, scale: []f32,
profile: Step_Profile,
profile_uniform: bool,
}
field_create :: proc(count: i64, seed: u32) -> ^Field {
return field_create_with_allocator(count, seed, context.allocator)
}
field_create_with_allocator :: proc(count: i64, seed: u32, allocator: mem.Allocator) -> ^Field {
if count <= 0 || count > MAX_COUNT || allocator.procedure == nil do return nil
arena, err := make([]f32, 5*int(count), allocator)
if err != nil do return nil
f, owner_err := new(Field, allocator)
if owner_err != nil {delete(arena, allocator); return nil}
n := int(count)
f^ = {allocator=allocator, count=count, population_seed=seed, arena=arena,
x=arena[:n], y=arena[n:2*n], vx=arena[2*n:3*n], vy=arena[3*n:4*n], scale=arena[4*n:],
profile=canonical_profile(), profile_uniform=true}
for i in 0..<n {
p := bunny_parameter(i64(i), seed)
f.x[i], f.y[i], f.vx[i], f.vy[i], f.scale[i] = p.x0, p.y0, p.vx0, p.vy0, p.scale
}
return f
}
// field_destroy consumes and clears the owner's pointer. Repeated cleanup is
// safe through that same owner; copied pointers/slices expire on the first call.
field_destroy :: proc(owner: ^^Field) -> bool {
if owner == nil || owner^ == nil do return true
f := owner^
allocator := f.allocator
if f.arena != nil {
if delete(f.arena, allocator) != nil do return false
f.arena = nil
}
if mem.free(f, allocator) != nil do return false
owner^ = nil
return true
}
// bounce_axis is the common semi-implicit Euler wall rule: clamp an overshoot
// and invert that component's velocity. This intentionally discards overshoot;
// it is a discrete bunnymark workload, not an exact collision solver.
bounce_axis :: proc(pos_in, vel_in, size: f32) -> (f32, f32) {
if pos_in > size do return size, -vel_in
if pos_in < 0 do return 0, -vel_in
return pos_in, vel_in
}
// integrate uses explicit f32 temporaries and operation order shared with
// GDScript Vector2 arithmetic. Speed scales both axes' displacement, while
// gravity changes velocity. Changing settings continues from current state.
integrate :: proc(x, y, vx, vy: f32, profile: Step_Profile) -> (f32, f32, f32, f32) {
gravity_step := profile.gravity * profile.dt
new_vy := vy + gravity_step
travel_dt := profile.dt * profile.speed_scale
dx := vx * travel_dt
dy := new_vy * travel_dt
px, out_vx := bounce_axis(x + dx, vx, profile.bounds_x)
py, out_vy := bounce_axis(y + dy, new_vy, profile.bounds_y)
return px, py, out_vx, out_vy
}
field_step :: proc(f: ^Field, dt, bounds_x, bounds_y, speed_scale, gravity: f32) -> bool {
p := Step_Profile{dt, bounds_x, bounds_y, speed_scale, gravity}
if f == nil || !valid_profile(p) do return false
if f.frame == 0 {f.profile = p} else if !profile_equal(f.profile, p) {
f.profile = p
f.profile_uniform = false
}
f.frame += 1
for i in 0..<int(f.count) {
f.x[i], f.y[i], f.vx[i], f.vy[i] = integrate(f.x[i], f.y[i], f.vx[i], f.vy[i], p)
}
return true
}
// field_write_layout only packs live state. It neither advances nor replays it.
field_write_layout :: proc(f: ^Field, sink: []f32) -> bool {
if f == nil || i64(len(sink)) != f.count*STRIDE do return false
for i in 0..<int(f.count) {
sink[i*STRIDE+0] = f.scale[i]
sink[i*STRIDE+1] = 0
sink[i*STRIDE+2] = 0
sink[i*STRIDE+3] = f.x[i]
sink[i*STRIDE+4] = 0
sink[i*STRIDE+5] = f.scale[i]
sink[i*STRIDE+6] = 0
sink[i*STRIDE+7] = f.y[i]
}
return true
}
// reference_state independently replays one canonical bunny in O(frames).
// It intentionally calls neither field_step nor integrate/bounce_axis nor the
// layout writer. Shared seed identity is checked with fixed vectors in tests.
reference_state :: proc(index, frames: i64, seed: u32) -> Bunny_Parameter {
p := bunny_parameter(index, seed)
for _ in 0..<frames {
gravity_step := f32(900) * CANONICAL_DT
p.vy0 = p.vy0 + gravity_step
dx := p.vx0 * CANONICAL_DT
dy := p.vy0 * CANONICAL_DT
p.x0 = p.x0 + dx
p.y0 = p.y0 + dy
if p.x0 > 1280 {p.x0=1280; p.vx0=-p.vx0} else if p.x0 < 0 {p.x0=0; p.vx0=-p.vx0}
if p.y0 > 720 {p.y0=720; p.vy0=-p.vy0} else if p.y0 < 0 {p.y0=0; p.vy0=-p.vy0}
}
return p
}
// oracle_instance compares exact float bits, including signed zero. It accepts
// either padded server storage or the engine getter's six-float Transform2D.
oracle_instance :: proc(sink: []f32, index, frames: i64, seed: u32) -> bool {
if frames < 0 || index < 0 do return false
p := reference_state(index, frames, seed)
expected: [STRIDE]f32
if len(sink) == STRIDE {expected = {p.scale, 0, 0, p.x0, 0, p.scale, 0, p.y0}}
else if len(sink) == TRANSFORM_2D_FLOATS {expected = {p.scale, 0, 0, p.scale, p.x0, p.y0, 0, 0}}
else {return false}
for v, i in sink {if transmute(u32)v != transmute(u32)expected[i] do return false}
return true
}
digest :: proc(buffer: []f32) -> u64 {
hash := u64(0xcbf29ce484222325)
for value in buffer {
bits := transmute(u32)value
for shift in 0..<4 {hash = (hash ~ u64((bits >> (u32(shift)*8)) & 0xFF))*0x100000001b3}
}
return hash
}