import { fnv1a64 } from "./replay.mjs"; export const SCHEDULER_SCHEMA = "sim_scheduler.v1"; export const SCHEDULER_PHASES = Object.freeze(["reconcile", "task", "tp", "kinematics", "motion", "hal_io", "callbacks", "publish"]); export const DEFAULT_TICK_BUDGET = 1_000_000; function call(core, name, returnType, argTypes, args) { if (typeof core?.ccall === "function") return core.ccall(name, returnType, argTypes, args); const direct = core?.[`_${name}`] ?? core?.[name]; if (typeof direct === "function") return direct(...args); throw new Error(`missing core export: ${name}`); } function clone(value) { return structuredClone(value); } export class SimScheduler { constructor(core, { budget = DEFAULT_TICK_BUDGET } = {}) { if (!Number.isSafeInteger(budget) || budget < 1 || budget > DEFAULT_TICK_BUDGET) throw new Error("scheduler_budget_invalid"); this.core = core; this.budget = budget; this.tick = 0; this.last_cycles = 0; this.phase_trace = [...SCHEDULER_PHASES]; this.coreScheduler = 0; this.coreSize = 0; this.ensureCore(); } ensureCore() { if (this.coreScheduler || typeof this.core?._sim_scheduler_size !== "function") return; if (typeof this.core?._malloc !== "function" || typeof this.core?._free !== "function") throw new Error("scheduler_core_memory_unavailable"); this.coreSize = call(this.core, "sim_scheduler_size", "number", [], []); this.coreScheduler = this.core._malloc(this.coreSize); if (!this.coreScheduler) throw new Error("scheduler_core_allocation_failed"); const result = call(this.core, "sim_scheduler_init", "number", ["number", "number"], [this.coreScheduler, this.budget]); if (result !== 0) throw new Error(`scheduler_core_init_${result}`); } export() { return { schema: SCHEDULER_SCHEMA, budget: this.budget, last_cycles: this.last_cycles, phase_trace: [...this.phase_trace], tick: this.tick, state_hash: this.hash() }; } hash() { const unsigned = { schema: SCHEDULER_SCHEMA, budget: this.budget, phase_trace: [...this.phase_trace], tick: this.tick }; return fnv1a64(unsigned); } step(cycles, callbacks = {}) { if (!Number.isSafeInteger(cycles) || cycles < 0) throw new Error("scheduler_cycles_invalid"); if (cycles > this.budget) throw new Error("scheduler_budget_exceeded"); if (this.coreScheduler) { const result = call(this.core, "sim_scheduler_step", "number", ["number", "number"], [this.coreScheduler, cycles]); if (result !== 0) throw new Error(`scheduler_core_step_${result}`); } for (let index = 0; index < cycles; index += 1) { const nextTick = this.tick + 1; try { for (const phase of SCHEDULER_PHASES) { this.phase_trace = [...SCHEDULER_PHASES]; if (typeof callbacks[phase] === "function") callbacks[phase](nextTick); } } catch (error) { // Callbacks may already have advanced motion inside this tick. Do // not replay it or retain the native scheduler's unexecuted future. this.tick = nextTick; this.last_cycles = index + 1; this.restore(this.export()); throw error; } this.tick = nextTick; } this.last_cycles = cycles; return this.export(); } restore(value) { if (!value || value.schema !== SCHEDULER_SCHEMA || !Number.isSafeInteger(value.budget) || value.budget < 1 || value.budget > DEFAULT_TICK_BUDGET || !Number.isSafeInteger(value.tick) || value.tick < 0 || !Number.isSafeInteger(value.last_cycles) || value.last_cycles < 0 || !Array.isArray(value.phase_trace) || value.phase_trace.join("\0") !== SCHEDULER_PHASES.join("\0")) throw new Error("scheduler_restore_invalid"); const unsigned = { schema: SCHEDULER_SCHEMA, budget: value.budget, phase_trace: [...value.phase_trace], tick: value.tick }; if (value.state_hash !== fnv1a64(unsigned)) throw new Error("scheduler_restore_hash"); this.budget = value.budget; this.tick = value.tick; this.last_cycles = value.last_cycles; this.phase_trace = [...value.phase_trace]; this.ensureCore(); if (this.coreScheduler) { const phases = new Uint32Array(SCHEDULER_PHASES.map((_, index) => index)); const pointer = this.core._malloc(phases.byteLength); this.core.HEAPU32.set(phases, pointer / 4); try { const result = call(this.core, "sim_scheduler_restore", "number", ["number", "number", "number", "number", "number", "number"], [this.coreScheduler, this.tick, this.budget, this.last_cycles, pointer, SCHEDULER_PHASES.length]); if (result !== 0) throw new Error(`scheduler_core_restore_${result}`); } finally { this.core._free(pointer); } } return this.export(); } destroy() { if (this.coreScheduler && typeof this.core?._free === "function") this.core._free(this.coreScheduler); this.coreScheduler = 0; } }