diff --git a/selfdrive/ui/mici/onroad/cameraview.py b/selfdrive/ui/mici/onroad/cameraview.py index f71835605..2b0d059c9 100644 --- a/selfdrive/ui/mici/onroad/cameraview.py +++ b/selfdrive/ui/mici/onroad/cameraview.py @@ -128,6 +128,8 @@ class CameraView(Widget): self._enhance_driver_val = rl.ffi.new("int[1]", [1 if stream_type == VisionStreamType.VISION_STREAM_DRIVER else 0]) self.frame: VisionBuf | None = None + self._last_frame_id = -1 + self._regressive_frame_count = 0 self.texture_y: rl.Texture | None = None self.texture_uv: rl.Texture | None = None @@ -160,6 +162,7 @@ class CameraView(Widget): # camerad allocation until this view happens to render again. self._clear_textures() self.frame = None + self._last_frame_id = -1 self.available_streams.clear() self.client = VisionIpcClient(self._name, self._stream_type, conflate=True) self._target_client = None @@ -209,6 +212,7 @@ class CameraView(Widget): self.shader.id = 0 self.frame = None + self._last_frame_id = -1 self.available_streams.clear() self.client = None self._target_client = None @@ -242,11 +246,13 @@ class CameraView(Widget): self._draw_placeholder(rect) return + if self._use_egl: + self._observe_displayed_frame() + # Try to get a new buffer without blocking buffer = self.client.recv(timeout_ms=0) if buffer: - self._texture_needs_update = True - self.frame = buffer + self._accept_frame(buffer, self.client.frame_id) elif not self.client.is_connected(): # ensure we clear the displayed frame when the connection is lost self.frame = None @@ -284,6 +290,25 @@ class CameraView(Widget): if self._placeholder_color: rl.draw_rectangle_rec(rect, self._placeholder_color) + def _observe_displayed_frame(self) -> None: + if self.frame is not None: + self._last_frame_id = max(self._last_frame_id, int(self.frame.frame_id)) + + def _accept_frame(self, frame: VisionBuf, packet_frame_id: int) -> bool: + content_frame_id = int(frame.frame_id) + if content_frame_id < self._last_frame_id: + self._regressive_frame_count += 1 + if self._regressive_frame_count == 1 or self._regressive_frame_count % 100 == 0: + message = f"Dropping regressive {self._name} frame: content={content_frame_id}, packet={packet_frame_id}, " + message += f"displayed={self._last_frame_id}, idx={frame.idx}, count={self._regressive_frame_count}" + cloudlog.warning(message) + return False + + self.frame = frame + self._last_frame_id = content_frame_id + self._texture_needs_update = True + return True + def _render_egl(self, src_rect: rl.Rectangle, dst_rect: rl.Rectangle) -> None: """Render using EGL for direct buffer access""" if self.frame is None or self.egl_texture is None: @@ -344,6 +369,7 @@ class CameraView(Widget): def _ensure_connection(self) -> bool: if not self.client.is_connected(): self.frame = None + self._last_frame_id = -1 self.available_streams.clear() # Throttle connection attempts @@ -392,6 +418,7 @@ class CameraView(Widget): # Switch to target self.client = self._target_client self._stream_type = self._target_stream_type + self._last_frame_id = int(self.frame.frame_id) if self.frame is not None else -1 self._texture_needs_update = True # Reset state diff --git a/selfdrive/ui/tests/test_camera_frame_order.py b/selfdrive/ui/tests/test_camera_frame_order.py index 199778d66..002f37695 100644 --- a/selfdrive/ui/tests/test_camera_frame_order.py +++ b/selfdrive/ui/tests/test_camera_frame_order.py @@ -1,4 +1,7 @@ -from openpilot.selfdrive.ui.onroad import cameraview +import pytest + +from openpilot.selfdrive.ui.mici.onroad import cameraview as mici_cameraview +from openpilot.selfdrive.ui.onroad import cameraview as big_cameraview class FakeFrame: @@ -7,7 +10,7 @@ class FakeFrame: self.idx = idx -def _camera_view(): +def _camera_view(cameraview): view = cameraview.CameraView.__new__(cameraview.CameraView) view._name = "camerad" view.frame = None @@ -18,9 +21,10 @@ def _camera_view(): return view -def test_reused_egl_slot_cannot_move_camera_backwards(monkeypatch): +@pytest.mark.parametrize("cameraview", [big_cameraview, mici_cameraview]) +def test_reused_egl_slot_cannot_move_camera_backwards(monkeypatch, cameraview): monkeypatch.setattr(cameraview.cloudlog, "warning", lambda *_args, **_kwargs: None) - view = _camera_view() + view = _camera_view(cameraview) displayed = FakeFrame(frame_id=10, idx=0) assert view._accept_frame(displayed, packet_frame_id=10) @@ -36,8 +40,9 @@ def test_reused_egl_slot_cannot_move_camera_backwards(monkeypatch): assert view._regressive_frame_count == 1 -def test_newer_camera_frame_is_accepted(): - view = _camera_view() +@pytest.mark.parametrize("cameraview", [big_cameraview, mici_cameraview]) +def test_newer_camera_frame_is_accepted(cameraview): + view = _camera_view(cameraview) view._last_frame_id = 30 newer = FakeFrame(frame_id=31, idx=2)