Files
StarPilot/selfdrive/ui/tests/test_camera_frame_order.py
2026-08-03 12:21:03 -05:00

512 lines
17 KiB
Python

from types import SimpleNamespace
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:
def __init__(self, frame_id: int, idx: int):
self.frame_id = frame_id
self.idx = idx
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
view._texture_needs_update = False
view._external_texture_id = 0
view._closed = True
return view
def test_mici_uses_shared_camera_view():
assert issubclass(mici_cameraview.CameraView, big_cameraview.CameraView)
assert mici_cameraview.CameraView._use_upstream_engaged_color
assert not big_cameraview.CameraView._use_upstream_engaged_color
def test_pending_switch_is_cancelled_when_requested_stream_is_current():
view = _camera_view()
view._stream_type = big_cameraview.VisionStreamType.VISION_STREAM_ROAD
view._target_stream_type = big_cameraview.VisionStreamType.VISION_STREAM_DRIVER
view._target_client = object()
view._switching = True
view.switch_stream(big_cameraview.VisionStreamType.VISION_STREAM_ROAD)
assert view._target_client is None
assert view._target_stream_type is None
assert not view._switching
def test_reentry_switch_request_keeps_matching_candidate():
view = _camera_view()
candidate = object()
view._onroad_reentry_pending = True
view._reentry_stream_selected = True
view._target_stream_type = view._stream_type
view._target_client = candidate
view._switching = True
view.switch_stream(view._stream_type)
assert view._target_client is candidate
assert view._target_stream_type == view._stream_type
assert 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
view._clear_textures = lambda: None
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 clients == []
assert view.client is None
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)
monkeypatch.setattr(big_cameraview, "PC", False)
view = _camera_view()
displayed = FakeFrame(frame_id=10, idx=0)
assert view._accept_frame(displayed, packet_frame_id=10)
# camerad cycles back to this shared slot while it remains displayed.
displayed.frame_id = 30
view._observe_displayed_frame()
delayed = FakeFrame(frame_id=20, idx=1)
assert not view._accept_frame(delayed, packet_frame_id=20)
assert view.frame is displayed
assert view._last_frame_id == 30
assert view._regressive_frame_count == 1
def test_texture_camera_accepts_regressive_replay_frame(monkeypatch):
monkeypatch.setattr(big_cameraview, "PC", True)
view = _camera_view()
assert view._accept_frame(FakeFrame(frame_id=1140, idx=0), packet_frame_id=1140)
rewind = FakeFrame(frame_id=660, idx=1)
assert view._accept_frame(rewind, packet_frame_id=660)
assert view.frame is rewind
assert view._last_frame_id == 660
assert view._regressive_frame_count == 0
def test_newer_camera_frame_is_accepted():
view = _camera_view()
view._last_frame_id = 30
view._regressive_frame_count = 2
newer = FakeFrame(frame_id=31, idx=2)
assert view._accept_frame(newer, packet_frame_id=31)
assert view.frame is newer
assert view._last_frame_id == 31
assert view._regressive_frame_count == 0
assert view._texture_needs_update
def test_shared_camera_has_upstream_shaders_and_driver_enhancement():
assert "samplerExternalOES" in big_cameraview.FRAME_FRAGMENT_SHADER_EXTERNAL
assert "pow(color.rgb, vec3(1.0/1.28))" in big_cameraview.FRAME_FRAGMENT_SHADER_EXTERNAL
assert "uniform sampler2D texture0" in big_cameraview.FRAME_FRAGMENT_SHADER_YUV
assert "uniform sampler2D texture1" in big_cameraview.FRAME_FRAGMENT_SHADER_YUV
assert "uniform int enhance_driver" in big_cameraview.FRAME_FRAGMENT_SHADER_EXTERNAL
assert "uniform int enhance_driver" in big_cameraview.FRAME_FRAGMENT_SHADER_YUV
assert "uniform int engaged" not in big_cameraview.FRAME_FRAGMENT_SHADER_EXTERNAL
assert "uniform int engaged" not in big_cameraview.FRAME_FRAGMENT_SHADER_YUV
assert hasattr(big_cameraview.CameraView, "_render_egl")
assert hasattr(big_cameraview.CameraView, "_fallback_to_textures")
def test_shared_camera_falls_back_after_repeated_regressive_frames(monkeypatch):
monkeypatch.setattr(big_cameraview.cloudlog, "warning", lambda *_args, **_kwargs: None)
monkeypatch.setattr(big_cameraview, "PC", False)
view = _camera_view()
view._use_egl = True
view.frame = FakeFrame(frame_id=30, idx=0)
view._last_frame_id = 30
fallback_reasons = []
view._fallback_to_textures = fallback_reasons.append
for frame_id in (20, 19, 18):
assert not view._accept_frame(FakeFrame(frame_id=frame_id, idx=1), packet_frame_id=frame_id)
assert fallback_reasons == ["repeated regressive frames"]
assert view.frame.frame_id == 30
def test_shared_camera_fallback_reloads_texture_backend(monkeypatch):
view = _camera_view()
view._use_egl = True
view.shader = SimpleNamespace(id=1)
events = []
view._clear_textures = lambda: events.append("clear")
view._load_frame_shader = lambda: events.append(("shader", view._use_egl))
view._initialize_textures = lambda: events.append("textures")
monkeypatch.setattr(big_cameraview.cloudlog, "error", lambda *_args, **_kwargs: None)
monkeypatch.setattr(big_cameraview.rl, "unload_shader", lambda _shader: events.append("unload_shader"))
view._fallback_to_textures("test")
assert events == ["clear", "unload_shader", ("shader", False), "textures"]
assert not view._use_egl
def test_connection_retry_does_not_wait_for_advertisement_and_uses_fresh_candidate(monkeypatch):
view = _camera_view()
view._name = "camerad"
view._clear_textures = lambda: None
view.client = SimpleNamespace(is_connected=lambda: False)
view._target_client = None
view._target_stream_type = None
view._switching = False
view.available_streams = []
view.last_connection_attempt = 0.0
candidates = []
class FakeClient:
@staticmethod
def available_streams(_name, block=False):
pytest.fail("startup connection waited for stream advertisement")
def __init__(self, *_args, **_kwargs):
candidates.append(self)
self.connected = False
self.num_buffers = 0
def is_connected(self):
return self.connected
def connect(self, _block):
return False
monkeypatch.setattr(big_cameraview, "VisionIpcClient", FakeClient)
monkeypatch.setattr(big_cameraview.rl, "get_time", lambda: 1.0)
assert not view._ensure_connection()
assert view.client is None
assert len(candidates) == 1
monkeypatch.setattr(big_cameraview.rl, "get_time", lambda: 1.3)
assert not view._ensure_connection()
assert len(candidates) == 2
assert candidates[0] is not candidates[1]
def test_candidate_is_not_active_until_first_consistent_frame(monkeypatch):
view = _camera_view()
view._name = "camerad"
view._clear_textures = lambda: None
view._initialize_textures = lambda: None
view.client = None
view._target_client = None
view._target_stream_type = None
view._switching = False
view.available_streams = []
view.last_connection_attempt = 0.0
class FakeClient:
@staticmethod
def available_streams(_name, block=False):
return [view._stream_type]
def __init__(self, *_args, **_kwargs):
self.connected = False
self.num_buffers = 1
self.frame_id = -1
self.frames = [None, FakeFrame(frame_id=42, idx=0)]
def is_connected(self):
return self.connected
def connect(self, _block):
self.connected = True
return True
def recv(self, timeout_ms=0):
frame = self.frames.pop(0)
if frame is not None:
self.frame_id = frame.frame_id
return frame
monkeypatch.setattr(big_cameraview, "VisionIpcClient", FakeClient)
monkeypatch.setattr(big_cameraview.rl, "get_time", lambda: 1.0)
assert not view._ensure_connection()
assert view.client is None
candidate = view._target_client
assert candidate is not None
assert view._ensure_connection()
assert view.client is candidate
assert view.frame.frame_id == 42
assert view._target_client is None
assert not view._switching
def test_inconsistent_candidate_frame_is_discarded(monkeypatch):
view = _camera_view()
view._name = "camerad"
view._clear_textures = lambda: None
view.client = None
view._target_client = None
view._target_stream_type = None
view._switching = False
view.available_streams = []
view.last_connection_attempt = 0.0
class FakeClient:
@staticmethod
def available_streams(_name, block=False):
return [view._stream_type]
def __init__(self, *_args, **_kwargs):
self.connected = False
self.num_buffers = 1
self.frame_id = 10
def is_connected(self):
return self.connected
def connect(self, _block):
self.connected = True
return True
def recv(self, timeout_ms=0):
return FakeFrame(frame_id=9, idx=0)
monkeypatch.setattr(big_cameraview, "VisionIpcClient", FakeClient)
monkeypatch.setattr(big_cameraview.rl, "get_time", lambda: 1.0)
assert not view._ensure_connection()
assert view.client is None
assert view._target_client is None
assert view._target_stream_type == view._stream_type
assert not view._switching
def test_disconnected_candidate_is_discarded_without_reconnect():
view = _camera_view()
class Candidate:
num_buffers = 1
def __init__(self):
self.connected = True
def is_connected(self):
return self.connected
def connect(self, _block):
pytest.fail("discarded candidate was reconnected")
def recv(self, timeout_ms=0):
self.connected = False
return None
candidate = Candidate()
view._target_client = candidate
view._target_stream_type = view._stream_type
view._switching = True
view._handle_switch()
assert view._target_client is None
assert not view._switching
assert view._target_stream_type == view._stream_type
def test_steady_state_packet_content_mismatch_is_rejected():
view = _camera_view()
displayed = FakeFrame(frame_id=10, idx=0)
assert view._accept_frame(displayed, packet_frame_id=10)
delayed = FakeFrame(frame_id=12, idx=1)
assert not view._accept_frame(delayed, packet_frame_id=11)
assert view.frame is displayed
assert view._last_frame_id == 10
def test_egl_image_creation_failure_is_reported(monkeypatch):
view = _camera_view()
view.frame = SimpleNamespace(idx=0, width=1928, height=1208, stride=2048, fd=7, uv_offset=2473984)
view.egl_texture = SimpleNamespace(id=1)
view._external_texture_id = 11
view.egl_images = {}
monkeypatch.setattr(big_cameraview, "create_egl_image", lambda *_args: None)
assert not view._render_egl(None, None)
assert view.egl_images == {}
def test_invalid_egl_texture_is_reported_without_binding(monkeypatch):
view = _camera_view()
view.frame = SimpleNamespace(idx=0)
view.egl_texture = SimpleNamespace(id=0)
view._external_texture_id = 11
view.egl_images = {0: object()}
monkeypatch.setattr(big_cameraview, "bind_egl_image_to_texture",
lambda *_args: pytest.fail("invalid EGL texture was bound"))
assert not view._render_egl(None, None)
def test_invalid_external_texture_is_reported_without_binding(monkeypatch):
view = _camera_view()
view.frame = SimpleNamespace(idx=0)
view.egl_texture = SimpleNamespace(id=7)
view.egl_images = {0: object()}
monkeypatch.setattr(big_cameraview, "bind_egl_image_to_texture",
lambda *_args: pytest.fail("invalid external texture was bound"))
assert not view._render_egl(None, None)
def test_egl_render_always_ends_shader_mode(monkeypatch):
view = _camera_view()
view.frame = SimpleNamespace(idx=0, width=1928, height=1208)
view.egl_texture = SimpleNamespace(id=1, width=0, height=0)
view._external_texture_id = 11
view.egl_images = {0: object()}
view.shader = SimpleNamespace(id=1)
view._update_shader_state = lambda: None
events = []
monkeypatch.setattr(big_cameraview, "bind_egl_image_to_texture", lambda *_args: None)
monkeypatch.setattr(big_cameraview.rl, "begin_shader_mode", lambda *_args: events.append("begin"))
def fail_draw(*_args):
raise RuntimeError("draw failed")
monkeypatch.setattr(big_cameraview.rl, "draw_texture_pro", fail_draw)
monkeypatch.setattr(big_cameraview.rl, "end_shader_mode", lambda: events.append("end"))
with pytest.raises(RuntimeError, match="draw failed"):
view._render_egl(None, None)
assert events == ["begin", "end"]
def test_egl_render_keeps_external_and_raylib_texture_targets_separate(monkeypatch):
view = _camera_view()
image = object()
view.frame = SimpleNamespace(idx=3, width=1928, height=1208)
view.egl_texture = SimpleNamespace(id=7, width=1, height=1)
view._external_texture_id = 11
view.egl_images = {3: image}
view.shader = object()
view._update_shader_state = lambda: None
bound = []
drawn = []
monkeypatch.setattr(big_cameraview, "bind_egl_image_to_texture",
lambda texture_id, egl_image: bound.append((texture_id, egl_image)))
monkeypatch.setattr(big_cameraview.rl, "begin_shader_mode", lambda _shader: None)
monkeypatch.setattr(big_cameraview.rl, "end_shader_mode", lambda: None)
monkeypatch.setattr(big_cameraview.rl, "draw_texture_pro", lambda texture, *_args: drawn.append(texture.id))
assert view._render_egl(None, None)
assert bound == [(11, image)]
assert drawn == [7]
def test_driver_enhancement_tracks_active_stream(monkeypatch):
view = _camera_view()
view.shader = SimpleNamespace(id=1)
view._enhance_driver_loc = 2
view._enhance_driver_val = [0]
values = []
monkeypatch.setattr(big_cameraview.rl, "set_shader_value",
lambda _shader, _loc, value, _type: values.append(value[0]))
view._stream_type = big_cameraview.VisionStreamType.VISION_STREAM_ROAD
view._update_shader_state()
view._stream_type = big_cameraview.VisionStreamType.VISION_STREAM_DRIVER
view._update_shader_state()
view._stream_type = big_cameraview.VisionStreamType.VISION_STREAM_WIDE_ROAD
view._update_shader_state()
assert values == [0, 1, 0]
def test_texture_fallback_survives_egl_cleanup_failure(monkeypatch):
view = _camera_view()
view._use_egl = True
view.shader = SimpleNamespace(id=1)
events = []
def fail_cleanup():
raise RuntimeError("cleanup failed")
view._clear_textures = fail_cleanup
view._load_frame_shader = lambda: events.append(("shader", view._use_egl))
view._initialize_textures = lambda: events.append("textures")
monkeypatch.setattr(big_cameraview.cloudlog, "error", lambda *_args, **_kwargs: None)
monkeypatch.setattr(big_cameraview.cloudlog, "exception", lambda *_args, **_kwargs: None)
monkeypatch.setattr(big_cameraview.rl, "unload_shader", lambda _shader: events.append("unload_shader"))
view._fallback_to_textures("test")
assert not view._use_egl
assert events == ["unload_shader", ("shader", False), "textures"]