Gododin Gododin Documentation

Linked repository source

examples/slime_colony/declarations/slime_colony.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 is the authored native class package for the slime
// colony example. It is the single source for the Slime_Colony Node: the
// struct owns the real instance state, the unmarked lifecycle hooks own
// acquisition and release, and the marked procedures are the admitted Godot
// methods with real bodies. The image bridge is generated from this package's
// compiler graph.
package declarations

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

import "core:mem"

// Slime_Error is the adapter's named failure vocabulary. A gododin method
// returns one int result: 0 is success, a positive value is a `godot.Call_Error`
// passed through verbatim when the call authority or the bound surface refuses
// (the already-admitted vocabulary, never remapped), and a negative value names
// exactly one cause of this example's own. The caller already knows which
// operation failed; the value names why, and project/main.gd decodes both
// halves for its logs. Values are stable so recorded campaign logs stay
// comparable across builds.
Slime_Error :: enum i32 {
	Bounds                  = -1, // width, height, or agent count outside the subsystem's limits
	Already_Configured      = -2, // owners exist; shutdown or PREDELETE release first
	Not_Configured          = -3, // no live owner set for the requested work
	Sensor_Size_Unsupported = -4, // sensor_size is neither 1 nor 3
	Params_Invalid          = -5, // nonfinite or out-of-range live parameters
	Nutrient_Shape          = -15, // nutrient mask dims outside bounds or len != width*height
	Colony_Allocation       = -6, // subsystem arena creation failed
	CPU_Retirement_Refused  = -7, // an old colony free refused; both owners stay retained
	CPU_Cleanup_Refused     = -8, // cleanup free refused; owners retained for retry
	Self_Unavailable        = -9, // the engine self borrow or its Node projection was unavailable
	Display_Missing         = -10, // no child at the scene's declared Display path
	Display_Not_Sprite      = -11, // the bound child is not a Sprite2D
	Display_Set_Texture     = -12, // the engine refused the texture assignment
	Display_Detach          = -13, // shutdown could not clear the sprite's texture
	Image_Create            = -14, // the engine refused the R8 Image construction
	Texture_Create          = -15, // the engine refused the ImageTexture construction
	Texture_Release         = -16, // shutdown: the owned texture release was refused
	Image_Release           = -17, // shutdown: the owned image release was refused
	Kernel_Failed           = -18, // the colony step or field render refused
	Frame_Array_Create      = -19, // the frame's engine PackedByteArray construction failed
	Image_Set_Data          = -20, // the engine refused the frame upload into the Image
	Texture_Update          = -21, // the engine refused the texture upload
	Frame_Array_Release     = -22, // the frame array's engine destructor was refused
	Readback_Get_Data       = -23, // get_data failed during verify
	Readback_Scratch        = -24, // the verify scratch allocation failed
	Readback_Read           = -25, // the engine's bytes could not be read out
	Readback_Release        = -26, // the readback array's release was refused
	Readback_Mismatch       = -27, // engine bytes differ from the fresh render
	Image_Metadata          = -28, // Image size/format/mipmap metadata differs
	Image_Length            = -29, // the readback byte length differs from the grid
}

// Slime_Colony owns one pure colony, an Image and an ImageTexture. A frame
// advances the fixed-step kernel, renders R8 bytes into its retained arena,
// constructs one engine PackedByteArray, assigns it to the Image and updates
// the texture. No per-agent engine calls or Odin allocations occur. The
// facade performs one CPU copy into the new engine array; renderer upload
// costs are additional. Digest computation is an explicit inspection cost.
//
// The Image retains its PackedByteArray as copy-on-write state. Reacquiring
// that shared array through the facade's retained staging owner can detach
// its base pointer and violate that owner's contract. This implementation
// therefore creates a fresh engine array per upload; it does not prove every
// alternate Image/RenderingDevice staging design impossible or measured.
//
// The scene normally owns frame submission and checks its return code. The
// autonomous virtual remains available and records errors for its caller.
@(gododin_class="Node")
Slime_Colony :: struct {
	colony:        ^slime.Colony,
	retired_colony: ^slime.Colony, // At most one refused retirement, retained for retry.
	params:        slime.Params,
	allocator:     mem.Allocator,
	width:         i64,
	height:        i64,
	agent_count:   i64,
	seed:          i64,
	frames:        i64,
	driver_driven: bool,
	display_bound: bool,
	image:         godot.Owned_Ref(godot.Image),
	texture:       godot.Owned_Ref(godot.ImageTexture),
	last_digest:   u64,
	last_error:    i64,
}

// init captures the image allocator for every later allocation. The context
// allocator is only meaningful inside the init dispatch, so configure and its
// siblings allocate through the captured one — the same shape Cavern Lab's
// workspace uses. It acquires nothing else; every heavy owner is created by
// configure, which can report a named failure to GDScript.
slime_colony_init :: proc(self: ^Slime_Colony, allocator: mem.Allocator) -> bool {
	self.allocator = allocator
	return true
}

// deinit frees whatever pure Odin state still exists. It has no call
// authority, so it can only free the colony — the retained Image and
// ImageTexture references are released by the PREDELETE hook while a
// Main_Call is available, or eagerly through the explicit shutdown method.
// The cooperative shutdown pattern (Cavern Lab, multimesh stream) is the
// symmetric cleanup this owner set requires.
slime_colony_deinit :: proc(self: ^Slime_Colony) {
	_ = slime_colony_destroy_cpu(self)
}

// configure creates the whole owner set for one configuration: the colony in
// its single arena, one zeroed R8 Image of exactly width*height bytes, the
// ImageTexture created from it, and the display Sprite2D child's texture. It
// fails closed: any failure unwinds the owners created so far and returns a
// named result directly to the caller. Rejected bounds are the subsystem's
// own. Returns 0, a `godot.Call_Error` pass-through, or a negative
// `Slime_Error` (Bounds, Already_Configured, Colony_Allocation,
// CPU_Cleanup_Refused, Image_Create, Texture_Create, or bind_display's
// display vocabulary).
@(gododin)
slime_colony_configure :: proc(self: ^Slime_Colony, call: gd.Main_Call, width, height, agent_count, seed: i64) -> i64 {
	if error := gd.validate_main_call(call); error != .None do return i64(error)
	bounds_ok := width >= slime.MIN_DIMENSION && width <= slime.MAX_DIMENSION &&
		height >= slime.MIN_DIMENSION && height <= slime.MAX_DIMENSION &&
		width*height <= slime.MAX_CELLS &&
		agent_count >= slime.MIN_AGENTS && agent_count <= slime.MAX_AGENTS
	if !bounds_ok do return i64(Slime_Error.Bounds)
	if self.colony != nil || self.retired_colony != nil || self.image.live || self.texture.live do return i64(Slime_Error.Already_Configured)
	_, facade_ok := gd.main_facade(call)
	if !facade_ok do return i64(godot.Call_Error.Missing_Interface)

	colony, colony_ok := slime.slime_create(
		i32(width), i32(height), i32(agent_count), u32(seed & 0xFFFFFFFF), self.allocator,
	)
	if !colony_ok do return i64(Slime_Error.Colony_Allocation)
	self.colony = colony // Retain even if later Image creation or cleanup fails.
	image, image_error := godot.image_create(
		gd.godot_main_call(call), i32(width), i32(height), false,
		raw.raw_godot_class_image_enum_format_value_format_r8,
	)
	if image_error != .None || !image.live {
		if !slime_colony_destroy_cpu(self) do return i64(Slime_Error.CPU_Cleanup_Refused)
		return i64(Slime_Error.Image_Create)
	}
	self.colony = colony
	self.image = image
	texture, texture_error := godot.image_texture_create_from_image(gd.godot_main_call(call), &image.handle)
	if texture_error != .None || !texture.live {
		_ = slime_colony_shutdown(self, call)
		return i64(Slime_Error.Texture_Create)
	}
	self.texture = texture
	display_error := slime_colony_bind_display(self, call, &texture.handle)
	if display_error != 0 {
		_ = slime_colony_shutdown(self, call)
		return display_error
	}

	self.colony = colony
	self.params = colony.params
	self.width = width
	self.height = height
	self.agent_count = agent_count
	self.seed = seed & 0xFFFFFFFF
	self.frames = 0
	self.driver_driven = false
	self.display_bound = true
	self.image = image
	self.texture = texture
	self.last_digest = 0
	self.last_error = 0
	return 0
}

// bind_display resolves the scene-owned display Sprite2D by the scene's
// declared node name ("Display", see project/main.tscn) and points it at the
// retained texture — an explicit validated binding, not a child-index
// assumption: the scene may add or reorder siblings without breaking it, and
// a missing child and a wrong-class child are distinct named failures. The
// scene owns the node, its transform, and its material; this class only sets
// the texture. texture may be nil, which detaches it.
slime_colony_bind_display :: proc(self: ^Slime_Colony, call: gd.Main_Call, texture: ^godot.ImageTexture) -> i64 {
	engine_call := gd.godot_main_call(call)
	object, self_ok := gd.self(call)
	if !self_ok do return i64(Slime_Error.Self_Unavailable)
	node, cast_ok := godot.cast_Node(engine_call, &object)
	if !cast_ok do return i64(Slime_Error.Self_Unavailable)
	child, child_ok := child_by_name(engine_call, node, "Display")
	if !child_ok do return i64(Slime_Error.Display_Missing)
	sprite, sprite_ok := godot.cast_Sprite2D(engine_call, &child)
	if !sprite_ok do return i64(Slime_Error.Display_Not_Sprite)
	if set_error := godot.sprite2d_set_texture(engine_call, sprite, cast(^godot.Texture2D)texture); set_error != .None {
		return i64(Slime_Error.Display_Set_Texture)
	}
	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
}

// slime_colony_release_ref releases one owned reference-counted handle the
// constructor handed over, through the typed surface's one generic release
// path: godot.owned_ref_release runs the generated ref_counted_unreference
// operation and completes the last reference with the engine's own
// object_destroy. A refused release retains its live owner for a later
// cleanup attempt; an already-released owner reports success.
slime_colony_release_ref :: proc(self: ^Slime_Colony, call: gd.Main_Call, owner: ^godot.Owned_Ref($T)) -> bool {
	_ = self
	return godot.owned_ref_release(gd.godot_main_call(call), owner) == .None
}

// The engine delivers every notification through the class notification
// callback on the Main thread; PREDELETE is the supported last point with
// valid call authority before the instance is destroyed — the engine sends
// it before the instance's own free callback runs. Releasing the retained
// engine references here makes the cooperative shutdown optional for
// ordinary teardown: a scene that never calls shutdown still exits with
// zero owned references. The explicit shutdown method stays the eager path
// reconfiguration and the teardown probe need while the node is alive. A
// refused release here has no caller to report to; the owner stays retained
// and the engine's own close diagnostics name the leak.
@(gododin_virtual="_notification")
slime_colony_notification :: proc(self: ^Slime_Colony, call: gd.Main_Call, what: i64) {
	if what != raw.raw_godot_class_object_constant_notification_predelete do return
	if gd.validate_main_call(call) != .None do return
	if self.texture.live do _ = slime_colony_release_ref(self, call, &self.texture)
	if self.image.live do _ = slime_colony_release_ref(self, call, &self.image)
	_ = slime_colony_destroy_cpu(self)
}

// set_motion retunes the agent-motion half of the simulation live — the ADR
// 0057 reload journey's sliders — without touching any owner. dt is implicit
// in the params: every quantity is per fixed step. sensor_size is 1 or 3.
//
// The setter is split in two because the stable Host's variant wire carries
// at most eight parameters per call; a nine-parameter setter would be
// refused at dispatch, not degraded.
//
// Returns 0, a `godot.Call_Error` pass-through, or Sensor_Size_Unsupported /
// Params_Invalid. Limits are defined by slime.params_valid.
@(gododin)
slime_colony_set_motion :: proc(
	self:      ^Slime_Colony,
	call:      gd.Main_Call,
	move_speed, turn_speed, sensor_angle, sensor_distance: f64,
	sensor_size: i64,
) -> i64 {
	if error := gd.validate_main_call(call); error != .None do return i64(error)
	if sensor_size != 1 && sensor_size != 3 do return i64(Slime_Error.Sensor_Size_Unsupported)
	params := self.params if self.colony != nil else slime.DEFAULT_PARAMS
	params.move_speed = f32(move_speed)
	params.turn_speed = f32(turn_speed)
	params.sensor_angle = f32(sensor_angle)
	params.sensor_distance = f32(sensor_distance)
	params.sensor_size = i32(sensor_size)
	if !slime.params_valid(params) do return i64(Slime_Error.Params_Invalid)
	self.params = params
	if self.colony != nil do self.colony.params = params
	return 0
}

// set_field retunes the trail-field half of the simulation live: deposit,
// evaporation, steer jitter, and the display gain. decay must be 0..1.
//
// Returns 0, a `godot.Call_Error` pass-through, or Params_Invalid.
// Limits are defined by slime.params_valid.
@(gododin)
slime_colony_set_field :: proc(
	self:  ^Slime_Colony,
	call:  gd.Main_Call,
	deposit, decay, jitter, gain: f64,
) -> i64 {
	if error := gd.validate_main_call(call); error != .None do return i64(error)
	params := self.params if self.colony != nil else slime.DEFAULT_PARAMS
	params.deposit = f32(deposit)
	params.decay = f32(decay)
	params.jitter = f32(jitter)
	params.gain = f32(gain)
	if !slime.params_valid(params) do return i64(Slime_Error.Params_Invalid)
	self.params = params
	if self.colony != nil do self.colony.params = params
	return 0
}

// submit_frame is one workload frame: the kernel steps, the display bytes
// render into the kernel's arena buffer, one engine PackedByteArray is
// constructed from that buffer, the retained Image takes the bytes by
// reference, and one texture update uploads them. Nothing per-agent crosses
// the boundary and no Odin allocation happens.
//
// Returns 0, a `godot.Call_Error` pass-through, Not_Configured, Kernel_Failed,
// or upload_display's transport vocabulary.
@(gododin)
slime_colony_submit_frame :: proc(self: ^Slime_Colony, call: gd.Main_Call) -> i64 {
	if error := gd.validate_main_call(call); error != .None do return i64(error)
	if self.colony == nil || !self.image.live || !self.texture.live do return i64(Slime_Error.Not_Configured)
	if !slime.colony_step(self.colony, self.params) {
		self.last_error = i64(Slime_Error.Kernel_Failed)
		return self.last_error
	}
	self.last_error = slime_colony_upload_display(self, call)
	if self.last_error != 0 do return self.last_error
	self.frames += 1
	return 0
}

// render_frame uploads the current field's display without advancing the
// simulation — the GPU variant's presentation call after its own field
// update (DX-9).
//
// Returns 0, a `godot.Call_Error` pass-through, Not_Configured, or the
// upload vocabulary.
@(gododin)
slime_colony_render_frame :: proc(self: ^Slime_Colony, call: gd.Main_Call) -> i64 {
	if error := gd.validate_main_call(call); error != .None do return i64(error)
	if self.colony == nil || !self.image.live || !self.texture.live do return i64(Slime_Error.Not_Configured)
	self.last_error = slime_colony_upload_display(self, call)
	if self.last_error != 0 do return self.last_error
	self.frames += 1
	return 0
}

// Upload the current field without advancing it, also used when a replacement
// colony starts at frame zero. The retained Image is the CPU readback target.
// Returns 0, a `godot.Call_Error` pass-through, or Kernel_Failed /
// Frame_Array_Create / Image_Set_Data / Texture_Update / Frame_Array_Release.
slime_colony_upload_display :: proc(self: ^Slime_Colony, call: gd.Main_Call) -> i64 {
	api, facade_ok := gd.main_facade(call)
	if !facade_ok do return i64(godot.Call_Error.Missing_Interface)
	display := slime.colony_display(self.colony)
	if !slime.colony_render(self.colony, display) do return i64(Slime_Error.Kernel_Failed)
	frame, frame_error := render.packed_byte_array_from_bytes(api, display)
	if frame_error != .None do return i64(Slime_Error.Frame_Array_Create)
	if set_error := godot.image_set_data(
		gd.godot_main_call(call), &self.image.handle, i32(self.width), i32(self.height), false,
		raw.raw_godot_class_image_enum_format_value_format_r8, frame.value,
	); set_error != .None {
		_ = render.packed_byte_array_destroy(api, &frame)
		return i64(Slime_Error.Image_Set_Data)
	}
	if update_error := godot.image_texture_update(gd.godot_main_call(call), &self.texture.handle, &self.image.handle); update_error != .None {
		_ = render.packed_byte_array_destroy(api, &frame)
		return i64(Slime_Error.Texture_Update)
	}
	if destroy_error := render.packed_byte_array_destroy(api, &frame); destroy_error != .None do return i64(Slime_Error.Frame_Array_Release)
	return 0
}

// _process drives the autonomous path: when the scene is not driver-driven,
// the virtual itself submits the frame. With driver-driven submission on, it
// submits nothing, so a measurement loop owns every submission.
@(gododin_virtual="_process")
slime_colony_process :: proc(self: ^Slime_Colony, call: gd.Main_Call, delta: f64) {
	_ = delta
	if self.driver_driven do return
	if self.colony == nil do return
	self.last_error = slime_colony_submit_frame(self, call)
	if self.last_error != 0 do self.driver_driven = true
}

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

// set_agent_count replaces the colony at the new population. The replacement
// is created first; allocation failure leaves the previous colony running.
// An upload error after replacement is returned to the driver. Identity uses
// seed and index, so shared agents start at identical positions/headings.
// Subsequent paths differ because population changes the shared trail field.
// A successful replacement resets frame metadata and uploads its empty field.
//
// Returns 0, a `godot.Call_Error` pass-through, Not_Configured / Bounds /
// Colony_Allocation / CPU_Retirement_Refused, or upload_display's transport
// vocabulary.
@(gododin)
slime_colony_set_agent_count :: proc(self: ^Slime_Colony, call: gd.Main_Call, agent_count: i64) -> i64 {
	if error := gd.validate_main_call(call); error != .None do return i64(error)
	if self.colony == nil do return i64(Slime_Error.Not_Configured)
	if agent_count < slime.MIN_AGENTS || agent_count > slime.MAX_AGENTS do return i64(Slime_Error.Bounds)
	if replacement_error := slime_colony_replace_cpu(self, agent_count, self.seed); replacement_error != 0 {
		self.last_error = replacement_error
		return self.last_error
	}
	self.last_error = slime_colony_upload_display(self, call)
	return self.last_error
}

// reseed restarts the colony at its seeded frame zero — the example's
// determinism demo: the same seed paints the same picture again, because
// every agent's identity derives from the seed and index and no clock is
// read. Owners other than the colony are untouched.
//
// Returns 0, a `godot.Call_Error` pass-through, Not_Configured /
// Colony_Allocation / CPU_Retirement_Refused, or upload_display's transport
// vocabulary.
@(gododin)
slime_colony_reseed :: proc(self: ^Slime_Colony, call: gd.Main_Call, seed: i64) -> i64 {
	if error := gd.validate_main_call(call); error != .None do return i64(error)
	if self.colony == nil do return i64(Slime_Error.Not_Configured)
	if replacement_error := slime_colony_replace_cpu(self, self.agent_count, seed); replacement_error != 0 {
		self.last_error = replacement_error
		return self.last_error
	}
	self.last_error = slime_colony_upload_display(self, call)
	return self.last_error
}

// Replacement adopts the new state before retiring the old owner. If free
// refuses, both remain reachable; another replacement first retries that
// retirement, so repeated errors cannot accumulate untracked owners.
// Returns 0, Colony_Allocation, or CPU_Retirement_Refused.
slime_colony_replace_cpu :: proc(self: ^Slime_Colony, count, seed: i64) -> i64 {
	if self.retired_colony != nil && !slime.slime_destroy(&self.retired_colony) do return i64(Slime_Error.CPU_Retirement_Refused)
	replacement, ok := slime.slime_create(i32(self.width), i32(self.height), i32(count), u32(seed & 0xFFFFFFFF), self.allocator)
	if !ok do return i64(Slime_Error.Colony_Allocation)
	replacement.params = self.params
	self.retired_colony = self.colony
	self.colony = replacement
	self.agent_count = count
	self.seed = seed & 0xFFFFFFFF
	self.frames = 0
	self.last_digest = 0
	if self.retired_colony != nil && !slime.slime_destroy(&self.retired_colony) do return i64(Slime_Error.CPU_Retirement_Refused)
	return 0
}

// Shared by explicit shutdown and the last-chance pure deinit. Failed frees
// leave their individual owners intact for another explicit shutdown attempt.
slime_colony_destroy_cpu :: proc(self: ^Slime_Colony) -> bool {
	ok := true
	if self.colony != nil && !slime.slime_destroy(&self.colony) do ok = false
	if self.retired_colony != nil && !slime.slime_destroy(&self.retired_colony) do ok = false
	return ok
}

// verify reads the retained Image's bytes back through the engine's own
// get_data and compares them byte-for-byte against a fresh colony_render of
// the current kernel state. This checks CPU Image transport, not the GPU
// texture or an independent simulation algorithm. It allocates one scratch
// buffer outside the measured path and releases the engine's returned array.
//
// Returns 0, a `godot.Call_Error` pass-through, Not_Configured /
// Kernel_Failed, the readback vocabulary (Readback_Get_Data,
// Readback_Scratch, Readback_Read, Readback_Release, Readback_Mismatch), or
// the Image metadata/length comparisons (Image_Metadata, Image_Length).
@(gododin)
slime_colony_verify :: proc(self: ^Slime_Colony, call: gd.Main_Call) -> i64 {
	if error := gd.validate_main_call(call); error != .None do return i64(error)
	if self.colony == nil || !self.image.live do return i64(Slime_Error.Not_Configured)
	api, facade_ok := gd.main_facade(call)
	if !facade_ok do return i64(godot.Call_Error.Missing_Interface)
	engine_call := gd.godot_main_call(call)
	width, width_error := godot.image_get_width(engine_call, &self.image.handle)
	height, height_error := godot.image_get_height(engine_call, &self.image.handle)
	format, format_error := godot.image_get_format(engine_call, &self.image.handle)
	mipmaps, mipmaps_error := godot.image_has_mipmaps(engine_call, &self.image.handle)
	if width_error != .None || height_error != .None || format_error != .None || mipmaps_error != .None ||
	   i64(width) != self.width || i64(height) != self.height || mipmaps ||
	   format != raw.raw_godot_class_image_enum_format_value_format_r8 {
		return i64(Slime_Error.Image_Metadata)
	}
	display := slime.colony_display(self.colony)
	if !slime.colony_render(self.colony, display) do return i64(Slime_Error.Kernel_Failed)
	owned, get_error := godot.image_get_data(gd.godot_main_call(call), &self.image.handle)
	if get_error != .None do return i64(Slime_Error.Readback_Get_Data)
	cells := int(self.width * self.height)
	readback := render.Packed_Byte_Array{value = owned.value, live = true}
	length, length_error := render.packed_byte_array_length(api, &readback)
	if length_error != .None || length != cells {
		_ = godot.owned_value_destroy(engine_call.api, &owned)
		return i64(Slime_Error.Image_Length)
	}
	stored, stored_error := mem.alloc(cells, mem.DEFAULT_ALIGNMENT, self.allocator)
	if stored == nil || stored_error != nil {
		_ = godot.owned_value_destroy(gd.godot_main_call(call).api, &owned)
		return i64(Slime_Error.Readback_Scratch)
	}
	defer mem.free(stored, self.allocator)
	engine_bytes := mem.slice_ptr(cast(^u8)stored, cells)
	// owned_value_borrow re-presents the engine's array for the facade's
	// const read; the reference itself stays owned by `owned` and is released
	// through the engine's own destructor below.
	if read_error := render.packed_byte_array_read_exact(api, &readback, engine_bytes); read_error != .None {
		_ = godot.owned_value_destroy(gd.godot_main_call(call).api, &owned)
		return i64(Slime_Error.Readback_Read)
	}
	if destroy_error := godot.owned_value_destroy(gd.godot_main_call(call).api, &owned); destroy_error != .None do return i64(Slime_Error.Readback_Release)
	for i in 0..<cells {
		if engine_bytes[i] != display[i] do return i64(Slime_Error.Readback_Mismatch)
	}
	self.last_digest = slime.state_digest(self.colony)
	self.last_error = 0
	return 0
}

// shutdown retires every engine-side owner explicitly: the retained Image
// and ImageTexture references. The teardown hook has no call authority, so
// PREDELETE and this cooperative method are the two supported release points;
// this one is the eager path reconfiguration and the teardown probe need
// while the node is alive. It is idempotent: a second call reports 0 with
// nothing left to release.
//
// Returns 0, a `godot.Call_Error` pass-through, or Display_Detach /
// Texture_Release / Image_Release / CPU_Cleanup_Refused. A refused release
// leaves its owner live for a later attempt.
@(gododin)
slime_colony_shutdown :: proc(self: ^Slime_Colony, call: gd.Main_Call) -> i64 {
	if error := gd.validate_main_call(call); error != .None do return i64(error)
	_, facade_ok := gd.main_facade(call)
	if !facade_ok do return i64(godot.Call_Error.Missing_Interface)
	result: i64 = 0
	if self.display_bound {
		if slime_colony_bind_display(self, call, nil) != 0 do return i64(Slime_Error.Display_Detach)
		self.display_bound = false
	}
	if self.texture.live {
		if !slime_colony_release_ref(self, call, &self.texture) do result = i64(Slime_Error.Texture_Release)
	}
	if self.image.live {
		if !slime_colony_release_ref(self, call, &self.image) && result == 0 do result = i64(Slime_Error.Image_Release)
	}
	if !slime_colony_destroy_cpu(self) && result == 0 do result = i64(Slime_Error.CPU_Cleanup_Refused)
	if result == 0 {
		self.width = 0
		self.height = 0
		self.agent_count = 0
		self.frames = 0
		self.last_digest = 0
	}
	self.last_error = result
	return result
}

// set_attraction retunes how strongly the nutrient mask pulls agents (EX-SLIME).
// Zero — the default — keeps the unmasked canonical workload unchanged.
//
// Returns 0, a `godot.Call_Error` pass-through, or Params_Invalid.
@(gododin)
slime_colony_set_attraction :: proc(self: ^Slime_Colony, call: gd.Main_Call, attraction: f64) -> i64 {
	if error := gd.validate_main_call(call); error != .None do return i64(error)
	params := self.params if self.colony != nil else slime.DEFAULT_PARAMS
	params.attraction = f32(attraction)
	if !slime.params_valid(params) do return i64(Slime_Error.Params_Invalid)
	self.params = params
	if self.colony != nil do self.colony.params = params
	return 0
}

// set_nutrient copies one bounded whole-mask snapshot in as the colony's
// nutrient input (EX-SLIME): a Godot-owned PackedByteArray in row-major
// order whose byte strength (0..255) weights attraction. The kernel takes
// one Odin-owned copy; replacing, resizing, and clearing all share one real
// cleanup path in the subsystem. The mask covers the colony's toroidal world
// proportionally, so its dimensions follow the field's own bounds.
//
// Returns 0, a `godot.Call_Error` pass-through, Not_Configured, or
// Nutrient_Shape.
@(gododin)
slime_colony_set_nutrient :: proc(
	self:  ^Slime_Colony,
	call:  gd.Main_Call,
	mask:  []u8,
	width, height: i64,
) -> i64 {
	if error := gd.validate_main_call(call); error != .None do return i64(error)
	if self.colony == nil do return i64(Slime_Error.Not_Configured)
	if width < slime.MIN_DIMENSION || width > slime.MAX_DIMENSION 			|| height < slime.MIN_DIMENSION || height > slime.MAX_DIMENSION 			|| len(mask) != int(width) * int(height) {
		return i64(Slime_Error.Nutrient_Shape)
	}
	if !slime.colony_set_nutrient(self.colony, mask, i32(width), i32(height)) {
		return i64(Slime_Error.Kernel_Failed)
	}
	return 0
}

// clear_nutrient releases the nutrient input; attraction stops applying on
// the next step. Clearing an absent mask is a no-op success.
//
// Returns 0, a `godot.Call_Error` pass-through, or Not_Configured.
@(gododin)
slime_colony_clear_nutrient :: proc(self: ^Slime_Colony, call: gd.Main_Call) -> i64 {
	if error := gd.validate_main_call(call); error != .None do return i64(error)
	if self.colony == nil do return i64(Slime_Error.Not_Configured)
	slime.colony_clear_nutrient(self.colony)
	return 0
}

// get_nutrient_cells reports how many bytes of the installed mask are
// non-zero — the UI's mask-presence fact. Zero also answers "no mask".
@(gododin)
slime_colony_get_nutrient_cells :: proc(self: ^Slime_Colony) -> i64 {
	if self.colony == nil || self.colony.nutrient == nil do return 0
	cells := 0
	for value in self.colony.nutrient {
		if value != 0 do cells += 1
	}
	return i64(cells)
}

// advance_agents runs only the agent phases of one tick (sense, steer,
// move, deposit), leaving the field update to the caller — the GPU
// comparison variant's split (DX-9). The canonical tick is unchanged.
//
// Returns 0, a `godot.Call_Error` pass-through, or Not_Configured.
@(gododin)
slime_colony_advance_agents :: proc(self: ^Slime_Colony, call: gd.Main_Call) -> i64 {
	if error := gd.validate_main_call(call); error != .None do return i64(error)
	if self.colony == nil do return i64(Slime_Error.Not_Configured)
	if !slime.colony_step_agents(self.colony, self.colony.params) do return i64(Slime_Error.Kernel_Failed)
	return 0
}

// field_update runs only the field phase of one tick (separable box blur,
// then decay) on the current field — the CPU reference for the GPU
// variant's bitwise validation (DX-9). It touches no agent state, so the
// comparison between this and the compute path is free of deposit-order
// asymmetry.
//
// Returns 0, a `godot.Call_Error` pass-through, or Not_Configured.
@(gododin)
slime_colony_field_update :: proc(self: ^Slime_Colony, call: gd.Main_Call) -> i64 {
	if error := gd.validate_main_call(call); error != .None do return i64(error)
	if self.colony == nil do return i64(Slime_Error.Not_Configured)
	if !slime.diffuse_pass(self.colony.grid, self.colony.scratch, self.colony.width, self.colony.height) {
		return i64(Slime_Error.Kernel_Failed)
	}
	if !slime.apply_decay(self.colony.grid, self.colony.params.decay) {
		return i64(Slime_Error.Kernel_Failed)
	}
	self.colony.steps_done += 1
	return 0
}

// grid_bytes borrows the colony's trail field as raw little-endian f32
// bytes (boid's state_bytes shape) for the caller's own upload or
// comparison; the span is valid until the next colony call.
@(gododin)
slime_colony_grid_bytes :: proc(self: ^Slime_Colony) -> []u8 {
	if self.colony == nil do return nil
	return (cast([^]u8)raw_data(self.colony.grid))[:len(self.colony.grid) * size_of(f32)]
}

// set_grid_bytes copies caller-owned field bytes (little-endian f32s) over
// the colony's grid — the GPU variant's readback path. Length must match.
//
// Returns 0, a `godot.Call_Error` pass-through, Not_Configured, or
// Grid_Shape (-16).
@(gododin)
slime_colony_set_grid_bytes :: proc(self: ^Slime_Colony, call: gd.Main_Call, values: []u8) -> i64 {
	if error := gd.validate_main_call(call); error != .None do return i64(error)
	if self.colony == nil do return i64(Slime_Error.Not_Configured)
	if len(values) != len(self.colony.grid) * size_of(f32) do return -16
	target := (cast([^]u8)raw_data(self.colony.grid))[:len(values)]
	copy(target, values)
	return 0
}

@(gododin)
slime_colony_get_frames :: proc(self: ^Slime_Colony) -> i64 {
	return self.frames
}

@(gododin)
slime_colony_get_width :: proc(self: ^Slime_Colony) -> i64 {
	return self.width
}

@(gododin)
slime_colony_get_height :: proc(self: ^Slime_Colony) -> i64 {
	return self.height
}

@(gododin)
slime_colony_get_agent_count :: proc(self: ^Slime_Colony) -> i64 {
	return self.agent_count
}

@(gododin)
slime_colony_get_seed :: proc(self: ^Slime_Colony) -> i64 {
	return self.seed
}

// get_digest computes the current whole-state fingerprint on demand. This
// linear inspection cost is outside submit_frame and its measured bracket.
// Frame-zero replacement state is visible immediately to callers.
@(gododin)
slime_colony_get_digest :: proc(self: ^Slime_Colony) -> i64 {
	self.last_digest = slime.state_digest(self.colony)
	return i64(self.last_digest)
}

// get_last_error reports the latest submission or replacement-upload error:
// 0, a positive `godot.Call_Error` pass-through, or a negative Slime_Error.
@(gododin)
slime_colony_get_last_error :: proc(self: ^Slime_Colony) -> i64 {
	return self.last_error
}

@(gododin)
slime_colony_get_configured :: proc(self: ^Slime_Colony) -> bool {
	return self.colony != nil || self.retired_colony != nil || self.image.live || self.texture.live
}