Skip to content

Commit db7567d

Browse files
committed
split debug logic into DebugGuiApplication
1 parent 16abf93 commit db7567d

File tree

2 files changed

+297
-230
lines changed

2 files changed

+297
-230
lines changed

system/ui/lib/application.py

Lines changed: 45 additions & 230 deletions
Original file line numberDiff line numberDiff line change
@@ -7,13 +7,13 @@
77
import pyray as rl
88
import threading
99
import platform
10-
from contextlib import contextmanager
1110
from collections.abc import Callable
1211
from collections import deque
1312
from dataclasses import dataclass
1413
from enum import StrEnum
1514
from typing import NamedTuple
1615
from importlib.resources import as_file, files
16+
from openpilot.common.params import Params
1717
from openpilot.common.swaglog import cloudlog
1818
from openpilot.system.hardware import HARDWARE, PC
1919
from openpilot.system.ui.lib.multilang import multilang
@@ -25,16 +25,9 @@
2525
FPS_CRITICAL_THRESHOLD = 0.5 # Critical threshold for triggering strict actions
2626
MOUSE_THREAD_RATE = 140 # touch controller runs at 140Hz
2727
MAX_TOUCH_SLOTS = 2
28-
TOUCH_HISTORY_TIMEOUT = 3.0 # Seconds before touch points fade out
2928

3029
ENABLE_VSYNC = os.getenv("ENABLE_VSYNC", "0") == "1"
31-
SHOW_FPS = os.getenv("SHOW_FPS") == "1"
32-
SHOW_TOUCHES = os.getenv("SHOW_TOUCHES") == "1"
3330
STRICT_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

3932
GL_VERSION = """
4033
#version 300 es
@@ -45,34 +38,6 @@
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-
7641
DEFAULT_TEXT_SIZE = 60
7742
DEFAULT_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

9663
def 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-
12081
class 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

Comments
 (0)