mirror of
https://github.com/firestar5683/StarPilot.git
synced 2026-07-31 06:02:15 +08:00
Performance Enhancing Rugs
This commit is contained in:
@@ -2,7 +2,6 @@ import time
|
||||
import pyray as rl
|
||||
from collections.abc import Callable
|
||||
from enum import IntEnum
|
||||
from openpilot.common.params import Params
|
||||
from openpilot.selfdrive.ui.ui_state import ui_state
|
||||
from openpilot.selfdrive.ui.widgets.offroad_alerts import UpdateAlert, OffroadAlert
|
||||
from openpilot.selfdrive.ui.widgets.exp_mode_button import ExperimentalModeButton
|
||||
@@ -32,7 +31,7 @@ class HomeLayoutState(IntEnum):
|
||||
class HomeLayout(Widget):
|
||||
def __init__(self):
|
||||
super().__init__()
|
||||
self.params = Params()
|
||||
self.params = ui_state.ui_params
|
||||
|
||||
self.update_alert = UpdateAlert()
|
||||
self.offroad_alert = OffroadAlert()
|
||||
|
||||
@@ -47,6 +47,11 @@ class MainLayout(Widget):
|
||||
if not self._onboarding_window.completed:
|
||||
gui_app.push_widget(self._onboarding_window)
|
||||
|
||||
# TICI/TIZI offroad views are mostly static. Keep onroad and interaction
|
||||
# at the normal rate, but avoid spending GPU/CPU budget redrawing idle UI.
|
||||
# MainLayout is only used by BIG UI, so MICI retains its existing scheduler.
|
||||
gui_app.configure_adaptive_rendering(gui_app.big_ui())
|
||||
|
||||
@staticmethod
|
||||
def _critical_full_alert_active() -> bool:
|
||||
if not ui_state.sm.recv_frame['selfdriveState']:
|
||||
@@ -60,6 +65,7 @@ class MainLayout(Widget):
|
||||
return
|
||||
|
||||
self._handle_onroad_transition()
|
||||
gui_app.set_render_mode(self._current_mode == MainState.ONROAD or ui_state.started)
|
||||
self._render_main_content()
|
||||
|
||||
def _setup_callbacks(self):
|
||||
@@ -105,11 +111,13 @@ class MainLayout(Widget):
|
||||
self._layouts[self._current_mode].hide_event()
|
||||
self._current_mode = layout
|
||||
self._layouts[self._current_mode].show_event()
|
||||
gui_app.request_high_fps()
|
||||
|
||||
def open_settings(self, panel_type: PanelType):
|
||||
self._layouts[MainState.SETTINGS].set_current_panel(panel_type)
|
||||
self._set_current_layout(MainState.SETTINGS)
|
||||
self._sidebar.set_visible(False)
|
||||
gui_app.request_high_fps()
|
||||
|
||||
def _on_settings_clicked(self):
|
||||
self.open_settings(PanelType.STARPILOT)
|
||||
|
||||
@@ -2,4 +2,4 @@ from openpilot.selfdrive.ui.ui_state import ui_state
|
||||
|
||||
|
||||
def restart_needed_callback(_):
|
||||
ui_state.params.put_bool("OnroadCycleRequested", True)
|
||||
ui_state.ui_params.put_bool("OnroadCycleRequested", True)
|
||||
|
||||
@@ -11,7 +11,6 @@ import pyray as rl
|
||||
import qrcode
|
||||
|
||||
from openpilot.common.basedir import BASEDIR
|
||||
from openpilot.common.params import Params
|
||||
from openpilot.common.swaglog import cloudlog
|
||||
from openpilot.selfdrive.ui.onroad.driver_camera_dialog import DriverCameraDialog
|
||||
from openpilot.selfdrive.ui.ui_state import ui_state
|
||||
@@ -112,7 +111,7 @@ class DeviceLayout(Widget):
|
||||
def __init__(self):
|
||||
super().__init__()
|
||||
|
||||
self._params = Params()
|
||||
self._params = ui_state.ui_params
|
||||
self._select_language_dialog: MultiOptionDialog | None = None
|
||||
self._driver_camera: DriverCameraDialog | None = None
|
||||
self._pair_device_dialog: PairingDialog | None = None
|
||||
|
||||
@@ -106,11 +106,11 @@ class SoftwareLayout(Widget):
|
||||
super().__init__()
|
||||
|
||||
self._onroad_label = ListItem(lambda: tr("Updates are only downloaded while the car is off."))
|
||||
self._version_item = text_item(lambda: tr("Current Version"), starpilot_display_description(ui_state.params.get("UpdaterCurrentDescription")))
|
||||
self._version_item = text_item(lambda: tr("Current Version"), starpilot_display_description(ui_state.ui_params.get("UpdaterCurrentDescription")))
|
||||
self._auto_updates_toggle = toggle_item(
|
||||
lambda: tr("Automatically Install Updates"),
|
||||
lambda: tr("Automatically install updates when parked with an active internet connection."),
|
||||
initial_state=ui_state.params.get_bool("AutomaticUpdates"),
|
||||
initial_state=ui_state.ui_params.get_bool("AutomaticUpdates"),
|
||||
callback=self._on_auto_updates_toggle,
|
||||
)
|
||||
self._download_btn = button_item(
|
||||
@@ -129,8 +129,8 @@ class SoftwareLayout(Widget):
|
||||
|
||||
# Branch switcher
|
||||
self._branch_btn = button_item(lambda: tr("Target Branch"), lambda: tr("SELECT"), callback=self._on_select_branch)
|
||||
self._branch_btn.set_visible(not ui_state.params.get_bool("IsTestedBranch"))
|
||||
self._branch_btn.action_item.set_value(ui_state.params.get("UpdaterTargetBranch") or "")
|
||||
self._branch_btn.set_visible(not ui_state.ui_params.get_bool("IsTestedBranch"))
|
||||
self._branch_btn.action_item.set_value(ui_state.ui_params.get("UpdaterTargetBranch") or "")
|
||||
self._branch_dialog: MultiOptionDialog | None = None
|
||||
|
||||
self._scroller = Scroller(
|
||||
@@ -159,11 +159,11 @@ class SoftwareLayout(Widget):
|
||||
self._onroad_label.set_visible(ui_state.is_onroad())
|
||||
|
||||
# Update current version and release notes
|
||||
current_desc = starpilot_display_description(ui_state.params.get("UpdaterCurrentDescription"))
|
||||
current_release_notes = (ui_state.params.get("UpdaterCurrentReleaseNotes") or b"").decode("utf-8", "replace")
|
||||
current_desc = starpilot_display_description(ui_state.ui_params.get("UpdaterCurrentDescription"))
|
||||
current_release_notes = (ui_state.ui_params.get("UpdaterCurrentReleaseNotes") or b"").decode("utf-8", "replace")
|
||||
self._version_item.action_item.set_text(current_desc)
|
||||
self._version_item.set_description(current_release_notes)
|
||||
self._auto_updates_toggle.action_item.set_state(ui_state.params.get_bool("AutomaticUpdates"))
|
||||
self._auto_updates_toggle.action_item.set_state(ui_state.ui_params.get_bool("AutomaticUpdates"))
|
||||
|
||||
# Update download button visibility and state
|
||||
self._download_btn.set_visible(ui_state.is_offroad())
|
||||
@@ -185,10 +185,10 @@ class SoftwareLayout(Widget):
|
||||
|
||||
# ── Normal updater state (only when fast update NOT active) ───
|
||||
if state.stage == FastUpdateStage.IDLE:
|
||||
updater_state = ui_state.params.get("UpdaterState") or "idle"
|
||||
failed_count = ui_state.params.get("UpdateFailedCount") or 0
|
||||
fetch_available = ui_state.params.get_bool("UpdaterFetchAvailable")
|
||||
update_available = ui_state.params.get_bool("UpdateAvailable")
|
||||
updater_state = ui_state.ui_params.get("UpdaterState") or "idle"
|
||||
failed_count = ui_state.ui_params.get("UpdateFailedCount") or 0
|
||||
fetch_available = ui_state.ui_params.get_bool("UpdaterFetchAvailable")
|
||||
update_available = ui_state.ui_params.get_bool("UpdateAvailable")
|
||||
|
||||
if updater_state != "idle":
|
||||
self._waiting_for_updater = False
|
||||
@@ -203,7 +203,7 @@ class SoftwareLayout(Widget):
|
||||
self._download_btn.action_item.set_value(tr("update available"))
|
||||
self._download_btn.action_item.set_text(tr("DOWNLOAD"))
|
||||
else:
|
||||
last_update = ui_state.params.get("LastUpdateTime")
|
||||
last_update = ui_state.ui_params.get("LastUpdateTime")
|
||||
if last_update:
|
||||
formatted = time_ago(last_update)
|
||||
self._download_btn.action_item.set_value(tr("up to date, last checked {}").format(formatted))
|
||||
@@ -219,14 +219,14 @@ class SoftwareLayout(Widget):
|
||||
update_available = False
|
||||
|
||||
# Update target branch button value
|
||||
current_branch = ui_state.params.get("UpdaterTargetBranch") or ""
|
||||
current_branch = ui_state.ui_params.get("UpdaterTargetBranch") or ""
|
||||
self._branch_btn.action_item.set_value(current_branch)
|
||||
|
||||
# Update install button
|
||||
self._install_btn.set_visible(ui_state.is_offroad() and update_available and state.stage == FastUpdateStage.IDLE)
|
||||
if update_available and state.stage == FastUpdateStage.IDLE:
|
||||
new_desc = starpilot_display_description(ui_state.params.get("UpdaterNewDescription"))
|
||||
new_release_notes = (ui_state.params.get("UpdaterNewReleaseNotes") or b"").decode("utf-8", "replace")
|
||||
new_desc = starpilot_display_description(ui_state.ui_params.get("UpdaterNewDescription"))
|
||||
new_release_notes = (ui_state.ui_params.get("UpdaterNewReleaseNotes") or b"").decode("utf-8", "replace")
|
||||
self._install_btn.action_item.set_text(tr("INSTALL"))
|
||||
self._install_btn.action_item.set_value(new_desc)
|
||||
self._install_btn.set_description(new_release_notes)
|
||||
@@ -327,7 +327,7 @@ class SoftwareLayout(Widget):
|
||||
threading.Thread(target=_run_worker, daemon=True).start()
|
||||
|
||||
def _on_auto_updates_toggle(self, enabled: bool):
|
||||
ui_state.params.put_bool("AutomaticUpdates", enabled)
|
||||
ui_state.ui_params.put_bool("AutomaticUpdates", enabled)
|
||||
|
||||
def _on_uninstall(self):
|
||||
def handle_step1(result):
|
||||
@@ -338,13 +338,13 @@ class SoftwareLayout(Widget):
|
||||
|
||||
def handle_step3(result3):
|
||||
if result3 == DialogResult.CONFIRM:
|
||||
ui_state.params.clear_all()
|
||||
ui_state.params.put_bool("DoUninstall", True)
|
||||
ui_state.ui_params.clear_all()
|
||||
ui_state.ui_params.put_bool("DoUninstall", True)
|
||||
|
||||
dialog = ConfirmDialog(tr("This is a complete factory reset and cannot be undone. Are you absolutely sure?"), tr("Reset"), callback=handle_step3)
|
||||
gui_app.push_widget(dialog)
|
||||
else:
|
||||
ui_state.params.put_bool("DoUninstall", True)
|
||||
ui_state.ui_params.put_bool("DoUninstall", True)
|
||||
|
||||
dialog = ConfirmDialog(
|
||||
tr("Do you want to perform a full factory reset? All saved assets and settings will be permanently deleted!"), tr("Factory Reset"), tr("Skip"), callback=handle_step2
|
||||
@@ -364,12 +364,12 @@ class SoftwareLayout(Widget):
|
||||
def _on_install_update(self):
|
||||
# Trigger reboot to install update
|
||||
self._install_btn.action_item.set_enabled(False)
|
||||
ui_state.params.put_bool("DoReboot", True)
|
||||
ui_state.ui_params.put_bool("DoReboot", True)
|
||||
|
||||
def _on_select_branch(self):
|
||||
# Get available branches and order
|
||||
current_git_branch = ui_state.params.get("GitBranch") or ""
|
||||
branches_str = ui_state.params.get("UpdaterAvailableBranches") or ""
|
||||
current_git_branch = ui_state.ui_params.get("GitBranch") or ""
|
||||
branches_str = ui_state.ui_params.get("UpdaterAvailableBranches") or ""
|
||||
branches = [b for b in branches_str.split(",") if b]
|
||||
|
||||
for b in [current_git_branch, "devel-staging", "devel", "nightly", "nightly-dev", "master"]:
|
||||
@@ -377,13 +377,13 @@ class SoftwareLayout(Widget):
|
||||
branches.remove(b)
|
||||
branches.insert(0, b)
|
||||
|
||||
current_target = ui_state.params.get("UpdaterTargetBranch") or ""
|
||||
current_target = ui_state.ui_params.get("UpdaterTargetBranch") or ""
|
||||
|
||||
def handle_selection(result):
|
||||
# Confirmed selection
|
||||
if result == DialogResult.CONFIRM and self._branch_dialog is not None and self._branch_dialog.selection:
|
||||
selection = self._branch_dialog.selection
|
||||
ui_state.params.put("UpdaterTargetBranch", selection)
|
||||
ui_state.ui_params.put("UpdaterTargetBranch", selection)
|
||||
self._branch_btn.action_item.set_value(selection)
|
||||
os.system("pkill -SIGUSR1 -f system.updated.updated")
|
||||
self._branch_dialog = None
|
||||
|
||||
@@ -13,6 +13,7 @@ from openpilot.system.ui.widgets import Widget, DialogResult
|
||||
from openpilot.system.ui.widgets.label import gui_label
|
||||
|
||||
from openpilot.selfdrive.ui.layouts.settings.starpilot.scribble import draw_custom_icon
|
||||
from openpilot.selfdrive.ui.lib.ui_param_cache import shared_ui_params
|
||||
|
||||
|
||||
GEOMETRY_OFFSET = 10
|
||||
@@ -3538,8 +3539,7 @@ class AetherCategoryDrawer(AetherSettingsView):
|
||||
|
||||
# Read driving side dynamically for ergonomic layout (LHD vs RHD)
|
||||
try:
|
||||
from openpilot.common.params import Params
|
||||
self._is_rhd = Params().get_bool("IsRHD")
|
||||
self._is_rhd = shared_ui_params().get_bool("IsRHD")
|
||||
except Exception:
|
||||
self._is_rhd = False
|
||||
|
||||
@@ -5476,4 +5476,3 @@ class TileGrid(Widget):
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -6,6 +6,7 @@ from enum import IntEnum
|
||||
import pyray as rl
|
||||
|
||||
from openpilot.common.params import Params
|
||||
from openpilot.selfdrive.ui.lib.ui_param_cache import shared_ui_params
|
||||
from openpilot.starpilot.common.starpilot_variables import update_starpilot_toggles
|
||||
from openpilot.system.ui.lib.multilang import tr
|
||||
from openpilot.system.ui.lib.application import gui_app
|
||||
@@ -13,75 +14,50 @@ from openpilot.system.ui.widgets import DialogResult, Widget
|
||||
from openpilot.system.ui.widgets.option_dialog import MultiOptionDialog
|
||||
from openpilot.selfdrive.ui.layouts.settings.starpilot.aethergrid import TileGrid, HubTile, ToggleTile, ValueTile, SliderTile, SPACING, AetherSliderDialog, AetherListColors
|
||||
from openpilot.selfdrive.ui.layouts.settings.starpilot.sectioned_panel import SectionedTileLayout, TileSection
|
||||
import time
|
||||
|
||||
|
||||
class FrameCachedParams:
|
||||
def __init__(self):
|
||||
self._params = Params()
|
||||
self._cache = {}
|
||||
self._last_frame_time = 0.0
|
||||
|
||||
def _check_clear_cache(self):
|
||||
now = time.monotonic()
|
||||
if now != self._last_frame_time:
|
||||
self._cache.clear()
|
||||
self._last_frame_time = now
|
||||
self._cache = shared_ui_params()
|
||||
|
||||
def get(self, key, **kwargs):
|
||||
self._check_clear_cache()
|
||||
cache_key = (key, "get", tuple(kwargs.items()))
|
||||
if cache_key not in self._cache:
|
||||
self._cache[cache_key] = self._params.get(key, **kwargs)
|
||||
return self._cache[cache_key]
|
||||
return self._cache.get(key, **kwargs)
|
||||
|
||||
def get_bool(self, key, **kwargs):
|
||||
self._check_clear_cache()
|
||||
cache_key = (key, "get_bool", tuple(kwargs.items()))
|
||||
if cache_key not in self._cache:
|
||||
self._cache[cache_key] = self._params.get_bool(key, **kwargs)
|
||||
return self._cache[cache_key]
|
||||
return self._cache.get_bool(key, **kwargs)
|
||||
|
||||
def get_int(self, key, **kwargs):
|
||||
self._check_clear_cache()
|
||||
cache_key = (key, "get_int", tuple(kwargs.items()))
|
||||
if cache_key not in self._cache:
|
||||
self._cache[cache_key] = self._params.get_int(key, **kwargs)
|
||||
return self._cache[cache_key]
|
||||
return self._cache.get_int(key, **kwargs)
|
||||
|
||||
def get_float(self, key, **kwargs):
|
||||
self._check_clear_cache()
|
||||
cache_key = (key, "get_float", tuple(kwargs.items()))
|
||||
if cache_key not in self._cache:
|
||||
self._cache[cache_key] = self._params.get_float(key, **kwargs)
|
||||
return self._cache[cache_key]
|
||||
return self._cache.get_float(key, **kwargs)
|
||||
|
||||
def _notify_changed(self):
|
||||
self._cache.clear()
|
||||
self._cache.invalidate()
|
||||
update_starpilot_toggles()
|
||||
|
||||
def put(self, key, val, **kwargs):
|
||||
self._params.put(key, val, **kwargs)
|
||||
self._cache.put(key, val, **kwargs)
|
||||
self._notify_changed()
|
||||
|
||||
def put_bool(self, key, val, **kwargs):
|
||||
self._params.put_bool(key, val, **kwargs)
|
||||
self._cache.put_bool(key, val, **kwargs)
|
||||
self._notify_changed()
|
||||
|
||||
def put_int(self, key, val, **kwargs):
|
||||
self._params.put_int(key, val, **kwargs)
|
||||
self._cache.put_int(key, val, **kwargs)
|
||||
self._notify_changed()
|
||||
|
||||
def put_float(self, key, val, **kwargs):
|
||||
self._params.put_float(key, val, **kwargs)
|
||||
self._cache.put_float(key, val, **kwargs)
|
||||
self._notify_changed()
|
||||
|
||||
def remove(self, key):
|
||||
self._params.remove(key)
|
||||
self._cache.remove(key)
|
||||
self._notify_changed()
|
||||
|
||||
def __getattr__(self, name):
|
||||
return getattr(self._params, name)
|
||||
return getattr(self._cache, name)
|
||||
|
||||
|
||||
class StarPilotPanelType(IntEnum):
|
||||
|
||||
@@ -1,9 +1,10 @@
|
||||
from __future__ import annotations
|
||||
import math
|
||||
import pyray as rl
|
||||
from openpilot.system.ui.lib.application import gui_app
|
||||
|
||||
|
||||
def draw_custom_icon(key: str, x: float, y: float, s: float, color: rl.Color):
|
||||
def _draw_custom_icon_geometry(key: str, x: float, y: float, s: float, color: rl.Color):
|
||||
|
||||
# Helper for drawing quadratic Bezier curves
|
||||
def draw_bezier(p0: rl.Vector2, p1: rl.Vector2, p2: rl.Vector2, thick: float):
|
||||
@@ -425,4 +426,43 @@ def draw_custom_icon(key: str, x: float, y: float, s: float, color: rl.Color):
|
||||
rl.draw_line_ex(rl.Vector2(x_c - 6.5 * s, y_c + 2.5 * s), rl.Vector2(x_c + 6.5 * s, y_c + 2.5 * s), 3.5 * s, color)
|
||||
|
||||
|
||||
def draw_custom_icon(key: str, x: float, y: float, s: float, color: rl.Color):
|
||||
"""Draw a custom icon, caching its static vector geometry on the GPU."""
|
||||
cache = getattr(gui_app, "cached_render_texture", None)
|
||||
if hasattr(color, "r"):
|
||||
color_key = (color.r, color.g, color.b, color.a)
|
||||
else:
|
||||
components = tuple(color)
|
||||
color_key = (*components[:3], components[3] if len(components) > 3 else 255)
|
||||
if cache is None:
|
||||
_draw_custom_icon_geometry(key, x, y, s, color)
|
||||
return
|
||||
|
||||
# A few strokes extend beyond the authored 60x60 canvas. Padding prevents
|
||||
# those edges from being clipped by the render texture.
|
||||
padding = max(1, int(math.ceil(3.0 * s)))
|
||||
icon_size = max(1, int(round(60.0 * s)))
|
||||
width = icon_size + 2 * padding
|
||||
height = width
|
||||
cache_key = f"aethergrid-icon:{key}:{round(s, 4)}:{width}:{height}:{color_key}"
|
||||
texture = cache(cache_key, width, height,
|
||||
lambda: _draw_custom_icon_geometry(key, padding, padding, s, color))
|
||||
if texture is None:
|
||||
_draw_custom_icon_geometry(key, x, y, s, color)
|
||||
return
|
||||
|
||||
# Render targets contain premultiplied alpha after drawing onto transparent
|
||||
# black. Use the matching blend mode to avoid applying translucent icon
|
||||
# alpha twice when compositing the cached texture.
|
||||
rl.begin_blend_mode(rl.BlendMode.BLEND_ALPHA_PREMULTIPLY)
|
||||
try:
|
||||
rl.draw_texture_pro(
|
||||
texture,
|
||||
rl.Rectangle(0, 0, width, -height),
|
||||
rl.Rectangle(x - padding, y - padding, width, height),
|
||||
rl.Vector2(0, 0),
|
||||
0.0,
|
||||
rl.WHITE,
|
||||
)
|
||||
finally:
|
||||
rl.end_blend_mode()
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
from cereal import log
|
||||
from openpilot.common.params import Params, UnknownKeyName
|
||||
from openpilot.common.params import UnknownKeyName
|
||||
from openpilot.system.ui.widgets import Widget
|
||||
from openpilot.system.ui.widgets.list_view import multiple_button_item, toggle_item
|
||||
from openpilot.system.ui.widgets.scroller_tici import Scroller
|
||||
@@ -38,7 +38,7 @@ DESCRIPTIONS = {
|
||||
class TogglesLayout(Widget):
|
||||
def __init__(self):
|
||||
super().__init__()
|
||||
self._params = Params()
|
||||
self._params = ui_state.ui_params
|
||||
self._sync_rhd_toggle()
|
||||
|
||||
# param, title, desc, icon, needs_restart
|
||||
|
||||
@@ -0,0 +1,107 @@
|
||||
"""Small, shared cache for read-mostly UI parameters.
|
||||
|
||||
Parameter reads are file-backed. The BIG UI asks for the same values from
|
||||
multiple widgets during a frame, so a short cache avoids repeated open/read/
|
||||
close cycles without making settings changes sticky: every write invalidates
|
||||
the affected key immediately and the short TTL bounds visibility of writes
|
||||
from other processes.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import time
|
||||
from collections.abc import Callable
|
||||
from typing import Any
|
||||
|
||||
from openpilot.common.params import Params
|
||||
|
||||
|
||||
class UIParamCache:
|
||||
def __init__(self, params: Params | Any | None = None, ttl: float = 0.1,
|
||||
clock: Callable[[], float] = time.monotonic):
|
||||
self._params = params if params is not None else Params()
|
||||
self._ttl = max(0.0, ttl)
|
||||
self._clock = clock
|
||||
self._cache: dict[tuple[Any, ...], tuple[float, Any]] = {}
|
||||
|
||||
@staticmethod
|
||||
def _cache_key(method: str, key: str, args: tuple[Any, ...], kwargs: dict[str, Any]) -> tuple[Any, ...]:
|
||||
# Params arguments are primitive values in UI call sites. repr keeps this
|
||||
# robust for an occasional list/dict default without requiring hashability.
|
||||
return (method, key, repr(args), repr(sorted(kwargs.items())))
|
||||
|
||||
def _read(self, method: str, key: str, *args: Any, **kwargs: Any) -> Any:
|
||||
cache_key = self._cache_key(method, key, args, kwargs)
|
||||
now = self._clock()
|
||||
cached = self._cache.get(cache_key)
|
||||
if cached is not None and now - cached[0] < self._ttl:
|
||||
return cached[1]
|
||||
|
||||
value = getattr(self._params, method)(key, *args, **kwargs)
|
||||
self._cache[cache_key] = (now, value)
|
||||
return value
|
||||
|
||||
def get(self, key: str, *args: Any, **kwargs: Any) -> Any:
|
||||
return self._read("get", key, *args, **kwargs)
|
||||
|
||||
def get_bool(self, key: str, *args: Any, **kwargs: Any) -> bool:
|
||||
return self._read("get_bool", key, *args, **kwargs)
|
||||
|
||||
def get_int(self, key: str, *args: Any, **kwargs: Any) -> int:
|
||||
return self._read("get_int", key, *args, **kwargs)
|
||||
|
||||
def get_float(self, key: str, *args: Any, **kwargs: Any) -> float:
|
||||
return self._read("get_float", key, *args, **kwargs)
|
||||
|
||||
def invalidate(self, key: str | None = None) -> None:
|
||||
if key is None:
|
||||
self._cache.clear()
|
||||
return
|
||||
self._cache = {cache_key: value for cache_key, value in self._cache.items()
|
||||
if cache_key[1] != key}
|
||||
|
||||
def put(self, key: str, value: Any, *args: Any, **kwargs: Any) -> None:
|
||||
self._params.put(key, value, *args, **kwargs)
|
||||
self.invalidate(key)
|
||||
|
||||
def put_bool(self, key: str, value: bool, *args: Any, **kwargs: Any) -> None:
|
||||
self._params.put_bool(key, value, *args, **kwargs)
|
||||
self.invalidate(key)
|
||||
|
||||
def put_int(self, key: str, value: int, *args: Any, **kwargs: Any) -> None:
|
||||
self._params.put_int(key, value, *args, **kwargs)
|
||||
self.invalidate(key)
|
||||
|
||||
def put_float(self, key: str, value: float, *args: Any, **kwargs: Any) -> None:
|
||||
self._params.put_float(key, value, *args, **kwargs)
|
||||
self.invalidate(key)
|
||||
|
||||
def put_nonblocking(self, key: str, value: Any) -> None:
|
||||
self._params.put_nonblocking(key, value)
|
||||
self.invalidate(key)
|
||||
|
||||
def put_bool_nonblocking(self, key: str, value: bool) -> None:
|
||||
self._params.put_bool_nonblocking(key, value)
|
||||
self.invalidate(key)
|
||||
|
||||
def remove(self, key: str) -> None:
|
||||
self._params.remove(key)
|
||||
self.invalidate(key)
|
||||
|
||||
def clear_all(self, *args: Any, **kwargs: Any) -> None:
|
||||
self._params.clear_all(*args, **kwargs)
|
||||
self.invalidate()
|
||||
|
||||
def __getattr__(self, name: str) -> Any:
|
||||
return getattr(self._params, name)
|
||||
|
||||
|
||||
_SHARED_UI_PARAMS: UIParamCache | None = None
|
||||
|
||||
|
||||
def shared_ui_params() -> UIParamCache:
|
||||
"""Return the cache shared by BIG UI views and settings panels."""
|
||||
global _SHARED_UI_PARAMS
|
||||
if _SHARED_UI_PARAMS is None:
|
||||
_SHARED_UI_PARAMS = UIParamCache()
|
||||
return _SHARED_UI_PARAMS
|
||||
@@ -145,7 +145,7 @@ class AugmentedRoadView(CameraView):
|
||||
pass
|
||||
|
||||
def _get_border_width(self) -> int:
|
||||
return get_border_width(UI_BORDER_SIZE, ui_state.params)
|
||||
return get_border_width(UI_BORDER_SIZE, ui_state.ui_params)
|
||||
|
||||
def _draw_border(self, rect: rl.Rectangle):
|
||||
border_width = self._get_border_width()
|
||||
@@ -179,7 +179,8 @@ class AugmentedRoadView(CameraView):
|
||||
return self._is_in_reverse()
|
||||
|
||||
def _update_reverse_driver_camera_state(self) -> bool:
|
||||
should_force_driver = ui_state.started and ui_state.params.get_bool("DriverCamera") and self._is_in_reverse()
|
||||
params = ui_state.ui_params
|
||||
should_force_driver = ui_state.started and params.get_bool("DriverCamera") and self._is_in_reverse()
|
||||
if not should_force_driver:
|
||||
self._reverse_driver_camera_frames = 0
|
||||
self._reverse_driver_camera_active = False
|
||||
@@ -191,7 +192,8 @@ class AugmentedRoadView(CameraView):
|
||||
|
||||
@staticmethod
|
||||
def _camera_view() -> int:
|
||||
camera_view = ui_state.params.get_int("CameraView", return_default=True, default=CAMERA_VIEW_WIDE)
|
||||
params = ui_state.ui_params
|
||||
camera_view = params.get_int("CameraView", return_default=True, default=CAMERA_VIEW_WIDE)
|
||||
if camera_view not in (CAMERA_VIEW_AUTO, CAMERA_VIEW_DRIVER, CAMERA_VIEW_STANDARD, CAMERA_VIEW_WIDE, CAMERA_VIEW_NONE):
|
||||
return CAMERA_VIEW_WIDE
|
||||
return camera_view
|
||||
|
||||
@@ -15,11 +15,13 @@ class DriverCameraDialog(CameraView):
|
||||
self.driver_state_renderer = DriverStateRenderer()
|
||||
# TODO: this can grow unbounded, should be given some thought
|
||||
device.add_interactive_timeout_callback(gui_app.pop_widget)
|
||||
ui_state.params.put_bool("IsDriverViewEnabled", True)
|
||||
gui_app.set_render_mode(True)
|
||||
ui_state.ui_params.put_bool("IsDriverViewEnabled", True)
|
||||
|
||||
def hide_event(self):
|
||||
super().hide_event()
|
||||
ui_state.params.put_bool("IsDriverViewEnabled", False)
|
||||
gui_app.set_render_mode(ui_state.started)
|
||||
ui_state.ui_params.put_bool("IsDriverViewEnabled", False)
|
||||
self.close()
|
||||
|
||||
def _handle_mouse_release(self, _):
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
import time
|
||||
import pyray as rl
|
||||
from openpilot.common.params import Params
|
||||
from openpilot.selfdrive.ui.ui_state import ui_state
|
||||
from openpilot.system.ui.lib.application import gui_app
|
||||
from openpilot.system.ui.widgets import Widget
|
||||
@@ -15,7 +14,7 @@ from openpilot.starpilot.common.experimental_state import (
|
||||
class ExpButton(Widget):
|
||||
def __init__(self, button_size: int, icon_size: int):
|
||||
super().__init__()
|
||||
self._params = Params()
|
||||
self._params = ui_state.ui_params
|
||||
self._experimental_mode: bool = False
|
||||
self._engageable: bool = False
|
||||
|
||||
|
||||
@@ -4,7 +4,6 @@ import pyray as rl
|
||||
from cereal import messaging, car
|
||||
from dataclasses import dataclass, field
|
||||
from openpilot.common.filter_simple import FirstOrderFilter
|
||||
from openpilot.common.params import Params
|
||||
from openpilot.common.constants import CV
|
||||
from openpilot.selfdrive.locationd.calibrationd import HEIGHT_INIT
|
||||
from openpilot.selfdrive.ui.lib.starpilot_theme import get_param_color, get_theme_color, get_visual_color, is_stock_color_scheme, with_alpha
|
||||
@@ -88,7 +87,7 @@ class ModelRenderer(Widget):
|
||||
self._rainbow_path = RainbowPath()
|
||||
|
||||
# Get longitudinal control setting from car parameters
|
||||
self._params = Params()
|
||||
self._params = ui_state.ui_params
|
||||
if car_params := self._params.get("CarParams"):
|
||||
cp = messaging.log_from_bytes(car_params, car.CarParams)
|
||||
self._longitudinal_control = cp.openpilotLongitudinalControl
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
from openpilot.selfdrive.ui.ui_state import ui_state
|
||||
|
||||
def get_compass_text() -> str | None:
|
||||
if not ui_state.params.get_bool("Compass"):
|
||||
params = ui_state.ui_params
|
||||
if not params.get_bool("Compass"):
|
||||
return None
|
||||
|
||||
# Retrieve bearing
|
||||
|
||||
@@ -3,7 +3,6 @@ import time
|
||||
import re
|
||||
import json
|
||||
from cereal import car
|
||||
from openpilot.common.params import Params
|
||||
from openpilot.selfdrive.ui.ui_state import ui_state
|
||||
from openpilot.system.ui.lib.application import gui_app, FontWeight, FONT_SCALE
|
||||
from openpilot.system.ui.lib.text_measure import measure_text_cached
|
||||
@@ -64,7 +63,7 @@ def _setting_changed(value: float, reference: float) -> bool:
|
||||
|
||||
class DeveloperSidebar:
|
||||
def __init__(self):
|
||||
self._params = Params()
|
||||
self._params = ui_state.ui_params
|
||||
self._font_bold = gui_app.font(FontWeight.SEMI_BOLD)
|
||||
self._last_toggles_check = 0.0
|
||||
self._cached_metrics = [0] * 7
|
||||
|
||||
@@ -100,8 +100,9 @@ class NavigationCardRenderer(Widget):
|
||||
self._collapsed = new_state
|
||||
|
||||
def _cancel_navigation(self) -> None:
|
||||
params = ui_state.ui_params
|
||||
try:
|
||||
ui_state.params.remove("NavDestination")
|
||||
params.remove("NavDestination")
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
@@ -162,14 +163,15 @@ class NavigationCardRenderer(Widget):
|
||||
return self._icons[icon_name]
|
||||
|
||||
def _update_state(self) -> None:
|
||||
self._enabled = ui_state.params.get_bool("NavigationUI")
|
||||
params = ui_state.ui_params
|
||||
self._enabled = params.get_bool("NavigationUI")
|
||||
self._valid = False
|
||||
self._interactive_rect = rl.Rectangle(0, 0, 0, 0)
|
||||
|
||||
if not self._enabled:
|
||||
return
|
||||
|
||||
if not (ui_state.params.get("NavDestination") or ""):
|
||||
if not (params.get("NavDestination") or ""):
|
||||
return
|
||||
|
||||
raw_state = ui_state.params_memory.get("NavInstructionState") or {}
|
||||
|
||||
@@ -6,7 +6,7 @@ _RADIUS = 36
|
||||
_FONT_SIZE = 36
|
||||
|
||||
def render_pedal_icons(start_x: float, start_y: float, font):
|
||||
params = ui_state.params
|
||||
params = ui_state.ui_params
|
||||
if not params.get_bool("PedalsOnUI"):
|
||||
return
|
||||
|
||||
|
||||
@@ -123,8 +123,9 @@ def _get_slc_state():
|
||||
plan = sm["starpilotPlan"]
|
||||
speed_limit_changed = plan.speedLimitChanged
|
||||
|
||||
show_slc = ui_state.params.get_bool("ShowSpeedLimits") or ui_state.params.get_bool("SpeedLimitController")
|
||||
hide_sl = ui_state.params.get_bool("HideSpeedLimit")
|
||||
params = ui_state.ui_params
|
||||
show_slc = params.get_bool("ShowSpeedLimits") or params.get_bool("SpeedLimitController")
|
||||
hide_sl = params.get_bool("HideSpeedLimit")
|
||||
unconfirmed_valid = plan.unconfirmedSlcSpeedLimit > 1
|
||||
# A pending (unconfirmed) limit overrides HideSpeedLimit so the prompt always shows.
|
||||
hide = not (speed_limit_changed and unconfirmed_valid) and hide_sl
|
||||
@@ -134,10 +135,10 @@ def _get_slc_state():
|
||||
return None
|
||||
|
||||
speed_conversion = CV.MS_TO_KPH if ui_state.is_metric else CV.MS_TO_MPH
|
||||
show_offset = ui_state.params.get_bool("ShowSLCOffset")
|
||||
show_offset = params.get_bool("ShowSLCOffset")
|
||||
|
||||
dashboard_sl = sm["starpilotCarState"].dashboardSpeedLimit if sm.valid.get("starpilotCarState", False) else 0.0
|
||||
vision_sl = ui_state.params_memory.get_float("VisionSpeedLimit") if ui_state.params.get_bool("VisionSpeedLimitDetection") else 0.0
|
||||
vision_sl = ui_state.params_memory.get_float("VisionSpeedLimit") if params.get_bool("VisionSpeedLimitDetection") else 0.0
|
||||
|
||||
slc_overridden_speed = plan.slcOverriddenSpeed
|
||||
# Driver override takes precedence over the planner's limit when active.
|
||||
@@ -171,7 +172,7 @@ def _get_slc_state():
|
||||
'speed_limit_changed': speed_limit_changed,
|
||||
'hide': hide,
|
||||
'show_offset': show_offset,
|
||||
'use_vienna': ui_state.params.get_bool("UseVienna"),
|
||||
'use_vienna': params.get_bool("UseVienna"),
|
||||
'offset_str': offset_str,
|
||||
'speed_conversion': speed_conversion,
|
||||
'speed_unit': " km/h" if ui_state.is_metric else " mph",
|
||||
@@ -498,4 +499,3 @@ def render_speed_limit_at(state: dict, rect: rl.Rectangle, expanded: bool = Fals
|
||||
_draw_sources_bubble(state, pill_rect, visual_rect)
|
||||
|
||||
return pill_rect
|
||||
|
||||
|
||||
@@ -56,7 +56,8 @@ def _csc_state():
|
||||
return None
|
||||
|
||||
plan = sm["starpilotPlan"]
|
||||
if plan.speedLimitChanged or not ui_state.params.get_bool("ShowCSCStatus"):
|
||||
params = ui_state.ui_params
|
||||
if plan.speedLimitChanged or not params.get_bool("ShowCSCStatus"):
|
||||
return None
|
||||
|
||||
car_state = sm["carState"]
|
||||
@@ -176,8 +177,9 @@ def render_background_effects(rect: rl.Rectangle, border_width: float):
|
||||
# 1. Turn Signal and Blind Spot indicators
|
||||
car_state = sm["carState"] if sm.valid.get("carState", False) else None
|
||||
if car_state:
|
||||
show_signal = ui_state.params.get_bool("SignalMetrics")
|
||||
show_blindspot = ui_state.params.get_bool("BlindSpotMetrics")
|
||||
params = ui_state.ui_params
|
||||
show_signal = params.get_bool("SignalMetrics")
|
||||
show_blindspot = params.get_bool("BlindSpotMetrics")
|
||||
if show_signal or show_blindspot:
|
||||
left_blindspot = car_state.leftBlindspot
|
||||
right_blindspot = car_state.rightBlindspot
|
||||
@@ -221,7 +223,7 @@ def render_background_effects(rect: rl.Rectangle, border_width: float):
|
||||
# 2. Steering Torque Border
|
||||
car_control = sm["carControl"] if sm.valid.get("carControl", False) else None
|
||||
if car_control:
|
||||
show_steering = ui_state.params.get_bool("ShowSteering")
|
||||
show_steering = ui_state.ui_params.get_bool("ShowSteering")
|
||||
if show_steering:
|
||||
torque = -car_control.actuators.torque
|
||||
abs_torque = abs(torque)
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
import pyray as rl
|
||||
import time
|
||||
from msgq.visionipc import VisionStreamType
|
||||
from openpilot.common.params import Params
|
||||
from openpilot.selfdrive.ui.onroad.augmented_road_view import AugmentedRoadView
|
||||
from openpilot.selfdrive.ui.onroad.starpilot.starpilot_border import render_behind, render_overlay, render_background_effects
|
||||
from openpilot.selfdrive.ui.onroad.starpilot.path import render_adjacent_lanes, render_path_edges
|
||||
@@ -31,7 +30,7 @@ AlertSize = log.SelfdriveState.AlertSize
|
||||
class StarPilotOnroadView(AugmentedRoadView):
|
||||
def __init__(self, stream_type: VisionStreamType = VisionStreamType.VISION_STREAM_ROAD):
|
||||
super().__init__(stream_type)
|
||||
self._params = Params()
|
||||
self._params = ui_state.ui_params
|
||||
|
||||
self._font_bold = gui_app.font(FontWeight.BOLD)
|
||||
self._font_medium = gui_app.font(FontWeight.MEDIUM)
|
||||
|
||||
@@ -14,7 +14,7 @@ def _draw_poly_outline(cx: float, cy: float, sides: int, radius: float, rotation
|
||||
rl.draw_line_ex(p1, p2, thickness, color)
|
||||
|
||||
def render_stopping_point(renderer, font):
|
||||
params = ui_state.params
|
||||
params = ui_state.ui_params
|
||||
if not params.get_bool("ShowStoppingPoint"):
|
||||
return
|
||||
|
||||
|
||||
@@ -12,7 +12,7 @@ class PedalIconsWidget(LayoutWidget):
|
||||
|
||||
@property
|
||||
def is_visible(self) -> bool:
|
||||
params = ui_state.params
|
||||
params = ui_state.ui_params
|
||||
if not params.get_bool("PedalsOnUI"):
|
||||
return False
|
||||
# Only render when car state is valid
|
||||
|
||||
@@ -12,7 +12,8 @@ class PersonalityButtonWidget(LayoutWidget):
|
||||
|
||||
@property
|
||||
def is_visible(self) -> bool:
|
||||
toggle_on = ui_state.params.get_bool("OnroadDistanceButton")
|
||||
params = ui_state.ui_params
|
||||
toggle_on = params.get_bool("OnroadDistanceButton")
|
||||
return bool(toggle_on and ui_state.started and ui_state.has_longitudinal_control)
|
||||
|
||||
def get_size(self) -> tuple[float, float]:
|
||||
|
||||
@@ -47,7 +47,8 @@ class SpeedLimitWidget(LayoutWidget):
|
||||
def _render(self, rect: rl.Rectangle) -> None:
|
||||
if self._slc_state is None:
|
||||
return
|
||||
expanded = ui_state.params.get_bool("SpeedLimitSources")
|
||||
params = ui_state.ui_params
|
||||
expanded = params.get_bool("SpeedLimitSources")
|
||||
self._pill_rect = render_speed_limit_at(self._slc_state, rect, expanded)
|
||||
|
||||
def _handle_mouse_press(self, mouse_pos) -> None:
|
||||
@@ -59,8 +60,9 @@ class SpeedLimitWidget(LayoutWidget):
|
||||
self._pill_rect.height + 40
|
||||
)
|
||||
if rl.check_collision_point_rec(mouse_pos, hit_rect):
|
||||
current = ui_state.params.get_bool("SpeedLimitSources")
|
||||
Params().put_bool("SpeedLimitSources", not current)
|
||||
params = ui_state.ui_params
|
||||
current = params.get_bool("SpeedLimitSources")
|
||||
params.put_bool("SpeedLimitSources", not current)
|
||||
return
|
||||
|
||||
state = _get_slc_state()
|
||||
|
||||
@@ -2,7 +2,7 @@ import importlib
|
||||
import sys
|
||||
import types
|
||||
import unittest
|
||||
from unittest.mock import MagicMock
|
||||
from unittest.mock import MagicMock, patch
|
||||
|
||||
|
||||
MODULE_NAME = "openpilot.selfdrive.ui.layouts.settings.starpilot.aethergrid"
|
||||
@@ -32,6 +32,7 @@ def _install_aethergrid_stubs():
|
||||
Texture2D=type("Texture2D", (), {}),
|
||||
Font=type("Font", (), {}),
|
||||
GuiTextAlignment=types.SimpleNamespace(TEXT_ALIGN_CENTER=0),
|
||||
BlendMode=types.SimpleNamespace(BLEND_ALPHA_PREMULTIPLY=1),
|
||||
WHITE=types.SimpleNamespace(r=255, g=255, b=255, a=255),
|
||||
draw_rectangle_rounded=lambda *a, **k: None,
|
||||
draw_rectangle_rounded_lines_ex=lambda *a, **k: None,
|
||||
@@ -44,6 +45,8 @@ def _install_aethergrid_stubs():
|
||||
draw_line_ex=lambda *a, **k: None,
|
||||
draw_triangle=lambda *a, **k: None,
|
||||
draw_texture_pro=lambda *a, **k: None,
|
||||
begin_blend_mode=lambda *a, **k: None,
|
||||
end_blend_mode=lambda *a, **k: None,
|
||||
draw_text_ex=lambda *a, **k: None,
|
||||
check_collision_point_rec=lambda p, r: (r.x <= p.x <= r.x + r.width) and (r.y <= p.y <= r.y + r.height),
|
||||
get_frame_time=lambda: 0.016,
|
||||
@@ -153,10 +156,13 @@ def _install_panel_stubs(aethergrid):
|
||||
sectioned_mod = types.ModuleType(SECTIONED_PANEL_MODULE_NAME)
|
||||
sectioned_mod.SectionedTileLayout = type("SectionedTileLayout", (), {})
|
||||
sectioned_mod.TileSection = type("TileSection", (), {})
|
||||
starpilot_variables_mod = types.ModuleType("openpilot.starpilot.common.starpilot_variables")
|
||||
starpilot_variables_mod.update_starpilot_toggles = lambda: None
|
||||
|
||||
_register_modules({
|
||||
SECTIONED_PANEL_MODULE_NAME: sectioned_mod,
|
||||
"openpilot.common.params": types.SimpleNamespace(Params=type("Params", (), {}), UnknownKeyName=Exception),
|
||||
"openpilot.starpilot.common.starpilot_variables": starpilot_variables_mod,
|
||||
MODULE_NAME: aethergrid,
|
||||
})
|
||||
|
||||
@@ -251,6 +257,58 @@ class TestAethergridContracts(unittest.TestCase):
|
||||
|
||||
self.assertEqual(tile.get_status(), "Download 50%")
|
||||
|
||||
def test_custom_icon_draws_directly_while_cache_fill_is_pending(self):
|
||||
mod = _import_aethergrid()
|
||||
scribble = sys.modules["openpilot.selfdrive.ui.layouts.settings.starpilot.scribble"]
|
||||
geometry = MagicMock()
|
||||
cache = MagicMock(return_value=None)
|
||||
|
||||
color = mod.rl.Color(255, 255, 255, 255)
|
||||
with patch.object(scribble, "_draw_custom_icon_geometry", geometry), \
|
||||
patch.object(scribble.gui_app, "cached_render_texture", cache, create=True):
|
||||
scribble.draw_custom_icon("sound", 10, 20, 1.0, color)
|
||||
|
||||
cache.assert_called_once()
|
||||
geometry.assert_called_once_with("sound", 10, 20, 1.0, color)
|
||||
|
||||
def test_custom_icon_uses_completed_cache_without_redrawing_geometry(self):
|
||||
mod = _import_aethergrid()
|
||||
scribble = sys.modules["openpilot.selfdrive.ui.layouts.settings.starpilot.scribble"]
|
||||
geometry = MagicMock()
|
||||
cache = MagicMock(return_value=object())
|
||||
draw_texture = MagicMock()
|
||||
|
||||
with patch.object(scribble, "_draw_custom_icon_geometry", geometry), \
|
||||
patch.object(scribble.gui_app, "cached_render_texture", cache, create=True), \
|
||||
patch.object(scribble.rl, "draw_texture_pro", draw_texture):
|
||||
scribble.draw_custom_icon("sound", 10, 20, 1.0, mod.rl.Color(255, 255, 255, 255))
|
||||
|
||||
geometry.assert_not_called()
|
||||
draw_texture.assert_called_once()
|
||||
destination = draw_texture.call_args.args[2]
|
||||
self.assertLess(destination.x, 10)
|
||||
self.assertLess(destination.y, 20)
|
||||
|
||||
def test_translucent_custom_icon_uses_premultiplied_blending(self):
|
||||
mod = _import_aethergrid()
|
||||
scribble = sys.modules["openpilot.selfdrive.ui.layouts.settings.starpilot.scribble"]
|
||||
geometry = MagicMock()
|
||||
cache = MagicMock(return_value=object())
|
||||
begin_blend = MagicMock()
|
||||
end_blend = MagicMock()
|
||||
|
||||
color = mod.rl.Color(160, 170, 185, 80)
|
||||
with patch.object(scribble, "_draw_custom_icon_geometry", geometry), \
|
||||
patch.object(scribble.gui_app, "cached_render_texture", cache, create=True), \
|
||||
patch.object(scribble.rl, "begin_blend_mode", begin_blend), \
|
||||
patch.object(scribble.rl, "end_blend_mode", end_blend):
|
||||
scribble.draw_custom_icon("first_aid", 10, 20, 0.8, color)
|
||||
|
||||
cache.assert_called_once()
|
||||
geometry.assert_not_called()
|
||||
begin_blend.assert_called_once_with(mod.rl.BlendMode.BLEND_ALPHA_PREMULTIPLY)
|
||||
end_blend.assert_called_once()
|
||||
|
||||
def test_tile_grid_reflows_to_wider_tiles_when_width_is_tight(self):
|
||||
mod = _import_aethergrid()
|
||||
grid = mod.TileGrid(columns=None, padding=mod.SPACING.tile_gap)
|
||||
|
||||
@@ -0,0 +1,121 @@
|
||||
import unittest
|
||||
|
||||
from openpilot.selfdrive.ui.lib.ui_param_cache import UIParamCache
|
||||
|
||||
|
||||
class FakeParams:
|
||||
def __init__(self):
|
||||
self.values = {"enabled": False, "count": 1}
|
||||
self.calls = []
|
||||
|
||||
def get(self, key, **_kwargs):
|
||||
self.calls.append(("get", key))
|
||||
return self.values.get(key)
|
||||
|
||||
def get_bool(self, key, **_kwargs):
|
||||
self.calls.append(("get_bool", key))
|
||||
return bool(self.values.get(key, False))
|
||||
|
||||
def get_int(self, key, **_kwargs):
|
||||
self.calls.append(("get_int", key))
|
||||
return int(self.values.get(key, 0))
|
||||
|
||||
def get_float(self, key, **_kwargs):
|
||||
self.calls.append(("get_float", key))
|
||||
return float(self.values.get(key, 0.0))
|
||||
|
||||
def put(self, key, value, **_kwargs):
|
||||
self.values[key] = value
|
||||
|
||||
def put_bool(self, key, value, **_kwargs):
|
||||
self.values[key] = value
|
||||
|
||||
def put_int(self, key, value, **_kwargs):
|
||||
self.values[key] = value
|
||||
|
||||
def put_float(self, key, value, **_kwargs):
|
||||
self.values[key] = value
|
||||
|
||||
def put_nonblocking(self, key, value):
|
||||
self.values[key] = value
|
||||
|
||||
def put_bool_nonblocking(self, key, value):
|
||||
self.values[key] = value
|
||||
|
||||
def remove(self, key):
|
||||
self.values.pop(key, None)
|
||||
|
||||
def clear_all(self, marker=None):
|
||||
self.calls.append(("clear_all", marker))
|
||||
self.values.clear()
|
||||
|
||||
|
||||
class TestUIParamCache(unittest.TestCase):
|
||||
def test_reads_are_shared_until_ttl(self):
|
||||
now = [0.0]
|
||||
params = FakeParams()
|
||||
cached = UIParamCache(params, ttl=0.1, clock=lambda: now[0])
|
||||
|
||||
self.assertFalse(cached.get_bool("enabled"))
|
||||
self.assertFalse(cached.get_bool("enabled"))
|
||||
self.assertEqual(params.calls, [("get_bool", "enabled")])
|
||||
|
||||
now[0] = 0.11
|
||||
params.values["enabled"] = True
|
||||
self.assertTrue(cached.get_bool("enabled"))
|
||||
self.assertEqual(params.calls.count(("get_bool", "enabled")), 2)
|
||||
|
||||
def test_writes_invalidate_immediately(self):
|
||||
params = FakeParams()
|
||||
cached = UIParamCache(params, ttl=10.0)
|
||||
|
||||
self.assertFalse(cached.get_bool("enabled"))
|
||||
cached.put_bool("enabled", True)
|
||||
self.assertTrue(cached.get_bool("enabled"))
|
||||
self.assertEqual(params.calls.count(("get_bool", "enabled")), 2)
|
||||
|
||||
def test_zero_ttl_disables_caching(self):
|
||||
params = FakeParams()
|
||||
cached = UIParamCache(params, ttl=0.0)
|
||||
|
||||
self.assertFalse(cached.get_bool("enabled"))
|
||||
params.values["enabled"] = True
|
||||
self.assertTrue(cached.get_bool("enabled"))
|
||||
self.assertEqual(params.calls.count(("get_bool", "enabled")), 2)
|
||||
|
||||
def test_all_write_paths_invalidate(self):
|
||||
operations = (
|
||||
lambda cache: cache.put("enabled", True),
|
||||
lambda cache: cache.put_bool("enabled", True),
|
||||
lambda cache: cache.put_int("enabled", 1),
|
||||
lambda cache: cache.put_float("enabled", 1.0),
|
||||
lambda cache: cache.put_nonblocking("enabled", True),
|
||||
lambda cache: cache.put_bool_nonblocking("enabled", True),
|
||||
)
|
||||
for operation in operations:
|
||||
with self.subTest(operation=operation):
|
||||
params = FakeParams()
|
||||
cached = UIParamCache(params, ttl=10.0)
|
||||
self.assertFalse(cached.get_bool("enabled"))
|
||||
operation(cached)
|
||||
self.assertTrue(cached.get_bool("enabled"))
|
||||
self.assertEqual(params.calls.count(("get_bool", "enabled")), 2)
|
||||
|
||||
def test_remove_and_clear_all_invalidate(self):
|
||||
params = FakeParams()
|
||||
cached = UIParamCache(params, ttl=10.0)
|
||||
|
||||
self.assertTrue(cached.get_int("count"))
|
||||
cached.remove("count")
|
||||
self.assertEqual(cached.get_int("count"), 0)
|
||||
|
||||
self.assertFalse(cached.get_bool("enabled"))
|
||||
params.values["enabled"] = True
|
||||
cached.clear_all("flag")
|
||||
params.values["enabled"] = True
|
||||
self.assertTrue(cached.get_bool("enabled"))
|
||||
self.assertIn(("clear_all", "flag"), params.calls)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
@@ -10,6 +10,7 @@ from openpilot.common.filter_simple import FirstOrderFilter
|
||||
from openpilot.common.params import Params
|
||||
from openpilot.common.swaglog import cloudlog
|
||||
from openpilot.selfdrive.ui.lib.prime_state import PrimeState
|
||||
from openpilot.selfdrive.ui.lib.ui_param_cache import shared_ui_params
|
||||
from openpilot.system.hardware.usb import chestnut_present
|
||||
from openpilot.system.ui.lib.application import gui_app
|
||||
from openpilot.system.hardware import HARDWARE, PC
|
||||
@@ -35,6 +36,9 @@ class UIState:
|
||||
|
||||
def _initialize(self):
|
||||
self.params = Params()
|
||||
# BIG UI views use this read-through cache; keep ``params`` untouched for
|
||||
# MICI and for non-rendering callers that rely on its exact semantics.
|
||||
self.ui_params = shared_ui_params() if gui_app.big_ui() else self.params
|
||||
self.params_memory = Params(memory=True)
|
||||
self.sm = messaging.SubMaster(
|
||||
[
|
||||
@@ -176,24 +180,26 @@ class UIState:
|
||||
self.light_sensor = -1
|
||||
|
||||
# Trust hardwared's filtered started state; raw ignition can flap on Toyota.
|
||||
force_onroad = self.params.get_bool("ForceOnroad")
|
||||
force_offroad = self.params.get_bool("ForceOffroad")
|
||||
# Use the BIG-UI cache here as this path runs once per render iteration.
|
||||
params = self.ui_params
|
||||
force_onroad = params.get_bool("ForceOnroad")
|
||||
force_offroad = params.get_bool("ForceOffroad")
|
||||
started = self.sm["deviceState"].started
|
||||
started |= force_onroad
|
||||
started &= not force_offroad
|
||||
self.started = started
|
||||
|
||||
# Update recording audio state
|
||||
self.recording_audio = self.params.get_bool("RecordAudio") and self.started
|
||||
self.recording_audio = params.get_bool("RecordAudio") and self.started
|
||||
|
||||
self.is_metric = self.params.get_bool("IsMetric")
|
||||
self.always_on_dm = self.params.get_bool("AlwaysOnDM")
|
||||
self.is_metric = params.get_bool("IsMetric")
|
||||
self.always_on_dm = params.get_bool("AlwaysOnDM")
|
||||
now = time.monotonic()
|
||||
if now - self._usbgpu_update_time >= USBGPU_POLL_INTERVAL:
|
||||
self.usbgpu = chestnut_present()
|
||||
self._usbgpu_update_time = now
|
||||
self.usbgpu_compiled = self.params.get_bool("UsbGpuCompiled")
|
||||
self.usbgpu_active = self.params.get_bool("UsbGpuActive")
|
||||
self.usbgpu_compiled = params.get_bool("UsbGpuCompiled")
|
||||
self.usbgpu_active = params.get_bool("UsbGpuActive")
|
||||
self.switchback_mode_enabled = self.params_memory.get_bool("SwitchbackModeEnabled") if self.started else False
|
||||
if self.sm.valid.get("starpilotCarState", False):
|
||||
starpilot_car_state = self.sm["starpilotCarState"]
|
||||
@@ -269,7 +275,7 @@ class Device:
|
||||
self._interactive_timeout_callbacks: list[Callable] = []
|
||||
self._prev_timed_out = False
|
||||
self._awake: bool = True
|
||||
self._params = Params()
|
||||
self._params = ui_state.ui_params
|
||||
|
||||
self._offroad_brightness: int = BACKLIGHT_OFFROAD
|
||||
self._last_brightness: int = 0
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
import pyray as rl
|
||||
from openpilot.common.params import Params
|
||||
from openpilot.system.ui.lib.application import gui_app, FontWeight, FONT_SCALE
|
||||
from openpilot.system.ui.lib.multilang import tr
|
||||
from openpilot.system.ui.lib.text_measure import measure_text_cached
|
||||
@@ -17,7 +16,7 @@ class ExperimentalModeButton(Widget):
|
||||
self.horizontal_padding = 25
|
||||
self.button_height = 125
|
||||
|
||||
self.params = Params()
|
||||
self.params = ui_state.ui_params
|
||||
self.experimental_mode = requested_experimental_mode(self.params, ui_state.params_memory)
|
||||
self.mode_variant = get_mode_banner_variant(self.params, ui_state.params_memory)
|
||||
|
||||
|
||||
@@ -30,6 +30,8 @@ FPS_CRITICAL_THRESHOLD = 0.5 # Critical threshold for triggering strict actions
|
||||
MOUSE_THREAD_RATE = 140 # touch controller runs at 140Hz
|
||||
DESKTOP_MOUSE_THREAD_RATE = int(os.getenv("DESKTOP_MOUSE_RATE", "500"))
|
||||
DESKTOP_CLICK_DEBOUNCE = float(os.getenv("DESKTOP_CLICK_DEBOUNCE", "0.2"))
|
||||
UI_IDLE_FPS = int(os.getenv("UI_IDLE_FPS", "0"))
|
||||
UI_INTERACTION_FPS_DURATION = 1.25
|
||||
MAX_TOUCH_SLOTS = 2
|
||||
TOUCH_HISTORY_TIMEOUT = 3.0 # Seconds before touch points fade out
|
||||
|
||||
@@ -496,7 +498,14 @@ class GuiApplication:
|
||||
self._ffmpeg_stop_event: threading.Event | None = None
|
||||
self._progress_hook: Callable[[str], None] | None = None
|
||||
self._textures: dict[str, rl.Texture] = {}
|
||||
self._cached_render_textures: dict[str, rl.RenderTexture] = {}
|
||||
self._pending_render_textures: dict[str, tuple[int, int, Callable[[], None]]] = {}
|
||||
self._target_fps: int = _DEFAULT_FPS
|
||||
self._full_target_fps: int = _DEFAULT_FPS
|
||||
self._idle_target_fps: int = max(10, _DEFAULT_FPS // 4)
|
||||
self._adaptive_rendering = False
|
||||
self._full_rate_rendering = False
|
||||
self._high_fps_until = 0.0
|
||||
self._last_fps_log_time: float = time.monotonic()
|
||||
self._burn_in_start_time = time.monotonic()
|
||||
self._frame = 0
|
||||
@@ -538,6 +547,50 @@ class GuiApplication:
|
||||
def target_fps(self):
|
||||
return self._target_fps
|
||||
|
||||
def _set_target_fps(self, fps: int) -> None:
|
||||
fps = max(1, int(fps))
|
||||
if fps == self._target_fps:
|
||||
return
|
||||
rl.set_target_fps(0 if OFFSCREEN else fps)
|
||||
self._target_fps = fps
|
||||
|
||||
def configure_adaptive_rendering(self, enabled: bool, idle_fps: int | None = None) -> None:
|
||||
"""Enable low-rate rendering for static BIG-UI offroad screens.
|
||||
|
||||
The normal target remains unchanged unless a caller opts in. This keeps
|
||||
MICI and all existing non-BIG layouts on their current scheduling path.
|
||||
"""
|
||||
# Recording feeds raw frames to ffmpeg at the fixed full FPS, so changing
|
||||
# the producer rate would make idle portions play back too quickly.
|
||||
self._adaptive_rendering = bool(enabled and not OFFSCREEN and not RECORD)
|
||||
if idle_fps is None or idle_fps <= 0:
|
||||
idle_fps = UI_IDLE_FPS if UI_IDLE_FPS > 0 else max(10, self._full_target_fps // 4)
|
||||
self._idle_target_fps = min(self._full_target_fps, max(1, int(idle_fps)))
|
||||
self._full_rate_rendering = False
|
||||
self._high_fps_until = time.monotonic() + UI_INTERACTION_FPS_DURATION if self._adaptive_rendering else 0.0
|
||||
if self._adaptive_rendering:
|
||||
self._apply_render_mode()
|
||||
else:
|
||||
self._set_target_fps(self._full_target_fps)
|
||||
|
||||
def request_high_fps(self, duration: float = UI_INTERACTION_FPS_DURATION) -> None:
|
||||
if not self._adaptive_rendering:
|
||||
return
|
||||
self._high_fps_until = max(self._high_fps_until, time.monotonic() + max(0.0, duration))
|
||||
self._apply_render_mode()
|
||||
|
||||
def set_render_mode(self, active: bool) -> None:
|
||||
if not self._adaptive_rendering:
|
||||
return
|
||||
self._full_rate_rendering = active
|
||||
self._apply_render_mode()
|
||||
|
||||
def _apply_render_mode(self) -> None:
|
||||
if not self._adaptive_rendering:
|
||||
return
|
||||
high_rate = self._full_rate_rendering or time.monotonic() < self._high_fps_until
|
||||
self._set_target_fps(self._full_target_fps if high_rate else self._idle_target_fps)
|
||||
|
||||
def request_close(self):
|
||||
self._window_close_requested = True
|
||||
|
||||
@@ -607,6 +660,7 @@ class GuiApplication:
|
||||
# OFFSCREEN disables FPS limiting for fast offline rendering (e.g. clips)
|
||||
rl.set_target_fps(0 if OFFSCREEN else fps)
|
||||
|
||||
self._full_target_fps = fps
|
||||
self._target_fps = fps
|
||||
self._set_styles()
|
||||
self._load_fonts()
|
||||
@@ -681,6 +735,7 @@ class GuiApplication:
|
||||
prev_widget.set_enabled(False)
|
||||
|
||||
self._nav_stack.append(widget)
|
||||
self.request_high_fps()
|
||||
widget.show_event()
|
||||
widget.set_enabled(True)
|
||||
|
||||
@@ -702,6 +757,7 @@ class GuiApplication:
|
||||
|
||||
widget = self._nav_stack.pop(idx_to_pop)
|
||||
widget.hide_event()
|
||||
self.request_high_fps()
|
||||
|
||||
def pop_widgets_to(self, widget: object, callback: Callable[[], None] | None = None, instant: bool = False):
|
||||
# Pops middle widgets instantly without animation then dismisses top, animated out if NavWidget
|
||||
@@ -750,6 +806,8 @@ class GuiApplication:
|
||||
|
||||
def set_should_render(self, should_render: bool):
|
||||
self._should_render = should_render
|
||||
if should_render:
|
||||
self.request_high_fps()
|
||||
|
||||
def texture(self, asset_path: str, width: int | None = None, height: int | None = None,
|
||||
alpha_premultiply=False, keep_aspect_ratio=True, flip_x: bool = False) -> rl.Texture:
|
||||
@@ -774,6 +832,62 @@ class GuiApplication:
|
||||
self._textures[cache_key] = texture_obj
|
||||
return texture_obj
|
||||
|
||||
def cached_render_texture(self, cache_key: str, width: int, height: int,
|
||||
render: Callable[[], None]) -> object | None:
|
||||
"""Return a cached texture, scheduling cache misses between frames.
|
||||
|
||||
Raylib render-texture modes are not nestable. Widgets call this while the
|
||||
main framebuffer (often another render texture) is active, so cache misses
|
||||
must be populated after the frame has been presented.
|
||||
"""
|
||||
cached = self._cached_render_textures.get(cache_key)
|
||||
if cached is not None:
|
||||
return cached.texture
|
||||
|
||||
self._pending_render_textures.setdefault(
|
||||
cache_key, (max(1, int(width)), max(1, int(height)), render)
|
||||
)
|
||||
return None
|
||||
|
||||
def _populate_render_texture_cache(self) -> None:
|
||||
pending = self._pending_render_textures
|
||||
self._pending_render_textures = {}
|
||||
for cache_key, (width, height, render) in pending.items():
|
||||
if cache_key in self._cached_render_textures:
|
||||
continue
|
||||
|
||||
cached = rl.load_render_texture(max(1, int(width)), max(1, int(height)))
|
||||
began_texture_mode = False
|
||||
began_blend_mode = False
|
||||
try:
|
||||
rl.begin_texture_mode(cached)
|
||||
began_texture_mode = True
|
||||
rl.clear_background(rl.Color(0, 0, 0, 0))
|
||||
# Preserve straight alpha while RGB is accumulated premultiplied. The
|
||||
# resulting texture can then be composited with BLEND_ALPHA_PREMULTIPLY
|
||||
# without squaring translucent vector alpha.
|
||||
rl.rl_set_blend_factors_separate(
|
||||
rl.RL_SRC_ALPHA, rl.RL_ONE_MINUS_SRC_ALPHA,
|
||||
rl.RL_ONE, rl.RL_ONE_MINUS_SRC_ALPHA,
|
||||
rl.RL_FUNC_ADD, rl.RL_FUNC_ADD,
|
||||
)
|
||||
rl.begin_blend_mode(rl.BlendMode.BLEND_CUSTOM_SEPARATE)
|
||||
began_blend_mode = True
|
||||
render()
|
||||
except Exception:
|
||||
if began_blend_mode:
|
||||
rl.end_blend_mode()
|
||||
if began_texture_mode:
|
||||
rl.end_texture_mode()
|
||||
rl.unload_render_texture(cached)
|
||||
raise
|
||||
else:
|
||||
rl.end_blend_mode()
|
||||
rl.end_texture_mode()
|
||||
rl.set_texture_filter(cached.texture, rl.TextureFilter.TEXTURE_FILTER_BILINEAR)
|
||||
rl.set_texture_wrap(cached.texture, rl.TextureWrap.TEXTURE_WRAP_CLAMP)
|
||||
self._cached_render_textures[cache_key] = cached
|
||||
|
||||
def _load_image_from_path(self, image_path: str, width: int | None = None, height: int | None = None,
|
||||
alpha_premultiply: bool = False, keep_aspect_ratio: bool = True, flip_x: bool = False) -> rl.Image:
|
||||
"""Load and resize an image, storing it for later automatic unloading."""
|
||||
@@ -850,6 +964,11 @@ class GuiApplication:
|
||||
rl.unload_texture(texture)
|
||||
self._textures = {}
|
||||
|
||||
for render_texture in self._cached_render_textures.values():
|
||||
rl.unload_render_texture(render_texture)
|
||||
self._cached_render_textures = {}
|
||||
self._pending_render_textures = {}
|
||||
|
||||
for font in self._fonts.values():
|
||||
rl.unload_font(font)
|
||||
self._fonts = {}
|
||||
@@ -890,6 +1009,7 @@ class GuiApplication:
|
||||
|
||||
while not (self._window_close_requested or rl.window_should_close()):
|
||||
self._mark_progress("gui_app.loop_start")
|
||||
self._apply_render_mode()
|
||||
if PC:
|
||||
# Thread is not used on PC, need to manually add mouse events.
|
||||
self._mouse._handle_mouse_event()
|
||||
@@ -898,6 +1018,7 @@ class GuiApplication:
|
||||
self._mouse_events = self._mouse.get_events()
|
||||
if len(self._mouse_events) > 0:
|
||||
self._last_mouse_event = self._mouse_events[-1]
|
||||
self.request_high_fps()
|
||||
|
||||
# Skip rendering when screen is off
|
||||
if not self._should_render:
|
||||
@@ -987,6 +1108,7 @@ class GuiApplication:
|
||||
self._mark_progress("gui_app.before_end_drawing")
|
||||
rl.end_drawing()
|
||||
self._mark_progress("gui_app.after_end_drawing")
|
||||
self._populate_render_texture_cache()
|
||||
|
||||
if RECORD:
|
||||
image = rl.load_image_from_texture(self._render_texture.texture)
|
||||
|
||||
Reference in New Issue
Block a user