diff --git a/system/ui/README.md b/system/ui/README.md index b124ae4d85..3f2562aae2 100644 --- a/system/ui/README.md +++ b/system/ui/README.md @@ -6,6 +6,7 @@ Quick start: * set `SHOW_FPS=1` to show the FPS * set `STRICT_MODE=1` to kill the app if it drops too much below 60fps * set `SCALE=1.5` to scale the entire UI by 1.5x +* set `BURN_IN=1` to get a burn-in heatmap version of the UI * https://www.raylib.com/cheatsheet/cheatsheet.html * https://electronstudio.github.io/raylib-python-cffi/README.html#quickstart diff --git a/system/ui/lib/application.py b/system/ui/lib/application.py index e9f5484a17..1d085a5a05 100644 --- a/system/ui/lib/application.py +++ b/system/ui/lib/application.py @@ -6,6 +6,7 @@ import signal import sys import pyray as rl import threading +import platform from contextlib import contextmanager from collections.abc import Callable from collections import deque @@ -34,6 +35,43 @@ SCALE = float(os.getenv("SCALE", "1.0")) PROFILE_RENDER = int(os.getenv("PROFILE_RENDER", "0")) PROFILE_STATS = int(os.getenv("PROFILE_STATS", "100")) # Number of functions to show in profile output +GL_VERSION = """ +#version 300 es +precision highp float; +""" +if platform.system() == "Darwin": + GL_VERSION = """ + #version 330 core + """ + +BURN_IN_MODE = "BURN_IN" in os.environ +BURN_IN_VERTEX_SHADER = GL_VERSION + """ +in vec3 vertexPosition; +in vec2 vertexTexCoord; +uniform mat4 mvp; +out vec2 fragTexCoord; +void main() { + fragTexCoord = vertexTexCoord; + gl_Position = mvp * vec4(vertexPosition, 1.0); +} +""" +BURN_IN_FRAGMENT_SHADER = GL_VERSION + """ +in vec2 fragTexCoord; +uniform sampler2D texture0; +out vec4 fragColor; +void main() { + vec4 sampled = texture(texture0, fragTexCoord); + float intensity = sampled.b; + // Map blue intensity to green -> yellow -> red to highlight burn-in risk. + vec3 start = vec3(0.0, 1.0, 0.0); + vec3 middle = vec3(1.0, 1.0, 0.0); + vec3 end = vec3(1.0, 0.0, 0.0); + vec3 gradient = mix(start, middle, clamp(intensity * 2.0, 0.0, 1.0)); + gradient = mix(gradient, end, clamp((intensity - 0.5) * 2.0, 0.0, 1.0)); + fragColor = vec4(gradient, sampled.a); +} +""" + DEFAULT_TEXT_SIZE = 60 DEFAULT_TEXT_COLOR = rl.WHITE @@ -155,6 +193,7 @@ class GuiApplication: self._scaled_width = int(self._width * self._scale) self._scaled_height = int(self._height * self._scale) self._render_texture: rl.RenderTexture | None = None + self._burn_in_shader: rl.Shader | None = None self._textures: dict[str, rl.Texture] = {} self._target_fps: int = _DEFAULT_FPS self._last_fps_log_time: float = time.monotonic() @@ -212,8 +251,10 @@ class GuiApplication: rl.set_config_flags(flags) rl.init_window(self._scaled_width, self._scaled_height, title) + needs_render_texture = self._scale != 1.0 or BURN_IN_MODE if self._scale != 1.0: rl.set_mouse_scale(1 / self._scale, 1 / self._scale) + if needs_render_texture: self._render_texture = rl.load_render_texture(self._width, self._height) rl.set_texture_filter(self._render_texture.texture, rl.TextureFilter.TEXTURE_FILTER_BILINEAR) rl.set_target_fps(fps) @@ -222,6 +263,8 @@ class GuiApplication: self._set_styles() self._load_fonts() self._patch_text_functions() + if BURN_IN_MODE and self._burn_in_shader is None: + self._burn_in_shader = rl.load_shader_from_memory(BURN_IN_VERTEX_SHADER, BURN_IN_FRAGMENT_SHADER) if not PC: self._mouse.start() @@ -337,6 +380,10 @@ class GuiApplication: rl.unload_render_texture(self._render_texture) self._render_texture = None + if self._burn_in_shader: + rl.unload_shader(self._burn_in_shader) + self._burn_in_shader = None + if not PC: self._mouse.stop() @@ -395,7 +442,14 @@ class GuiApplication: rl.clear_background(rl.BLACK) src_rect = rl.Rectangle(0, 0, float(self._width), -float(self._height)) dst_rect = rl.Rectangle(0, 0, float(self._scaled_width), float(self._scaled_height)) - rl.draw_texture_pro(self._render_texture.texture, src_rect, dst_rect, rl.Vector2(0, 0), 0.0, rl.WHITE) + texture = self._render_texture.texture + if texture: + if BURN_IN_MODE and self._burn_in_shader: + rl.begin_shader_mode(self._burn_in_shader) + rl.draw_texture_pro(texture, src_rect, dst_rect, rl.Vector2(0, 0), 0.0, rl.WHITE) + rl.end_shader_mode() + else: + rl.draw_texture_pro(texture, src_rect, dst_rect, rl.Vector2(0, 0), 0.0, rl.WHITE) if self._show_fps: rl.draw_fps(10, 10) diff --git a/system/ui/lib/shader_polygon.py b/system/ui/lib/shader_polygon.py index 28585b08ba..3cc480b33b 100644 --- a/system/ui/lib/shader_polygon.py +++ b/system/ui/lib/shader_polygon.py @@ -1,9 +1,8 @@ -import platform import pyray as rl import numpy as np from dataclasses import dataclass from typing import Any, Optional, cast -from openpilot.system.ui.lib.application import gui_app +from openpilot.system.ui.lib.application import gui_app, GL_VERSION MAX_GRADIENT_COLORS = 20 # includes stops as well @@ -29,16 +28,7 @@ class Gradient: self.stops = [i / max(1, color_count - 1) for i in range(color_count)] -VERSION = """ -#version 300 es -precision highp float; -""" -if platform.system() == "Darwin": - VERSION = """ - #version 330 core - """ - -FRAGMENT_SHADER = VERSION + """ +FRAGMENT_SHADER = GL_VERSION + """ in vec2 fragTexCoord; out vec4 finalColor; @@ -83,7 +73,7 @@ void main() { """ # Default vertex shader -VERTEX_SHADER = VERSION + """ +VERTEX_SHADER = GL_VERSION + """ in vec3 vertexPosition; in vec2 vertexTexCoord; out vec2 fragTexCoord;