Gododin Gododin Documentation

Linked repository source

examples/boid_flock/declarations/flock.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 declarations

import "core:mem"
import "core:math"

import raw "gododin:bindings/raw"
import godot "gododin:bindings/typed/godot"
import gd "gododin:bindings/typed/call"
import render "gododin:bindings/typed/render"
import flock "../subsystem"

// Flock drives the boid-flock workload: one authored Node3D whose retained
// state is a pure-Odin simulation owner plus one float32 staging owner, so
// every frame the simulation steps once, the transform writer fills exactly
// one staged store, and one multimesh_set_buffer call submits it. The
// MultiMesh resource itself stays scene-owned — this class retains no RID it
// must free. Explicit shutdown retires active and pending replacement owners.
//
// The reviewed render facade is bound for this project by discovery, exactly
// like the multimesh fixture's Stream_Field: the staging owner lives in the
// facade package and this file references it, so the bridge installs the
// facade API and its raw support kit for Main authority even though no render
// virtual exists.
//
// Frames come from one of two drivers, the fixture's split. Interactive runs
// submit inside _process, the ordinary authored virtual. The measurement
// campaign drives submit_frame from GDScript instead, so the driver can
// bracket exactly the staged frame — one simulation step, one writer pass,
// one publish, one server call — with the engine's own microsecond clock.
//
// Fixed timestep: the simulation never reads a clock. submit_frame advances
// it by FIXED_DT every call, so the driver's frame pacing changes speed only,
// never trajectory, and the same seed replays identically in or out of
// engine.
FIXED_DT :: 1.0 / 60.0

// Flock_Settings is the native settings Resource the Inspector edits and the
// saved scene retains through the Flock's `settings` property. It carries the
// seeded-population facts (count, seed) and the live steering configuration
// (separation, alignment, cohesion, perception, max speed) exactly as
// Flock_Set_Params validates them. The scene initializes its controls from
// this Resource; interactive slider edits are per-run and never write back to
// the shared .tres.
@(gododin_class="Resource")
Flock_Settings :: struct {
	count:           i64 `godot:"property"`,
	population_seed: i64 `godot:"property"`,
	separation:      f64 `godot:"property"`,
	alignment:       f64 `godot:"property"`,
	cohesion:        f64 `godot:"property"`,
	perception:      f64 `godot:"property"`,
	max_speed:       f64 `godot:"property"`,
}

@(gododin_class="Node3D")
Flock :: struct {
	capacity:         i64,
	population_seed:  i64 `godot:"property"`,
	frame:            i64,
	frames_submitted: i64,
	driver_driven:    bool,
	allocator:        mem.Allocator, // the Host-provided allocator handed to init
	params:           flock.Params,
	// The scene-owned settings Resource: the saved authoring truth the
	// scene's controls initialize from (EX-BOID).
	settings: gd.Ref(Flock_Settings) `godot:"property"`,
	// One stream owns the whole submission pipeline for the scene's RID:
	// the staged store, the pending retirement, and the write/publish/
	// submit sequence. The RID itself stays scene-owned inside it.
	stream: render.MultiMesh_Stream,
	sim:    ^flock.Flock,
	retiring_sim: ^flock.Flock,
	last_error: i64,
	// The written windows of the last submitted frame for the three
	// representative instances, saved during submit_frame so `verify` can
	// compare the engine's readback bitwise against exactly the floats this
	// frame wrote — the simulation cannot be recomputed after the fact
	// because it has already stepped on.
	verify_windows: [3 * flock.FLOATS_PER_INSTANCE]f32,
}

// init zeroes the instance; it owns nothing yet. deinit releases the
// simulation arena if a cooperative shutdown never came: the teardown hook
// has no authority over the staging owner (its destructor needs the facade),
// so the staging contract stays cooperative exactly like the fixture's, but
// the pure-Odin arena can always be freed symmetrically here.
flock_init :: proc(self: ^Flock, allocator: mem.Allocator) -> bool {
	self^ = Flock{allocator = allocator}
	return true
}

flock_deinit :: proc(self: ^Flock) {
	if self.sim != nil {
		_ = flock.flock_destroy(&self.sim)
	}
	if self.retiring_sim != nil do _ = flock.flock_destroy(&self.retiring_sim)
}

// Flock_Setup binds the scene's MultiMesh resource, allocates the requested
// instance capacity through the engine's own server API, creates the retained
// staging owner for that capacity, and creates the seeded simulation. The
// RID it yields is a callback-scoped fact; only the RID value, plain numbers,
// and the two owners stay in authored state.
//
// Errors: -1 call validation, -2 capacity range, -3 already set up,
// -4 self, -5 Node3D cast, -6 parent, -7 missing parent, -8 unused
// (child fetch merged into -9), -9 no MultiMeshInstance3D child at the
// scene's declared MMI path, -10 MultiMeshInstance3D cast, -11 base read,
// -12 unused (the stream open owns the server calls), -13 stream open failed
// (server allocation or staged store creation), -14 facade,
// -15 unused (merged into -13), -16 simulation creation.
@(gododin)
flock_setup :: proc(self: ^Flock, call: gd.Main_Call, count: i64, population_seed: i64) -> i64 {
	if gd.validate_main_call(call) != .None do return -1
	if count <= 0 || count > 1_000_000 do return -2
	if render.multimesh_stream_owned(&self.stream) || self.sim != nil || self.retiring_sim != nil do return -3
	object, ok := gd.self(call)
	if !ok do return -4
	n3d, cast_ok := godot.cast_Node3D(gd.godot_main_call(call), &object)
	if !cast_ok do return -5
	// The scene owns its MultiMesh: this node's child named "MMI" (see
	// main.tscn) is the workload's MultiMeshInstance3D, and its server base is
	// the RID. Nothing here constructs or frees server storage. The name is
	// the scene's explicit binding, so adding or reordering siblings cannot
	// silently rebind the workload.
	child, child_ok := child_by_name(gd.godot_main_call(call), cast(^godot.Node)n3d, "MMI")
	if !child_ok do return -9
	scene_object := godot.Object{ptr = child.ptr}
	mmi, mmi_ok := godot.cast_MultiMeshInstance3D(gd.godot_main_call(call), &scene_object)
	if !mmi_ok do return -10
	base, base_error := godot.visual_instance3d_get_base(gd.godot_main_call(call), cast(^godot.VisualInstance3D)mmi)
	if base_error != .None do return -11
	api, facade_ok := gd.main_facade(call)
	if !facade_ok do return -14
	stream, open_error := render.multimesh_stream_open(
		render.main_call(api),
		base.value,
		render.MultiMesh_Layout{transform_3d = true},
		int(count),
	)
	if open_error != .None do return -13
	sim, sim_ok := flock.flock_create(i32(count), population_seed, flock.DEFAULT_PARAMS, self.allocator)
	if !sim_ok {
		// Keep the foreign owner reachable even if its retirement itself fails.
		self.stream = stream
		_ = flock_shutdown(self, call)
		return -16 - i64(flock.create_status)
	}
	self.stream = stream
	self.capacity = count
	self.population_seed = population_seed
	self.frame = 0
	self.frames_submitted = 0
	self.driver_driven = false
	self.last_error = 0
	self.params = flock.DEFAULT_PARAMS
	self.sim = sim
	return 0
}

// Flock_Set_Params live-updates the steering weights, perception radius, and
// speed cap — the reload journey's knobs. The world extents and the arena
// sizing are creation facts and stay untouched, and the perception floor is
// the one the simulation arena was sized for. The authored surface carries
// the engine's f64 floats; each is validated in f64 and narrowed once to the
// simulation's f32 here, at the boundary, never inside the hot loop.
//
// Errors: -1 call validation, -2 not set up, -3 invalid narrowed parameters.
@(gododin)
flock_set_params :: proc(self: ^Flock, call: gd.Main_Call, separation, alignment, cohesion, perception, max_speed: f64) -> i64 {
	if gd.validate_main_call(call) != .None do return -1
	if self.sim == nil do return -2
	candidate := self.params
	candidate.separation = f32(separation)
	candidate.alignment = f32(alignment)
	candidate.cohesion = f32(cohesion)
	candidate.perception = f32(perception)
	candidate.max_speed = f32(max_speed)
	if !flock.params_valid(candidate) do return -3
	self.params = candidate
	return 0
}

// Flock_Set_Predator arms or clears the fleeing target in world coordinates.
// With `active` false the predator position is stored but ignored.
//
// The authored surface carries the engine's f64 floats, narrowed once to the
// simulation's f32 here at the boundary.
//
// Errors: -1 call validation, -2 not set up, -3 invalid narrowed coordinates.
@(gododin)
flock_set_predator :: proc(self: ^Flock, call: gd.Main_Call, x, y, z: f64, active: bool) -> i64 {
	if gd.validate_main_call(call) != .None do return -1
	if self.sim == nil do return -2
	candidate := self.params
	candidate.predator = {f32(x), f32(y), f32(z)}
	candidate.predator_active = active
	if !flock.params_valid(candidate) do return -3
	self.params = candidate
	return 0
}

// Flock_Submit_Frame is one workload frame: the simulation steps by FIXED_DT,
// the transform writer fills the staged store, the publisher releases it, and
// one coarse server call submits it. The written windows of the three
// representative instances are saved for `verify`. Nothing per-instance
// crosses the boundary, nothing allocates, and the engine consumes the
// submission synchronously, so the next frame rewrites the same store.
//
// Errors: -1 call validation, -2 not set up, -3 facade, -4 staging write,
// -5 transform write refused, -6 publish, -7 unused (owned by submit),
// -8 submit refused (no new publication, zero RID handle, or refused engine call).
@(gododin)
flock_submit_frame :: proc(self: ^Flock, call: gd.Main_Call) -> i64 {
	if gd.validate_main_call(call) != .None do return -1
	if !self.stream.staging.live || self.sim == nil || self.capacity <= 0 do return -2
	api, facade_ok := gd.main_facade(call)
	if !facade_ok do return -3
	view, write_error := render.multimesh_stream_write(render.main_call(api), &self.stream)
	if write_error != .None do return -4
	if !flock.flock_step(self.sim, f32(FIXED_DT), self.params) do return -5
	if !flock.flock_write_transforms(self.sim, view) do return -5
	for representative, position in ([3]i64{0, self.capacity / 2, self.capacity - 1}) {
		source := int(representative) * flock.FLOATS_PER_INSTANCE
		for component in 0..<flock.FLOATS_PER_INSTANCE {
			self.verify_windows[position*flock.FLOATS_PER_INSTANCE + component] =
				view[source + component]
		}
	}
	if _, publish_error := render.multimesh_stream_publish(&self.stream, .Main); publish_error != .None do return -6
	if submit_error := render.multimesh_stream_submit(render.main_call(api), &self.stream); submit_error != .None do return -8
	self.frame += 1
	self.frames_submitted += 1
	return 0
}

// Flock_Process drives the autonomous path: when the scene is not
// driver-driven, _process itself submits the frame. With driver-driven
// submission on, this virtual submits nothing, so the measured loop owns
// every submission.
@(gododin_virtual="_process")
flock_process :: proc(self: ^Flock, call: gd.Main_Call, delta: f64) {
	_ = delta
	if self.driver_driven || self.last_error != 0 || self.sim == nil do return
	self.last_error = flock_submit_frame(self, call)
}

// Flock_Set_Driver_Driven hands the frame loop to the GDScript driver for the
// measurement campaign, or returns it to the autonomous virtual.
//
// Errors: -1 call validation.
@(gododin)
flock_set_driver_driven :: proc(self: ^Flock, call: gd.Main_Call, driven: bool) -> i64 {
	if gd.validate_main_call(call) != .None do return -1
	self.driver_driven = driven
	return 0
}

// Flock_Set_Count exercises the explicit agent-count change: a new simulation
// arena and a new staging owner are created first, the same scene-owned RID
// is re-allocated at the new capacity through the server, and the old owners
// retire only after the whole change succeeded — the stream_field_set_capacity
// pattern, extended to the simulation owner. Failures before the engine resize leave the previous active state. After
// the engine extent changes, the matching new owners are adopted; failed old
// retirement remains owned and is retried by shutdown or the next resize. The new
// simulation restarts at its seeded frame zero, which is the reload journey's
// determinism demo at a new size.
//
// Errors: -1 call validation, -2 capacity range, -3 not set up, -4 facade,
// -5 simulation creation, -6 stream resize failed (staged store creation,
// server re-allocation, or a pending retirement that refused to die),
// -7/-8 unused (folded into -6), -9 pending owner retirement.
@(gododin)
flock_set_count :: proc(self: ^Flock, call: gd.Main_Call, count: i64) -> i64 {
	if gd.validate_main_call(call) != .None do return -1
	if count <= 0 || count > 1_000_000 do return -2
	if !self.stream.staging.live || self.sim == nil do return -3
	api, facade_ok := gd.main_facade(call)
	if !facade_ok do return -4
	if !retire_previous(self, call) do return -9
	sim, sim_ok := flock.flock_create(i32(count), self.population_seed, self.params, self.allocator)
	if !sim_ok do return -5
	// Create-first inside the stream: the new store and the server
	// re-allocation succeed before anything is adopted; a refusing pending
	// retirement fails here with the new capacity already live.
	if resize_error := render.multimesh_stream_resize(render.main_call(api), &self.stream, int(count)); resize_error != .None {
		self.retiring_sim = sim
		_ = retire_previous(self, call)
		return -6
	}
	// Engine extent has changed: adopt the matching simulation. A failed
	// retirement stays reachable and is retried by shutdown/resize.
	self.retiring_sim = self.sim
	self.sim = sim
	self.capacity = count
	self.frame = 0
	self.last_error = 0
	self.verify_windows = {}
	if !retire_previous(self, call) do return -9
	return 0
}

// Flock_Verify reads the engine's complete packed transform buffer and compares
// three representative windows bitwise with the last submitted frame. The bulk
// getter is deliberate: in dev5 Compatibility the individual transform getter
// creates a CPU cache that later retains submissions, invalidating staging reuse.
// This readback allocates/stalls outside measurement and releases its owned array
// before returning, so verification can be followed by more submitted frames.
//
// Requires transform-only MultiMesh data, physics interpolation off, and calls
// on the render thread. See dev5 drivers/gles3/storage/mesh_storage.cpp:1934,
// 2092, 2133: bulk readback does not create the per-instance data cache.
// Errors: -1 call validation, -2 not set up, -3 nothing submitted,
// -4 rendering server, -5 facade, -10 bulk getter, -11 readback extent/view,
// -12 readback release, -(20+i) bitwise mismatch for representative i.
@(gododin)
flock_verify :: proc(self: ^Flock, call: gd.Main_Call) -> i64 {
	if gd.validate_main_call(call) != .None do return -1
	if self.capacity <= 0 || self.sim == nil do return -2
	if self.frame == 0 do return -3
	engine_call := gd.godot_main_call(call)
	server, server_error := godot.singleton_rendering_server(engine_call)
	if server_error != .None do return -4
	api, facade_ok := gd.main_facade(call)
	if !facade_ok do return -5
	owned, get_error := godot.rendering_server_multimesh_get_buffer(engine_call, &server, self.stream.rid)
	if get_error != .None do return -10
	// This facade value only borrows `owned`; never destroy both representations.
	readback := render.Packed_Float32_Array{value=godot.owned_value_borrow(&owned), live=true}
	base, length, read_error := render.packed_float32_array_read_base(api, &readback)
	result := i64(0)
	if read_error != .None || base == nil || length != int(self.capacity)*flock.FLOATS_PER_INSTANCE {
		result = -11
	} else {
		stored := mem.slice_ptr(base, length)
		for representative, position in ([3]i64{0, self.capacity / 2, self.capacity - 1}) {
			written := self.verify_windows[position*flock.FLOATS_PER_INSTANCE : (position+1)*flock.FLOATS_PER_INSTANCE]
			start := int(representative)*flock.FLOATS_PER_INSTANCE
			for value, component in written {
				if transmute(u32)value != transmute(u32)stored[start+component] {
					result = -(20+i64(position))
					break
				}
			}
			if result != 0 do break
		}
	}
	if godot.owned_value_destroy(engine_call.api, &owned) != .None do return -12
	return result
}

// Flock_Shutdown retires the retained owners explicitly: the staging owner
// through the facade and the simulation arena through its own allocator. The
// teardown hook has no authority, so cooperative shutdown — the compositor's
// pattern — is the symmetric cleanup these owners' lifetime contract
// requires. It tolerates a partial state and reports retired-both success.
//
// Errors: -1 call validation, -2 stream or staged-store retirement,
// -3 arena release.
@(gododin)
flock_shutdown :: proc(self: ^Flock, call: gd.Main_Call) -> i64 {
	if gd.validate_main_call(call) != .None do return -1
	if !retire_previous(self, call) do return -2
	if !render.multimesh_stream_owned(&self.stream) && self.sim == nil do return 0
	if render.multimesh_stream_owned(&self.stream) {
		api, facade_ok := gd.main_facade(call)
		if !facade_ok do return -2
		if close_error := render.multimesh_stream_close(render.main_call(api), &self.stream); close_error != .None do return -2
	}
	if self.sim != nil {
		if !flock.flock_destroy(&self.sim) do return -3
	}
	self.capacity = 0
	self.frame = 0
	return 0
}

@(gododin)
flock_count :: proc(self: ^Flock) -> i64 {
	return self.capacity
}

@(gododin)
flock_frames_submitted :: proc(self: ^Flock) -> i64 {
	return self.frames_submitted
}

@(gododin)
flock_max_neighbors :: proc(self: ^Flock) -> i64 {
	return i64(flock.max_neighbors_observed(self.sim))
}

@(gododin)
flock_staging_live :: proc(self: ^Flock) -> bool {
	return render.multimesh_stream_owned(&self.stream)
}

// retire_previous preserves failed cleanup owners for a later retry. The
// stream's pending retirement lives inside the stream owner now, so this
// handles only the simulation.
retire_previous :: proc(self: ^Flock, call: gd.Main_Call) -> bool {
	if self.retiring_sim != nil {
		if !flock.flock_destroy(&self.retiring_sim) do return false
	}
	return true
}

// Flock_Error reports the last autonomous submit failure. The scene stops and
// retires its owners instead of silently continuing with stale rendering.
@(gododin)
flock_error :: proc(self: ^Flock) -> i64 { return self.last_error }

// Flock_State_Bytes copies the nine SoA state arrays through the borrowed-byte
// result path for initialization and evaluation only. Godot owns its result;
// this coarse snapshot is never part of the measured staged frame.
@(gododin)
flock_state_bytes :: proc(self: ^Flock) -> []u8 {
	if self.sim == nil do return nil
	return (cast([^]u8)raw_data(self.sim.px))[:int(self.sim.count)*9*size_of(f32)]
}

// Flock_Verify_State compares a GDScript one-step result with native state.
// The caller must begin both steps from identical state. f32 arithmetic and
// accumulation differences use an explicit tolerance; this is not bitwise replay.
// Errors: -1 shape/state, -2 tolerance, -(10+component) invalid or mismatched value.
@(gododin)
flock_verify_state :: proc(self: ^Flock, state: []f32, tolerance: f64) -> i64 {
	if self.sim == nil || len(state) != int(self.sim.count)*9 do return -1
	if !(tolerance > 0 && tolerance <= 0.001) do return -2
	for array, a in ([9][]f32{self.sim.px, self.sim.py, self.sim.pz, self.sim.vx, self.sim.vy, self.sim.vz, self.sim.fx, self.sim.fy, self.sim.fz}) {
		for value, i in array {
			actual := state[a*int(self.sim.count)+i]
			if !flock.finite(value) || !flock.finite(actual) || math.abs(f64(value)-f64(actual)) > tolerance do return -(10+i64(a))
		}
	}
	return 0
}


// child_by_name resolves one direct child of `node` by the scene's declared
// name through the generated surface alone: the interface's own Latin-1
// StringName constructor, the generated String and NodePath constructors, and
// get_node_or_null. Every intermediate engine value is released on every
// path, including the constructor-slot binds when they fail after an
// allocation. This is a bind-time helper, never a hot path. ok is false when
// the call is stale, any engine step refuses, or the path resolves to
// nothing; the caller's class validation distinguishes a wrong-kind target.
child_by_name :: proc(call: godot.Call, node: ^godot.Node, name: cstring) -> (godot.Node, bool) {
	if error := godot.validate_call(call); error != .None do return {}, false
	api := call.api
	name_sn: raw.StringName
	api.string_name_new_latin1(
		raw.GDExtensionUninitializedStringNamePtr(rawptr(&name_sn)),
		transmute(^i8)name,
		0,
	)
	defer api.string_name_destructor(godot.type_pointer(rawptr(&name_sn)))
	if godot.bind_construct_string_from_string_name(api) != .None do return {}, false
	text, text_error := godot.construct_string_from_string_name(call, name_sn)
	if text_error != .None do return {}, false
	if godot.bind_construct_node_path_from_string(api) != .None {
		_ = godot.owned_string_destroy(api, &text)
		return {}, false
	}
	path, path_error := godot.construct_node_path_from_string(call, text.value)
	text_destroy_error := godot.owned_string_destroy(api, &text)
	if path_error != .None || text_destroy_error != .None do return {}, false
	defer godot.owned_value_destroy(api, &path)
	found, found_error := godot.node_get_node_or_null(call, node, path.value)
	if found_error != .None do return {}, false
	return found, found.ptr != nil
}