diff --git a/selfdrive/ui/onroad/augmented_road_view.py b/selfdrive/ui/onroad/augmented_road_view.py index a07f4419b..5d4c23dcb 100644 --- a/selfdrive/ui/onroad/augmented_road_view.py +++ b/selfdrive/ui/onroad/augmented_road_view.py @@ -116,11 +116,12 @@ class AugmentedRoadView(CameraView): # Draw all UI overlays if self._draw_road_overlays: self.model_renderer.render(self._content_rect) + self._render_extra_road_overlays(self._content_rect) if self._draw_hud_controls: self._hud_renderer.render(self._content_rect) - self.alert_renderer.render(self._content_rect) if self._draw_driver_state: self.driver_state_renderer.render(self._content_rect) + self.alert_renderer.render(self._content_rect) # Custom UI extension point - add custom overlays here # Use self._content_rect for positioning within camera bounds @@ -136,6 +137,10 @@ class AugmentedRoadView(CameraView): msg.uiDebug.drawTimeMillis = (time.monotonic() - start_draw) * 1000 self._pm.send('uiDebug', msg) + def _render_extra_road_overlays(self, rect: rl.Rectangle) -> None: + """Render subclass road overlays inside the content scissor, above the model and below the HUD.""" + pass + def _handle_mouse_press(self, _): if not self._hud_renderer.user_interacting() and self._click_callback is not None: self._click_callback() diff --git a/selfdrive/ui/onroad/starpilot/starpilot_onroad_view.py b/selfdrive/ui/onroad/starpilot/starpilot_onroad_view.py index f38320724..5d8accc93 100644 --- a/selfdrive/ui/onroad/starpilot/starpilot_onroad_view.py +++ b/selfdrive/ui/onroad/starpilot/starpilot_onroad_view.py @@ -90,8 +90,6 @@ class StarPilotOnroadView(AugmentedRoadView): self._render_slc() self._render_overlays() self._render_road_name() - if self._draw_road_overlays: - self._render_path_features(self._content_rect) def _draw_border(self, rect: rl.Rectangle): border_width = self._get_border_width() @@ -102,8 +100,7 @@ class StarPilotOnroadView(AugmentedRoadView): render_overlay(border_rect, border_width) def _render_slc(self): - alert_showing, alert_size = self.alert_renderer.will_render() - if alert_showing is not None and alert_size == AlertSize.full: + if self._full_alert_showing(): return if self._speed_limit_widget.is_visible: self._speed_limit_widget.render(self._speed_limit_widget.rect) @@ -136,19 +133,14 @@ class StarPilotOnroadView(AugmentedRoadView): self._torque_bar.render(self._content_rect) rl.end_scissor_mode() - def _render_path_features(self, rect: rl.Rectangle): - """Render path-related features (adjacent paths, blind spot, path edges).""" + def _render_extra_road_overlays(self, rect: rl.Rectangle) -> None: + """Render path features in the parent's clipped road-overlay layer.""" mr = self.model_renderer # Only render if we have path data if not mr._path.projected_points.size: return - rl.begin_scissor_mode( - int(rect.x), int(rect.y), - int(rect.width), int(rect.height), - ) - # Path edges (always rendered if track_edge_vertices exist) if mr._track_edge_vertices.size >= 4: render_path_edges(mr) @@ -159,7 +151,9 @@ class StarPilotOnroadView(AugmentedRoadView): # Render stopping point atop the path render_stopping_point(mr, self._font_bold) - rl.end_scissor_mode() + def _full_alert_showing(self) -> bool: + alert_showing, _ = self.alert_renderer.will_render() + return alert_showing is not None and alert_showing.size == AlertSize.full def _render_standstill_timer(self): if not self._params.get_bool("stopped_timer"): @@ -385,6 +379,9 @@ class StarPilotOnroadView(AugmentedRoadView): render_weather_icon(weather_rect) def _render_road_name(self): + if self._full_alert_showing(): + return + toggles = ui_state.starpilot_toggles road_name_on = bool(toggles.get("road_name_ui", self._params.get_bool("RoadNameUI"))) if not road_name_on: diff --git a/selfdrive/ui/tests/test_onroad_render_layers.py b/selfdrive/ui/tests/test_onroad_render_layers.py new file mode 100644 index 000000000..3638f3034 --- /dev/null +++ b/selfdrive/ui/tests/test_onroad_render_layers.py @@ -0,0 +1,208 @@ +import importlib +import sys +from types import ModuleType, SimpleNamespace + + +def _load_augmented_road_view(monkeypatch): + def stub_module(name, **attributes): + module = ModuleType(name) + for key, value in attributes.items(): + setattr(module, key, value) + monkeypatch.setitem(sys.modules, name, module) + + class CameraView: + def _render(self, _rect): + self.events.append("camera") + + class UIStatus: + DISENGAGED = 0 + OVERRIDE = 1 + ENGAGED = 2 + + stub_module( + "openpilot.selfdrive.ui.ui_state", + ui_state=SimpleNamespace(started=True, sm=SimpleNamespace()), + UIStatus=UIStatus, + ) + stub_module("openpilot.selfdrive.ui.lib.starpilot_visuals", get_border_width=lambda *_args: 0) + stub_module("openpilot.selfdrive.ui.lib.starpilot_status", get_screen_edge_color=lambda *_args: None) + stub_module("openpilot.selfdrive.ui.onroad.alert_renderer", AlertRenderer=object) + stub_module("openpilot.selfdrive.ui.onroad.driver_state", DriverStateRenderer=object) + stub_module("openpilot.selfdrive.ui.onroad.hud_renderer", HudRenderer=object) + stub_module("openpilot.selfdrive.ui.onroad.model_renderer", ModelRenderer=object) + stub_module("openpilot.selfdrive.ui.onroad.cameraview", CameraView=CameraView) + stub_module("openpilot.system.ui.lib.application", gui_app=SimpleNamespace(target_fps=20)) + + module_name = "openpilot.selfdrive.ui.onroad.augmented_road_view" + monkeypatch.delitem(sys.modules, module_name, raising=False) + return importlib.import_module(module_name) + + +def _load_starpilot_onroad_view(monkeypatch): + def stub_module(name, **attributes): + module = ModuleType(name) + for key, value in attributes.items(): + setattr(module, key, value) + monkeypatch.setitem(sys.modules, name, module) + + class AugmentedRoadView: + pass + + dummy_widget = type("DummyWidget", (), {}) + color = SimpleNamespace(r=0, g=0, b=0, a=255) + + stub_module("openpilot.selfdrive.ui.onroad.augmented_road_view", AugmentedRoadView=AugmentedRoadView) + stub_module( + "openpilot.selfdrive.ui.onroad.starpilot.starpilot_border", + render_behind=lambda *_args: None, + render_overlay=lambda *_args: None, + render_background_effects=lambda *_args: None, + ) + stub_module( + "openpilot.selfdrive.ui.onroad.starpilot.path", + render_adjacent_lanes=lambda *_args: None, + render_path_edges=lambda *_args: None, + ) + stub_module("openpilot.selfdrive.ui.ui_state", ui_state=SimpleNamespace()) + stub_module("openpilot.selfdrive.ui.onroad.starpilot.torque_bar", TorqueBar=dummy_widget) + stub_module("openpilot.selfdrive.ui.onroad.starpilot.widget_layout_manager", WidgetLayoutManager=dummy_widget) + stub_module( + "openpilot.selfdrive.ui.onroad.starpilot.widgets", + SetSpeedWidget=dummy_widget, + SpeedLimitWidget=dummy_widget, + PedalIconsWidget=dummy_widget, + AetherGaugeWidget=dummy_widget, + PersonalityButtonWidget=dummy_widget, + DriverMonitorWidget=dummy_widget, + SteeringWheelWidget=dummy_widget, + ) + stub_module( + "openpilot.selfdrive.ui.onroad.starpilot.stopping_point", + render_stopping_point=lambda *_args: None, + ) + stub_module( + "openpilot.selfdrive.ui.onroad.starpilot.pause_indicators", + render_lateral_paused=lambda *_args: None, + render_longitudinal_paused=lambda *_args: None, + ) + stub_module("openpilot.selfdrive.ui.onroad.starpilot.weather_icon", render_weather_icon=lambda *_args: None) + stub_module( + "openpilot.selfdrive.ui.lib.starpilot_status", + get_screen_edge_color=lambda *_args: color, + ENGAGED_COLOR=color, + EXPERIMENTAL_COLOR=color, + TRAFFIC_COLOR=color, + ) + stub_module( + "openpilot.system.ui.lib.application", + MousePos=object, + gui_app=SimpleNamespace(font=lambda *_args: None), + FontWeight=SimpleNamespace(BOLD=0, MEDIUM=1), + ) + stub_module( + "openpilot.system.ui.lib.text_measure", + draw_text_with_shadow=lambda *_args: None, + measure_text_cached=lambda *_args: SimpleNamespace(x=0, y=0), + ) + + module_name = "openpilot.selfdrive.ui.onroad.starpilot.starpilot_onroad_view" + monkeypatch.delitem(sys.modules, module_name, raising=False) + return importlib.import_module(module_name) + + +def test_extra_road_overlays_render_between_model_and_hud_and_alerts_last(monkeypatch): + augmented_road_view = _load_augmented_road_view(monkeypatch) + events = [] + + class LayeredRoadView(augmented_road_view.AugmentedRoadView): + def _render_extra_road_overlays(self, _rect): + events.append("road_overlays") + + class Renderer: + def __init__(self, name): + self.name = name + + def render(self, _rect): + events.append(self.name) + + view = object.__new__(LayeredRoadView) + view.events = events + view.stream_type = augmented_road_view.ROAD_CAM + view._camera_view = lambda: augmented_road_view.CAMERA_VIEW_STANDARD + view._switch_stream_if_needed = lambda *_args: None + view._is_in_reverse = lambda: False + view._update_calibration = lambda: None + view._get_border_width = lambda: 0 + view._draw_border = lambda _rect: events.append("border") + view.model_renderer = Renderer("model") + view._hud_renderer = Renderer("hud") + view.driver_state_renderer = Renderer("driver_state") + view.alert_renderer = Renderer("alert") + view._draw_driver_state = True + view._pm = SimpleNamespace(send=lambda *_args: events.append("publish")) + + monkeypatch.setattr(augmented_road_view.rl, "begin_scissor_mode", lambda *_args: events.append("scissor_begin")) + monkeypatch.setattr(augmented_road_view.rl, "end_scissor_mode", lambda: events.append("scissor_end")) + monkeypatch.setattr( + augmented_road_view.messaging, + "new_message", + lambda *_args: SimpleNamespace(uiDebug=SimpleNamespace(drawTimeMillis=0.0)), + ) + + view._render(augmented_road_view.rl.Rectangle(0, 0, 100, 50)) + + assert events == [ + "scissor_begin", + "camera", + "model", + "road_overlays", + "hud", + "driver_state", + "alert", + "scissor_end", + "border", + "publish", + ] + + +def test_full_alert_detection_uses_the_alert_size(monkeypatch): + starpilot_onroad_view = _load_starpilot_onroad_view(monkeypatch) + view = object.__new__(starpilot_onroad_view.StarPilotOnroadView) + + view.alert_renderer = SimpleNamespace( + will_render=lambda: (SimpleNamespace(size=starpilot_onroad_view.AlertSize.full), False), + ) + assert view._full_alert_showing() + + view.alert_renderer = SimpleNamespace( + will_render=lambda: (SimpleNamespace(size=starpilot_onroad_view.AlertSize.mid), False), + ) + assert not view._full_alert_showing() + + view.alert_renderer = SimpleNamespace(will_render=lambda: (None, True)) + assert not view._full_alert_showing() + + +def test_starpilot_road_overlays_use_the_parent_scissor(monkeypatch): + starpilot_onroad_view = _load_starpilot_onroad_view(monkeypatch) + events = [] + view = object.__new__(starpilot_onroad_view.StarPilotOnroadView) + view.model_renderer = SimpleNamespace( + _path=SimpleNamespace(projected_points=SimpleNamespace(size=1)), + _track_edge_vertices=SimpleNamespace(size=4), + ) + view._font_bold = object() + + monkeypatch.setattr(starpilot_onroad_view, "render_path_edges", lambda *_args: events.append("path_edges")) + monkeypatch.setattr(starpilot_onroad_view, "render_adjacent_lanes", lambda *_args: events.append("adjacent_lanes")) + monkeypatch.setattr(starpilot_onroad_view, "render_stopping_point", lambda *_args: events.append("stopping_point")) + + def fail_scissor(*_args): + raise AssertionError("road overlay changed the parent scissor") + + monkeypatch.setattr(starpilot_onroad_view.rl, "begin_scissor_mode", fail_scissor) + monkeypatch.setattr(starpilot_onroad_view.rl, "end_scissor_mode", fail_scissor) + + view._render_extra_road_overlays(object()) + + assert events == ["path_edges", "adjacent_lanes", "stopping_point"]