结论:MetaDrive 世界进程每圈(100 Hz 节拍)都从 env.vehicle 读状态并发给父进程,但只有 rk.frame % 5 == 0 才调用 env.step(),所以名义物理/图像步进约 20 Hz,状态管道最多在同一物理状态上重复发约五次,并不代表 100 Hz 独立物理观测;帧号和物理时戳没有随管道消息一起发送。父进程控制归零的判定 time.monotonic() - reset_time <= 2 也不是「重置后两秒窗口」,因为 reset_time 初始化为 0 且源码中没有后续赋值。以下为固定快照 c8fb906 的静态分析与假依赖探针,未安装 MetaDrive/Panda3D,未做物理仿真测试。
进程与物理时间结构
spawn_world() 为路面相机和可选广角相机配置同一 W×H 画面,关闭车内置渲染、交通密度和多数终止条件,并设置 physics_world_step_size=TICKS_PER_FRAME/100=0.05 秒、decision_repeat=1。MetaDriveWorld.__init__() 创建共享 RGB 数组、控制/仿真状态/车辆状态三条 Pipe 与事件,再启动子进程;父进程随后无超时地等待首条车辆状态,才把状态队列从 starting 推到 started。子进程初始化失败或卡在首次发送前,启动缺少明确 deadline。
子进程 apply_metadrive_patches() 在进程内替换 EngineCore.add_image_sensor、ImageObservation.observe 和可选的到达目的地判断。它们是模块/类级 monkeypatch,没有在函数末尾恢复;这适合隔离的一次性仿真子进程,不宜嵌入可同时运行多实例的插件宿主。
| 量 | 取值 | 说明 |
|---|---|---|
| 主循环节拍 | Ratekeeper(100, None) |
每圈读车辆状态并发送 |
| 物理 / 图像步进 | 每 5 圈一次,约 20 Hz | 仅 rk.frame % 5 == 0 时 env.step() 并写 RGB |
physics_world_step_size |
0.05 秒 | 由 TICKS_PER_FRAME/100 得到 |
decision_repeat |
1 | — |
| 状态管道重复 | 同一物理状态最多约 5 次 | 帧号与物理时戳未随消息发送 |
控制输入、重置与状态归一化
父进程 apply_controls() 仅在 time.monotonic() - reset_time <= 2 时把控制置零;但 reset_time 初始化为 0,源码中没有后续赋值,reset() 只将 should_reset 置真。因此这不是重置后两秒的归零窗口:单调时钟已超过 2 秒时,重置后下一条命令仍携带当下转角/油门,同时只附带一次重置标志。两项原函数假时钟探针确认该边界和严格 >2 判定。
子进程 controls_recv.poll()/recv() 在每圈 drain 到最后一条命令,以 steer_angle/(MAX_STEERING*8) 限幅到 [-1,1],再作为环境动作。控制输入是「最近值」,而不是逐条执行或有序回放。探针给同一轮预置 [转角, 油门, True] 后紧接 [..., False],确认只执行初始化时的第一次 env.reset(),没有执行请求的第二次重置;反转布尔顺序时则执行第二次重置。父类还存有 self.steer_ratio=15,但当前控制映射使用子进程局部值 8,不能把它当成已生效的同一参数。
父进程 read_state() drain 状态管道,收到 done 才设退出事件;read_sensors() drain 车辆管道,把最新速度/角度/位置赋给 SimulatorState,用简化平面投影算 GPS,并在第一次观察到已使能时通知子进程开始计时。三项假管道/时钟探针确认收到首条车辆消息后 state.valid=True,随后即使假管道停发且时间推进到 100 秒,函数也不清除此位或首次使能时刻。
终止判定与故障路径
env.step() 分支每五圈检查环境 terminated;仅 test_run 才把越出当前车道或到达 test_duration 转成 done。子进程发送终止状态后未立即 break,实际退出依赖父进程读到 done、设置 exit_event 并令外层桥关闭。
| 检查项 | 条件与边界 |
|---|---|
| 终止检测 | 每五圈检查;仅 test_run 才把越出车道/到达 test_duration 转 done |
| 未移动检查 | 使能五秒后、每约 29 秒检查一次;distance_moved 是相对上次检查基线的位移累计,不是逐帧里程积分 |
| 位移累计探针 | 连续两次相同 (2, 0) 位置、基线 (0, 0),累计量从 0 变成 4 |
| 关闭 | close() 设退出事件后无超时 join();SimulatorBridge.close() 先设相机线程退出标志再调用它 |
| 阻塞风险 | 子进程若阻塞于 send()、环境调用或图像获取,单独设置事件不能保证有限时间退出;未见对 MetaDriveEnv 的显式 close()/finally |
这些是进程生命周期审计点,不是已复现的泄漏或死锁。闭环 test_driving() 被跳过,MetaDrive 适配测试还可因依赖缺失跳过。
硬件与插件化要求
世界进程适合做可替换的仿真后端插件,但其控制、状态、图像应有同一帧 ID、采样时戳和时钟域;对父子进程握手、心跳、队列/管道背压、关闭/强制终止设置 deadline。生产车辆适配器不能复用「最近控制值」或 state.valid 常真语义,而应由受信任核心按消息年龄、授权状态和硬件回执仲裁。仿真中的 20 Hz 物理步进与 100 Hz 状态转发,也必须在测试报告中分开标注。本专题没有证明 MetaDrive 动力学精度、相机图像一致性或实际闭环性能。
常见问题
状态按 100 Hz 发,就等于 100 Hz 物理观测吗?
不等于。主循环每 5 圈才调用一次 env.step(),名义物理/图像步进约 20 Hz,状态管道最多在同一物理状态上重复发约五次;且帧号与物理时戳未随消息发送,父进程无法仅凭消息内容重建精确曝光时刻。
重置后两秒内控制会归零吗?
不是这样。apply_controls() 的 <= 2 判定使用 reset_time,而它初始化为 0 且源码无后续赋值,所以这不是「重置后两秒窗口」;单调时钟超过 2 秒时,重置后下一条命令仍携带当下转角/油门。
排队的重置命令会被逐条执行吗?
不会。controls_recv.poll()/recv() 每圈 drain 到最后一条,控制是「最近值」;探针显示预置 [转角, 油门, True] 后紧接 [..., False] 只执行初始化时的第一次 env.reset()。
车辆状态会因失联而自动失效吗?
不会。探针确认收到首条车辆消息后 state.valid=True,此后即使假管道停发、时间推进 100 秒也不清除该位;新硬件插件应显式设置消息年龄门。
延伸阅读
- 仿真相机的传感器时序:帧节拍、曝光与仿真步进的错位 —— 与本题 20 Hz 物理步进直接对照
- 相机帧的时间同步契约:时间戳、帧号与时钟域 —— 状态与图像要绑定同一帧 ID 的背景
- 车辆模型、动力学与控制的边界:从仿真到实车不能直接复用什么 —— 生产适配器与仿真语义的差别
- openpilot 的 100 Hz 控制回路:开源 ADAS 能给车载设备什么 —— 100 Hz 回路对时序与新鲜度的实际要求
- 车载信号为什么会说谎:TTL、源时间与 500 ms 新鲜度阈值 —— 与
state.valid常真语义相对照的新鲜度门设计
参考资料
- 项目 02 ——
openpilot-research/METADRIVE_WORLD_PROCESS_LIFECYCLE_DEEP_DIVE.md(世界进程的物理步进、管道状态、重置边界与退出路径)
说明:本文为对固定源码快照的静态结构分析,未运行完整测试套件、未连接硬件、未验证目标车型的真实参数组合。文中所有阈值、行号索引与配置均出自源文档,标注为未验证的结论保留其原有边界。
更新记录
首次发布 2026-10-11 最近核对:2026-10-11
发表回复