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
}