Performance Enhancing Rugs

This commit is contained in:
firestarsdog
2026-07-28 10:26:32 -04:00
parent a4a1d98f14
commit 7fbab917fa
30 changed files with 559 additions and 117 deletions
+1 -2
View File
@@ -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()
+8
View File
@@ -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)
+1 -1
View File
@@ -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)
+1 -2
View File
@@ -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
+24 -24
View File
@@ -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()
+2 -2
View File
@@ -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
+107
View File
@@ -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
+5 -3
View File
@@ -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
+4 -2
View File
@@ -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 -2
View File
@@ -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
+1 -2
View File
@@ -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
+2 -1
View File
@@ -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 {}
+1 -1
View File
@@ -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()
+59 -1
View File
@@ -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)
+121
View File
@@ -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()
+14 -8
View File
@@ -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 -2
View File
@@ -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)
+122
View File
@@ -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)