You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
225 lines
7.0 KiB
225 lines
7.0 KiB
import atexit
|
|
import os
|
|
import time
|
|
import pyray as rl
|
|
from enum import IntEnum
|
|
from openpilot.common.basedir import BASEDIR
|
|
from openpilot.common.swaglog import cloudlog
|
|
from openpilot.system.hardware import HARDWARE
|
|
|
|
DEFAULT_FPS = 60
|
|
FPS_LOG_INTERVAL = 5 # Seconds between logging FPS drops
|
|
FPS_DROP_THRESHOLD = 0.9 # FPS drop threshold for triggering a warning
|
|
FPS_CRITICAL_THRESHOLD = 0.5 # Critical threshold for triggering strict actions
|
|
|
|
ENABLE_VSYNC = os.getenv("ENABLE_VSYNC") == "1"
|
|
DEBUG_FPS = os.getenv("DEBUG_FPS") == '1'
|
|
STRICT_MODE = os.getenv("STRICT_MODE") == '1'
|
|
|
|
DEFAULT_TEXT_SIZE = 60
|
|
DEFAULT_TEXT_COLOR = rl.Color(200, 200, 200, 255)
|
|
ASSETS_DIR = os.path.join(BASEDIR, "selfdrive/assets")
|
|
FONT_DIR = os.path.join(BASEDIR, "selfdrive/assets/fonts")
|
|
|
|
|
|
class FontWeight(IntEnum):
|
|
BLACK = 0
|
|
BOLD = 1
|
|
EXTRA_BOLD = 2
|
|
EXTRA_LIGHT = 3
|
|
MEDIUM = 4
|
|
NORMAL = 5
|
|
SEMI_BOLD = 6
|
|
THIN = 7
|
|
|
|
|
|
class GuiApplication:
|
|
def __init__(self, width: int, height: int):
|
|
self._fonts: dict[FontWeight, rl.Font] = {}
|
|
self._width = width
|
|
self._height = height
|
|
self._textures: dict[str, rl.Texture] = {}
|
|
self._target_fps: int = DEFAULT_FPS
|
|
self._last_fps_log_time: float = time.monotonic()
|
|
self._window_close_requested = False
|
|
self._trace_log_callback = None
|
|
|
|
def request_close(self):
|
|
self._window_close_requested = True
|
|
|
|
def init_window(self, title: str, fps: int = DEFAULT_FPS):
|
|
atexit.register(self.close) # Automatically call close() on exit
|
|
|
|
HARDWARE.set_display_power(True)
|
|
HARDWARE.set_screen_brightness(65)
|
|
|
|
self._set_log_callback()
|
|
rl.set_trace_log_level(rl.TraceLogLevel.LOG_ALL)
|
|
|
|
flags = rl.ConfigFlags.FLAG_MSAA_4X_HINT
|
|
if ENABLE_VSYNC:
|
|
flags |= rl.ConfigFlags.FLAG_VSYNC_HINT
|
|
rl.set_config_flags(flags)
|
|
|
|
rl.init_window(self._width, self._height, title)
|
|
rl.set_target_fps(fps)
|
|
|
|
self._target_fps = fps
|
|
self._set_styles()
|
|
self._load_fonts()
|
|
|
|
|
|
def texture(self, asset_path: str, width: int, height: int, alpha_premultiply=False, keep_aspect_ratio=True):
|
|
cache_key = f"{asset_path}_{width}_{height}_{alpha_premultiply}{keep_aspect_ratio}"
|
|
if cache_key in self._textures:
|
|
return self._textures[cache_key]
|
|
|
|
texture_obj = self._load_texture_from_image(os.path.join(ASSETS_DIR, asset_path), width, height, alpha_premultiply, keep_aspect_ratio)
|
|
self._textures[cache_key] = texture_obj
|
|
return texture_obj
|
|
|
|
def _load_texture_from_image(self, image_path: str, width: int, height: int, alpha_premultiply = False, keep_aspect_ratio=True):
|
|
"""Load and resize a texture, storing it for later automatic unloading."""
|
|
if image_path.endswith('.svg'):
|
|
image = self._load_image_from_svg(image_path)
|
|
else:
|
|
image = rl.load_image(image_path)
|
|
|
|
if alpha_premultiply:
|
|
rl.image_alpha_premultiply(image)
|
|
|
|
# Resize with aspect ratio preservation if requested
|
|
if keep_aspect_ratio:
|
|
orig_width = image.width
|
|
orig_height = image.height
|
|
|
|
scale_width = width / orig_width
|
|
scale_height = height / orig_height
|
|
|
|
# Calculate new dimensions
|
|
scale = min(scale_width, scale_height)
|
|
new_width = int(orig_width * scale)
|
|
new_height = int(orig_height * scale)
|
|
|
|
rl.image_resize(image, new_width, new_height)
|
|
else:
|
|
rl.image_resize(image, width, height)
|
|
|
|
texture = rl.load_texture_from_image(image)
|
|
# Set texture filtering to smooth the result
|
|
rl.set_texture_filter(texture, rl.TextureFilter.TEXTURE_FILTER_BILINEAR)
|
|
|
|
rl.unload_image(image)
|
|
return texture
|
|
|
|
def _load_image_from_svg(self, svg_path: str):
|
|
# TODO: Implement SVG loading
|
|
assert(0)
|
|
|
|
def close(self):
|
|
if not rl.is_window_ready():
|
|
return
|
|
|
|
for texture in self._textures.values():
|
|
rl.unload_texture(texture)
|
|
self._textures = {}
|
|
|
|
for font in self._fonts.values():
|
|
rl.unload_font(font)
|
|
self._fonts = {}
|
|
|
|
rl.close_window()
|
|
|
|
def render(self):
|
|
try:
|
|
while not (self._window_close_requested or rl.window_should_close()):
|
|
rl.begin_drawing()
|
|
rl.clear_background(rl.BLACK)
|
|
|
|
yield
|
|
|
|
if DEBUG_FPS:
|
|
rl.draw_fps(10, 10)
|
|
|
|
rl.end_drawing()
|
|
self._monitor_fps()
|
|
except KeyboardInterrupt:
|
|
pass
|
|
|
|
def font(self, font_weight: FontWeight=FontWeight.NORMAL):
|
|
return self._fonts[font_weight]
|
|
|
|
@property
|
|
def width(self):
|
|
return self._width
|
|
|
|
@property
|
|
def height(self):
|
|
return self._height
|
|
|
|
def _load_fonts(self):
|
|
font_files = (
|
|
"Inter-Black.ttf",
|
|
"Inter-Bold.ttf",
|
|
"Inter-ExtraBold.ttf",
|
|
"Inter-ExtraLight.ttf",
|
|
"Inter-Medium.ttf",
|
|
"Inter-Regular.ttf",
|
|
"Inter-SemiBold.ttf",
|
|
"Inter-Thin.ttf"
|
|
)
|
|
|
|
for index, font_file in enumerate(font_files):
|
|
font = rl.load_font_ex(os.path.join(FONT_DIR, font_file), 120, None, 0)
|
|
rl.set_texture_filter(font.texture, rl.TextureFilter.TEXTURE_FILTER_BILINEAR)
|
|
self._fonts[index] = font
|
|
|
|
rl.gui_set_font(self._fonts[FontWeight.NORMAL])
|
|
|
|
def _set_styles(self):
|
|
rl.gui_set_style(rl.GuiControl.DEFAULT, rl.GuiControlProperty.BORDER_WIDTH, 0)
|
|
rl.gui_set_style(rl.GuiControl.DEFAULT, rl.GuiDefaultProperty.TEXT_SIZE, DEFAULT_TEXT_SIZE)
|
|
rl.gui_set_style(rl.GuiControl.DEFAULT, rl.GuiDefaultProperty.BACKGROUND_COLOR, rl.color_to_int(rl.BLACK))
|
|
rl.gui_set_style(rl.GuiControl.DEFAULT, rl.GuiControlProperty.TEXT_COLOR_NORMAL, rl.color_to_int(DEFAULT_TEXT_COLOR))
|
|
rl.gui_set_style(rl.GuiControl.DEFAULT, rl.GuiControlProperty.BASE_COLOR_NORMAL, rl.color_to_int(rl.Color(50, 50, 50, 255)))
|
|
|
|
def _set_log_callback(self):
|
|
@rl.ffi.callback("void(int, char *, void *)")
|
|
def trace_log_callback(log_level, text, args):
|
|
try:
|
|
text_str = rl.ffi.string(text).decode('utf-8')
|
|
except (TypeError, UnicodeDecodeError):
|
|
text_str = str(text)
|
|
|
|
if log_level == rl.TraceLogLevel.LOG_ERROR:
|
|
cloudlog.error(f"raylib: {text_str}")
|
|
elif log_level == rl.TraceLogLevel.LOG_WARNING:
|
|
cloudlog.warning(f"raylib: {text_str}")
|
|
elif log_level == rl.TraceLogLevel.LOG_INFO:
|
|
cloudlog.info(f"raylib: {text_str}")
|
|
elif log_level == rl.TraceLogLevel.LOG_DEBUG:
|
|
cloudlog.debug(f"raylib: {text_str}")
|
|
else:
|
|
cloudlog.error(f"raylib: Unknown level {log_level}: {text_str}")
|
|
|
|
# Store callback reference
|
|
self._trace_log_callback = trace_log_callback
|
|
rl.set_trace_log_callback(self._trace_log_callback)
|
|
|
|
def _monitor_fps(self):
|
|
fps = rl.get_fps()
|
|
|
|
# Log FPS drop below threshold at regular intervals
|
|
if fps < self._target_fps * FPS_DROP_THRESHOLD:
|
|
current_time = time.monotonic()
|
|
if current_time - self._last_fps_log_time >= FPS_LOG_INTERVAL:
|
|
cloudlog.warning(f"FPS dropped below {self._target_fps}: {fps}")
|
|
self._last_fps_log_time = current_time
|
|
|
|
# Strict mode: terminate UI if FPS drops too much
|
|
if STRICT_MODE and fps < self._target_fps * FPS_CRITICAL_THRESHOLD:
|
|
cloudlog.error(f"FPS dropped critically below {fps}. Shutting down UI.")
|
|
os._exit(1)
|
|
|
|
|
|
gui_app = GuiApplication(2160, 1080)
|
|
|