Gododin Gododin Documentation

Linked repository source

examples/cavern_lab/declarations/cavern_lab.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 Cavern Lab.
// It is the single source for the CavernGenerator 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 canonical project key stays configuration (gododin.toml), and
// the image bridge is generated from this package's compiler graph.
package declarations

import "core:mem"
import "core:time"

import gd "gododin:bindings/typed/call"

import cavern "../subsystem"

// CaveGenerationSettings is the native settings Resource the Inspector edits
// and the saved expedition scene retains through the generator's `settings`
// property. It carries the six seeded-generation parameters; the expedition
// reads it rather than keeping a second set of defaults. Per-run tuning is an
// explicit copy taken when a request is submitted — playing or resetting never
// writes back to the shared .tres.
@(gododin_class="Resource")
Cave_Generation_Settings :: struct {
	width:               i64 `godot:"property"`,
	height:              i64 `godot:"property"`,
	seed:                i64 `godot:"property"`,
	fill_percent:        i64 `godot:"property"`,
	smoothing_steps:     i64 `godot:"property"`,
	minimum_region_size: i64 `godot:"property"`,
}

// Generation_Terminal is the payload of the generator's terminal signals.
// Scalar request identity and status only: packed arrays are not admitted
// signal payloads, and the published cell bytes stay reachable through
// published_cells, never across a signal emission.
Generation_Terminal :: struct {
	request_id:  i64,
	error:       i64,
	compute_usec: i64,
}

// CavernGenerator is a deliberately narrow native Node. Godot owns settings,
// presentation, Image, ImageTexture, and scene orchestration; this Node owns
// one coarse request into the pure Odin cavern package and the observable
// diagnostics of the last request.
@(gododin_class="Node")
Cavern_Generator :: struct {
	workspace:        ^cavern.Workspace,
	worker:           ^cavern.Worker,
	diagnostics:      cavern.Diagnostics,
	last_error:       i64,
	generation_usec:  i64,
	invocation_count: i64,
	// Background publication state: the newest completed result retained for
	// the engine to copy, plus the discard counters the workload reports.
	background_cells:      []u8,
	background_request_id: u64,
	background_error:      i64,
	background_hash:       i64,
	discarded_results:     i64,
	background_usec:       i64,
	// The scene-owned settings Resource and the terminal completion signals.
	// The signals carry scalar identity only; the scene-facing game pumps them
	// from its own frame loop and defers scene mutation out of the dispatch.
	settings:               gd.Ref(Cave_Generation_Settings) `godot:"property"`,
	generation_completed:   gd.Signal(Generation_Terminal),
	generation_failed:      gd.Signal(Generation_Terminal),
	// The explicit accepted-grid identity: a per-instance sequence number and
	// content hash for the grid accept_grid last loaded. Classification runs
	// against those accepted bytes, so the game can always name which
	// displayed grid — generated, refined, or baked — its placement decisions
	// came from.
	accepted_grid_id:   i64,
	accepted_grid_hash: i64,
}

// init acquires the sole workspace allocation for this instance. It receives
// the image's allocator and must stay paired with deinit, which tolerates any
// partial state.
cavern_generator_init :: proc(self: ^Cavern_Generator, allocator: mem.Allocator) -> bool {
	workspace, created := cavern.workspace_create(allocator)
	if !created do return false
	self.workspace = workspace
	worker, worker_created := cavern.worker_create(allocator)
	if !worker_created {
		_ = cavern.workspace_destroy(&self.workspace)
		return false
	}
	self.worker = worker
	return true
}

cavern_generator_deinit :: proc(self: ^Cavern_Generator) {
	// Close, cancel, and drain the background worker before any instance
	// memory is released; an outstanding request joins within the shutdown
	// budget. The retained publication and the worker itself are freed here.
	if self.worker != nil {
		if self.background_cells != nil {
			cavern.worker_release_result(self.worker, self.background_cells)
			self.background_cells = nil
		}
		_ = cavern.worker_destroy(&self.worker)
	}
	if self.workspace != nil do _ = cavern.workspace_destroy(&self.workspace)
}

// generate runs one seeded, smoothed, region-connected request and returns the
// borrowed cell span. The span is valid until another successful generate or
// process_mask call on this instance; the Host copies it into the engine's
// PackedByteArray exactly once.
// Rejected settings report the subsystem error through get_last_error and return
// no bytes. A seed outside the exact u32
// range cannot be represented by the subsystem and is rejected the same way.
@(gododin)
cavern_generator_generate :: proc(
	self: ^Cavern_Generator,
	width, height, seed, fill_percent, smoothing_steps, minimum_region_size: i64,
) -> []u8 {
	if seed < 0 || seed > i64(max(u32)) {
		self.last_error = i64(cavern.Error.Validation_Failed)
		return nil
	}
	started := time.tick_now()
	generated := cavern.generate(self.workspace, {
		width=int(width),
		height=int(height),
		seed=u32(seed),
		fill_percent=int(fill_percent),
		smoothing_steps=int(smoothing_steps),
		minimum_region_size=int(minimum_region_size),
	})
	self.generation_usec = i64(time.duration_microseconds(time.tick_since(started)))
	self.invocation_count += 1
	self.last_error = i64(generated.error)
	self.diagnostics = generated.diagnostics
	return generated.cells
}

// process_mask refines Godot-owned occupancy bytes using the same smoothing,
// region culling, and connection rules as generate. The callback-scoped input
// is read-only and never retained. The pure kernel owns its mutable working grid;
// the Host copies the final result once into a new Godot PackedByteArray.
// Empty/wrong-sized masks and bytes other than 0/255 return an empty result and
// a named subsystem error through get_last_error. Width and height are 8..1024.
@(gododin)
cavern_generator_process_mask :: proc(
	self: ^Cavern_Generator,
	mask: []u8,
	width, height, smoothing_steps, minimum_region_size: i64,
) -> []u8 {
	started := time.tick_now()
	processed := cavern.process_mask(self.workspace, mask, int(width), int(height), int(smoothing_steps), int(minimum_region_size))
	self.generation_usec = i64(time.duration_microseconds(time.tick_since(started)))
	self.invocation_count += 1
	self.last_error = i64(processed.error)
	self.diagnostics = processed.diagnostics
	return processed.cells
}

// accept_grid loads Godot-owned occupancy bytes as the explicit classification
// grid without any refinement: classification has to run against the exact
// bytes the scene displays, whatever produced them — a background publication,
// a refined mask, or a baked asset. Rejected inputs leave the previously
// accepted grid in place and report the named subsystem error through
// get_last_error; a success returns the new accepted-grid id (a per-instance
// counter starting at 1), with the content hash reachable through
// get_accepted_grid_hash.
@(gododin)
cavern_generator_accept_grid :: proc(
	self: ^Cavern_Generator,
	cells: []u8,
	width, height: i64,
) -> i64 {
	accept_error := cavern.accept_grid(self.workspace, cells, int(width), int(height))
	self.last_error = i64(accept_error)
	if accept_error != .None do return 0
	self.accepted_grid_id += 1
	self.accepted_grid_hash = i64(cavern.hash_cells(self.workspace.cells[:width*height]))
	return self.accepted_grid_id
}

@(gododin)
cavern_generator_get_accepted_grid_id :: proc(self: ^Cavern_Generator) -> i64 {
	return self.accepted_grid_id
}

@(gododin)
cavern_generator_get_accepted_grid_hash :: proc(self: ^Cavern_Generator) -> i64 {
	return self.accepted_grid_hash
}

// classify_positions classifies Godot-owned candidate positions against the
// accepted grid — the bytes accept_grid last loaded, or the result of the most
// recent synchronous generate/process_mask — and returns the borrowed flag
// span — 255 marks placeable, 0 marks blocked — valid until the next
// classify_positions call. Positions,
// radii, and the origin are callback-scoped read-only borrows of the caller's
// own engine storage (ADR 0060); y is ignored because the grid is the cave
// floor plane. Rejected inputs return no bytes and a named subsystem error
// through get_last_error.
@(gododin)
cavern_generator_classify_positions :: proc(
	self: ^Cavern_Generator,
	positions: []cavern.Vec3,
	radii: []f32,
	origin: cavern.Vec3,
	cell_size: f64,
) -> []u8 {
	classified := cavern.classify_positions(self.workspace, positions, radii, origin, cell_size)
	self.invocation_count += 1
	self.last_error = i64(classified.error)
	return classified.flags
}

@(gododin)
cavern_generator_get_last_error :: proc(self: ^Cavern_Generator) -> i64 {
	return self.last_error
}

@(gododin)
cavern_generator_get_generation_usec :: proc(self: ^Cavern_Generator) -> i64 {
	return self.generation_usec
}

@(gododin)
cavern_generator_get_seeded_walls :: proc(self: ^Cavern_Generator) -> i64 {
	return i64(self.diagnostics.seeded_walls)
}

@(gododin)
cavern_generator_get_smoothed_walls :: proc(self: ^Cavern_Generator) -> i64 {
	return i64(self.diagnostics.smoothed_walls)
}

@(gododin)
cavern_generator_get_regions_before_cull :: proc(self: ^Cavern_Generator) -> i64 {
	return i64(self.diagnostics.regions_before_cull)
}

@(gododin)
cavern_generator_get_culled_regions :: proc(self: ^Cavern_Generator) -> i64 {
	return i64(self.diagnostics.culled_regions)
}

@(gododin)
cavern_generator_get_culled_cells :: proc(self: ^Cavern_Generator) -> i64 {
	return i64(self.diagnostics.culled_cells)
}

@(gododin)
cavern_generator_get_connections :: proc(self: ^Cavern_Generator) -> i64 {
	return i64(self.diagnostics.connections)
}

@(gododin)
cavern_generator_get_final_regions :: proc(self: ^Cavern_Generator) -> i64 {
	return i64(self.diagnostics.final_regions)
}

@(gododin)
cavern_generator_get_floor_cells :: proc(self: ^Cavern_Generator) -> i64 {
	return i64(self.diagnostics.floor_cells)
}

@(gododin)
cavern_generator_get_result_hash :: proc(self: ^Cavern_Generator) -> i64 {
	return i64(self.diagnostics.result_hash)
}

@(gododin)
cavern_generator_get_invocation_count :: proc(self: ^Cavern_Generator) -> i64 {
	return self.invocation_count
}

@(gododin)
cavern_generator_get_workspace_allocations :: proc(self: ^Cavern_Generator) -> i64 {
	return i64(self.workspace.allocation_count)
}

// background_submit queues one seeded generation on this instance's background
// worker and returns the new request id, or a negative id with the named
// subsystem error through get_last_error. While a computation is active the
// request coalesces onto the single replaceable pending slot; the superseded
// pending request is dropped without ever starting. Nothing here blocks on the
// worker.
@(gododin)
cavern_generator_background_submit :: proc(
	self: ^Cavern_Generator,
	width, height, seed, fill_percent, smoothing_steps, minimum_region_size: i64,
) -> i64 {
	if seed < 0 || seed > i64(max(u32)) {
		self.last_error = i64(cavern.Error.Validation_Failed)
		return -1
	}
	id, submit_error := cavern.worker_submit(self.worker, {
		width               = int(width),
		height              = int(height),
		seed                = u32(seed),
		fill_percent        = int(fill_percent),
		smoothing_steps     = int(smoothing_steps),
		minimum_region_size = int(minimum_region_size),
	})
	self.last_error = i64(submit_error)
	return id
}

// background_cancel asks the active computation to stop at its next loop
// boundary and drops the pending request outright. It reports 1 when anything
// was cancellable and 0 when the worker was idle.
@(gododin)
cavern_generator_background_cancel :: proc(self: ^Cavern_Generator) -> i64 {
	return cavern.worker_cancel(self.worker)
}

// background_poll consumes the newest completed background result from the
// bounded handoff and returns its cells as a retained borrowed span, valid
// until the next successful poll or instance teardown; the Host copies it into
// the engine's PackedByteArray exactly once. Obsolete results — anything that
// is not the latest request — are released here without ever being published,
// and count through get_background_discarded_results. An empty return means
// nothing new completed; get_background_status distinguishes that from a
// cancelled or failed result already reported.
@(gododin)
cavern_generator_background_poll :: proc(self: ^Cavern_Generator) -> []u8 {
	latest := cavern.worker_latest_request_id(self.worker)
	for {
		result, ok := cavern.worker_take(self.worker)
		if !ok do break
		if result.request_id != latest {
			self.discarded_results += 1
			cavern.worker_release_result(self.worker, result.cells)
			continue
		}
		if self.background_cells != nil {
			cavern.worker_release_result(self.worker, self.background_cells)
		}
		self.background_cells = result.cells
		self.background_request_id = result.request_id
		self.background_error = i64(result.error)
		self.background_hash = i64(result.diagnostics.result_hash)
		return self.background_cells
	}
	return nil
}

@(gododin)
cavern_generator_background_status :: proc(self: ^Cavern_Generator) -> i64 {
	return i64(cavern.worker_status(self.worker))
}

// pump_completion is the scene-facing completion pump. It drains the same
// bounded handoff background_poll drains — the newest record wins, obsolete
// records are released and counted — stores the result in the instance's
// single publication slot, and reports the terminal state through the
// generation_completed / generation_failed signals with scalar payloads only.
// A deliberately cancelled record is stored and reported by id but emits no
// terminal signal; the caller reads published_error to distinguish it. The
// return value is the newly published request id, or 0 when nothing new
// completed. Byte retrieval stays out of the emission frame: callers take the
// cells through published_cells after the dispatch returns.
@(gododin)
cavern_generator_pump_completion :: proc(self: ^Cavern_Generator, call: gd.Main_Call) -> i64 {
	latest := cavern.worker_latest_request_id(self.worker)
	for {
		result, ok := cavern.worker_take(self.worker)
		if !ok do break
		if result.request_id != latest {
			self.discarded_results += 1
			cavern.worker_release_result(self.worker, result.cells)
			continue
		}
		if self.background_cells != nil {
			cavern.worker_release_result(self.worker, self.background_cells)
		}
		self.background_cells = result.cells
		self.background_request_id = result.request_id
		self.background_error = i64(result.error)
		self.background_hash = i64(result.diagnostics.result_hash)
		self.background_usec = result.compute_usec
		terminal := Generation_Terminal{
			request_id  = i64(result.request_id),
			error       = i64(result.error),
			compute_usec = result.compute_usec,
		}
		if result.error == .None {
			if gd.emit(call, self.generation_completed, terminal) != .None do return 0
		} else if result.error != .Cancelled {
			if gd.emit(call, self.generation_failed, terminal) != .None do return 0
		}
		return i64(result.request_id)
	}
	return 0
}

// published_cells returns the stored publication's cells as a borrowed span,
// valid until the next successful pump, poll, or instance teardown; the Host
// copies it into the engine's PackedByteArray exactly once. A request id other
// than the published one is stale retrieval and returns no bytes.
@(gododin)
cavern_generator_published_cells :: proc(self: ^Cavern_Generator, request_id: i64) -> []u8 {
	if request_id < 0 || u64(request_id) != self.background_request_id do return nil
	return self.background_cells
}

// published_error reports the stored publication's terminal subsystem error:
// None for a completed cave, Cancelled for a deliberately cancelled request,
// or the named kernel failure.
@(gododin)
cavern_generator_published_error :: proc(self: ^Cavern_Generator) -> i64 {
	return self.background_error
}

@(gododin)
cavern_generator_background_error :: proc(self: ^Cavern_Generator) -> i64 {
	return i64(cavern.worker_error(self.worker))
}

@(gododin)
cavern_generator_background_latest_request_id :: proc(self: ^Cavern_Generator) -> i64 {
	return i64(cavern.worker_latest_request_id(self.worker))
}

@(gododin)
cavern_generator_background_published_request_id :: proc(self: ^Cavern_Generator) -> i64 {
	return i64(self.background_request_id)
}

@(gododin)
cavern_generator_background_superseded_results :: proc(self: ^Cavern_Generator) -> i64 {
	return i64(cavern.worker_superseded_results(self.worker))
}

@(gododin)
cavern_generator_background_discarded_results :: proc(self: ^Cavern_Generator) -> i64 {
	return self.discarded_results
}

@(gododin)
cavern_generator_background_dropped_pending :: proc(self: ^Cavern_Generator) -> i64 {
	return i64(cavern.worker_dropped_pending(self.worker))
}

@(gododin)
cavern_generator_background_compute_usec :: proc(self: ^Cavern_Generator) -> i64 {
	return cavern.worker_compute_usec(self.worker)
}

@(gododin)
cavern_generator_background_cancellation_ack_usec :: proc(self: ^Cavern_Generator) -> i64 {
	return cavern.worker_cancellation_ack_usec(self.worker)
}

@(gododin)
cavern_generator_background_peak_bytes :: proc(self: ^Cavern_Generator) -> i64 {
	return cavern.worker_peak_memory(self.worker)
}

@(gododin)
cavern_generator_background_last_hash :: proc(self: ^Cavern_Generator) -> i64 {
	return self.background_hash
}

// debug_inject_failure is fixture-only scaffolding for the Phase 13 lifecycle
// campaign, admitted so the headless GDScript campaign can arm this
// instance's controlled worker failure injection. The next
// generation_failures background requests finish with the kernel's
// Validation_Failed result (a completed computation whose cells are dropped
// before publication, exactly as the kernel's internal validation failure
// behaves), and the next workspace_failures worker workspace creations fail,
// leaving that worker permanently unusable. Counters are consumed once under
// the worker mutex and are never armed by any product path.
@(gododin)
cavern_generator_debug_inject_failure :: proc(
	self: ^Cavern_Generator,
	generation_failures, workspace_failures: i64,
) -> i64 {
	cavern.worker_debug_inject(self.worker, i32(generation_failures), i32(workspace_failures))
	return 0
}