Skip to content

Commit 204ae9d

Browse files
Fix Thunderscope hanging during sim tests and recompile shaders when OpenGL context changes (UBC-Thunderbots#3526)
* Fix Thunderscope hanging during sim tests and force recompilation of shaders between runs * Monkey patch ShaderProgram.program to recompile shaders when OpenGL context changes * Add return docstring * [pre-commit.ci lite] apply automatic fixes * Revert "[pre-commit.ci lite] apply automatic fixes" This reverts commit b16342a. --------- Co-authored-by: pre-commit-ci-lite[bot] <117423508+pre-commit-ci-lite[bot]@users.noreply.github.com>
1 parent eb7b3ae commit 204ae9d

4 files changed

Lines changed: 91 additions & 61 deletions

File tree

src/software/thunderscope/common/proto_configuration_widget.py

Lines changed: 2 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -77,23 +77,11 @@ def __init__(
7777
layout.addWidget(self.search_query)
7878
layout.addWidget(self.param_tree)
7979

80-
self.run_onetime_async(
81-
ProtoConfigurationWidget.DELAYED_CONFIGURATION_TIMEOUT_S,
80+
QTimer.singleShot(
81+
int(self.DELAYED_CONFIGURATION_TIMEOUT_S * MILLISECONDS_PER_SECOND),
8282
self.send_proto_to_fullsystem,
8383
)
8484

85-
def run_onetime_async(self, time_in_seconds: float, func: Callable) -> None:
86-
"""Starting a timer that runs a given function after a given
87-
amount of seconds one time asynchronously.
88-
89-
:time_in_seconds: the amount of time in seconds
90-
:func: the function that is going to be ran
91-
"""
92-
self.timer = QTimer()
93-
self.timer.setSingleShot(True)
94-
self.timer.timeout.connect(func)
95-
self.timer.start(round(time_in_seconds * MILLISECONDS_PER_SECOND))
96-
9785
def create_widget(self) -> tuple[QHBoxLayout, QHBoxLayout]:
9886
"""Creating widgets that are used to load, save parameters
9987

src/software/thunderscope/gl/graphics/gl_robot.py

Lines changed: 43 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -11,38 +11,6 @@
1111
import numpy as np
1212

1313

14-
ROBOT_SHADER = shaders.ShaderProgram(
15-
"robot-shader",
16-
[
17-
shaders.VertexShader("""
18-
varying vec3 normal;
19-
void main() {
20-
// find vertex normals and positions
21-
normal = normalize(gl_Normal);
22-
gl_FrontColor = gl_Color;
23-
gl_BackColor = gl_Color;
24-
gl_Position = ftransform();
25-
}
26-
"""),
27-
shaders.FragmentShader("""
28-
varying vec3 normal;
29-
void main() {
30-
// create an alternate robot color (blue becomes teal, yellow becomes orange)
31-
vec3 color = gl_Color.rgb;
32-
vec3 color_bright = vec3(color.r, 0.65, color.b);
33-
34-
// this is the vector pointing forward from the robot
35-
vec3 front = vec3(-1.0, 1.0, 0.0);
36-
37-
// mix colors depending on proximity of the face to the front of the robot
38-
float fac = pow(max(dot(normal, front), 0.0), 0.5);
39-
gl_FragColor = vec4(mix(color, color_bright, fac), 1.0);
40-
}
41-
"""),
42-
],
43-
)
44-
45-
4614
class GLRobot(GLMeshItem):
4715
"""Displays a 3D mesh representing a robot"""
4816

@@ -61,7 +29,7 @@ def __init__(
6129
meshdata=self.__get_mesh_data(),
6230
color=color,
6331
smooth=False,
64-
shader=ROBOT_SHADER,
32+
shader="robotShader",
6533
)
6634

6735
self.x = 0
@@ -130,3 +98,45 @@ def __get_mesh_data(self) -> MeshData:
13098
vertexes=np.array(points),
13199
faces=np.array(faces),
132100
)
101+
102+
103+
def init_robot_shader():
104+
"""Adds a shader (robotShader) for rendering robots to the
105+
global list of shaders. The front of the robot is brightened
106+
to make it easier to see which way they're facing.
107+
"""
108+
shaders.Shaders.append(
109+
shaders.ShaderProgram(
110+
"robotShader",
111+
[
112+
shaders.VertexShader("""
113+
varying vec3 normal;
114+
void main() {
115+
// find vertex normals and positions
116+
normal = normalize(gl_Normal);
117+
gl_FrontColor = gl_Color;
118+
gl_BackColor = gl_Color;
119+
gl_Position = ftransform();
120+
}
121+
"""),
122+
shaders.FragmentShader("""
123+
varying vec3 normal;
124+
void main() {
125+
// create an alternate robot color (blue becomes teal, yellow becomes orange)
126+
vec3 color = gl_Color.rgb;
127+
vec3 color_bright = vec3(color.r, 0.65, color.b);
128+
129+
// this is the vector pointing forward from the robot
130+
vec3 front = vec3(-1.0, 1.0, 0.0);
131+
132+
// mix colors depending on proximity of the face to the front of the robot
133+
float fac = pow(max(dot(normal, front), 0.0), 0.5);
134+
gl_FragColor = vec4(mix(color, color_bright, fac), 1.0);
135+
}
136+
"""),
137+
],
138+
)
139+
)
140+
141+
142+
init_robot_shader()

src/software/thunderscope/gl/helpers/gl_patches.py

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,8 @@
11
from pyqtgraph.opengl.GLGraphicsItem import GLGraphicsItem
22
from pyqtgraph.opengl.GLViewWidget import GLViewMixin
3+
from pyqtgraph.opengl.shaders import ShaderProgram
4+
from pyqtgraph.Qt import QtGui
5+
from typing import Callable
36

47

58
def GLGraphicsItem_setParentItem_patched(self, parent: GLGraphicsItem) -> None:
@@ -66,6 +69,27 @@ def GLViewMixin_removeItem_patched(self, item: GLGraphicsItem) -> None:
6669
self.update()
6770

6871

72+
def ShaderProgram_program_patched(original: Callable) -> Callable:
73+
"""Returns a patched version of ShaderProgram.program that forces
74+
recompilation of the shader program when the OpenGL context changes.
75+
76+
:param original: the original ShaderProgram.program method
77+
:return: the patched ShaderProgram.program method
78+
"""
79+
80+
def patched(self):
81+
ctx = QtGui.QOpenGLContext.currentContext()
82+
if not hasattr(self, "gl_ctx") or self.gl_ctx != ctx:
83+
self.gl_ctx = ctx
84+
self.prog = None
85+
for shader in self.shaders:
86+
shader.compiled = None
87+
return original(self)
88+
89+
return patched
90+
91+
6992
GLGraphicsItem.setParentItem = GLGraphicsItem_setParentItem_patched
7093
GLViewMixin.addItem = GLViewMixin_addItem_patched
7194
GLViewMixin.removeItem = GLViewMixin_removeItem_patched
95+
ShaderProgram.program = ShaderProgram_program_patched(ShaderProgram.program)

src/software/thunderscope/thunderscope.py

Lines changed: 22 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -40,9 +40,6 @@ def __init__(
4040
The interval in milliseconds to refresh all the widgets.
4141
"""
4242
self.refresh_interval_ms = refresh_interval_ms
43-
self.refresh_timers = []
44-
45-
self.tabs = QTabWidget()
4643

4744
# ProtoUnixIOs
4845
#
@@ -59,13 +56,9 @@ def __init__(
5956

6057
# initialize proto unix io map with initial values or default if not provided
6158
self.proto_unix_io_map = config.proto_unix_io_map
62-
self.tab_dock_map = {}
6359

64-
# iterate through each tab and add one by one
65-
for tab in config.tabs:
66-
self.tab_dock_map[tab.name] = tab.dock_area
67-
self.tabs.addTab(tab.dock_area, tab.name)
68-
self.register_refresh_function(tab.refresh)
60+
self.tabs = QTabWidget()
61+
self.tab_dock_map = {}
6962

7063
self.window = QMainWindow()
7164
self.window.setCentralWidget(self.tabs)
@@ -74,6 +67,11 @@ def __init__(
7467
)
7568
self.window.setWindowTitle("Thunderscope")
7669

70+
for tab in config.tabs:
71+
self.tab_dock_map[tab.name] = tab.dock_area
72+
self.tabs.addTab(tab.dock_area, tab.name)
73+
self.register_refresh_function(tab.refresh)
74+
7775
# Load the layout file if it exists
7876
path = layout_path if layout_path else LAST_OPENED_LAYOUT_PATH
7977
try:
@@ -197,15 +195,17 @@ def register_refresh_function(self, refresh_func: Callable[[], None]) -> None:
197195
198196
:param refresh_func: The function to call at refresh_interval_ms
199197
"""
200-
refresh_timer = QtCore.QTimer()
198+
refresh_timer = QtCore.QTimer(parent=self.window)
201199
refresh_timer.setTimerType(QtCore.Qt.TimerType.PreciseTimer)
202200
refresh_timer.timeout.connect(lambda: refresh_func())
203201
refresh_timer.start(self.refresh_interval_ms)
204202

205-
self.refresh_timers.append(refresh_timer)
206-
207203
def show(self) -> None:
208-
"""Show the main window"""
204+
"""Show the main window.
205+
206+
This method will start the Qt event loop and block until the
207+
window is closed.
208+
"""
209209
self.window.showMaximized()
210210
pyqtgraph.exec()
211211

@@ -214,5 +214,13 @@ def is_open(self) -> bool:
214214
return self.window.isVisible()
215215

216216
def close(self) -> None:
217-
"""Close the main window"""
217+
"""Close the main window.
218+
219+
Once the window is closed, it will be deleted from memory to
220+
prevent it from lingering around and interfering with any new
221+
Thunderscope instances we may create. Therefore, you should NOT
222+
attempt to reopen the window after it has been closed since the
223+
reference to it will be invalid.
224+
"""
225+
self.window.setAttribute(QtCore.Qt.WA_DeleteOnClose)
218226
QtCore.QTimer.singleShot(0, self.window.close)

0 commit comments

Comments
 (0)