diff --git a/selfdrive/ui/mici/onroad/augmented_road_view.py b/selfdrive/ui/mici/onroad/augmented_road_view.py index 7354d5b52..e4cded2d3 100644 --- a/selfdrive/ui/mici/onroad/augmented_road_view.py +++ b/selfdrive/ui/mici/onroad/augmented_road_view.py @@ -835,6 +835,9 @@ class AugmentedRoadView(CameraView): self._reverse_driver_camera_active = False return + if getattr(self, "_onroad_reentry_pending", False): + self._refresh_available_streams() + if self._update_reverse_driver_camera_state(): self.switch_stream(DRIVER_CAM) return @@ -857,7 +860,8 @@ class AugmentedRoadView(CameraView): else: target = ROAD_CAM - if self.stream_type != target or (self._switching and self._target_stream_type != target): + if (getattr(self, "_onroad_reentry_pending", False) or + self.stream_type != target or (self._switching and self._target_stream_type != target)): self.switch_stream(target) def _update_calibration(self): diff --git a/selfdrive/ui/mici/tests/test_camera_cleanup.py b/selfdrive/ui/mici/tests/test_camera_cleanup.py index 954c42e62..94e4655d9 100644 --- a/selfdrive/ui/mici/tests/test_camera_cleanup.py +++ b/selfdrive/ui/mici/tests/test_camera_cleanup.py @@ -164,3 +164,34 @@ def test_reverse_activation_cancels_mismatched_pending_switch(): assert view._target_client is None assert view._target_stream_type is None assert not view._switching + + +def test_onroad_transition_marks_camera_reentry(monkeypatch): + module = big_cameraview + + class FakeClient: + pass + + view = module.CameraView.__new__(module.CameraView) + view._name = "camerad" + view._stream_type = object() + view.client = FakeClient() + view.frame = None + view.available_streams = [] + view._target_client = None + view._target_stream_type = None + view._switching = False + view._texture_needs_update = False + view._regressive_frame_count = 1 + view._closed = True + view._onroad_reentry_pending = False + view._reentry_stream_selected = False + view._clear_textures = lambda: None + + monkeypatch.setattr(module, "VisionIpcClient", lambda *_args, **_kwargs: FakeClient()) + monkeypatch.setattr(module.ui_state, "is_onroad", lambda: True) + + view._offroad_transition() + + assert view._onroad_reentry_pending + assert not view._reentry_stream_selected diff --git a/selfdrive/ui/onroad/augmented_road_view.py b/selfdrive/ui/onroad/augmented_road_view.py index bab824643..a07f4419b 100644 --- a/selfdrive/ui/onroad/augmented_road_view.py +++ b/selfdrive/ui/onroad/augmented_road_view.py @@ -205,6 +205,9 @@ class AugmentedRoadView(CameraView): self._reverse_driver_camera_active = False return + if getattr(self, "_onroad_reentry_pending", False): + self._refresh_available_streams() + if self._update_reverse_driver_camera_state(): target = DRIVER_CAM elif camera_view == CAMERA_VIEW_DRIVER: @@ -225,7 +228,8 @@ class AugmentedRoadView(CameraView): else: target = ROAD_CAM - if self.stream_type != target or (self._switching and self._target_stream_type != target): + if (getattr(self, "_onroad_reentry_pending", False) or + self.stream_type != target or (self._switching and self._target_stream_type != target)): self.switch_stream(target) def _update_calibration(self): diff --git a/selfdrive/ui/onroad/cameraview.py b/selfdrive/ui/onroad/cameraview.py index 4bae30f46..22e6f0690 100644 --- a/selfdrive/ui/onroad/cameraview.py +++ b/selfdrive/ui/onroad/cameraview.py @@ -130,6 +130,8 @@ class CameraView(Widget): self._placeholder_color: rl.Color | None = None self._closed = False + self._onroad_reentry_pending = False + self._reentry_stream_selected = False if self._use_egl and not self._create_egl_texture(): cloudlog.error("CameraView EGL texture creation failed, falling back to texture rendering") @@ -164,12 +166,23 @@ class CameraView(Widget): self._switching = False self._texture_needs_update = True self.last_connection_attempt = 0.0 + self._onroad_reentry_pending = ui_state.is_onroad() + self._reentry_stream_selected = False def _set_placeholder_color(self, color: rl.Color): """Set a placeholder color to be drawn when no frame is available.""" self._placeholder_color = color + def _refresh_available_streams(self) -> None: + streams = VisionIpcClient.available_streams(self._name, block=False) + if streams: + self.available_streams = list(streams) + def switch_stream(self, stream_type: VisionStreamType) -> None: + if getattr(self, "_onroad_reentry_pending", False): + self._select_reentry_stream(stream_type) + return + if self._switching: if self._target_stream_type == stream_type: return @@ -194,6 +207,23 @@ class CameraView(Widget): self._target_stream_type = None self._switching = False + def _select_reentry_stream(self, stream_type: VisionStreamType) -> None: + """Select the desired stream before displaying any post-transition frame.""" + self._cancel_pending_switch() + + if self._stream_type != stream_type: + old_client = self.client + self.client = None + del old_client + self.client = VisionIpcClient(self._name, stream_type, conflate=True) + self._stream_type = stream_type + + self.frame = None + self._last_frame_id = -1 + self._regressive_frame_count = 0 + self._texture_needs_update = True + self._reentry_stream_selected = True + @property def stream_type(self) -> VisionStreamType: return self._stream_type @@ -219,6 +249,8 @@ class CameraView(Widget): self.available_streams.clear() self.client = None self._target_client = None + self._onroad_reentry_pending = False + self._reentry_stream_selected = False def __del__(self): self.close() @@ -245,6 +277,10 @@ class CameraView(Widget): if self._switching: self._handle_switch() + if self._onroad_reentry_pending and not self._reentry_stream_selected: + # Standalone CameraView users have no higher-level stream selector. + self._select_reentry_stream(self._stream_type) + if not self._ensure_connection(): self._draw_placeholder(rect) return @@ -333,6 +369,8 @@ class CameraView(Widget): self._last_frame_id = content_frame_id self._regressive_frame_count = 0 self._texture_needs_update = True + self._onroad_reentry_pending = False + self._reentry_stream_selected = False return True def _render_egl(self, src_rect: rl.Rectangle, dst_rect: rl.Rectangle) -> bool: @@ -475,6 +513,9 @@ class CameraView(Widget): # Initialize textures for new stream self._initialize_textures() + available_streams = getattr(self.client, "available_streams", None) + if available_streams is not None: + self.available_streams = available_streams(self._name, block=False) def _initialize_textures(self): self._clear_textures() diff --git a/selfdrive/ui/tests/test_camera_frame_order.py b/selfdrive/ui/tests/test_camera_frame_order.py index 93e0993f6..d698b0452 100644 --- a/selfdrive/ui/tests/test_camera_frame_order.py +++ b/selfdrive/ui/tests/test_camera_frame_order.py @@ -15,6 +15,7 @@ class FakeFrame: def _camera_view(): view = big_cameraview.CameraView.__new__(big_cameraview.CameraView) view._name = "camerad" + view._stream_type = big_cameraview.VisionStreamType.VISION_STREAM_ROAD view.frame = None view._last_frame_id = -1 view._regressive_frame_count = 0 @@ -41,6 +42,67 @@ def test_pending_switch_is_cancelled_when_requested_stream_is_current(): assert not view._switching +def test_onroad_reentry_selects_requested_stream_before_rendering(monkeypatch): + view = _camera_view() + view._name = "camerad" + view._stream_type = big_cameraview.VisionStreamType.VISION_STREAM_WIDE_ROAD + view.client = object() + view._target_client = object() + view._target_stream_type = big_cameraview.VisionStreamType.VISION_STREAM_ROAD + view._switching = True + view._onroad_reentry_pending = True + view._reentry_stream_selected = False + clients = [] + + class FakeClient: + def __init__(self, name, stream_type, conflate): + self.name = name + self.stream_type = stream_type + self.conflate = conflate + clients.append(self) + + monkeypatch.setattr(big_cameraview, "VisionIpcClient", FakeClient) + view.switch_stream(big_cameraview.VisionStreamType.VISION_STREAM_ROAD) + + assert len(clients) == 1 + assert view.client is clients[0] + assert view.client.stream_type == big_cameraview.VisionStreamType.VISION_STREAM_ROAD + assert view.stream_type == big_cameraview.VisionStreamType.VISION_STREAM_ROAD + assert view._target_client is None + assert view._target_stream_type is None + assert not view._switching + assert view._reentry_stream_selected + + +def test_onroad_reentry_guard_clears_on_first_fresh_frame(): + view = _camera_view() + view._onroad_reentry_pending = True + view._reentry_stream_selected = True + + assert view._accept_frame(FakeFrame(frame_id=1, idx=0), packet_frame_id=1) + assert not view._onroad_reentry_pending + assert not view._reentry_stream_selected + + +def test_standalone_camera_reentry_selects_configured_stream(): + view = _camera_view() + view._switching = False + view._onroad_reentry_pending = True + view._reentry_stream_selected = False + selected = [] + placeholders = [] + view._select_reentry_stream = lambda stream_type: ( + selected.append(stream_type), setattr(view, "_reentry_stream_selected", True) + ) + view._draw_placeholder = lambda rect: placeholders.append(rect) + view._ensure_connection = lambda: False + + view._render(object()) + + assert selected == [view._stream_type] + assert len(placeholders) == 1 + + def test_reused_egl_slot_cannot_move_camera_backwards(monkeypatch): monkeypatch.setattr(big_cameraview.cloudlog, "warning", lambda *_args, **_kwargs: None) view = _camera_view()