/* ============================================================================ * ik-policy-glue.js —— 查看器接入 Transformer 策略(URL 加 ?policy=1 启用) * ?policy=1 用 models/traj_policy.onnx * ?policy=models/xxx.onnx 指定模型 * &ep=webgpu 改用 WebGPU 后端(默认 wasm-SIMD 多线程) * &threads=N wasm 线程数(默认 min(16, ⌊0.6×逻辑核⌋);2026-10-18 R4 实测 28 核机 16 最优、24 反而变慢、4 线程 P99 37 ms) * &proxy=0 关闭推理代理 Worker(默认开:wasm 推理不占主线程,见下) * &probe=1 允许加载 --random 导出的延迟探针模型(默认拒绝,防把随机策略当真模型跑) * 2026-10-18 第五轮:ort.env.wasm.proxy=true 把 wasm 推理挪到 ort 自带的 Worker。此前推理在主线程同步算, * R4 实测时 CDP 15 s 拿不到执行槽,50 Hz tick 也被它拖住——丢步不只来自推理时长本身。 * 目标变化(松手求解 / 实时追踪)都交给 ArmPolicy.Runner:50 Hz 执行时钟放在 Web Worker(不受 rAF 60 Hz 与后台节流影响), * 每 3 步推理一次,安全层认证后才执行;ik-glue 的逐帧碰撞守卫不参与(策略执行不走 animTo),由 Runner 安全层保证。 * onnxruntime-web 默认从 jsDelivr 加载;离线部署把 ort.min.js 与 *.wasm 放到 vendor/ort/ 并加 &ortLocal=1。 * 多线程 wasm 需要页面跨源隔离(COOP/COEP 头,见 ml/README.md);没有时自动退回单线程。 * ==========================================================================*/ (function () { 'use strict'; const qs = new URLSearchParams(location.search); if (!qs.has('policy')) return; const MODEL = qs.get('policy') && qs.get('policy') !== '1' ? qs.get('policy') : 'models/traj_policy.onnx'; const ORT_BASE = qs.get('ortLocal') === '1' ? 'vendor/ort/' : 'https://cdn.jsdelivr.net/npm/onnxruntime-web@1.20.1/dist/'; const badge = () => document.getElementById('ikBadge'); const say = (t, cls) => { const b = badge(); if (b) { b.textContent = t; b.className = 'badge ' + (cls || 'wait'); } console.log('[policy]', t); }; let ready = false, runner = null, running = false, worker = null; window.__POLICY = { active: () => ready, setGoal: () => false, stats: () => runner && runner.stats }; function loadScript(src) { return new Promise((res, rej) => { const s = document.createElement('script'); s.src = src; s.onload = res; s.onerror = () => rej(new Error('load ' + src)); document.head.appendChild(s); }); } async function waitIK() { for (let i = 0; i < 200 && !(window.IK && window.IK.solver && window.ArmPolicy); i++) await new Promise(r => setTimeout(r, 50)); if (!window.IK) throw new Error('IK 未就绪'); } async function boot() { try { await waitIK(); const S = window.IK.solver; const meta = await (await fetch(MODEL.replace('.onnx', '.meta.json'))).json(); if (meta.fp !== S.modelFingerprint()) throw new Error('模型碰撞指纹 ' + meta.fp + ' ≠ 当前 ' + S.modelFingerprint() + ':collide.json 已变,须重训'); if (meta.feat_ver !== ArmPolicy.FEAT_VER) throw new Error('特征版本不符:模型 ' + meta.feat_ver + ' / 运行时 ' + ArmPolicy.FEAT_VER); if (meta.tcp0 && meta.tcp0.some((v, i) => Math.abs(v - S.TCP0[i]) > 0.01)) throw new Error('TCP 与训练时不同:' + S.TCP0 + ' vs ' + meta.tcp0); const probe = !!meta.random_init; if (probe && qs.get('probe') !== '1') throw new Error('这是随机初始化的延迟探针模型(meta.random_init),不是训练产物;仅测延迟时加 &probe=1'); if (!window.ort) await loadScript(ORT_BASE + 'ort.min.js'); ort.env.wasm.wasmPaths = ORT_BASE; const hc = navigator.hardwareConcurrency || 1; ort.env.wasm.numThreads = !self.crossOriginIsolated ? 1 : (+qs.get('threads') || Math.max(1, Math.min(16, Math.floor(hc * 0.6)))); const ep = qs.get('ep') === 'webgpu' ? ['webgpu', 'wasm'] : ['wasm']; let sess = null, proxy = ep[0] === 'wasm' && qs.get('proxy') !== '0'; // proxy 只支持 wasm EP if (proxy) { ort.env.wasm.proxy = true; try { sess = await ort.InferenceSession.create(MODEL, { executionProviders: ep, graphOptimizationLevel: 'all' }); } catch (e) { console.warn('[policy] 代理 Worker 不可用,退回主线程推理:', e); proxy = false; ort.env.wasm.proxy = false; sess = null; } } if (!sess) sess = await ort.InferenceSession.create(MODEL, { executionProviders: ep, graphOptimizationLevel: 'all' }); const infer = ArmPolicy.ortInfer(ort, sess, { encOnce: !!meta.enc_once }); runner = new ArmPolicy.Runner(S, infer, {}); /* 预热:首次推理有编译开销 */ await infer(new Float32Array(8 * ArmPolicy.F_DIM), new Float32Array(8 * ArmPolicy.H * 6), 8); const tick = URL.createObjectURL(new Blob(['let h=null;onmessage=e=>{clearInterval(h);if(e.data>0)h=setInterval(()=>postMessage(0),e.data)}'], { type: 'text/javascript' })); worker = new Worker(tick); worker.onmessage = onTick; window.__POLICY = { active: () => ready, setGoal: (T, R, tool) => { if (tool) { say('策略模式暂不支持工具偏移 ≠ 0,已交回规划器', 'badb'); return false; } if (!running || !runner.q) runner.reset(window.IK.curQ()); runner.setGoal(T, R); start(); return true; }, stop, stats: () => runner.stats, runner: () => runner, latency: () => { const p = (a, x) => { const s = a.slice().sort((u, v) => u - v); return s.length ? s[Math.min(s.length - 1, Math.floor(s.length * x))] : null; }; return { inferP50: p(runner.stats.inferMs, .5), inferP99: p(runner.stats.inferMs, .99), safetyP50: p(runner.stats.safetyMs, .5), safetyP99: p(runner.stats.safetyMs, .99), threads: ort.env.wasm.numThreads, ep: ep[0], proxy, encOnce: !!meta.enc_once, probe }; } }; ready = true; const how = ep[0] + ',' + ort.env.wasm.numThreads + ' 线程' + (proxy ? ',推理在 Worker' : ',推理在主线程'); if (probe) say('随机探针模型(仅测延迟,动作无意义):' + how, 'badb'); else say('策略模式就绪(' + how + '):拖动目标即由策略执行', 'ok'); } catch (e) { say('策略模式不可用:' + e.message, 'badb'); console.error(e); } } function start() { if (running) return; running = true; worker.postMessage(ArmPolicy.DT * 1000); } function stop() { running = false; if (worker) worker.postMessage(0); } function onTick() { if (!running) return; if (runner.needPlan()) runner.plan().catch(e => { console.error(e); stop(); say('策略推理出错:' + e.message, 'badb'); }); const q = runner.tick(); if (q) window.IK.setJoints(q, false); if (runner.done && !runner.queue.length) { stop(); const { e, oe } = runner.err(runner.q); say('策略到达 · 误差 ' + e.toFixed(2) + ' mm' + (runner.goal.R ? ' / ' + oe.toFixed(2) + '°' : '') + ' · 刹停 ' + runner.stats.brakes + ' 次', 'ok'); } else if (runner.brakeRun >= 2) say('策略:安全层连续拦截,减速重规划中…', 'wait'); } boot(); })();