diff --git a/selfdrive/ui/onroad/starpilot/favorite_radial_menu.py b/selfdrive/ui/onroad/starpilot/favorite_radial_menu.py index 44bda9b6bf..e257d37088 100644 --- a/selfdrive/ui/onroad/starpilot/favorite_radial_menu.py +++ b/selfdrive/ui/onroad/starpilot/favorite_radial_menu.py @@ -70,11 +70,13 @@ class FavoriteRadialMenu: def __init__(self, params: Any, params_memory: Any, option_provider: Callable[[], Iterable[dict[str, Any]]], *, - clock: Callable[[], float] = time.monotonic): + clock: Callable[[], float] = time.monotonic, + cache_render_texture: Callable[..., Any] | None = None): self._params = params self._params_memory = params_memory self._option_provider = option_provider self._clock = clock + self._cache_render_texture = cache_render_texture self._state = self.STATE_COLLAPSED self._selected_slot: int | None = None @@ -723,11 +725,48 @@ class FavoriteRadialMenu: x0 = self._rect.x y0 = self._rect.y + self._rect.height is_pressed = self._corner_press is not None - size = 150.0 * scale + texture_size = max(1, int(round(size))) + texture_scale = texture_size / 150.0 + # Cache the hint's two appearances instead of rebuilding the mesh every frame. + cache = self._cache_render_texture + texture = None + if cache is not None: + variant = "pressed" if is_pressed else "idle" + texture = cache( + f"favorite-corner-hint:{texture_size}:{variant}", + texture_size, + texture_size, + lambda: FavoriteRadialMenu._draw_corner_hint_geometry( + texture_scale, is_pressed, 0.0, float(texture_size), + ), + ) + + if texture is not None: + rl.begin_blend_mode(rl.BlendMode.BLEND_ALPHA_PREMULTIPLY) + try: + rl.draw_texture_pro( + texture, + rl.Rectangle(0, 0, texture_size, -texture_size), + rl.Rectangle(x0, y0 - size, size, size), + rl.Vector2(0, 0), + 0.0, + rl.WHITE, + ) + finally: + rl.end_blend_mode() + return + + self._draw_corner_hint_geometry(scale, is_pressed, x0, y0) + + @classmethod + def _draw_corner_hint_geometry(cls, scale: float, is_pressed: bool, + x0: float, y0: float) -> None: + size = 150.0 * scale + steps = 48 - # 1. Precompute edge vertices to minimize per-frame allocations + # Build the gradient mesh once for this cached appearance. v_origin = rl.Vector2(x0, y0) inv_steps = 1.0 / steps pts_top = [rl.Vector2(x0, y0 - size * (k * inv_steps)) for k in range(steps + 1)] @@ -747,7 +786,7 @@ class FavoriteRadialMenu: purple_a = int(purple_max_alpha * ((1.0 - t_mid) ** 1.75)) for col in (rl.Color(8, 6, 18, base_a) if base_a > 0 else None, - self._sample_aether_color(t_mid, purple_a) if purple_a > 0 else None): + cls._sample_aether_color(t_mid, purple_a) if purple_a > 0 else None): if col is None: continue if i == 0: @@ -757,8 +796,8 @@ class FavoriteRadialMenu: rl.draw_triangle(v_tb, v_ra, v_rb, col) # 3. Ultra-Polished Frosted-Glass Vector Arrow (Nestled deep in purple corner) - center = self.corner_center(self._rect) - cx, cy = center.x, center.y + inset = (cls.CORNER_HINT_RING_RADIUS + cls.CORNER_HINT_EDGE_MARGIN) * scale + cx, cy = x0 + inset, y0 - inset tip = rl.Vector2(cx + 15.0 * scale, cy - 15.0 * scale) tail = rl.Vector2(cx - 15.0 * scale, cy + 15.0 * scale) diff --git a/selfdrive/ui/onroad/starpilot/starpilot_onroad_view.py b/selfdrive/ui/onroad/starpilot/starpilot_onroad_view.py index 4dceb40a8c..d04546c882 100644 --- a/selfdrive/ui/onroad/starpilot/starpilot_onroad_view.py +++ b/selfdrive/ui/onroad/starpilot/starpilot_onroad_view.py @@ -52,6 +52,7 @@ class StarPilotOnroadView(AugmentedRoadView): ui_state.ui_params, ui_state.params_memory, self._favorite_slot_options, + cache_render_texture=gui_app.cached_render_texture, ) self._favorite_input_consumed = False diff --git a/selfdrive/ui/tests/test_favorite_radial_menu.py b/selfdrive/ui/tests/test_favorite_radial_menu.py index 53debdfdcc..2b40169063 100644 --- a/selfdrive/ui/tests/test_favorite_radial_menu.py +++ b/selfdrive/ui/tests/test_favorite_radial_menu.py @@ -77,14 +77,15 @@ def _open_picker(menu, rect, slot_index=0): assert menu.state == FavoriteRadialMenu.STATE_PICKER -def _menu(clock, options=None): +def _menu(clock, options=None, cache_render_texture=None): params = FakeParams() memory = FakeParams() options = options or [ {"key": "FeatureToggle", "label": "Feature Toggle", "description": "The primary test setting.", "section": "Testing"}, {"key": "OtherToggle", "label": "Other Toggle", "description": "Another test setting.", "section": "Testing"}, ] - return FavoriteRadialMenu(params, memory, lambda: options, clock=lambda: clock[0]), params, memory + return FavoriteRadialMenu(params, memory, lambda: options, clock=lambda: clock[0], + cache_render_texture=cache_render_texture), params, memory def test_radial_menu_opens_from_corner_tap_and_arranges_three_slots_on_an_arc(): @@ -116,6 +117,50 @@ def test_corner_hint_keeps_visible_ring_near_content_corner(): assert menu._contains(menu._corner_touch_zone_for(rect), center) +def test_corner_hint_caches_idle_and_pressed_art(monkeypatch): + clock = [0.0] + cached = {} + requests = [] + geometry = [] + draws = [] + blend_modes = [] + + def cache(key, width, height, render): + requests.append((key, width, height, render)) + return cached.get(key) + + menu, _params, _memory = _menu(clock, cache_render_texture=cache) + menu._rect = rl.Rectangle(0, 0, 2160, 1080) + monkeypatch.setattr(FavoriteRadialMenu, "_draw_corner_hint_geometry", + staticmethod(lambda *args: geometry.append(args))) + monkeypatch.setattr(rl, "draw_texture_pro", lambda *args: draws.append(args)) + monkeypatch.setattr(rl, "begin_blend_mode", blend_modes.append) + monkeypatch.setattr(rl, "end_blend_mode", lambda: None) + + menu._draw_corner_hint() + idle_key, width, height, render_idle = requests[-1] + assert idle_key == "favorite-corner-hint:150:idle" + assert (width, height) == (150, 150) + assert geometry == [(1.0, False, 0, 1080)] # Original drawing remains the cache-miss fallback. + + render_idle() + assert geometry[-1] == (1.0, False, 0.0, 150.0) # Cached art is drawn in local texture coordinates. + + texture = object() + cached[idle_key] = texture + menu._draw_corner_hint() + assert len(geometry) == 2 + assert draws[-1][0] is texture + destination = draws[-1][2] + assert (destination.x, destination.y, destination.width, destination.height) == (0, 930, 150, 150) + assert blend_modes == [rl.BlendMode.BLEND_ALPHA_PREMULTIPLY] + + menu._corner_press = object() + menu._draw_corner_hint() + assert requests[-1][0] == "favorite-corner-hint:150:pressed" + assert geometry[-1] == (1.0, True, 0, 1080) + + def test_radial_menu_opens_from_diagonal_inward_swipe_and_auto_collapses_after_six_seconds(): clock = [0.0] menu, _params, _memory = _menu(clock)