Skip to content

Commit bc435e4

Browse files
authored
Better canvas cleanup, avoids segfaults on some systems with glfw+wgpu (#73)
1 parent 714469c commit bc435e4

File tree

6 files changed

+41
-24
lines changed

6 files changed

+41
-24
lines changed

rendercanvas/_context.py

+4
Original file line numberDiff line numberDiff line change
@@ -76,3 +76,7 @@ def present(self):
7676

7777
# This is a stub
7878
return {"method": "skip"}
79+
80+
def _release(self):
81+
"""Release resources. Called by the canvas when it's closed."""
82+
pass

rendercanvas/_events.py

+2-2
Original file line numberDiff line numberDiff line change
@@ -66,7 +66,7 @@ def __init__(self):
6666
self._event_handlers = defaultdict(list)
6767
self._closed = False
6868

69-
def _set_closed(self):
69+
def _release(self):
7070
self._closed = True
7171
self._pending_events.clear()
7272
self._event_handlers.clear()
@@ -222,7 +222,7 @@ async def emit(self, event):
222222
callback(event)
223223
# Close?
224224
if event_type == "close":
225-
self._set_closed()
225+
self._release()
226226

227227
async def close(self):
228228
"""Close the event handler.

rendercanvas/_loop.py

+1-1
Original file line numberDiff line numberDiff line change
@@ -133,7 +133,7 @@ async def _loop_task(self):
133133
for canvas in self.get_canvases():
134134
if not getattr(canvas, "_rc_closed_by_loop", False):
135135
canvas._rc_closed_by_loop = True
136-
canvas._rc_close()
136+
canvas.close()
137137
del canvas
138138

139139
finally:

rendercanvas/base.py

+14-1
Original file line numberDiff line numberDiff line change
@@ -180,7 +180,7 @@ def _final_canvas_init(self):
180180
def __del__(self):
181181
# On delete, we call the custom destroy method.
182182
try:
183-
self._rc_close()
183+
self.close()
184184
except Exception:
185185
pass
186186
# Since this is sometimes used in a multiple inheritance, the
@@ -438,6 +438,19 @@ def get_pixel_ratio(self):
438438

439439
def close(self):
440440
"""Close the canvas."""
441+
# Clear the draw-function, to avoid it holding onto e.g. wgpu objects.
442+
self._draw_frame = None
443+
# Clear the canvas context too.
444+
if hasattr(self._canvas_context, "_release"):
445+
# ContextInterface (and GPUCanvasContext) has _release()
446+
try:
447+
self._canvas_context._release()
448+
except Exception:
449+
pass
450+
self._canvas_context = None
451+
# Clean events. Should already have happened in loop, but the loop may not be running.
452+
self._events._release()
453+
# Let the subclass clean up.
441454
self._rc_close()
442455

443456
def get_closed(self):

rendercanvas/glfw.py

+19-19
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,6 @@
1111

1212
import sys
1313
import time
14-
import atexit
1514

1615
import glfw
1716

@@ -151,14 +150,21 @@ def enable_glfw():
151150
glfw._rc_alive = True
152151

153152

154-
@atexit.register
155-
def terminate_glfw():
156-
glfw.terminate()
157-
glfw._rc_alive = False
158-
159-
160153
class GlfwCanvasGroup(BaseCanvasGroup):
161-
pass
154+
glfw = glfw # make sure we can access the glfw module in the __del__
155+
156+
def __del__(self):
157+
# Because this object is used as a class attribute (on the canvas), this
158+
# __del__ method gets called later than a function registed to atexit.
159+
# This is important when used in combination with wgpu, where the release of the surface
160+
# should happen before the termination of glfw. On some systems this can otherwiser
161+
# result in a segfault, see https://github.com/pygfx/pygfx/issues/642
162+
try:
163+
self.glfw._rc_alive = False
164+
self.glfw.terminate()
165+
except Exception:
166+
pass
167+
super().__del__()
162168

163169

164170
class GlfwRenderCanvas(BaseRenderCanvas):
@@ -260,7 +266,7 @@ def _on_want_close(self, *args):
260266
def _maybe_close(self):
261267
if self._window is not None:
262268
if glfw.window_should_close(self._window):
263-
self._rc_close()
269+
self.close()
264270

265271
def _set_logical_size(self, new_logical_size):
266272
if self._window is None:
@@ -341,10 +347,6 @@ def _rc_set_logical_size(self, width, height):
341347
self._set_logical_size((float(width), float(height)))
342348

343349
def _rc_close(self):
344-
if not glfw._rc_alive:
345-
# May not always be able to close the proper way on system exit
346-
self._window = None
347-
return
348350
if self._window is not None:
349351
glfw.destroy_window(self._window) # not just glfw.hide_window
350352
self._window = None
@@ -353,14 +355,13 @@ def _rc_close(self):
353355
# But on some systems glfw needs a bit of time to properly close the window.
354356
if not self._rc_canvas_group.get_canvases():
355357
poll_glfw_briefly(0.05)
356-
# Could also terminate glfw, but we don't know if the application is using glfw in other places.
357-
# terminate_glfw()
358358

359359
def _rc_get_closed(self):
360360
return self._window is None
361361

362362
def _rc_set_title(self, title):
363-
glfw.set_window_title(self._window, title)
363+
if self._window is not None:
364+
glfw.set_window_title(self._window, title)
364365

365366
# %% Turn glfw events into rendercanvas events
366367

@@ -565,14 +566,13 @@ def _on_char(self, window, char):
565566

566567
def poll_glfw_briefly(poll_time=0.1):
567568
"""Briefly poll glfw for a set amount of time.
568-
569569
Intended to work around the bug that destroyed windows sometimes hang
570570
around if the mainloop exits: https://github.com/glfw/glfw/issues/1766
571-
572571
I found that 10ms is enough, but make it 100ms just in case. You should
573572
only run this right after your mainloop stops.
574-
575573
"""
574+
if not glfw._rc_alive:
575+
return
576576
end_time = time.perf_counter() + poll_time
577577
while time.perf_counter() < end_time:
578578
glfw.wait_events_timeout(end_time - time.perf_counter())

tests/test_loop.py

+1-1
Original file line numberDiff line numberDiff line change
@@ -44,7 +44,7 @@ def __init__(self, refuse_close=False):
4444
def _rc_gui_poll(self):
4545
pass
4646

47-
def _rc_close(self):
47+
def close(self):
4848
# Called by the loop to close a canvas
4949
if not self.refuse_close:
5050
self.is_closed = True

0 commit comments

Comments
 (0)