Prevent XYZAC TP queue starvation

This commit is contained in:
2026-09-02 10:41:21 +08:00
parent 0a22c7bf1b
commit 32adda73d6
10 changed files with 77 additions and 16 deletions

View File

@@ -43,12 +43,13 @@
"line_count": 4711,
"preview_segments": 4684,
"fixed_tick_samples": 3000,
"execution_line": 147,
"execution_trace_entries": 145,
"execution_line": 337,
"execution_trace_entries": 335,
"motion_authority": "wasm",
"running_queue_gaps": 0,
"motion_delta_snapshots": 0,
"ledger_audit_window": 32,
"output_sha256": "c97caef2ea5cf3841e1238ec443d0eebadad8cb0048af829111c06e99c9fcdd7"
"output_sha256": "c97b5fbb43a6e81a8ee15272bf71aa75ec0ba79be28a34f30b4221d50251c77d"
},
"native_tests": {
"wasm_build": "passed",
@@ -56,7 +57,7 @@
},
"published_browser_smoke": {
"url": "https://49.232.250.35:58010/",
"release": "20260902-linuxcnc-wasm-switchkins-r7",
"release": "20260902-linuxcnc-wasm-continuous-r10",
"status": "verified",
"startup_pause_resume_stop": "verified",
"pause_tick": 140,
@@ -64,15 +65,23 @@
"stop_state": "IDLE",
"browser_errors": 0,
"output_sha256": "bdf705473e6cda94d721aa23329257adf4a68b705add7650da5cc47012ab70b0",
"continuous_runtime": {
"sampled_running_frames": 33,
"queue_gaps": 0,
"first_running": {"current_line": 62, "queue_depth": 3, "velocity": 20, "active_id": 16},
"last_running": {"current_line": 69, "queue_depth": 2, "velocity": 16.6666666667, "active_id": 17},
"screenshot_sha256": "c93fd23e648e49bfa9359af245372f03f3fccee123497909f3dcc005f5a59749",
"browser_errors": 0
},
"screenshot_sha256": {
"authority": "e58205228a04b386c7b7514bf03c71df7b53f66f2d9ebe2292931b36ef0edb55",
"running": "7fc474d0cb31a0fdd4afb198061e9812c753684e3d2963cc004ec4483c5c4990",
"switchkins": "920b85ad7fd6451dfb134255ffdcc4f03a7aa615ebf486c63cde8582e17af195"
},
"resource_sha256": {
"index-DEinWp01.js": "79f5e933909fc631186db9b5091b6239f30c1ade612bb908a4ab9fcf79965343",
"sim-worker-entry-7q53Ew6x.js": "e5c239104d6676f242ea45cb3ddaf78a379aaaecaca5ff26031dee1ba9496e7c",
"linuxcnc-sim-core-CJilJ2YZ.wasm": "13e2746899b0cac5cf91189c00a6174160202f8e632b53c5701e9664335c97cf"
"index-Byst0JhC.js": "26d3f21f496c8223218761ba641dab8b4f36fd18892f260c7bf5b5cfa83cafec",
"sim-worker-entry-BTVfbu6g.js": "adf2ac5621ba388408f31ceab4bbcbfa6ba2923577d8eb69aa79ea54f3be0134",
"linuxcnc-sim-core-B2786u4k.wasm": "3163f5ff63c5f3e1d2c07d1d948fce328bf3ad919eee188e432f7ba57468fc71"
},
"authority_dom": {
"data-axis-authority": "linuxcnc-wasm-worker",

View File

@@ -129,13 +129,20 @@ source_length: 200509
line_count: 4711
preview_segments: 4684
fixed_tick_samples: 3000
execution_line: 147
execution_trace_entries: 145
execution_line: 337
execution_trace_entries: 335
motion_authority: wasm
motion_delta_snapshots: 0
ledger_audit_window: 32
running_queue_gaps: 0 (excluding the initial startup tick)
```
## 长距离连续性判定
`3D_Chips.ngc` 第 17 行设置 `G64 P.1`,后续是大量连续短段。对标 LinuxCNC source TP 时,正常表现是 TP 在当前段运行期间持续保留前瞻队列;G64 不应在普通同模式段之间出现人为的零速度空队列 tick。此前 Worker 在等待当前段完全结束后才入队下一段,造成截图中可见的短暂停顿,这不是源 LinuxCNC 的合理结果。
已修复为:同一运动学模式的下一段可在当前 TC 活动时进入 source TP;M428/M429/M430 模式变化仍由 WASM Task/TP 同步边界约束;队列容量由 `sim_linuxcnc_tp_queue_capacity` 读取,按真实 `TP_DEFAULT_QUEUE_SIZE` 限流。1000 tick 连续性回归中,除启动第 1 tick 外 `program_status=running && queue_depth=0` 的次数为 `0`;第 82 行附近仍保持 31 段前瞻队列。
## 当前未闭合项
以下项目仍明确记录为 `blocked_by_abi`,没有伪造通过:
@@ -163,6 +170,26 @@ bash -n run-5axis-rtcp.sh
本轮结果:WASM 构建通过,原生 CTest 22/22 通过,XYZAC 聚合按钮测试通过,3D_Chips 固定 tick 长程序测试通过。Jog Speed、Max Velocity、Spindle Override 已改为 WASM TP/IO 状态和命令;前端运行态不再使用 React 默认值。发布站点 UI 运行/暂停/恢复/停止也已在 Chromium 仿真环境执行;Safari 和真实物理断电不属于本项目范围。
本轮连续性修复后,`3D_Chips.ngc` 3000 tick 执行到第 337 行,`running_queue_gaps=0`;这证明长距离运行中的中间停顿已不再由 Worker 队列饥饿制造。
发布站点 r10 连续性验证:
```text
url: https://49.232.250.35:58010/
release: 20260902-linuxcnc-wasm-continuous-r10
resource_sha256: index-Byst0JhC.js 26d3f21f496c8223218761ba641dab8b4f36fd18892f260c7bf5b5cfa83cafec
sim-worker-entry-BTVfbu6g.js adf2ac5621ba388408f31ceab4bbcbfa6ba2923577d8eb69aa79ea54f3be0134
linuxcnc-sim-core-B2786u4k.wasm 3163f5ff63c5f3e1d2c07d1d948fce328bf3ad919eee188e432f7ba57468fc71
3D_Chips.ngc: loaded -> POWER -> HOME ALL -> RUNNING
sampled_running_frames: 33
queue_gaps: 0
first_running: current_line=62, queue_depth=3, velocity=20, active_id=16
last_running: current_line=69, queue_depth=2, velocity=16.6666666667, active_id=17
browser_errors: 0
screenshot: /tmp/published-r10-continuous.png
screenshot_sha256: c93fd23e648e49bfa9359af245372f03f3fccee123497909f3dcc005f5a59749
```
发布站点 r5 浏览器冒烟:
```text
@@ -178,7 +205,7 @@ and view controls: clicked with no browser errors
output_sha256: bdf705473e6cda94d721aa23329257adf4a68b705add7650da5cc47012ab70b0
```
发布站点 r6(本轮 WASM authority 版本)浏览器验收:
发布站点 r6(历史 WASM authority 版本)浏览器验收:
```text
url: https://49.232.250.35:58010/
@@ -194,6 +221,8 @@ screenshots: /tmp/published-r6-authority.png (e58205228a04b386c7b7514bf03c71df7b
/tmp/published-r6-running.png (7fc474d0cb31a0fdd4afb198061e9812c753684e3d2963cc004ec4483c5c4990)
```
当前发布资源为 r10;r6 资源 hash 仅作为历史记录保留。
发布站点 r7(IDENTITY/TCP: XYZAC/USERK 切换版本)浏览器验收:
```text
@@ -214,10 +243,12 @@ panel_button_test_output 9bddcd8b0219925e00598e081dc7a4ed0f4868e8822a21f0ccd08d8
button_constraints_output 5b6f4ce1dec03764dcb234793ff96fc2f04d15ab954620a44dbb1210ec64a5a5
feed_authority_output deab4a51ee0e56d4f8134efba4b7a97a2b88f37d9165cf730207f71886df8526
3d_chips_test_output c97caef2ea5cf3841e1238ec443d0eebadad8cb0048af829111c06e99c9fcdd7
3d_chips_no_gap_output c97b5fbb43a6e81a8ee15272bf71aa75ec0ba79be28a34f30b4221d50251c77d
wasm_io_source 1cb066d185dd2c5bbf435e022c2518483bd44962e5a0303248757b2da51740f7
wasm_io_c_source 09bce1430d6f71702ca0b3bf29ac50db377a6f232ce0d1a9b2c1eaac7ea2162e
panel_button_test_source 80782fc1f85f88beb48ec3dfaf638a93f2edb912e694840199dbff504477fe74
switchkins_direct_test_output 24e6cb8ec1195a97f71294b49217db0cbc0d7c3ecfa4bc8aa244ba3921c816a7
panel_r10_output 9bddcd8b0219925e00598e081dc7a4ed0f4868e8822a21f0ccd08d8e9d355d45
```
报告对应提交前工作树保留了既有 coverage 大文件和 LinuxCNC 子模块改动;本轮提交包含 WASM TP/IO/HOME authority、无 JS JOG 运动计算、前端 pending 投影、长程序快照性能修正、聚合测试和本报告。

View File

@@ -308,6 +308,6 @@ if(EMSCRIPTEN)
string(REPLACE "_sim_device_init" "_sim_device_io_size','_sim_device_init" _sim_module_link_flags "${_sim_module_link_flags}")
string(REPLACE "_sim_motion_step','_sim_motion_read_state" "_sim_motion_step','_sim_motion_set_profile','_sim_motion_set_lookahead','_sim_motion_set_feed_override','_sim_motion_peek','_sim_motion_read_state" _sim_module_link_flags "${_sim_module_link_flags}")
string(REPLACE "_sim_motion_state_hash','_sim_motion_canonical" "_sim_motion_state_hash','_sim_motion_plan_hash','_sim_motion_canonical" _sim_module_link_flags "${_sim_module_link_flags}")
string(REPLACE "_sim_motion_init" "_sim_motion_init','_sim_linuxcnc_tp_size','_sim_linuxcnc_tp_init','_sim_linuxcnc_tp_reset','_sim_linuxcnc_tp_set_profile','_sim_linuxcnc_tp_set_feed_override','_sim_linuxcnc_tp_set_velocity_scale','_sim_linuxcnc_tp_set_jog_scale','_sim_linuxcnc_tp_set_axis_limit','_sim_linuxcnc_tp_jog','_sim_linuxcnc_tp_velocity_scale','_sim_linuxcnc_tp_velocity_limit','_sim_linuxcnc_tp_jog_scale','_sim_linuxcnc_tp_jog_velocity','_sim_linuxcnc_tp_feed_override','_sim_linuxcnc_tp_set_dynamics','_sim_linuxcnc_tp_set_paused','_sim_linuxcnc_tp_abort','_sim_linuxcnc_tp_set_position','_sim_linuxcnc_tp_set_goal_position','_sim_linuxcnc_tp_add_line','_sim_linuxcnc_tp_add_circle','_sim_linuxcnc_tp_step','_sim_linuxcnc_tp_idle','_sim_linuxcnc_tp_read_state','_sim_linuxcnc_tp_queue_depth','_sim_linuxcnc_tp_queue_segment','_sim_linuxcnc_tp_restore_progress','_sim_linuxcnc_tp_state_hash','_sim_linuxcnc_tp_canonical" _sim_module_link_flags "${_sim_module_link_flags}")
string(REPLACE "_sim_motion_init" "_sim_motion_init','_sim_linuxcnc_tp_size','_sim_linuxcnc_tp_queue_capacity','_sim_linuxcnc_tp_init','_sim_linuxcnc_tp_reset','_sim_linuxcnc_tp_set_profile','_sim_linuxcnc_tp_set_feed_override','_sim_linuxcnc_tp_set_velocity_scale','_sim_linuxcnc_tp_set_jog_scale','_sim_linuxcnc_tp_set_axis_limit','_sim_linuxcnc_tp_jog','_sim_linuxcnc_tp_velocity_scale','_sim_linuxcnc_tp_velocity_limit','_sim_linuxcnc_tp_jog_scale','_sim_linuxcnc_tp_jog_velocity','_sim_linuxcnc_tp_feed_override','_sim_linuxcnc_tp_set_dynamics','_sim_linuxcnc_tp_set_paused','_sim_linuxcnc_tp_abort','_sim_linuxcnc_tp_set_position','_sim_linuxcnc_tp_set_goal_position','_sim_linuxcnc_tp_add_line','_sim_linuxcnc_tp_add_circle','_sim_linuxcnc_tp_step','_sim_linuxcnc_tp_idle','_sim_linuxcnc_tp_read_state','_sim_linuxcnc_tp_queue_depth','_sim_linuxcnc_tp_queue_segment','_sim_linuxcnc_tp_restore_progress','_sim_linuxcnc_tp_state_hash','_sim_linuxcnc_tp_canonical" _sim_module_link_flags "${_sim_module_link_flags}")
set_target_properties(sim_core_module PROPERTIES LINK_FLAGS "${_sim_module_link_flags}")
endif()

View File

@@ -18,6 +18,7 @@ typedef struct sim_linuxcnc_tp_segment {
} sim_linuxcnc_tp_segment;
uint32_t sim_linuxcnc_tp_size(void);
uint32_t sim_linuxcnc_tp_queue_capacity(const sim_linuxcnc_tp *planner);
int32_t sim_linuxcnc_tp_init(sim_linuxcnc_tp *planner, double cycle_time,
double velocity_limit, double acceleration_limit);
int32_t sim_linuxcnc_tp_reset(sim_linuxcnc_tp *planner);

View File

@@ -130,6 +130,7 @@ static uint64_t hash_bytes(const unsigned char *bytes, size_t length) {
}
uint32_t sim_linuxcnc_tp_size(void) { return (uint32_t)sizeof(sim_linuxcnc_tp); }
uint32_t sim_linuxcnc_tp_queue_capacity(const sim_linuxcnc_tp *planner) { return planner == NULL || !planner->initialized ? 0u : (uint32_t)TP_DEFAULT_QUEUE_SIZE; }
int32_t sim_linuxcnc_tp_init(sim_linuxcnc_tp *planner, double cycle_time, double velocity_limit, double acceleration_limit) {
if (planner == NULL || !isfinite(cycle_time) || cycle_time <= 0.0 || cycle_time > 60.0 || !isfinite(velocity_limit) || velocity_limit <= 0.0 || !isfinite(acceleration_limit) || acceleration_limit <= 0.0) return -1200;

View File

@@ -18,6 +18,7 @@ int main(void) {
sim_posemath_pose second = pose(20, 20, 10);
assert(planner != NULL);
assert(sim_linuxcnc_tp_init(planner, 0.001, 35.0, 300.0) == 0);
assert(sim_linuxcnc_tp_queue_capacity(planner) > 0);
assert(sim_linuxcnc_tp_set_position(planner, &origin) == 0);
assert(sim_linuxcnc_tp_set_profile(planner, 0, 0.0) == 0);
assert(sim_linuxcnc_tp_add_line(planner, &first, 20.0, 300.0, 16, 1) == 0);

View File

@@ -465,7 +465,7 @@ export function App() {
const allHomed = homedAxes.length >= 5 && homedAxes.slice(0, 5).every(Boolean);
const xyzacCommand = (kind: string, payload: Record<string, unknown> = {}, action?: string) => { void dispatch(kind, payload, true, action ?? null).then(() => { if (kind === "VISUAL_CLEAR") setPlotCleared(true); }).catch(() => undefined); };
const position = xyzacView.pose ? `X${Number(xyzacView.pose.x).toFixed(3)} Y${Number(xyzacView.pose.y).toFixed(3)} Z${Number(xyzacView.pose.z).toFixed(3)} A${Number(xyzacView.pose.a).toFixed(3)} C${Number(xyzacView.pose.c).toFixed(3)}` : "WASM state pending";
return <main className="axis-app" data-axis-feature="AXIS-UI-01" data-axis-authority="linuxcnc-wasm-worker" data-axis-state-source={wasmSnapshotReady ? "linuxcnc-wasm" : "pending"} data-axis-motion-authority={String(state.snapshot?.motion_authority ?? "pending")} data-axis-task-authority={String(state.snapshot?.task_authority ?? "pending")} data-axis-run-enabled={String(gates.run.enabled)} data-axis-homed={JSON.stringify(state.snapshot?.motion?.homed_axes ?? state.snapshot?.motion?.homed ?? null)} data-axis-program-status={String(state.program?.program_status ?? "idle")}>
return <main className="axis-app" data-axis-feature="AXIS-UI-01" data-axis-authority="linuxcnc-wasm-worker" data-axis-state-source={wasmSnapshotReady ? "linuxcnc-wasm" : "pending"} data-axis-motion-authority={String(state.snapshot?.motion_authority ?? "pending")} data-axis-task-authority={String(state.snapshot?.task_authority ?? "pending")} data-axis-tp-queue-depth={String((wasmMotion as { queue_depth?: number }).queue_depth ?? "")} data-axis-tp-velocity={String((wasmMotion as { velocity?: number }).velocity ?? "")} data-axis-tp-active-id={String((wasmMotion as { active_id?: number }).active_id ?? "")} data-axis-current-line={String(visibleCurrentLine ?? "")} data-axis-run-enabled={String(gates.run.enabled)} data-axis-homed={JSON.stringify(state.snapshot?.motion?.homed_axes ?? state.snapshot?.motion?.homed ?? null)} data-axis-program-status={String(state.program?.program_status ?? "idle")}>
<header className="axis-window-header" data-axis-feature="AXIS-SHELL-01"><div className="axis-brand"><img src={axisIcon("axis-24x24.png")} alt="AXIS" /> <strong>AXIS</strong><span>LinuxCNC Web/WASM</span></div><span className={`connection-state ${state.status}`} data-axis-feature="AXIS-SHELL-01" role="status">{state.status.toUpperCase()}</span><span className="axis-state-badge" data-axis-state={taskState} aria-label={`Machine ${machineState}, operation ${operationState}`}>{taskState}</span></header>
<nav className="axis-menubar" data-axis-feature="AXIS-UI-02" aria-label="AXIS menu"><Menu label="File"><MenuAction onClick={() => { setProgramText(DEFAULT_PROGRAM); setProgramPath(PROGRAM_PATH); }} disabled={programRunning || programLoading}>Reload editor</MenuAction><MenuAction onClick={() => setCommandStatus("Tool data editor is browser-backed")}>Edit tool data...</MenuAction><MenuAction onClick={() => setCommandStatus("Quit is unavailable in a browser tab")}>Quit</MenuAction></Menu><Menu label="Machine"><MenuAction shortcut="F1" onClick={toggleEstop}>{isEstop ? "Cancel Emergency Stop" : "Emergency Stop"}</MenuAction><MenuAction shortcut="F2" onClick={powerOn} disabled={!gates.power.enabled}>{machineOn ? "Power off" : "Power on"}</MenuAction><MenuAction onClick={resetMachine} disabled={!gates.reset.enabled}>Reset machine</MenuAction><MenuAction shortcut="R" onClick={runProgram} disabled={!gates.run.enabled || programLoading}>Run program</MenuAction><MenuAction shortcut="T" onClick={() => execute("PROGRAM_STEP")} disabled={!gates.step.enabled || programLoading}>Step</MenuAction><MenuAction shortcut="P" onClick={() => execute("PROGRAM_PAUSE")} disabled={!gates.pause.enabled || programLoading}>Pause</MenuAction><MenuAction shortcut="S" onClick={() => execute("PROGRAM_RESUME")} disabled={!gates.resume.enabled || programLoading}>Resume</MenuAction><MenuAction shortcut="Esc" onClick={stopProgram} disabled={!gates.abort.enabled}>Stop</MenuAction></Menu><Menu label="View"><MenuAction onClick={() => setRightTab("preview")}>Preview</MenuAction><MenuAction onClick={() => setRightTab("dro")}>DRO</MenuAction><MenuAction onClick={enterManualMode} disabled={programRunning || programLoading}>Manual Control</MenuAction><MenuAction onClick={() => setLeftTab("mdi")} disabled={programRunning || programLoading}>MDI</MenuAction></Menu><Menu label="Help"><MenuAction onClick={() => setCommandStatus("LinuxCNC AXIS source-compatible controls")}>About AXIS</MenuAction></Menu></nav>
<section className="axis-toolbar" data-axis-feature="AXIS-UI-05" aria-label="AXIS toolbar"><IconButton feature="AXIS-MACHINE-01" label={isEstop ? "Cancel Emergency Stop [F1]" : "Emergency Stop [F1]"} asset="tool_estop.gif" danger onClick={toggleEstop} pressed={isEstop} /><IconButton feature="AXIS-MACHINE-02" label={machineOn ? "Power off [F2]" : "Power on [F2]"} asset="tool_power.gif" disabled={!gates.power.enabled} onClick={powerOn} /><IconButton feature="AXIS-MACHINE-03" label="Reset machine" asset="tool_reload.gif" disabled={!gates.reset.enabled} onClick={resetMachine} /><Separator /><IconButton feature="AXIS-FILE-01" label="Open G-Code file [O]" asset="tool_open.gif" disabled={programRunning || programLoading} onClick={openProgramFile} /><IconButton feature="AXIS-FILE-02" label="Load exact XYZAC TRT case" asset="tool_verify.gif" disabled={!gates.load.enabled} onClick={loadCase} /><IconButton feature="AXIS-FILE-04" label="Reopen editor defaults" asset="tool_reload.gif" onClick={() => { setProgramText(DEFAULT_PROGRAM); setProgramPath(PROGRAM_PATH); setPlotCleared(false); }} /><Separator /><IconButton feature="AXIS-MACHINE-03" label="Begin executing current file [R]" asset="tool_run.gif" disabled={!gates.run.enabled} onClick={runProgram} /><IconButton feature="AXIS-MACHINE-05" label="Execute next line [T]" asset="tool_step.gif" disabled={!gates.step.enabled} onClick={() => execute("PROGRAM_STEP")} /><IconButton feature="AXIS-MACHINE-06" label={programPaused ? "Resume execution [S]" : "Pause execution [P]"} asset={programPaused ? "resume_inhibit.gif" : "tool_pause.gif"} disabled={programPaused ? !gates.resume.enabled : !gates.pause.enabled} onClick={() => execute(programPaused ? "PROGRAM_RESUME" : "PROGRAM_PAUSE")} /><IconButton feature="AXIS-MACHINE-07" label="Stop program execution [ESC]" asset="tool_stop.gif" disabled={!gates.abort.enabled} onClick={stopProgram} /><Separator /><IconButton feature="AXIS-MACHINE-09" label="Toggle skip lines with slash" asset="tool_blockdelete.gif" disabled /><IconButton feature="AXIS-MACHINE-08" label="Toggle optional pause" asset="tool_optpause.gif" disabled /><Separator /><IconButton feature="AXIS-VIEW-01" label="Zoom in" asset="tool_zoomin.gif" onClick={() => setZoom((value) => Math.min(2, value + .2))} /><IconButton feature="AXIS-VIEW-01" label="Zoom out" asset="tool_zoomout.gif" onClick={() => setZoom((value) => Math.max(.5, value - .2))} /><IconButton feature="AXIS-VIEW-01" label="Top view" asset="tool_axis_z.gif" pressed={view === "z"} onClick={() => { setView("z"); setRightTab("preview"); }} /><IconButton feature="AXIS-VIEW-01" label="Rotated top view" asset="tool_axis_z2.gif" pressed={view === "z2"} onClick={() => { setView("z2"); setRightTab("preview"); }} /><IconButton feature="AXIS-VIEW-01" label="Side view" asset="tool_axis_x.gif" pressed={view === "x"} onClick={() => { setView("x"); setRightTab("preview"); }} /><IconButton feature="AXIS-VIEW-01" label="Front view" asset="tool_axis_y.gif" pressed={view === "y"} onClick={() => { setView("y"); setRightTab("preview"); }} /><IconButton feature="AXIS-VIEW-01" label="Inverted front view" asset="tool_axis_y2.png" pressed={view === "y2"} onClick={() => { setView("y2"); setRightTab("preview"); }} /><IconButton feature="AXIS-VIEW-01" label="Perspective view" asset="tool_axis_p.gif" pressed={view === "p"} onClick={() => { setView("p"); setRightTab("preview"); }} /><IconButton feature="AXIS-VIEW-02" label="Drag / rotate mode" asset="tool_rotate.gif" pressed={rotateMode} onClick={() => setRotateMode((value) => !value)} /><Separator /><IconButton feature="AXIS-VIEW-15" label="Clear live plot [Ctrl-K]" asset="tool_clear.gif" onClick={() => xyzacCommand("VISUAL_CLEAR", {}, "vismachClear")} /></section>

View File

@@ -1552,7 +1552,12 @@ export class SimSession {
// planner queue. Let the motion phase consume one segment
// before reading another source line instead of converting
// normal planner backpressure into a program fault.
if (this.wasmMotion.read().state.queue_depth >= this.wasmMotion.queueCapacity) return;
// TP_DEFAULT_QUEUE_SIZE includes the active TC slot. Leave
// one slot available for the active segment before appending
// another lookahead TC; the upstream queue otherwise rejects
// the boundary insertion even though its reported depth is
// one below the configured capacity.
if (this.wasmMotion.read().state.queue_depth >= this.wasmMotion.queueCapacity - 1) return;
if (this.pendingXyzacMotion) {
const pendingMode = Number(this.pendingXyzacMotion.kinematics_mode ?? 0);
if (this.xyzacAppendDeferredOnActive) return;
@@ -1562,7 +1567,14 @@ export class SimSession {
: Number(queueState.active_id ?? 0) === this.xyzacDeferredBlockedActiveId;
const deferredSwitchMotion = this.xyzacDeferredPhysicalSwitchMode !== null && pendingMode === this.xyzacDeferredPhysicalSwitchMode && this.xyzacQueuedLookaheadTargetId === 0 && deferredControlReady;
const queuedLookaheadReady = deferredSwitchMotion || (this.xyzacAllowQueuedLookahead && this.xyzacLastServoActiveId === this.xyzacQueuedLookaheadTargetId);
if (queueState.queue_depth !== 0 && !queuedLookaheadReady) return;
// Same-mode segments may be queued while the current TC is
// active. LinuxCNC TP needs that lookahead to blend G64
// paths; withholding it until the queue drains creates a
// real zero-velocity servo sample between long runs. A
// kinematics-mode change remains synchronized below and is
// deliberately deferred until its existing TC boundary.
const pendingModeChanged = pendingMode !== this.xyzacLastEnqueuedKinematicsMode;
if (queueState.queue_depth !== 0 && pendingModeChanged && !queuedLookaheadReady) return;
if (queuedLookaheadReady && !deferredSwitchMotion && this.xyzacAllowQueuedLookahead) {
// The next segment is appended after the first servo
// sample of the now-active target, matching LinuxCNC's

View File

@@ -46,6 +46,7 @@ const run = send("PROGRAM_RUN");
assert.equal(run.kind, "COMMAND_RESULT");
let stepped = null;
let executionTraceEntries = 0;
let runningQueueGaps = 0;
// The first long Z feed is planned at the configured LinuxCNC TP limit. Give
// the fixed-tick simulation enough servo time to complete it before checking
// that the interpreter has advanced beyond the historical line-82 boundary.
@@ -55,6 +56,7 @@ for (let cycle = 0; cycle < cyclesToRun; cycle += 1) {
stepped = send("SIM_STEP", { cycles: 1, response: "motion_delta" });
assert.equal(stepped.kind, "COMMAND_RESULT");
executionTraceEntries += stepped.payload.execution_trace.length;
if (cycle > 0 && stepped.payload.program.program_status === "running" && Number(stepped.payload.wasm_motion.state.queue_depth) === 0) runningQueueGaps += 1;
}
assert.equal(stepped.kind, "COMMAND_RESULT");
assert.equal(stepped.payload.program.program_status, "running");
@@ -65,6 +67,7 @@ assert.equal(stepped.payload.motion_authority, "wasm");
assert.equal(Object.hasOwn(stepped.payload.xyzac_ledger.entries.at(-1), "response"), false);
assert.equal(session.timeline.snapshots.snapshots.size, 0);
assert.equal(session.ledger.audit({ maxEntries: 32 }).entries.length, 32);
assert.equal(runningQueueGaps, 0);
console.log(JSON.stringify({
schema: "xyzac-3d-chips-program.v1",
@@ -77,6 +80,7 @@ console.log(JSON.stringify({
cycles: cyclesToRun,
execution_line: stepped.payload.program.current_line,
execution_trace: executionTraceEntries,
running_queue_gaps: runningQueueGaps,
motion_authority: stepped.payload.motion_authority,
motion_delta_snapshots: session.timeline.snapshots.snapshots.size,
ledger_audit_window: session.ledger.audit({ maxEntries: 32 }).entries.length,

View File

@@ -17,11 +17,11 @@ function toPose(values) { return Object.fromEntries(["x", "y", "z", "a", "b", "c
export class WasmLinuxcncTp {
constructor(core, { cycleTime = 0.001, velocityLimit = 35, accelerationLimit = 300, limits = {} } = {}) {
this.core = core;
this.available = ["sim_linuxcnc_tp_size", "sim_linuxcnc_tp_init", "sim_linuxcnc_tp_read_state", "sim_linuxcnc_tp_step", "sim_linuxcnc_tp_idle", "sim_linuxcnc_tp_add_line", "sim_linuxcnc_tp_set_profile", "sim_linuxcnc_tp_set_feed_override", "sim_linuxcnc_tp_set_velocity_scale", "sim_linuxcnc_tp_set_jog_scale", "sim_linuxcnc_tp_set_axis_limit", "sim_linuxcnc_tp_jog", "sim_linuxcnc_tp_velocity_scale", "sim_linuxcnc_tp_velocity_limit", "sim_linuxcnc_tp_jog_scale", "sim_linuxcnc_tp_jog_velocity", "sim_linuxcnc_tp_feed_override", "sim_linuxcnc_tp_set_dynamics", "sim_linuxcnc_tp_set_paused", "sim_linuxcnc_tp_abort", "sim_linuxcnc_tp_set_goal_position", "sim_linuxcnc_tp_queue_depth", "sim_linuxcnc_tp_queue_segment", "sim_linuxcnc_tp_restore_progress"].every((name) => typeof core?.[`_${name}`] === "function" || typeof core?.[name] === "function");
this.available = ["sim_linuxcnc_tp_size", "sim_linuxcnc_tp_queue_capacity", "sim_linuxcnc_tp_init", "sim_linuxcnc_tp_read_state", "sim_linuxcnc_tp_step", "sim_linuxcnc_tp_idle", "sim_linuxcnc_tp_add_line", "sim_linuxcnc_tp_set_profile", "sim_linuxcnc_tp_set_feed_override", "sim_linuxcnc_tp_set_velocity_scale", "sim_linuxcnc_tp_set_jog_scale", "sim_linuxcnc_tp_set_axis_limit", "sim_linuxcnc_tp_jog", "sim_linuxcnc_tp_velocity_scale", "sim_linuxcnc_tp_velocity_limit", "sim_linuxcnc_tp_jog_scale", "sim_linuxcnc_tp_jog_velocity", "sim_linuxcnc_tp_feed_override", "sim_linuxcnc_tp_set_dynamics", "sim_linuxcnc_tp_set_paused", "sim_linuxcnc_tp_abort", "sim_linuxcnc_tp_set_goal_position", "sim_linuxcnc_tp_queue_depth", "sim_linuxcnc_tp_queue_segment", "sim_linuxcnc_tp_restore_progress"].every((name) => typeof core?.[`_${name}`] === "function" || typeof core?.[name] === "function");
this.cycleTime = cycleTime;
this.velocityLimit = velocityLimit;
this.accelerationLimit = accelerationLimit;
this.queueCapacity = 2000;
this.queueCapacity = 0;
this.planner = 0;
this.posePointer = 0;
this.centerPointer = 0;
@@ -47,6 +47,8 @@ export class WasmLinuxcncTp {
if (![this.planner, this.posePointer, this.centerPointer, this.normalPointer, this.statePointer].every(Boolean)) throw new Error("linuxcnc_tp_allocation_failed");
const result = call(core, "sim_linuxcnc_tp_init", "number", ["number", "number", "number", "number"], [this.planner, cycleTime, velocityLimit, accelerationLimit]);
if (result !== 0) throw new Error(`linuxcnc_tp_init_${result}`);
this.queueCapacity = Number(call(core, "sim_linuxcnc_tp_queue_capacity", "number", ["number"], [this.planner]));
if (!Number.isSafeInteger(this.queueCapacity) || this.queueCapacity < 1) throw new Error("linuxcnc_tp_queue_capacity_invalid");
this.setPosition({ x: 0, y: 0, z: 0, a: 0, b: 0, c: 0, u: 0, v: 0, w: 0 });
this.setProfile(0);
for (const [axis, limit] of Object.entries(limits)) {