@ -3,18 +3,22 @@ import cffi
import os
import os
import time
import time
import pyray as rl
import pyray as rl
import threading
from collections . abc import Callable
from collections . abc import Callable
from collections import deque
from collections import deque
from dataclasses import dataclass
from dataclasses import dataclass
from enum import IntEnum
from enum import IntEnum
from typing import NamedTuple
from importlib . resources import as_file , files
from importlib . resources import as_file , files
from openpilot . common . swaglog import cloudlog
from openpilot . common . swaglog import cloudlog
from openpilot . system . hardware import HARDWARE
from openpilot . system . hardware import HARDWARE , PC
from openpilot . common . realtime import Ratekeeper
DEFAULT_FPS = int ( os . getenv ( " FPS " , " 60 " ) )
DEFAULT_FPS = int ( os . getenv ( " FPS " , " 60 " ) )
FPS_LOG_INTERVAL = 5 # Seconds between logging FPS drops
FPS_LOG_INTERVAL = 5 # Seconds between logging FPS drops
FPS_DROP_THRESHOLD = 0.9 # FPS drop threshold for triggering a warning
FPS_DROP_THRESHOLD = 0.9 # FPS drop threshold for triggering a warning
FPS_CRITICAL_THRESHOLD = 0.5 # Critical threshold for triggering strict actions
FPS_CRITICAL_THRESHOLD = 0.5 # Critical threshold for triggering strict actions
MOUSE_THREAD_RATE = 140 # touch controller runs at 140Hz
ENABLE_VSYNC = os . getenv ( " ENABLE_VSYNC " , " 0 " ) == " 1 "
ENABLE_VSYNC = os . getenv ( " ENABLE_VSYNC " , " 0 " ) == " 1 "
SHOW_FPS = os . getenv ( " SHOW_FPS " ) == " 1 "
SHOW_FPS = os . getenv ( " SHOW_FPS " ) == " 1 "
@ -47,6 +51,68 @@ class ModalOverlay:
callback : Callable | None = None
callback : Callable | None = None
class MousePos ( NamedTuple ) :
x : float
y : float
class MouseEvent ( NamedTuple ) :
pos : MousePos
left_pressed : bool
left_released : bool
left_down : bool
t : float
class MouseState :
def __init__ ( self ) :
self . _events : deque [ MouseEvent ] = deque ( maxlen = MOUSE_THREAD_RATE ) # bound event list
self . _prev_mouse_event : MouseEvent | None = None
self . _rk = Ratekeeper ( MOUSE_THREAD_RATE )
self . _lock = threading . Lock ( )
self . _exit_event = threading . Event ( )
self . _thread = None
def get_events ( self ) - > list [ MouseEvent ] :
with self . _lock :
events = list ( self . _events )
self . _events . clear ( )
return events
def start ( self ) :
self . _exit_event . clear ( )
if self . _thread is None or not self . _thread . is_alive ( ) :
self . _thread = threading . Thread ( target = self . _run_thread , daemon = True )
self . _thread . start ( )
def stop ( self ) :
self . _exit_event . set ( )
if self . _thread is not None and self . _thread . is_alive ( ) :
self . _thread . join ( )
def _run_thread ( self ) :
while not self . _exit_event . is_set ( ) :
rl . poll_input_events ( )
self . _handle_mouse_event ( )
self . _rk . keep_time ( )
def _handle_mouse_event ( self ) :
mouse_pos = rl . get_mouse_position ( )
ev = MouseEvent (
MousePos ( mouse_pos . x , mouse_pos . y ) ,
rl . is_mouse_button_pressed ( rl . MouseButton . MOUSE_BUTTON_LEFT ) ,
rl . is_mouse_button_released ( rl . MouseButton . MOUSE_BUTTON_LEFT ) ,
rl . is_mouse_button_down ( rl . MouseButton . MOUSE_BUTTON_LEFT ) ,
time . monotonic ( ) ,
)
# Only add changes
if self . _prev_mouse_event is None or ev [ : - 1 ] != self . _prev_mouse_event [ : - 1 ] :
with self . _lock :
self . _events . append ( ev )
self . _prev_mouse_event = ev
class GuiApplication :
class GuiApplication :
def __init__ ( self , width : int , height : int ) :
def __init__ ( self , width : int , height : int ) :
self . _fonts : dict [ FontWeight , rl . Font ] = { }
self . _fonts : dict [ FontWeight , rl . Font ] = { }
@ -63,8 +129,11 @@ class GuiApplication:
self . _trace_log_callback = None
self . _trace_log_callback = None
self . _modal_overlay = ModalOverlay ( )
self . _modal_overlay = ModalOverlay ( )
self . _mouse = MouseState ( )
self . _mouse_events : list [ MouseEvent ] = [ ]
# Debug variables
# Debug variables
self . _mouse_history : deque [ rl . Vector2 ] = deque ( maxlen = DEFAULT_FPS * 2 )
self . _mouse_history : deque [ MousePos ] = deque ( maxlen = MOUSE_THREAD_RATE )
def request_close ( self ) :
def request_close ( self ) :
self . _window_close_requested = True
self . _window_close_requested = True
@ -94,6 +163,9 @@ class GuiApplication:
self . _set_styles ( )
self . _set_styles ( )
self . _load_fonts ( )
self . _load_fonts ( )
if not PC :
self . _mouse . start ( )
def set_modal_overlay ( self , overlay , callback : Callable | None = None ) :
def set_modal_overlay ( self , overlay , callback : Callable | None = None ) :
self . _modal_overlay = ModalOverlay ( overlay = overlay , callback = callback )
self . _modal_overlay = ModalOverlay ( overlay = overlay , callback = callback )
@ -154,11 +226,25 @@ class GuiApplication:
rl . unload_render_texture ( self . _render_texture )
rl . unload_render_texture ( self . _render_texture )
self . _render_texture = None
self . _render_texture = None
if not PC :
self . _mouse . stop ( )
rl . close_window ( )
rl . close_window ( )
@property
def mouse_events ( self ) - > list [ MouseEvent ] :
return self . _mouse_events
def render ( self ) :
def render ( self ) :
try :
try :
while not ( self . _window_close_requested or rl . window_should_close ( ) ) :
while not ( self . _window_close_requested or rl . window_should_close ( ) ) :
if PC :
# Thread is not used on PC, need to manually add mouse events
self . _mouse . _handle_mouse_event ( )
# Store all mouse events for the current frame
self . _mouse_events = self . _mouse . get_events ( )
if self . _render_texture :
if self . _render_texture :
rl . begin_texture_mode ( self . _render_texture )
rl . begin_texture_mode ( self . _render_texture )
rl . clear_background ( rl . BLACK )
rl . clear_background ( rl . BLACK )
@ -196,9 +282,10 @@ class GuiApplication:
rl . draw_fps ( 10 , 10 )
rl . draw_fps ( 10 , 10 )
if SHOW_TOUCHES :
if SHOW_TOUCHES :
if rl . is_mouse_button_pressed ( rl . MouseButton . MOUSE_BUTTON_LEFT ) :
for mouse_event in self . _mouse_events :
self . _mouse_history . clear ( )
if mouse_event . left_pressed :
self . _mouse_history . append ( rl . get_mouse_position ( ) )
self . _mouse_history . clear ( )
self . _mouse_history . append ( mouse_event . pos )
if self . _mouse_history :
if self . _mouse_history :
mouse_pos = self . _mouse_history [ - 1 ]
mouse_pos = self . _mouse_history [ - 1 ]