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
}