77import pyray as rl
88import threading
99import platform
10- from contextlib import contextmanager
1110from collections .abc import Callable
1211from collections import deque
1312from dataclasses import dataclass
1413from enum import StrEnum
1514from typing import NamedTuple
1615from importlib .resources import as_file , files
16+ from openpilot .common .params import Params
1717from openpilot .common .swaglog import cloudlog
1818from openpilot .system .hardware import HARDWARE , PC
1919from openpilot .system .ui .lib .multilang import multilang
2525FPS_CRITICAL_THRESHOLD = 0.5 # Critical threshold for triggering strict actions
2626MOUSE_THREAD_RATE = 140 # touch controller runs at 140Hz
2727MAX_TOUCH_SLOTS = 2
28- TOUCH_HISTORY_TIMEOUT = 3.0 # Seconds before touch points fade out
2928
3029ENABLE_VSYNC = os .getenv ("ENABLE_VSYNC" , "0" ) == "1"
31- SHOW_FPS = os .getenv ("SHOW_FPS" ) == "1"
32- SHOW_TOUCHES = os .getenv ("SHOW_TOUCHES" ) == "1"
3330STRICT_MODE = os .getenv ("STRICT_MODE" ) == "1"
34- SCALE = float (os .getenv ("SCALE" , "1.0" ))
35- GRID_SIZE = int (os .getenv ("GRID" , "0" ))
36- PROFILE_RENDER = int (os .getenv ("PROFILE_RENDER" , "0" ))
37- PROFILE_STATS = int (os .getenv ("PROFILE_STATS" , "100" )) # Number of functions to show in profile output
3831
3932GL_VERSION = """
4033#version 300 es
4538 #version 330 core
4639 """
4740
48- BURN_IN_MODE = "BURN_IN" in os .environ
49- BURN_IN_VERTEX_SHADER = GL_VERSION + """
50- in vec3 vertexPosition;
51- in vec2 vertexTexCoord;
52- uniform mat4 mvp;
53- out vec2 fragTexCoord;
54- void main() {
55- fragTexCoord = vertexTexCoord;
56- gl_Position = mvp * vec4(vertexPosition, 1.0);
57- }
58- """
59- BURN_IN_FRAGMENT_SHADER = GL_VERSION + """
60- in vec2 fragTexCoord;
61- uniform sampler2D texture0;
62- out vec4 fragColor;
63- void main() {
64- vec4 sampled = texture(texture0, fragTexCoord);
65- float intensity = sampled.b;
66- // Map blue intensity to green -> yellow -> red to highlight burn-in risk.
67- vec3 start = vec3(0.0, 1.0, 0.0);
68- vec3 middle = vec3(1.0, 1.0, 0.0);
69- vec3 end = vec3(1.0, 0.0, 0.0);
70- vec3 gradient = mix(start, middle, clamp(intensity * 2.0, 0.0, 1.0));
71- gradient = mix(gradient, end, clamp((intensity - 0.5) * 2.0, 0.0, 1.0));
72- fragColor = vec4(gradient, sampled.a);
73- }
74- """
75-
7641DEFAULT_TEXT_SIZE = 60
7742DEFAULT_TEXT_COLOR = rl .WHITE
7843
@@ -92,6 +57,8 @@ class FontWeight(StrEnum):
9257 BOLD = "Inter-Bold.fnt"
9358 UNIFONT = "unifont.fnt"
9459
60+ # Forward declaration for the type checker; actual instance assigned at bottom.
61+ gui_app : "GuiApplication"
9562
9663def font_fallback (font : rl .Font ) -> rl .Font :
9764 """Fall back to unifont for languages that require it."""
@@ -111,12 +78,6 @@ class MousePos(NamedTuple):
11178 y : float
11279
11380
114- class MousePosWithTime (NamedTuple ):
115- x : float
116- y : float
117- t : float
118-
119-
12081class MouseEvent (NamedTuple ):
12182 pos : MousePos
12283 slot : int
@@ -186,15 +147,9 @@ def __init__(self, width: int, height: int):
186147 self ._width = width
187148 self ._height = height
188149
189- if PC and os .getenv ("SCALE" ) is None :
190- self ._scale = self ._calculate_auto_scale ()
191- else :
192- self ._scale = SCALE
193-
150+ self ._scale = 1.0
194151 self ._scaled_width = int (self ._width * self ._scale )
195152 self ._scaled_height = int (self ._height * self ._scale )
196- self ._render_texture : rl .RenderTexture | None = None
197- self ._burn_in_shader : rl .Shader | None = None
198153 self ._textures : dict [str , rl .Texture ] = {}
199154 self ._target_fps : int = _DEFAULT_FPS
200155 self ._last_fps_log_time : float = time .monotonic ()
@@ -210,24 +165,12 @@ def __init__(self, width: int, height: int):
210165
211166 self ._should_render = True
212167
213- # Debug variables
214- self ._mouse_history : deque [MousePosWithTime ] = deque (maxlen = MOUSE_THREAD_RATE )
215- self ._show_touches = SHOW_TOUCHES
216- self ._show_fps = SHOW_FPS
217- self ._grid_size = GRID_SIZE
218- self ._profile_render_frames = PROFILE_RENDER
219- self ._render_profiler = None
220- self ._render_profile_start_time = None
221-
222168 @property
223169 def frame (self ):
224170 return self ._frame
225171
226- def set_show_touches (self , show : bool ):
227- self ._show_touches = show
228-
229- def set_show_fps (self , show : bool ):
230- self ._show_fps = show
172+ def set_show_touches (self , show : bool ): pass
173+ def set_show_fps (self , show : bool ): pass
231174
232175 @property
233176 def target_fps (self ):
@@ -237,69 +180,34 @@ def request_close(self):
237180 self ._window_close_requested = True
238181
239182 def init_window (self , title : str , fps : int = _DEFAULT_FPS ):
240- with self ._startup_profile_context ():
241- def _close (sig , frame ):
242- self .close ()
243- sys .exit (0 )
244- signal .signal (signal .SIGINT , _close )
245- atexit .register (self .close )
246-
247- self ._set_log_callback ()
248- rl .set_trace_log_level (rl .TraceLogLevel .LOG_WARNING )
249-
250- flags = rl .ConfigFlags .FLAG_MSAA_4X_HINT
251- if ENABLE_VSYNC :
252- flags |= rl .ConfigFlags .FLAG_VSYNC_HINT
253- rl .set_config_flags (flags )
254-
255- rl .init_window (self ._scaled_width , self ._scaled_height , title )
256- needs_render_texture = self ._scale != 1.0 or BURN_IN_MODE
257- if self ._scale != 1.0 :
258- rl .set_mouse_scale (1 / self ._scale , 1 / self ._scale )
259- if needs_render_texture :
260- self ._render_texture = rl .load_render_texture (self ._width , self ._height )
261- rl .set_texture_filter (self ._render_texture .texture , rl .TextureFilter .TEXTURE_FILTER_BILINEAR )
262- rl .set_target_fps (fps )
263-
264- self ._target_fps = fps
265- self ._set_styles ()
266- self ._load_fonts ()
267- self ._patch_text_functions ()
268- if BURN_IN_MODE and self ._burn_in_shader is None :
269- self ._burn_in_shader = rl .load_shader_from_memory (BURN_IN_VERTEX_SHADER , BURN_IN_FRAGMENT_SHADER )
270-
271- if not PC :
272- self ._mouse .start ()
273-
274- @contextmanager
275- def _startup_profile_context (self ):
276- if "PROFILE_STARTUP" not in os .environ :
277- yield
278- return
183+ def _close (sig , frame ):
184+ self .close ()
185+ sys .exit (0 )
186+ signal .signal (signal .SIGINT , _close )
187+ atexit .register (self .close )
188+
189+ self ._set_log_callback ()
190+ rl .set_trace_log_level (rl .TraceLogLevel .LOG_WARNING )
279191
280- import cProfile
281- import io
282- import pstats
192+ flags = rl .ConfigFlags .FLAG_MSAA_4X_HINT
193+ if ENABLE_VSYNC :
194+ flags |= rl .ConfigFlags .FLAG_VSYNC_HINT
195+ rl .set_config_flags (flags )
283196
284- profiler = cProfile .Profile ()
285- start_time = time .monotonic ()
286- profiler .enable ()
197+ rl .init_window (self ._scaled_width , self ._scaled_height , title )
287198
288- # do the init
289- yield
199+ if self . _scale != 1.0 :
200+ rl . set_mouse_scale ( 1 / self . _scale , 1 / self . _scale )
290201
291- profiler .disable ()
292- elapsed_ms = (time .monotonic () - start_time ) * 1e3
202+ rl .set_target_fps (fps )
293203
294- stats_stream = io . StringIO ()
295- pstats . Stats ( profiler , stream = stats_stream ). sort_stats ( "cumtime" ). print_stats ( 25 )
296- print ( " \n === Startup profile ===" )
297- print ( stats_stream . getvalue (). rstrip () )
204+ self . _target_fps = fps
205+ self . _set_styles ( )
206+ self . _load_fonts ( )
207+ self . _patch_text_functions ( )
298208
299- green = "\033 [92m"
300- reset = "\033 [0m"
301- print (f"{ green } UI window ready in { elapsed_ms :.1f} ms{ reset } " )
302- sys .exit (0 )
209+ if not PC :
210+ self ._mouse .start ()
303211
304212 def set_modal_overlay (self , overlay , callback : Callable | None = None ):
305213 if self ._modal_overlay .overlay is not None :
@@ -378,14 +286,6 @@ def close(self):
378286 rl .unload_font (font )
379287 self ._fonts = {}
380288
381- if self ._render_texture is not None :
382- rl .unload_render_texture (self ._render_texture )
383- self ._render_texture = None
384-
385- if self ._burn_in_shader :
386- rl .unload_shader (self ._burn_in_shader )
387- self ._burn_in_shader = None
388-
389289 if not PC :
390290 self ._mouse .stop ()
391291
@@ -401,12 +301,6 @@ def last_mouse_event(self) -> MouseEvent:
401301
402302 def render (self ):
403303 try :
404- if self ._profile_render_frames > 0 :
405- import cProfile
406- self ._render_profiler = cProfile .Profile ()
407- self ._render_profile_start_time = time .monotonic ()
408- self ._render_profiler .enable ()
409-
410304 while not (self ._window_close_requested or rl .window_should_close ()):
411305 if PC :
412306 # Thread is not used on PC, need to manually add mouse events
@@ -425,52 +319,28 @@ def render(self):
425319 yield False
426320 continue
427321
428- if self ._render_texture :
429- rl .begin_texture_mode (self ._render_texture )
430- rl .clear_background (rl .BLACK )
431- else :
432- rl .begin_drawing ()
433- rl .clear_background (rl .BLACK )
322+ self ._begin_frame ()
434323
435324 # Handle modal overlay rendering and input processing
436325 if self ._handle_modal_overlay ():
437326 yield False
438327 else :
439328 yield True
440329
441- if self ._render_texture :
442- rl .end_texture_mode ()
443- rl .begin_drawing ()
444- rl .clear_background (rl .BLACK )
445- src_rect = rl .Rectangle (0 , 0 , float (self ._width ), - float (self ._height ))
446- dst_rect = rl .Rectangle (0 , 0 , float (self ._scaled_width ), float (self ._scaled_height ))
447- texture = self ._render_texture .texture
448- if texture :
449- if BURN_IN_MODE and self ._burn_in_shader :
450- rl .begin_shader_mode (self ._burn_in_shader )
451- rl .draw_texture_pro (texture , src_rect , dst_rect , rl .Vector2 (0 , 0 ), 0.0 , rl .WHITE )
452- rl .end_shader_mode ()
453- else :
454- rl .draw_texture_pro (texture , src_rect , dst_rect , rl .Vector2 (0 , 0 ), 0.0 , rl .WHITE )
455-
456- if self ._show_fps :
457- rl .draw_fps (10 , 10 )
458-
459- if self ._show_touches :
460- self ._draw_touch_points ()
461-
462- if self ._grid_size > 0 :
463- self ._draw_grid ()
464-
465- rl .end_drawing ()
330+ self ._end_frame ()
466331 self ._monitor_fps ()
467332 self ._frame += 1
468333
469- if self ._profile_render_frames > 0 and self ._frame >= self ._profile_render_frames :
470- self ._output_render_profile ()
471334 except KeyboardInterrupt :
472335 pass
473336
337+ def _begin_frame (self ):
338+ rl .begin_drawing ()
339+ rl .clear_background (rl .BLACK )
340+
341+ def _end_frame (self ):
342+ rl .end_drawing ()
343+
474344 def font (self , font_weight : FontWeight = FontWeight .NORMAL ) -> rl .Font :
475345 return self ._fonts [font_weight ]
476346
@@ -590,69 +460,14 @@ def _monitor_fps(self):
590460 cloudlog .error (f"FPS dropped critically below { fps } . Shutting down UI." )
591461 os ._exit (1 )
592462
593- def _draw_touch_points (self ):
594- current_time = time .monotonic ()
595-
596- for mouse_event in self ._mouse_events :
597- if mouse_event .left_pressed :
598- self ._mouse_history .clear ()
599- self ._mouse_history .append (MousePosWithTime (mouse_event .pos .x * self ._scale , mouse_event .pos .y * self ._scale , current_time ))
600-
601- # Remove old touch points that exceed the timeout
602- while self ._mouse_history and (current_time - self ._mouse_history [0 ].t ) > TOUCH_HISTORY_TIMEOUT :
603- self ._mouse_history .popleft ()
604-
605- if self ._mouse_history :
606- mouse_pos = self ._mouse_history [- 1 ]
607- rl .draw_circle (int (mouse_pos .x ), int (mouse_pos .y ), 15 , rl .RED )
608- for idx , mouse_pos in enumerate (self ._mouse_history ):
609- perc = idx / len (self ._mouse_history )
610- color = rl .Color (min (int (255 * (1.5 - perc )), 255 ), int (min (255 * (perc + 0.5 ), 255 )), 50 , 255 )
611- rl .draw_circle (int (mouse_pos .x ), int (mouse_pos .y ), 5 , color )
612-
613- def _draw_grid (self ):
614- grid_color = rl .Color (60 , 60 , 60 , 255 )
615- # Draw vertical lines
616- x = 0
617- while x <= self ._scaled_width :
618- rl .draw_line (x , 0 , x , self ._scaled_height , grid_color )
619- x += self ._grid_size
620- # Draw horizontal lines
621- y = 0
622- while y <= self ._scaled_height :
623- rl .draw_line (0 , y , self ._scaled_width , y , grid_color )
624- y += self ._grid_size
625-
626- def _output_render_profile (self ):
627- import io
628- import pstats
629-
630- self ._render_profiler .disable ()
631- elapsed_ms = (time .monotonic () - self ._render_profile_start_time ) * 1e3
632- avg_frame_time = elapsed_ms / self ._frame if self ._frame > 0 else 0
633-
634- stats_stream = io .StringIO ()
635- pstats .Stats (self ._render_profiler , stream = stats_stream ).sort_stats ("cumtime" ).print_stats (PROFILE_STATS )
636- print ("\n === Render loop profile ===" )
637- print (stats_stream .getvalue ().rstrip ())
638-
639- green = "\033 [92m"
640- reset = "\033 [0m"
641- print (f"\n { green } Rendered { self ._frame } frames in { elapsed_ms :.1f} ms{ reset } " )
642- print (f"{ green } Average frame time: { avg_frame_time :.2f} ms ({ 1000 / avg_frame_time :.1f} FPS){ reset } " )
643- sys .exit (0 )
644-
645- def _calculate_auto_scale (self ) -> float :
646- # Create temporary window to query monitor info
647- rl .init_window (1 , 1 , "" )
648- w , h = rl .get_monitor_width (0 ), rl .get_monitor_height (0 )
649- rl .close_window ()
650-
651- if w == 0 or h == 0 or (w >= self ._width and h >= self ._height ):
652- return 1.0
463+ # Lazy factory to create the app
464+ def _create_gui_app (width = 2160 , height = 1080 ):
465+ is_release = Params ().get_bool ("IsReleaseBranch" )
466+ if not is_release :
467+ from openpilot .system .ui .lib .debug_application import DebugGuiApplication
468+ return DebugGuiApplication (width , height )
653469
654- # Apply 0.95 factor for window decorations/taskbar margin
655- return max (0.3 , min (w / self ._width , h / self ._height ) * 0.95 )
470+ return GuiApplication (width , height )
656471
657472
658- gui_app = GuiApplication (2160 , 1080 )
473+ gui_app = _create_gui_app (2160 , 1080 )
0 commit comments