Gododin Gododin Documentation

Linked repository source

examples/rewind_lab/declarations/time_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

import "core:mem"
import raw "gododin:bindings/raw"
import godot "gododin:bindings/typed/godot"
import gd "gododin:bindings/typed/call"
import render "gododin:bindings/typed/render"
import rewind "../subsystem"

// Time_Lab owns one Box2D world from the pure rewind subsystem and three
// MultiMesh streams, one per scene-owned MultiMesh RID. The scene owns the
// child MultiMeshInstance2D pair for moving bodies (Boxes and Circles) and
// the static structure mesh (Statics), plus their MultiMesh resources; the
// streams allocate their server data and change their contents, and this
// class retains no RID it must free. Moving bodies submit one bulk buffer
// per mesh per frame call; the static structure submits once per configure
// and is never touched again. The fixed tick, the input log, the history
// window, and exact restore all belong to the subsystem; this class
// choreographs the boundary.
//
// Instance colors ride the staged stride (see the subsystem's
// RENDER_STRIDE facts): frozen bodies read ice, spawned balls read green,
// live spec bodies read warm material, statics read slate. Each stream is
// opened with the transform+color layout those colors require.
//
// Explicit shutdown runs on every scene exit before authority disappears.
@(gododin_class="Node2D")
Time_Lab :: struct {
	allocator: mem.Allocator,

	// One stream owns one mesh's whole submission pipeline: the server
	// allocation at its extent, the staged store, and the write/publish/
	// submit sequence. The RID stays scene-owned inside the stream.
	box_stream:    render.MultiMesh_Stream,
	circle_stream: render.MultiMesh_Stream,
	static_stream: render.MultiMesh_Stream,

	world:         ^rewind.World,
	retired_world: ^rewind.World,

	ticks_run:   i64,
	submissions: i64,
}

// Time_Lab_Init records the Host-provided owning allocator once, so the
// subsystem world allocates and deallocates symmetrically across callbacks.
time_lab_init :: proc(self: ^Time_Lab, allocator: mem.Allocator) -> bool {
	self.allocator = allocator
	return true
}

// Time_Lab_Deinit retires the pure worlds under their recorded allocator.
// The streams are deliberately not touched: retiring them needs facade call
// authority the teardown hook does not have — the explicit shutdown
// operation is the cooperative counterpart, as in Bunny_Field.
time_lab_deinit :: proc(self: ^Time_Lab) {
	if self.world != nil {
		_ = rewind.world_destroy(&self.world)
	}
	_ = rewind.world_destroy(&self.retired_world)
}

// bind_multimesh resolves the scene's child MultiMeshInstance2D by the
// scene's declared node name and takes its MultiMesh resource's server RID.
// The name is the scene's explicit binding (Boxes, Circles, Statics — see
// project/main.tscn), so adding or reordering siblings cannot silently
// rebind a workload. The scene keeps ownership of the RID; the stream
// opened on it allocates the data extent. get_multimesh returns an owned
// reference, released on every path. The step code names where the bind
// failed: 1 child lookup, 2 not a MultiMeshInstance2D, 3 multimesh missing,
// 4 rid, 6 stream open (server allocation or staged store creation), 8
// reference release.
bind_multimesh :: proc(call: gd.Main_Call, n2d: ^godot.Node2D, name: cstring, api: ^render.Bound_API, extent: i64) -> (stream: render.MultiMesh_Stream, code: i64) {
	child, child_ok := child_by_name(gd.godot_main_call(call), cast(^godot.Node)n2d, name)
	if !child_ok do return {}, 1
	scene_object := godot.Object{ptr = child.ptr}
	mmi, mmi_ok := godot.cast_MultiMeshInstance2D(gd.godot_main_call(call), &scene_object)
	if !mmi_ok do return {}, 2
	multimesh, multimesh_error := godot.multi_mesh_instance2d_get_multimesh(gd.godot_main_call(call), mmi)
	if multimesh_error != .None || !multimesh.live do return {}, 3
	refc := godot.RefCounted{ptr = multimesh.handle.ptr}
	defer {
		if _, err := godot.ref_counted_unreference(gd.godot_main_call(call), &refc); err != .None {
			code = 8
		}
	}
	resource := godot.Resource{ptr = multimesh.handle.ptr}
	handle, rid_error := godot.resource_get_rid(gd.godot_main_call(call), &resource)
	if rid_error != .None || !handle.live do return {}, 4
	// The transform+color layout the instance colors ride: the stream owns
	// the rid validation, the server allocation at `extent`, and the staged
	// store of exactly extent*RENDER_STRIDE floats.
	opened, open_error := render.multimesh_stream_open(
		render.main_call(api),
		handle.value,
		render.MultiMesh_Layout{colors = true},
		int(extent),
	)
	if open_error != .None do return {}, 6
	return opened, 0
}

// Time_Lab_Setup validates the authored configuration, binds the scene's
// three MultiMeshes (Boxes, Circles, Statics — each by its declared scene
// name) through one stream open per mesh, builds the subsystem world, and
// submits the static structure plus the initial state so the contraption is
// visible before the first advance. Stream extents are the body cap for
// moving bodies and the preset body bound for statics.
//
// Error codes: -1 invalid call token, -2 invalid configuration, -3 setup
// already live, -4 authored object unavailable, -5 not a Node2D, -40 facade
// unavailable, then per mesh -1x boxes, -2x circles, -3x statics with the
// bind_multimesh step digit; -44 world creation; -5x the initial
// submissions failed.
@(gododin)
time_lab_setup :: proc(self: ^Time_Lab, call: gd.Main_Call, preset, seed: i64, restitution_scale, sub_steps, window_ticks, body_cap: i64) -> (code: i64) {
	if gd.validate_main_call(call) != .None do return -1
	if preset < 0 || preset >= i64(rewind.PRESET_COUNT) do return -2
	cfg := rewind.default_config()
	cfg.preset = rewind.Preset_Id(preset)
	cfg.seed = u32(seed & 0xFFFFFFFF)
	cfg.restitution_scale = f32(restitution_scale)
	cfg.sub_steps = i32(sub_steps)
	cfg.window_ticks = window_ticks
	cfg.body_cap = i32(body_cap)
	if !rewind.valid_config(cfg) do return -2
	occupied := render.multimesh_stream_owned(&self.box_stream) ||
		render.multimesh_stream_owned(&self.circle_stream) ||
		render.multimesh_stream_owned(&self.static_stream)
	if occupied || self.world != nil || self.retired_world != nil do return -3
	object, ok := gd.self(call)
	if !ok do return -4
	n2d, cast_ok := godot.cast_Node2D(gd.godot_main_call(call), &object)
	if !cast_ok do return -5
	api, facade_ok := gd.main_facade(call)
	if !facade_ok do return -40

	box_stream, box_code := bind_multimesh(call, n2d, "Boxes", api, body_cap)
	if box_code != 0 do return -10 + box_code
	self.box_stream = box_stream // Reachable by shutdown even if a later step fails.
	circle_stream, circle_code := bind_multimesh(call, n2d, "Circles", api, body_cap)
	if circle_code != 0 do return -20 + circle_code
	self.circle_stream = circle_stream
	static_stream, static_code := bind_multimesh(call, n2d, "Statics", api, rewind.MAX_PRESET_BODIES)
	if static_code != 0 do return -30 + static_code
	self.static_stream = static_stream
	world := rewind.world_create(cfg, self.allocator)
	if world == nil do return -44
	self.world = world
	self.ticks_run = 0
	self.submissions = 0

	if submit_code := submit_statics(self, call); submit_code != 0 do return -50 + submit_code
	if submit_code := submit_dynamics(self, call); submit_code != 0 do return -60 + submit_code
	return 0
}

// submit_dynamics packs both moving meshes and submits each exactly once:
// two staged writes, two publishes, two server calls, one per mesh. Zero
// area tail rows keep dead slots invisible without per-frame count calls.
// Error codes: -1 facade unavailable, -2/-3 box write/publish, -4/-5
// circle write/publish, -6/-7 box/circle submit.
submit_dynamics :: proc(self: ^Time_Lab, call: gd.Main_Call) -> i64 {
	api, facade_ok := gd.main_facade(call)
	if !facade_ok do return -1
	view, write_error := render.multimesh_stream_write(render.main_call(api), &self.box_stream)
	if write_error != .None do return -2
	if !rewind.write_dynamic_layout(self.world, view, .box, i64(self.box_stream.capacity)) do return -2
	if _, publish_error := render.multimesh_stream_publish(&self.box_stream, .Main); publish_error != .None do return -3
	view, write_error = render.multimesh_stream_write(render.main_call(api), &self.circle_stream)
	if write_error != .None do return -4
	if !rewind.write_dynamic_layout(self.world, view, .circle, i64(self.circle_stream.capacity)) do return -4
	if _, publish_error := render.multimesh_stream_publish(&self.circle_stream, .Main); publish_error != .None do return -5
	if submit_error := render.multimesh_stream_submit(render.main_call(api), &self.box_stream); submit_error != .None do return -6
	if submit_error := render.multimesh_stream_submit(render.main_call(api), &self.circle_stream); submit_error != .None do return -7
	self.submissions += 2
	return 0
}

// submit_statics packs and submits the static structure once per
// configure; nothing in the frame loop touches this mesh again.
// Error codes follow submit_dynamics' shape for the single static mesh.
submit_statics :: proc(self: ^Time_Lab, call: gd.Main_Call) -> i64 {
	api, facade_ok := gd.main_facade(call)
	if !facade_ok do return -1
	view, write_error := render.multimesh_stream_write(render.main_call(api), &self.static_stream)
	if write_error != .None do return -2
	if !rewind.write_static_layout(self.world, view, i64(self.static_stream.capacity)) do return -2
	if _, publish_error := render.multimesh_stream_publish(&self.static_stream, .Main); publish_error != .None do return -3
	if submit_error := render.multimesh_stream_submit(render.main_call(api), &self.static_stream); submit_error != .None do return -6
	self.submissions += 1
	return 0
}

// Time_Lab_Advance runs the given number of fixed ticks through the pure
// subsystem, then submits both moving meshes exactly once — the frame's
// whole engine cost is those two bulk calls. Returns the new playhead in
// `played` on success.
//
// Error codes: -1 invalid call token, -2 setup not live, -3 tick count out
// of range, -4 a tick failed, -1x the submit_dynamics code of the same
// name.
@(gododin)
time_lab_advance :: proc(self: ^Time_Lab, call: gd.Main_Call, ticks: i64) -> (played: i64) {
	if gd.validate_main_call(call) != .None do return -1
	if self.world == nil || !render.multimesh_stream_owned(&self.box_stream) || !render.multimesh_stream_owned(&self.circle_stream) do return -2
	if ticks <= 0 || ticks > self.world.config.window_ticks do return -3
	for _ in 0..<ticks {
		if !rewind.advance(self.world) do return -4
	}
	self.ticks_run += ticks
	if submit_code := submit_dynamics(self, call); submit_code != 0 do return -10 + submit_code
	return self.world.tick
}

// Time_Lab_Rewind_To restores an exact recorded tick and submits both
// moving meshes once. The restore itself is the subsystem's rebuild plus
// replay from the epoch; its cost is reported in restore_steps.
//
// Error codes: -1 invalid call token, -2 setup not live, -3 the target is
// refused (future, below the window base, or below the epoch), -4 the
// restore failed, -1x the submit_dynamics code of the same name.
@(gododin)
time_lab_rewind_to :: proc(self: ^Time_Lab, call: gd.Main_Call, target: i64) -> (replayed: i64) {
	if gd.validate_main_call(call) != .None do return -1
	if self.world == nil || !render.multimesh_stream_owned(&self.box_stream) || !render.multimesh_stream_owned(&self.circle_stream) do return -2
	if target > self.world.tick do return -3
	if !rewind.rewind_to(self.world, target) do return -4
	self.ticks_run = self.world.tick
	if submit_code := submit_dynamics(self, call); submit_code != 0 do return -10 + submit_code
	return self.world.tick
}

// Time_Lab_Verify reads both moving meshes and the static mesh back from
// the engine's server storage and compares every staged float bitwise
// against a fresh packing of live solver state — the bulk readback, the
// same one Bunny_Field earns its oracle through.
//
// Error codes: -1 invalid call token, -2 setup not live, -3 facade
// unavailable, -4 rendering server unavailable, -1x/-2x/-3x per mesh
// (x1 view, x2 layout, x3 get_buffer, x4 extent, x5 bitwise mismatch, x6
// readback release).
@(gododin)
time_lab_verify :: proc(self: ^Time_Lab, call: gd.Main_Call) -> i64 {
	if gd.validate_main_call(call) != .None do return -1
	if self.world == nil || !render.multimesh_stream_owned(&self.box_stream) ||
		!render.multimesh_stream_owned(&self.circle_stream) ||
		!render.multimesh_stream_owned(&self.static_stream) {
		return -2
	}
	api, facade_ok := gd.main_facade(call)
	if !facade_ok do return -3
	server, server_error := godot.singleton_rendering_server(gd.godot_main_call(call))
	if server_error != .None do return -4

	verify_one :: proc(self: ^Time_Lab, api: ^render.Bound_API, call: gd.Main_Call, server: ^godot.RenderingServer, stream: ^render.MultiMesh_Stream, base_code: i64) -> i64 {
		view, view_error := render.multimesh_stream_write(render.main_call(api), stream)
		if view_error != .None do return base_code + 1
		packed_ok := false
		switch base_code {
		case -10:
			packed_ok = rewind.write_dynamic_layout(self.world, view, .box, i64(stream.capacity))
		case -20:
			packed_ok = rewind.write_dynamic_layout(self.world, view, .circle, i64(stream.capacity))
		case -30:
			packed_ok = rewind.write_static_layout(self.world, view, i64(stream.capacity))
		}
		if !packed_ok do return base_code + 2
		engine_call := gd.godot_main_call(call)
		owned, get_error := godot.rendering_server_multimesh_get_buffer(engine_call, server, stream.rid)
		if get_error != .None do return base_code + 3
		result := i64(0)
		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)
		if read_error != .None || base == nil || length != int(stream.capacity) * rewind.RENDER_STRIDE {
			result = base_code + 4
		} else {
			stored := mem.slice_ptr(base, length)
			for i in 0..<len(stored) {
				if transmute(u32)stored[i] != transmute(u32)view[i] {
					result = base_code + 5
					break
				}
			}
		}
		if godot.owned_value_destroy(engine_call.api, &owned) != .None do return base_code + 6
		return result
	}

	if code := verify_one(self, api, call, &server, &self.box_stream, -10); code != 0 do return code
	if code := verify_one(self, api, call, &server, &self.circle_stream, -20); code != 0 do return code
	if code := verify_one(self, api, call, &server, &self.static_stream, -30); code != 0 do return code
	return 0
}

// Time_Lab_Shutdown retires every retained owner explicitly: the three
// streams (each close also retires that stream's pending retirement, if
// any) and the pure worlds. The teardown hook has no authority, so
// cooperative shutdown is the symmetric cleanup this owner set's lifetime
// contract requires.
//
// Error codes: -1 invalid call token, -2 facade unavailable, -3 the
// retired world could not be destroyed, -4 a stream or the live world
// could not retire.
@(gododin)
time_lab_shutdown :: proc(self: ^Time_Lab, call: gd.Main_Call) -> i64 {
	if gd.validate_main_call(call) != .None do return -1
	code := i64(0)
	if !rewind.world_destroy(&self.retired_world) && code == 0 do code = -3
	if render.multimesh_stream_owned(&self.box_stream) ||
		render.multimesh_stream_owned(&self.circle_stream) ||
		render.multimesh_stream_owned(&self.static_stream) {
		api, facade_ok := gd.main_facade(call)
		if !facade_ok do return -2
		if render.multimesh_stream_close(render.main_call(api), &self.box_stream) != .None && code == 0 do code = -4
		if render.multimesh_stream_close(render.main_call(api), &self.circle_stream) != .None && code == 0 do code = -4
		if render.multimesh_stream_close(render.main_call(api), &self.static_stream) != .None && code == 0 do code = -4
	}
	if self.world != nil {
		if !rewind.world_destroy(&self.world) && code == 0 do code = -4
	}
	if code == 0 {
		self.ticks_run, self.submissions = 0, 0
	}
	return code
}

@(gododin)
time_lab_tick :: proc(self: ^Time_Lab) -> i64 {
	if self.world == nil do return -1
	return self.world.tick
}

@(gododin)
time_lab_body_count :: proc(self: ^Time_Lab) -> i64 {
	if self.world == nil do return -1
	return i64(self.world.dynamic_count)
}

@(gododin)
time_lab_restore_steps :: proc(self: ^Time_Lab) -> i64 {
	if self.world == nil do return -1
	return self.world.last_restore_steps
}

@(gododin)
time_lab_rebuilds :: proc(self: ^Time_Lab) -> i64 {
	if self.world == nil do return -1
	return self.world.rebuild_count
}

@(gododin)
time_lab_submissions :: proc(self: ^Time_Lab) -> i64 {
	return self.submissions
}

// Time_Lab_State_Digest returns the subsystem's running FNV chain as the
// raw u64 bit pattern; Godot sees an i64 fingerprint, not a number.
@(gododin)
time_lab_state_digest :: proc(self: ^Time_Lab) -> i64 {
	if self.world == nil do return 0
	return cast(i64)rewind.state_digest(self.world)
}

// Time_Lab_Recorded_Digest returns the history ring's recorded digest for a
// past tick inside the retained window — the oracle the scrub proof compares
// the live digest against.
//
// Error codes: -1 setup not live, -2 the tick has no record (outside the
// window or at or ahead of the playhead).
@(gododin)
time_lab_recorded_digest :: proc(self: ^Time_Lab, tick: i64) -> i64 {
	if self.world == nil do return -1
	digest, ok := rewind.ring_digest(self.world, tick)
	if !ok do return -2
	return cast(i64)digest
}

// Time_Lab_Window_Base returns the earliest tick a rewind may target: the
// epoch or the window base, whichever is later — the same bound
// Time_Lab_Rewind_To enforces. The timeline slider reads it as its minimum.
@(gododin)
time_lab_window_base :: proc(self: ^Time_Lab) -> i64 {
	if self.world == nil do return -1
	base := self.world.tick - self.world.ring_count
	if self.world.epoch_tick > base {
		return self.world.epoch_tick
	}
	return base
}

// Time_Lab_Freeze appends a freeze command at the current playhead tick.
// The velocity parking happens at command execution, so the command is
// replayed history like any other input: rewinding before it reproduces
// the unfrozen timeline exactly.
//
// Error codes: -1 setup not live, -2 the slot is out of range, -3 the log
// refused the append.
@(gododin)
time_lab_freeze :: proc(self: ^Time_Lab, slot: i64) -> i64 {
	if self.world == nil do return -1
	if slot < 0 || slot >= i64(self.world.dynamic_count) do return -2
	if !rewind.append_command(self.world, rewind.Command{tick = self.world.tick, kind = .freeze, slot = i32(slot)}) do return -3
	return 0
}

// Time_Lab_Unfreeze appends the counterpart command; the subsystem
// restores the parked velocity bitwise at execution.
//
// Error codes match Time_Lab_Freeze.
@(gododin)
time_lab_unfreeze :: proc(self: ^Time_Lab, slot: i64) -> i64 {
	if self.world == nil do return -1
	if slot < 0 || slot >= i64(self.world.dynamic_count) do return -2
	if !rewind.append_command(self.world, rewind.Command{tick = self.world.tick, kind = .unfreeze, slot = i32(slot)}) do return -3
	return 0
}

// Time_Lab_Pick resolves a world point to the nearest dynamic slot within
// the radius — the freeze gun's target query. Statics are not pickable;
// spawned balls are.
//
// Return codes: >= 0 the slot, -1 no dynamic slot within the radius (or a
// non-finite or negative payload, which the subsystem refuses), -2 setup
// not live.
@(gododin)
time_lab_pick :: proc(self: ^Time_Lab, x, y, radius: f64) -> i64 {
	if self.world == nil do return -2
	return i64(rewind.pick_slot(self.world, f32(x), f32(y), f32(radius)))
}

// Time_Lab_Frozen reports whether one dynamic slot is currently frozen:
// 1 frozen, 0 not.
//
// Error codes: -1 setup not live, -2 the slot is out of range.
@(gododin)
time_lab_frozen :: proc(self: ^Time_Lab, slot: i64) -> i64 {
	if self.world == nil do return -1
	if slot < 0 || slot >= i64(self.world.dynamic_count) do return -2
	if self.world.frozen & (u64(1) << uint(slot)) != 0 do return 1
	return 0
}

// Time_Lab_Frozen_Count returns the number of currently frozen dynamic
// slots.
@(gododin)
time_lab_frozen_count :: proc(self: ^Time_Lab) -> i64 {
	if self.world == nil do return -1
	count := i64(0)
	for s in 0..<self.world.dynamic_count {
		if self.world.frozen & (u64(1) << uint(s)) != 0 {
			count += 1
		}
	}
	return count
}

// 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
}