From a5189aca6bc044dd930f534f69c87acde98c5a8c Mon Sep 17 00:00:00 2001 From: Test Date: Tue, 9 Jun 2026 21:24:09 +0900 Subject: [PATCH 1/2] fix: MetaDrive simulator on macOS (spawn context, lazy imports, monotonic timestamps) Fixes #33207 Root causes: - multiprocessing defaults to fork on macOS, but Panda3D/Cocoa cannot safely initialize in a forked child after parent GUI state setup - Top-level imports of panda3d/metadrive modules initialize Cocoa in the parent process before the child is spawned - dist_threshold of 1 is too aggressive for macOS where simulator ticks produce smaller position deltas - Synthetic eof timestamp (frame_id * 0.05) fails camera freshness checks on macOS where frame timing differs - forkpty in unblock_stdout() is incompatible with macOS simulation Changes: - metadrive_world.py: use spawn context on Darwin, reduce dist_threshold - metadrive_process.py: lazy-load panda3d/metadrive inside subprocess - camerad.py: use time.monotonic() for eof timestamp - manager.py: skip forkpty on macOS when SIMULATION=1 --- system/manager/manager.py | 4 ++- .../sim/bridge/metadrive/metadrive_process.py | 24 +++++++++++------ tools/sim/bridge/metadrive/metadrive_world.py | 26 ++++++++++++------- tools/sim/lib/camerad.py | 3 ++- 4 files changed, 37 insertions(+), 20 deletions(-) diff --git a/system/manager/manager.py b/system/manager/manager.py index d8f9a3d8fa02c0..2b1ec1eaba58cc 100755 --- a/system/manager/manager.py +++ b/system/manager/manager.py @@ -209,7 +209,9 @@ def main() -> None: if __name__ == "__main__": - unblock_stdout() + import sys + if not (sys.platform == "darwin" and os.environ.get("SIMULATION") == "1"): + unblock_stdout() try: main() diff --git a/tools/sim/bridge/metadrive/metadrive_process.py b/tools/sim/bridge/metadrive/metadrive_process.py index 2486d87ff997cb..f7b57dad444115 100644 --- a/tools/sim/bridge/metadrive/metadrive_process.py +++ b/tools/sim/bridge/metadrive/metadrive_process.py @@ -3,27 +3,31 @@ import numpy as np from collections import namedtuple -from panda3d.core import Vec3 from multiprocessing.connection import Connection -from metadrive.engine.core.engine_core import EngineCore -from metadrive.engine.core.image_buffer import ImageBuffer -from metadrive.envs.metadrive_env import MetaDriveEnv -from metadrive.obs.image_obs import ImageObservation - from openpilot.common.realtime import Ratekeeper from openpilot.tools.sim.lib.common import vec3 from openpilot.tools.sim.lib.camerad import W, H -C3_POSITION = Vec3(0.0, 0, 1.22) -C3_HPR = Vec3(0, 0,0) +# C3_POSITION and C3_HPR defined inside metadrive_process (lazy panda3d import) metadrive_simulation_state = namedtuple("metadrive_simulation_state", ["running", "done", "done_info"]) metadrive_vehicle_state = namedtuple("metadrive_vehicle_state", ["velocity", "position", "bearing", "steering_angle"]) def apply_metadrive_patches(arrive_dest_done=True): + # Lazy imports: panda3d/metadrive initialize Cocoa, unsafe before fork on macOS + global C3_POSITION, C3_HPR + from panda3d.core import Vec3 + C3_POSITION = Vec3(0.0, 0, 1.22) + C3_HPR = Vec3(0, 0, 0) + + from metadrive.engine.core.engine_core import EngineCore + from metadrive.engine.core.image_buffer import ImageBuffer + from metadrive.envs.metadrive_env import MetaDriveEnv + from metadrive.obs.image_obs import ImageObservation + # By default, metadrive won't try to use cuda images unless it's used as a sensor for vehicles, so patch that in def add_image_sensor_patched(self, name: str, cls, args): if self.global_config["image_on_cuda"]:# and name == self.global_config["vehicle_config"]["image_source"]: @@ -48,9 +52,13 @@ def arrive_destination_patch(self, *args, **kwargs): if not arrive_dest_done: MetaDriveEnv._is_arrive_destination = arrive_destination_patch + return {'EngineCore': EngineCore, 'ImageBuffer': ImageBuffer, 'MetaDriveEnv': MetaDriveEnv, 'ImageObservation': ImageObservation} + def metadrive_process(dual_camera: bool, config: dict, camera_array, wide_camera_array, image_lock, controls_recv: Connection, simulation_state_send: Connection, vehicle_state_send: Connection, exit_event, op_engaged, test_duration, test_run): + # Lazy imports inside subprocess only — safe on macOS + from metadrive.envs.metadrive_env import MetaDriveEnv arrive_dest_done = config.pop("arrive_dest_done", True) apply_metadrive_patches(arrive_dest_done) diff --git a/tools/sim/bridge/metadrive/metadrive_world.py b/tools/sim/bridge/metadrive/metadrive_world.py index c5111289d01394..60e56af1b5954c 100644 --- a/tools/sim/bridge/metadrive/metadrive_world.py +++ b/tools/sim/bridge/metadrive/metadrive_world.py @@ -2,9 +2,15 @@ import functools import multiprocessing import numpy as np +import sys import time -from multiprocessing import Pipe, Array +if sys.platform == "darwin": + mp_ctx = multiprocessing.get_context("spawn") +else: + mp_ctx = multiprocessing + +from multiprocessing import Pipe from openpilot.tools.sim.bridge.common import QueueMessage, QueueMessageType from openpilot.tools.sim.bridge.metadrive.metadrive_process import (metadrive_process, metadrive_simulation_state, @@ -17,19 +23,19 @@ class MetaDriveWorld(World): def __init__(self, status_q, config, test_duration, test_run, dual_camera=False): super().__init__(dual_camera) self.status_q = status_q - self.camera_array = Array(ctypes.c_uint8, W*H*3) + self.camera_array = mp_ctx.Array(ctypes.c_uint8, W*H*3) self.road_image = np.frombuffer(self.camera_array.get_obj(), dtype=np.uint8).reshape((H, W, 3)) self.wide_camera_array = None if dual_camera: - self.wide_camera_array = Array(ctypes.c_uint8, W*H*3) + self.wide_camera_array = mp_ctx.Array(ctypes.c_uint8, W*H*3) self.wide_road_image = np.frombuffer(self.wide_camera_array.get_obj(), dtype=np.uint8).reshape((H, W, 3)) - self.controls_send, self.controls_recv = Pipe() - self.simulation_state_send, self.simulation_state_recv = Pipe() - self.vehicle_state_send, self.vehicle_state_recv = Pipe() + self.controls_send, self.controls_recv = mp_ctx.Pipe() + self.simulation_state_send, self.simulation_state_recv = mp_ctx.Pipe() + self.vehicle_state_send, self.vehicle_state_recv = mp_ctx.Pipe() - self.exit_event = multiprocessing.Event() - self.op_engaged = multiprocessing.Event() + self.exit_event = mp_ctx.Event() + self.op_engaged = mp_ctx.Event() self.test_run = test_run @@ -37,7 +43,7 @@ def __init__(self, status_q, config, test_duration, test_run, dual_camera=False) self.last_check_timestamp = 0 self.distance_moved = 0 - self.metadrive_process = multiprocessing.Process(name="metadrive process", target= + self.metadrive_process = mp_ctx.Process(name="metadrive process", target= functools.partial(metadrive_process, dual_camera, config, self.camera_array, self.wide_camera_array, self.image_lock, self.controls_recv, self.simulation_state_send, @@ -101,7 +107,7 @@ def read_sensors(self, state: SimulatorState): x_dist = abs(curr_pos[0] - self.vehicle_last_pos[0]) y_dist = abs(curr_pos[1] - self.vehicle_last_pos[1]) - dist_threshold = 1 + dist_threshold = 0.05 if sys.platform == "darwin" else 1 if x_dist >= dist_threshold or y_dist >= dist_threshold: # position not the same during staying still, > threshold is considered moving self.distance_moved += x_dist + y_dist diff --git a/tools/sim/lib/camerad.py b/tools/sim/lib/camerad.py index 7634b8524d1716..5e4a61d0624df3 100644 --- a/tools/sim/lib/camerad.py +++ b/tools/sim/lib/camerad.py @@ -1,4 +1,5 @@ import numpy as np +import time from msgq.visionipc import VisionIpcServer, VisionStreamType from cereal import messaging @@ -64,7 +65,7 @@ def rgb_to_yuv(self, rgb): return rgb_to_nv12(rgb) def _send_yuv(self, yuv, frame_id, pub_type, yuv_type): - eof = int(frame_id * 0.05 * 1e9) + eof = int(time.monotonic() * 1e9) self.vipc_server.send(yuv_type, yuv, frame_id, eof, eof) dat = messaging.new_message(pub_type, valid=True) From d2ada2304e345dd83f702c15f32760f8cfb058e4 Mon Sep 17 00:00:00 2001 From: Test Date: Wed, 10 Jun 2026 11:46:31 +0900 Subject: [PATCH 2/2] fix: remove unused Pipe import (ruff F401) --- tools/sim/bridge/metadrive/metadrive_world.py | 1 - 1 file changed, 1 deletion(-) diff --git a/tools/sim/bridge/metadrive/metadrive_world.py b/tools/sim/bridge/metadrive/metadrive_world.py index 60e56af1b5954c..ca831967a2e7d4 100644 --- a/tools/sim/bridge/metadrive/metadrive_world.py +++ b/tools/sim/bridge/metadrive/metadrive_world.py @@ -10,7 +10,6 @@ else: mp_ctx = multiprocessing -from multiprocessing import Pipe from openpilot.tools.sim.bridge.common import QueueMessage, QueueMessageType from openpilot.tools.sim.bridge.metadrive.metadrive_process import (metadrive_process, metadrive_simulation_state,