Gododin Gododin Documentation

Linked repository source

examples/rewind_lab/subsystem/rewind.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 rewind owns a Box2D simulation with its own fixed tick, input log,
// bounded history, and exact restore. The tick and the state belong to this
// package, never to the engine: Godot later drives ticks and reads staged
// transforms, but it never owns simulation time.
//
// Imports are vendor:box2d and Odin core only — no Godot, Gododin, or
// Raylib, per the suite's pure-subsystem convention. One owned allocation
// per world (the World value itself, which embeds every ring and log it
// needs); Box2D's internal storage belongs to the C library and never
// flows through Odin allocators.
//
// Determinism discipline: fixed 60 Hz tick, f32 throughout, array-only
// iteration (no map iteration anywhere in the tick), one seeded RNG stream
// per world, and single-threaded solving — WorldDef's task callbacks stay
// unused, warm starting and sleeping stay disabled (see rebuild).
//
// Restore exactness is path-based, and that is a measured engine fact, not
// an assumption: Box2D's broadphase tree, pair ordering, and manifold
// history evolve as a world ages, so a world rebuilt and teleported into
// mid-history state diverges measurably from one that stepped there — the
// step is not a pure function of (transforms, velocities, types). What IS
// a pure function is the path from a rebuild: a world rebuilt from the
// spec and replayed to tick T is bitwise identical to any other world that
// reached T the same way. Restore therefore means: rebuild from the epoch
// spec and replay every tick from the epoch start. Cost is one rebuild
// plus (target - epoch) steps; the receipt records what that costs, and
// the design doc's anchor-spacing/cache follow-ups are what re-enters if
// scrubbing cost ever demands them.
//
// Body identity is ours, not Box2D's: dynamic bodies occupy slots
// 0..dynamic_count-1 (spec dynamics first in spec order, then spawned balls
// in spawn order). Box2D BodyId handles are per-world-build and are
// remapped on every rebuild, so commands always speak slot indices.
package rewind

import b2 "../box2d"
import "core:math"
import "core:mem"

// Fixed-tick constants. One tick is one b2.World_Step call at 60 Hz.
TICK_RATE :: 60
TICK_DT :: f32(1) / f32(TICK_RATE)
DEFAULT_SUB_STEPS :: i32(4)
MAX_SUB_STEPS :: i32(8)

// History bounds. The ring is the retained history window of per-tick
// digests; comparisons and the HUD's playhead read it.
DEFAULT_WINDOW_TICKS :: 3600
MIN_WINDOW_TICKS :: 60
MAX_WINDOW_TICKS :: 7200

// Roster bounds. Preset dynamics and spawned balls share the same slots,
// so the cap is total dynamic bodies, in the boid-admission style.
MAX_DYNAMIC_SLOTS :: 64
DEFAULT_BODY_CAP :: 48
MAX_PRESET_BODIES :: 48
MAX_PRESET_JOINTS :: 24

// Log bounds.
MAX_COMMANDS :: 16384

// Numeric admission bounds, boid style: they prevent finite-but-overflowing
// arithmetic, nothing more.
COORD_LIMIT :: f32(2048)
IMPULSE_LIMIT :: f32(10000)
DEFAULT_RESTITUTION_SCALE :: f32(1)

// The solver's 2D vector type, aliased for spec readability.
Vec2 :: b2.Vec2

// A contraption preset identifier. Presets are pure values of Spec_Preset,
// built identically on every call — they are the rebuild authority.
Preset_Id :: enum {
	pyramid_stack,
	newtons_cradle,
	chain_bridge,
	domino_spiral,
	seesaw,
	pendulum_chain,
}

PRESET_COUNT :: len(Preset_Id)

Spec_Shape_Kind :: enum {
	box,
	circle,
}

Spec_Body :: struct {
	kind:       Spec_Shape_Kind,
	is_dynamic: bool,
	hx, hy:     f32, // box half extents
	radius:     f32, // circle radius
	x, y:       f32,
	angle:      f32,
	density:    f32,
	friction:   f32,
	restitution: f32,
}

Spec_Joint_Kind :: enum {
	revolute,
	weld,
}

Spec_Joint :: struct {
	kind:                     Spec_Joint_Kind,
	a, b:                     i32, // indices into the preset's body list
	anchor_a, anchor_b:       Vec2,
	enable_limit:             bool,
	lower_angle, upper_angle: f32,
}

// Spec_Preset is the construction record: bodies, then joints referencing
// body indices. The ground slab leads every preset by convention.
Spec_Preset :: struct {
	id:            Preset_Id,
	gravity:       Vec2,
	body_count:    i32,
	dynamic_count: i32,
	joint_count:   i32,
	bodies:        [MAX_PRESET_BODIES]Spec_Body,
	joints:        [MAX_PRESET_JOINTS]Spec_Joint,
}

// Authored configuration, mirroring the Time_Lab properties one for one.
// Scalars only, with admission bounds in valid_config.
Config :: struct {
	preset:            Preset_Id,
	seed:              u32,
	gravity:           Vec2,
	restitution_scale: f32,
	sub_steps:         i32,
	window_ticks:      i64,
	body_cap:          i32,
}

default_config :: proc() -> Config {
	return {
		preset            = .pyramid_stack,
		seed              = u32(20260916),
		gravity           = {0, -10},
		restitution_scale = DEFAULT_RESTITUTION_SCALE,
		sub_steps         = DEFAULT_SUB_STEPS,
		window_ticks      = DEFAULT_WINDOW_TICKS,
		body_cap          = DEFAULT_BODY_CAP,
	}
}

valid_config :: proc(c: Config) -> bool {
	if math.is_nan(c.gravity[0]) || math.is_nan(c.gravity[1]) || math.is_inf(c.gravity[0]) || math.is_inf(c.gravity[1]) {
		return false
	}
	if math.abs(c.gravity[0]) > 1000 || math.abs(c.gravity[1]) > 1000 {
		return false
	}
	return c.restitution_scale >= 0 && c.restitution_scale <= 1 &&
		c.sub_steps >= 1 && c.sub_steps <= MAX_SUB_STEPS &&
		c.window_ticks >= MIN_WINDOW_TICKS && c.window_ticks <= MAX_WINDOW_TICKS &&
		c.body_cap >= 1 && c.body_cap <= MAX_DYNAMIC_SLOTS
}

// Commands are per-tick records; a replay is (epoch rebuild + command
// replay). Payloads reuse fields by kind: fling carries (x, y) as the world
// impulse; spawn_ball carries (x, y) as the world point.
Command_Kind :: enum {
	fling,
	freeze,
	unfreeze,
	spawn_ball,
	switch_preset,
	clear_spawned,
}

Command :: struct {
	tick:   i64,
	kind:   Command_Kind,
	slot:   i32,
	x, y:   f32,
	preset: Preset_Id,
}

valid_command :: proc(c: Command) -> bool {
	for v in ([2]f32{c.x, c.y}) {
		if math.is_nan(v) || math.is_inf(v) || math.abs(v) > COORD_LIMIT {
			return false
		}
	}
	if c.kind == .fling {
		for v in ([2]f32{c.x, c.y}) {
			if math.abs(v) > IMPULSE_LIMIT {
				return false
			}
		}
	}
	return c.tick >= 0 && int(c.slot) >= 0 && int(c.preset) < PRESET_COUNT
}

// One seeded RNG stream per world, carried across rebuilds verbatim so a
// replay consumes it in exactly the original order. A small mixed LCG;
// nothing here is statistical quality.
RNG :: distinct u64

rng_init :: proc(seed: u32) -> RNG {
	return RNG(u64(seed) * 0x9E3779B97F4A7C15 ~ 0xD1B54A32D192ED03 + 1)
}

rng_next :: proc(r: ^RNG) -> u32 {
	r^ = r^ * 6364136223846793005 + 1442695040888963407
	return u32(r^ >> 33)
}

// Per-tick cheap state: the running digest chain, the spine every
// comparison reads. Parked velocities, cursor, and spawn bookkeeping are
// deliberately not copied per tick — restore reconstructs them by
// re-executing history, so per-tick copies would be dead weight.
Tick_Record :: struct {
	digest: u64,
}

World :: struct {
	allocator: mem.Allocator,
	world_id:  b2.WorldId,
	config:    Config,
	spec:      Spec_Preset,

	rng: RNG,
	tick: i64, // ticks executed; the playhead's exclusive end

	// The epoch is the last rebuild point: the tick the current preset's
	// world was constructed at, its preset, and the stream state at that
	// moment. Exact restore replays from exactly here.
	epoch_tick:   i64,
	epoch_preset: Preset_Id,
	epoch_rng:    RNG,

	// Slot roster. Spec dynamics fill 0..spec_dynamic_count-1; spawned
	// balls fill spec_dynamic_count..dynamic_count-1.
	ids:                [MAX_DYNAMIC_SLOTS]b2.BodyId,
	dynamic_count:      i32,
	spec_dynamic_count: i32,
	spawned_count:      i32,
	frozen:             u64,
	parked_vx:          [MAX_DYNAMIC_SLOTS]f32,
	parked_vy:          [MAX_DYNAMIC_SLOTS]f32,
	parked_w:           [MAX_DYNAMIC_SLOTS]f32,
	// Spawned balls' radii, remembered for the staged render layout and
	// cleared on every rebuild; spec slots stay zero.
	spawn_radii:        [MAX_DYNAMIC_SLOTS]f32,

	// Input log, sorted by tick and append-only except for the documented
	// branch cut: appending at a tick that already has commands, or at any
	// tick below the log's last, replaces that tick and the entire later
	// branch (rewind-then-act semantics; see README). A preset switch
	// clears the log entirely: the old branch's commands belong to the old
	// epoch.
	commands:      [MAX_COMMANDS]Command,
	command_count: int,
	cursor:        int, // first command not yet consumed by the tick loop

	// History ring over the retained window. Record for tick t lives at
	// ring[t % MAX_WINDOW_TICKS]; records exist for ticks
	// [tick - ring_count, tick).
	ring:       [MAX_WINDOW_TICKS]Tick_Record,
	ring_count: i64,

	// Telemetry, owned by this package and reported by the eval/HUD.
	rebuild_count:      i64,
	last_restore_steps: i64,
}

// world_create validates the configuration and builds the initial world at
// tick 0. The World value embeds all its tables, so this is the package's
// one warm allocation.
world_create :: proc(cfg: Config, allocator: mem.Allocator) -> ^World {
	if allocator.procedure == nil || !valid_config(cfg) {
		return nil
	}
	w, err := new(World, allocator)
	if err != nil {
		return nil
	}
	w^ = World{allocator = allocator, config = cfg}
	w.epoch_preset = cfg.preset
	w.epoch_rng = rng_init(cfg.seed)
	w.rng = w.epoch_rng
	if !rebuild(w, cfg.preset, 0) {
		if b2.World_IsValid(w.world_id) {
			b2.DestroyWorld(w.world_id)
		}
		mem.free(w, allocator)
		return nil
	}
	return w
}

// world_destroy consumes the owner's pointer, per the suite's explicit
// teardown convention. Repeated cleanup is safe through that owner.
world_destroy :: proc(owner: ^^World) -> bool {
	if owner == nil || owner^ == nil {
		return true
	}
	w := owner^
	if b2.World_IsValid(w.world_id) {
		b2.DestroyWorld(w.world_id)
	}
	w.world_id = {}
	allocator := w.allocator
	w^ = World{}
	if mem.free(w, allocator) != nil {
		return false
	}
	owner^ = nil
	return true
}

// rebuild tears down any live simulation and constructs the named preset
// fresh at the given epoch tick. The RNG stream is whatever the caller
// left in w.epoch_rng; rebuild never touches it. rebuild() is the exactness
// atom: every timeline this package certifies passes through it.
rebuild :: proc(w: ^World, preset: Preset_Id, epoch_tick: i64) -> bool {
	if b2.World_IsValid(w.world_id) {
		b2.DestroyWorld(w.world_id)
		w.world_id = {}
	}
	spec := preset_build(preset)
	def := b2.DefaultWorldDef()
	// The authored configuration owns gravity; the spec's own gravity field
	// stays a documentation default for preset consumers.
	def.gravity = w.config.gravity
	// Exact restore additionally requires each step to be a pure function
	// of captured state: warm-start contact/joint impulses and per-body
	// sleep timers are solver state a rebuild does not reconstruct, so
	// both stay off. Cold-start solving and no sleeping cost some stack
	// stability and settle speed — the receipt records what that costs —
	// and buy bitwise replay.
	world_id := b2.CreateWorld(def)
	if !b2.World_IsValid(world_id) {
		return false
	}
	b2.World_EnableWarmStarting(world_id, false)
	b2.World_EnableSleeping(world_id, false)
	w.world_id = world_id
	w.spec = spec
	w.dynamic_count = 0
	w.spec_dynamic_count = spec.dynamic_count
	w.spawned_count = 0
	w.frozen = 0
	for i in 0..<MAX_DYNAMIC_SLOTS {
		w.parked_vx[i] = 0
		w.parked_vy[i] = 0
		w.parked_w[i] = 0
		w.spawn_radii[i] = 0
	}
	w.tick = epoch_tick
	w.epoch_tick = epoch_tick
	w.epoch_preset = preset
	w.rebuild_count += 1

	// Bodies, in spec order. Dynamic bodies take slots in the same order.
	ids: [MAX_PRESET_BODIES]b2.BodyId
	for i in 0..<spec.body_count {
		body := &spec.bodies[i]
		body_def := b2.DefaultBodyDef()
		if body.is_dynamic {
			body_def.type = b2.BodyType.dynamicBody
		} else {
			body_def.type = b2.BodyType.staticBody
		}
		body_def.position = {body.x, body.y}
		body_def.rotation = b2.MakeRot(body.angle)
		id := b2.CreateBody(world_id, body_def)
		if !b2.Body_IsValid(id) {
			return false
		}
		ids[i] = id
		shape_def := b2.DefaultShapeDef()
		shape_def.material.friction = body.friction
		shape_def.material.restitution = clamp01(body.restitution * w.config.restitution_scale)
		shape_def.density = body.density
		switch body.kind {
		case .box:
			polygon := b2.MakeBox(body.hx, body.hy)
			if !b2.Shape_IsValid(b2.CreatePolygonShape(id, shape_def, &polygon)) {
				return false
			}
		case .circle:
			circle := b2.Circle{radius = body.radius}
			if !b2.Shape_IsValid(b2.CreateCircleShape(id, shape_def, &circle)) {
				return false
			}
		}
		if body.is_dynamic {
			w.ids[w.dynamic_count] = id
			w.dynamic_count += 1
		}
	}

	// Joints, resolved against the just-created ids.
	for j in 0..<spec.joint_count {
		joint := &spec.joints[j]
		switch joint.kind {
		case .revolute:
			rev := b2.DefaultRevoluteJointDef()
			rev.bodyIdA = ids[joint.a]
			rev.bodyIdB = ids[joint.b]
			rev.localAnchorA = joint.anchor_a
			rev.localAnchorB = joint.anchor_b
			rev.enableLimit = joint.enable_limit
			rev.lowerAngle = joint.lower_angle
			rev.upperAngle = joint.upper_angle
			if !b2.Joint_IsValid(b2.CreateRevoluteJoint(world_id, rev)) {
				return false
			}
		case .weld:
			weld := b2.DefaultWeldJointDef()
			weld.bodyIdA = ids[joint.a]
			weld.bodyIdB = ids[joint.b]
			weld.localAnchorA = joint.anchor_a
			weld.localAnchorB = joint.anchor_b
			if !b2.Joint_IsValid(b2.CreateWeldJoint(world_id, weld)) {
				return false
			}
		}
	}

	// The cursor lands on the first command at or after the epoch tick.
	w.cursor = first_command_at_or_after(w, epoch_tick)
	return true
}

clamp01 :: proc(v: f32) -> f32 {
	if v < 0 {
		return 0
	}
	if v > 1 {
		return 1
	}
	return v
}

first_command_at_or_after :: proc(w: ^World, tick: i64) -> int {
	for i in 0..<w.command_count {
		if w.commands[i].tick >= tick {
			return i
		}
	}
	return w.command_count
}

// append_command admits a command at the present or future playhead.
// Appending at a tick that already has commands, or below the log's last
// tick, performs the documented branch cut: that tick and every later
// command are replaced, which is rewind-then-act semantics. Editing the
// executed past (below the playhead) is refused.
append_command :: proc(w: ^World, cmd: Command) -> bool {
	if w == nil || !valid_command(cmd) || cmd.tick < w.tick {
		return false
	}
	if w.command_count >= MAX_COMMANDS {
		return false
	}
	if w.command_count > 0 && cmd.tick < w.commands[w.command_count - 1].tick {
		w.command_count = first_command_at_or_after(w, cmd.tick)
	}
	w.commands[w.command_count] = cmd
	w.command_count += 1
	if w.cursor > w.command_count {
		w.cursor = w.command_count
	}
	return true
}

// advance executes one fixed tick: consume this tick's commands, step the
// solver, record the digest. It allocates nothing.
advance :: proc(w: ^World) -> bool {
	if w == nil || !b2.World_IsValid(w.world_id) {
		return false
	}
	for w.cursor < w.command_count && w.commands[w.cursor].tick == w.tick {
		apply_command(w, &w.commands[w.cursor])
		w.cursor += 1
	}
	b2.World_Step(w.world_id, TICK_DT, w.config.sub_steps)
	w.tick += 1
	w.ring[w.tick % MAX_WINDOW_TICKS] = Tick_Record{digest = state_digest(w)}
	if w.ring_count < w.config.window_ticks {
		w.ring_count += 1
	}
	return true
}

apply_command :: proc(w: ^World, cmd: ^Command) {
	switch cmd.kind {
	case .fling:
		if int(cmd.slot) < int(w.dynamic_count) && b2.Body_IsValid(w.ids[cmd.slot]) {
			b2.Body_ApplyLinearImpulseToCenter(w.ids[cmd.slot], {cmd.x, cmd.y}, true)
		}
	case .freeze:
		slot := cmd.slot
		mask := u64(1) << uint(slot)
		if int(slot) < int(w.dynamic_count) && w.frozen & mask == 0 && b2.Body_IsValid(w.ids[slot]) {
			v := b2.Body_GetLinearVelocity(w.ids[slot])
			w.parked_vx[slot] = v[0]
			w.parked_vy[slot] = v[1]
			w.parked_w[slot] = b2.Body_GetAngularVelocity(w.ids[slot])
			b2.Body_SetType(w.ids[slot], b2.BodyType.staticBody)
			w.frozen |= mask
		}
	case .unfreeze:
		slot := cmd.slot
		mask := u64(1) << uint(slot)
		if w.frozen & mask != 0 && int(slot) < int(w.dynamic_count) && b2.Body_IsValid(w.ids[slot]) {
			b2.Body_SetType(w.ids[slot], b2.BodyType.dynamicBody)
			b2.Body_SetLinearVelocity(w.ids[slot], {w.parked_vx[slot], w.parked_vy[slot]})
			b2.Body_SetAngularVelocity(w.ids[slot], w.parked_w[slot])
			w.frozen &= ~mask
		}
	case .spawn_ball:
		if w.dynamic_count < w.config.body_cap {
			radius := 0.25 + 0.15 * f32(rng_next(&w.rng)) / f32(0xFFFFFFFF)
			body_def := b2.DefaultBodyDef()
			body_def.type = b2.BodyType.dynamicBody
			body_def.position = {cmd.x, cmd.y}
			id := b2.CreateBody(w.world_id, body_def)
			if b2.Body_IsValid(id) {
				shape_def := b2.DefaultShapeDef()
				shape_def.material.friction = 0.3
				shape_def.material.restitution = clamp01(0.3 * w.config.restitution_scale)
				shape_def.density = 1
				circle := b2.Circle{radius = radius}
				if b2.Shape_IsValid(b2.CreateCircleShape(id, shape_def, &circle)) {
					w.ids[w.dynamic_count] = id
					w.spawn_radii[w.dynamic_count] = radius
					w.dynamic_count += 1
					w.spawned_count += 1
				}
			}
		}
	case .switch_preset:
		// A switch starts a new epoch at the current tick: the stream
		// continues from where it is, stale history is dropped, and the
		// log clears — commands addressed to the old epoch's slots do not
		// survive it. Restores before this tick are refused (the old
		// epoch is gone by design).
		w.epoch_rng = w.rng
		w.command_count = 0
		w.cursor = 0
		w.ring_count = 0
		rebuild(w, cmd.preset, w.tick)
	case .clear_spawned:
		slot := w.dynamic_count - w.spawned_count
		for s in slot..<w.dynamic_count {
			if b2.Body_IsValid(w.ids[s]) {
				b2.DestroyBody(w.ids[s])
			}
			w.frozen &= ~(u64(1) << uint(s))
		}
		w.dynamic_count = slot
		w.spawned_count = 0
	}
}

// state_digest is the running checksum: the identity inputs (tick, roster,
// frozen mask) plus every dynamic slot's full solver state, hashed with
// FNV-1a over raw f32 bits, bunny style.
state_digest :: proc(w: ^World) -> u64 {
	hash := u64(0xcbf29ce484222325)
	mix :: proc(hash: ^u64, v: f32) {
		bits := transmute(u32)v
		for shift in 0..<4 {
			hash^ = (hash^ ~ u64((bits >> (u32(shift) * 8)) & 0xFF)) * 0x100000001b3
		}
	}
	mix_u64 :: proc(hash: ^u64, v: u64) {
		for shift in 0..<8 {
			hash^ = (hash^ ~ ((v >> (u64(shift) * 8)) & 0xFF)) * 0x100000001b3
		}
	}
	mix_u64(&hash, u64(w.tick))
	mix_u64(&hash, u64(w.dynamic_count))
	mix_u64(&hash, w.frozen)
	for s in 0..<w.dynamic_count {
		id := w.ids[s]
		pos := b2.Body_GetPosition(id)
		rot := b2.Body_GetRotation(id)
		vel := b2.Body_GetLinearVelocity(id)
		mix(&hash, pos[0])
		mix(&hash, pos[1])
		mix(&hash, rot.c)
		mix(&hash, rot.s)
		mix(&hash, vel[0])
		mix(&hash, vel[1])
		mix(&hash, b2.Body_GetAngularVelocity(id))
	}
	return hash
}

// rewind_to restores the world to an exact recorded tick: rebuild from the
// epoch spec and replay every recorded command from the epoch start. The
// restored state is bitwise identical to any world that reached the target
// the same way — including the un-rewound timeline this one came from.
// Cost is one rebuild plus (target - epoch) steps, reported in telemetry.
rewind_to :: proc(w: ^World, target: i64) -> bool {
	if w == nil || target < 0 || target > w.tick {
		return false
	}
	base := w.tick - w.ring_count
	if target < base || target < w.epoch_tick {
		return false
	}

	// Replay from the epoch. The stream resumes from the epoch's own
	// state, and the rebuild wipes solver history the old world carried.
	w.rng = w.epoch_rng
	if !rebuild(w, w.epoch_preset, w.epoch_tick) {
		return false
	}
	steps := target - w.epoch_tick
	for steps > 0 {
		if !advance(w) {
			return false
		}
		steps -= 1
	}
	w.last_restore_steps = target - w.epoch_tick
	return true
}

// oracle_restore is the elementwise oracle: recompute the target tick in a
// scratch world by replay from the epoch — the same path rewind_to takes,
// built independently — then compare the live world's roster elementwise,
// bitwise. The scratch is born straight at the epoch (create, then rebuild
// at the epoch tick with the epoch's stream): replaying a switch command
// would not work, because a pre-loaded switch erases the post-switch
// commands the live log carries.
oracle_restore :: proc(w: ^World, target: i64, scratch_allocator: mem.Allocator) -> bool {
	if w == nil || target < 0 || target > w.tick || target < w.epoch_tick {
		return false
	}
	scratch := world_create(w.config, scratch_allocator)
	if scratch == nil {
		return false
	}
	defer world_destroy(&scratch)
	scratch.rng = w.epoch_rng
	if !rebuild(scratch, w.epoch_preset, w.epoch_tick) {
		return false
	}
	for i in 0..<w.command_count {
		cmd := w.commands[i]
		if cmd.tick < w.epoch_tick {
			continue
		}
		if cmd.tick >= target {
			break
		}
		if !append_command(scratch, cmd) {
			return false
		}
	}
	for scratch.tick < target {
		if !advance(scratch) {
			return false
		}
	}
	return state_equal(w, scratch)
}

// state_equal compares two worlds elementwise, bitwise — the suite's
// payload-without-holding pattern applied to solver state.
state_equal :: proc(a, b: ^World) -> bool {
	if a == nil || b == nil {
		return a == b
	}
	if a.dynamic_count != b.dynamic_count || a.frozen != b.frozen || a.spawned_count != b.spawned_count {
		return false
	}
	for s in 0..<a.dynamic_count {
		pa, pb := b2.Body_GetPosition(a.ids[s]), b2.Body_GetPosition(b.ids[s])
		ra, rb := b2.Body_GetRotation(a.ids[s]), b2.Body_GetRotation(b.ids[s])
		va, vb := b2.Body_GetLinearVelocity(a.ids[s]), b2.Body_GetLinearVelocity(b.ids[s])
		wa, wb := b2.Body_GetAngularVelocity(a.ids[s]), b2.Body_GetAngularVelocity(b.ids[s])
		pairs := [8]f32{pa[0], pa[1], ra.c, ra.s, va[0], va[1], wa, 0}
		pairb := [8]f32{pb[0], pb[1], rb.c, rb.s, vb[0], vb[1], wb, 0}
		for i in 0..<7 {
			if transmute(u32)pairs[i] != transmute(u32)pairb[i] {
				return false
			}
		}
	}
	return true
}

// ring_digest returns the recorded digest for a tick inside the window.
ring_digest :: proc(w: ^World, tick: i64) -> (u64, bool) {
	if w == nil {
		return 0, false
	}
	base := w.tick - w.ring_count
	if tick < base || tick >= w.tick {
		return 0, false
	}
	return w.ring[tick % MAX_WINDOW_TICKS].digest, true
}

// pick_slot returns the dynamic slot nearest to (x, y) within max_dist, or
// -1 when none qualifies. This is the freeze gun's target query. The scan
// runs in slot order, and an equal distance never displaces an earlier
// slot, so ties resolve to the lowest slot deterministically. A radius
// whose square overflows to +Inf keeps working: every finite distance then
// compares below the bound and the nearest slot still wins.
pick_slot :: proc(w: ^World, x, y, max_dist: f32) -> i32 {
	if w == nil || max_dist < 0 {
		return -1
	}
	for v in ([3]f32{x, y, max_dist}) {
		if math.is_nan(v) || math.is_inf(v) {
			return -1
		}
	}
	best := i32(-1)
	best_d2 := max_dist * max_dist
	for s in 0..<w.dynamic_count {
		p := b2.Body_GetPosition(w.ids[s])
		dx, dy := p[0] - x, p[1] - y
		d2 := dx*dx + dy*dy
		if d2 <= best_d2 && (best < 0 || d2 < best_d2) {
			best, best_d2 = s, d2
		}
	}
	return best
}

// slot_state writes one dynamic slot's solver state as seven floats:
// x, y, rot.c, rot.s, vx, vy, w. This is simulation truth for tests and
// oracles; MultiMesh packing is the authored class's earned-bulk job and
// lives there, not here.
slot_state :: proc(w: ^World, slot: i32, sink: []f32) -> bool {
	if w == nil || slot < 0 || int(slot) >= int(w.dynamic_count) || len(sink) < 7 {
		return false
	}
	pos := b2.Body_GetPosition(w.ids[slot])
	rot := b2.Body_GetRotation(w.ids[slot])
	vel := b2.Body_GetLinearVelocity(w.ids[slot])
	sink[0], sink[1] = pos[0], pos[1]
	sink[2], sink[3] = rot.c, rot.s
	sink[4], sink[5] = vel[0], vel[1]
	sink[6] = b2.Body_GetAngularVelocity(w.ids[slot])
	return true
}