diff --git a/examples/cube.py b/examples/cube.py index 5867748f..d752da7c 100644 --- a/examples/cube.py +++ b/examples/cube.py @@ -19,47 +19,43 @@ # %% Entrypoints (sync and async) -def setup_cube(canvas, power_preference="high-performance", limits=None): - """Regular function to setup a viz on the given canvas.""" +def setup_drawing_sync(canvas, power_preference="high-performance", limits=None): + """Setup to draw a rotating cube on the given canvas. + + The given canvas must implement WgpuCanvasInterface, but nothing more. + Returns the draw function. + """ adapter = wgpu.gpu.request_adapter_sync(power_preference=power_preference) device = adapter.request_device_sync(required_limits=limits) pipeline_layout, uniform_buffer, bind_groups = create_pipeline_layout(device) + pipeline_kwargs = get_render_pipeline_kwargs(canvas, device, pipeline_layout) + + render_pipeline = device.create_render_pipeline(**pipeline_kwargs) - render_pipeline = get_render_pipeline_sync(canvas, device, pipeline_layout) - draw_function = get_draw_function( - canvas, device, render_pipeline, uniform_buffer, bind_groups + return get_draw_function( + canvas, device, render_pipeline, uniform_buffer, bind_groups, asynchronous=False ) - canvas.request_draw(draw_function) +async def setup_drawing_async(canvas, limits=None): + """Setup to async-draw a rotating cube on the given canvas. -async def setup_cube_async(canvas, limits=None): - """Async function to setup a viz on the given canvas.""" + The given canvas must implement WgpuCanvasInterface, but nothing more. + Returns the draw function. + """ adapter = await wgpu.gpu.request_adapter_async(power_preference="high-performance") device = await adapter.request_device_async(required_limits=limits) pipeline_layout, uniform_buffer, bind_groups = create_pipeline_layout(device) + pipeline_kwargs = get_render_pipeline_kwargs(canvas, device, pipeline_layout) - render_pipeline = await get_render_pipeline_async(canvas, device, pipeline_layout) - draw_function = get_draw_function( - canvas, device, render_pipeline, uniform_buffer, bind_groups - ) - - canvas.request_draw(draw_function) + render_pipeline = await device.create_render_pipeline_async(**pipeline_kwargs) - -def get_render_pipeline_sync(canvas, device, *args): - return device.create_render_pipeline( - **get_render_pipeline_kwargs(canvas, device, *args) - ) - - -async def get_render_pipeline_async(canvas, device, *args): - return await device.create_render_pipeline_async( - **get_render_pipeline_kwargs(canvas, device, *args) + return get_draw_function( + canvas, device, render_pipeline, uniform_buffer, bind_groups, asynchronous=True ) @@ -127,6 +123,12 @@ def create_pipeline_layout(device): usage=wgpu.BufferUsage.UNIFORM | wgpu.BufferUsage.COPY_DST, ) + # Create another buffer to copy data to it (by mapping it and then copying the data) + uniform_buffer.copy_buffer = device.create_buffer( + size=uniform_data.nbytes, + usage=wgpu.BufferUsage.MAP_WRITE | wgpu.BufferUsage.COPY_SRC, + ) + # Create texture, and upload data texture = device.create_texture( size=texture_size, @@ -214,7 +216,9 @@ def create_pipeline_layout(device): return pipeline_layout, uniform_buffer, bind_groups -def get_draw_function(canvas, device, render_pipeline, uniform_buffer, bind_groups): +def get_draw_function( + canvas, device, render_pipeline, uniform_buffer, bind_groups, *, asynchronous +): # Create vertex buffer, and upload data vertex_buffer = device.create_buffer_with_data( data=vertex_data, usage=wgpu.BufferUsage.VERTEX @@ -225,7 +229,7 @@ def get_draw_function(canvas, device, render_pipeline, uniform_buffer, bind_grou data=index_data, usage=wgpu.BufferUsage.INDEX ) - def draw_frame(): + def update_transform(): # Update uniform transform a1 = -0.3 a2 = time.time() @@ -256,17 +260,36 @@ def draw_frame(): ) uniform_data["transform"] = rot2 @ rot1 @ ortho - # Upload the uniform struct - tmp_buffer = device.create_buffer_with_data( - data=uniform_data, usage=wgpu.BufferUsage.COPY_SRC + def upload_uniform_buffer_sync(): + if True: + tmp_buffer = uniform_buffer.copy_buffer + tmp_buffer.map_sync(wgpu.MapMode.WRITE) + tmp_buffer.write_mapped(uniform_data) + tmp_buffer.unmap() + else: + tmp_buffer = device.create_buffer_with_data( + data=uniform_data, usage=wgpu.BufferUsage.COPY_SRC + ) + command_encoder = device.create_command_encoder() + command_encoder.copy_buffer_to_buffer( + tmp_buffer, 0, uniform_buffer, 0, uniform_data.nbytes ) + device.queue.submit([command_encoder.finish()]) + async def upload_uniform_buffer_async(): + tmp_buffer = uniform_buffer.copy_buffer + await tmp_buffer.map_async(wgpu.MapMode.WRITE) + tmp_buffer.write_mapped(uniform_data) + tmp_buffer.unmap() command_encoder = device.create_command_encoder() command_encoder.copy_buffer_to_buffer( tmp_buffer, 0, uniform_buffer, 0, uniform_data.nbytes ) + device.queue.submit([command_encoder.finish()]) + def draw_frame(): current_texture_view = canvas.get_context().get_current_texture().create_view() + command_encoder = device.create_command_encoder() render_pass = command_encoder.begin_render_pass( color_attachments=[ { @@ -289,9 +312,20 @@ def draw_frame(): device.queue.submit([command_encoder.finish()]) - canvas.request_draw() + def draw_frame_sync(): + update_transform() + upload_uniform_buffer_sync() + draw_frame() + + async def draw_frame_async(): + update_transform() + await upload_uniform_buffer_async() + draw_frame() - return draw_frame + if asynchronous: + return draw_frame_async + else: + return draw_frame_sync # %% WGSL @@ -426,5 +460,12 @@ def draw_frame(): canvas = WgpuCanvas(size=(640, 480), title="wgpu cube example") - setup_cube(canvas) + draw_frame = setup_drawing_sync(canvas) + + def animate(): + draw_frame() + canvas.request_draw() + + canvas.request_draw(animate) + run() diff --git a/examples/gui_asyncio.py b/examples/gui_asyncio.py index dbb4a9dd..319058cf 100644 --- a/examples/gui_asyncio.py +++ b/examples/gui_asyncio.py @@ -12,36 +12,26 @@ import glfw -from gui_direct import MinimalGlfwCanvas -from triangle import setup_triangle_async # noqa: F401, RUF100 -from cube import setup_cube_async # noqa: F401, RUF100 +from gui_direct import MinimalGlfwCanvas, poll_glfw_briefly +# from triangle import setup_drawing_async +from cube import setup_drawing_async -async def main_loop(): - # create a window with glfw - glfw.init() - # disable automatic API selection, we are not using opengl - glfw.window_hint(glfw.CLIENT_API, glfw.NO_API) - glfw.window_hint(glfw.RESIZABLE, True) - window = glfw.create_window(640, 480, "wgpu with asyncio", None, None) +async def main_loop(): # create canvas - canvas = MinimalGlfwCanvas(window) - # await setup_triangle_async(canvas) - await setup_cube_async(canvas) + canvas = MinimalGlfwCanvas("wgpu gui asyncio") + draw_frame = await setup_drawing_async(canvas) last_frame_time = time.perf_counter() frame_count = 0 - while True: + while not glfw.window_should_close(canvas.window): await asyncio.sleep(0.01) # process inputs glfw.poll_events() - # break on close - if glfw.window_should_close(window): - break # draw a frame - canvas.draw_frame() + await draw_frame() # present the frame to the screen canvas.context.present() # stats @@ -51,10 +41,9 @@ async def main_loop(): print(f"{frame_count/etime:0.1f} FPS") last_frame_time, frame_count = time.perf_counter(), 0 - # dispose all resources and quit - glfw.hide_window(window) - glfw.destroy_window(window) - glfw.terminate() + # dispose resources + glfw.destroy_window(canvas.window) + poll_glfw_briefly() if __name__ == "__main__": diff --git a/examples/gui_auto.py b/examples/gui_auto.py index 9f21ba9d..9571f71d 100644 --- a/examples/gui_auto.py +++ b/examples/gui_auto.py @@ -6,12 +6,18 @@ from wgpu.gui.auto import WgpuCanvas, run -from triangle import setup_triangle # noqa: F401, RUF100 -from cube import setup_cube # noqa: F401, RUF100 +from triangle import setup_drawing_sync +# from cube import setup_drawing_sync canvas = WgpuCanvas(title=f"Triangle example on {WgpuCanvas.__name__}") -setup_triangle(canvas) +draw_frame = setup_drawing_sync(canvas) + + +@canvas.request_draw +def animate(): + draw_frame() + canvas.request_draw() if __name__ == "__main__": diff --git a/examples/gui_direct.py b/examples/gui_direct.py index cae8fb82..0cb9bac3 100644 --- a/examples/gui_direct.py +++ b/examples/gui_direct.py @@ -9,65 +9,59 @@ # run_example = false import time +import atexit import glfw from wgpu.backends.wgpu_native import GPUCanvasContext -from wgpu.gui.glfw import get_glfw_present_info, get_physical_size +from wgpu.gui.glfw import get_glfw_present_info, poll_glfw_briefly -from triangle import setup_triangle # noqa: F401, RUF100 -from cube import setup_cube # noqa: F401, RUF100 +# from triangle import setup_drawing_sync +from cube import setup_drawing_sync + +# Setup glfw +glfw.init() +atexit.register(glfw.terminate) class MinimalGlfwCanvas: # implements WgpuCanvasInterface - """Minimal canvas interface implementation triangle.py has everything it needs to draw.""" + """Minimal canvas interface required by wgpu.""" + + def __init__(self, title): + # disable automatic API selection, we are not using opengl + glfw.window_hint(glfw.CLIENT_API, glfw.NO_API) + glfw.window_hint(glfw.RESIZABLE, True) - def __init__(self, window): - self._window = window + self.window = glfw.create_window(640, 480, title, None, None) self.context = GPUCanvasContext(self) - self.draw_frame = None def get_present_info(self): """get window and display id, includes some triage to deal with OS differences""" - return get_glfw_present_info(self._window) + return get_glfw_present_info(self.window) def get_physical_size(self): """get framebuffer size in integer pixels""" - return get_physical_size(self._window) + psize = glfw.get_framebuffer_size(self.window) + return int(psize[0]), int(psize[1]) def get_context(self, kind="webgpu"): return self.context - def request_draw(self, func=None): - # A method from WGPUCanvasBase that is called by triangle.py - if func is not None: - self.draw_frame = func - def main(): - # create a window with glfw - glfw.init() - # disable automatic API selection, we are not using opengl - glfw.window_hint(glfw.CLIENT_API, glfw.NO_API) - glfw.window_hint(glfw.RESIZABLE, True) - window = glfw.create_window(640, 480, "wgou demo glfw direct", None, None) - # create canvas - canvas = MinimalGlfwCanvas(window) - setup_cube(canvas) + canvas = MinimalGlfwCanvas("wgpu gui direct") + draw_frame = setup_drawing_sync(canvas) last_frame_time = time.perf_counter() frame_count = 0 # render loop - while True: + while not glfw.window_should_close(canvas.window): # process inputs glfw.poll_events() - # break on close - if glfw.window_should_close(window): - break # draw a frame - canvas.draw_frame() + draw_frame() # present the frame to the screen canvas.context.present() # stats @@ -77,10 +71,9 @@ def main(): print(f"{frame_count/etime:0.1f} FPS") last_frame_time, frame_count = time.perf_counter(), 0 - # dispose all resources and quit - glfw.destroy_window(window) - glfw.poll_events() - glfw.terminate() + # dispose resources + glfw.destroy_window(canvas.window) + poll_glfw_briefly() if __name__ == "__main__": diff --git a/examples/gui_glfw.py b/examples/gui_glfw.py index e958a5ff..354a4a7a 100644 --- a/examples/gui_glfw.py +++ b/examples/gui_glfw.py @@ -6,12 +6,18 @@ from wgpu.gui.glfw import WgpuCanvas, run -from triangle import setup_triangle # noqa: F401, RUF100 -from cube import setup_cube # noqa: F401, RUF100 +from triangle import setup_drawing_sync +# from cube import setup_drawing_sync canvas = WgpuCanvas(title=f"Triangle example on {WgpuCanvas.__name__}") -setup_triangle(canvas) +draw_frame = setup_drawing_sync(canvas) + + +@canvas.request_draw +def animate(): + draw_frame() + canvas.request_draw() if __name__ == "__main__": diff --git a/examples/gui_qt.py b/examples/gui_qt.py index 4a0ee987..c69c22f1 100644 --- a/examples/gui_qt.py +++ b/examples/gui_qt.py @@ -18,14 +18,20 @@ from wgpu.gui.qt import WgpuCanvas # noqa: E402 -from triangle import setup_triangle # noqa -from cube import setup_cube # noqa +from triangle import setup_drawing_sync # noqa: E402 app = QtWidgets.QApplication([]) canvas = WgpuCanvas(title=f"Triangle example on {WgpuCanvas.__name__}") -setup_triangle(canvas) +draw_frame = setup_drawing_sync(canvas) + + +@canvas.request_draw +def animate(): + draw_frame() + canvas.request_draw() + # Enter Qt event loop (compatible with qt5/qt6) app.exec() if hasattr(app, "exec") else app.exec_() diff --git a/examples/gui_qt_embed.py b/examples/gui_qt_embed.py index b0bea621..98209828 100644 --- a/examples/gui_qt_embed.py +++ b/examples/gui_qt_embed.py @@ -19,8 +19,7 @@ from wgpu.gui.qt import WgpuWidget # noqa: E402 -from triangle import setup_triangle # noqa -from cube import setup_cube # noqa +from triangle import setup_drawing_sync # noqa: E402 class ExampleWidget(QtWidgets.QWidget): @@ -49,8 +48,11 @@ def __init__(self): app = QtWidgets.QApplication([]) example = ExampleWidget() -setup_triangle(example.canvas1) -setup_triangle(example.canvas2) +draw_frame1 = setup_drawing_sync(example.canvas1) +draw_frame2 = setup_drawing_sync(example.canvas2) + +example.canvas1.request_draw(draw_frame1) +example.canvas2.request_draw(draw_frame2) # Enter Qt event loop (compatible with qt5/qt6) app.exec() if hasattr(app, "exec") else app.exec_() diff --git a/examples/gui_subprocess.py b/examples/gui_subprocess.py index 7dcb6d19..938be014 100644 --- a/examples/gui_subprocess.py +++ b/examples/gui_subprocess.py @@ -18,8 +18,9 @@ from wgpu.gui import WgpuCanvasBase -# Import the (async) function that we must call to run the visualization -from triangle import setup_triangle +# Import the function that we must call to run the visualization +from triangle import setup_drawing_sync +# from cube import setup_drawing_sync code = """ @@ -81,5 +82,6 @@ def _request_draw(self): canvas = ProxyCanvas() # Go! -setup_triangle(canvas) +draw_frame = setup_drawing_sync(canvas) +canvas.request_draw(draw_frame) time.sleep(3) diff --git a/examples/gui_trio.py b/examples/gui_trio.py index 38626473..67c349c2 100644 --- a/examples/gui_trio.py +++ b/examples/gui_trio.py @@ -19,36 +19,26 @@ import trio import glfw -from gui_direct import MinimalGlfwCanvas -from triangle import setup_triangle_async # noqa: F401, RUF100 -from cube import setup_cube_async # noqa: F401, RUF100 +from gui_direct import MinimalGlfwCanvas, poll_glfw_briefly +# from triangle import setup_drawing_async +from cube import setup_drawing_async -async def main_loop(): - # create a window with glfw - glfw.init() - # disable automatic API selection, we are not using opengl - glfw.window_hint(glfw.CLIENT_API, glfw.NO_API) - glfw.window_hint(glfw.RESIZABLE, True) - window = glfw.create_window(640, 480, "wgpu with trio", None, None) +async def main_loop(): # create canvas - canvas = MinimalGlfwCanvas(window) - # await setup_triangle_async(canvas) - await setup_cube_async(canvas) + canvas = MinimalGlfwCanvas("wgpu gui trio") + draw_frame = await setup_drawing_async(canvas) last_frame_time = time.perf_counter() frame_count = 0 - while True: + while not glfw.window_should_close(canvas.window): await trio.sleep(0.01) # process inputs glfw.poll_events() - # break on close - if glfw.window_should_close(window): - break # draw a frame - canvas.draw_frame() + await draw_frame() # present the frame to the screen canvas.context.present() # stats @@ -58,10 +48,9 @@ async def main_loop(): print(f"{frame_count/etime:0.1f} FPS") last_frame_time, frame_count = time.perf_counter(), 0 - # dispose all resources and quit - glfw.destroy_window(window) - glfw.poll_events() - glfw.terminate() + # dispose resources + glfw.destroy_window(canvas.window) + poll_glfw_briefly() if __name__ == "__main__": diff --git a/examples/gui_wx.py b/examples/gui_wx.py index e38c14cd..1726133a 100644 --- a/examples/gui_wx.py +++ b/examples/gui_wx.py @@ -7,12 +7,14 @@ import wx from wgpu.gui.wx import WgpuCanvas -from triangle import setup_triangle # noqa: F401, RUF100 -from cube import setup_cube # noqa: F401, RUF100 +from triangle import setup_drawing_sync +# from cube import setup_drawing_sync app = wx.App() canvas = WgpuCanvas(title=f"Triangle example on {WgpuCanvas.__name__}") -setup_triangle(canvas) +draw_func = setup_drawing_sync(canvas) +canvas.request_draw(draw_func) + app.MainLoop() diff --git a/examples/gui_wx_embed.py b/examples/gui_wx_embed.py index a4fc7736..dc595a02 100644 --- a/examples/gui_wx_embed.py +++ b/examples/gui_wx_embed.py @@ -7,8 +7,7 @@ import wx from wgpu.gui.wx import WgpuWidget -from triangle import setup_triangle # noqa: F401, RUF100 -from cube import setup_cube # noqa: F401, RUF100 +from triangle import setup_drawing_sync class Example(wx.Frame): @@ -36,7 +35,10 @@ def __init__(self): app = wx.App() example = Example() -setup_triangle(example.canvas1) -setup_triangle(example.canvas2) +draw_frame1 = setup_drawing_sync(example.canvas1) +draw_frame2 = setup_drawing_sync(example.canvas2) + +example.canvas1.request_draw(draw_frame1) +example.canvas2.request_draw(draw_frame2) app.MainLoop() diff --git a/examples/triangle.py b/examples/triangle.py index 5668a822..496f65d7 100644 --- a/examples/triangle.py +++ b/examples/triangle.py @@ -23,38 +23,38 @@ # %% Entrypoints (sync and async) -def setup_triangle(canvas, power_preference="high-performance", limits=None): - """Regular function to setup a viz on the given canvas.""" +def setup_drawing_sync(canvas, power_preference="high-performance", limits=None): + """Setup to draw a triangle on the given canvas. + + The given canvas must implement WgpuCanvasInterface, but nothing more. + Returns the draw function. + """ adapter = wgpu.gpu.request_adapter_sync(power_preference=power_preference) device = adapter.request_device_sync(required_limits=limits) - render_pipeline = get_render_pipeline_sync(canvas, device) - draw_function = get_draw_function(canvas, device, render_pipeline) - - canvas.request_draw(draw_function) + pipeline_kwargs = get_render_pipeline_kwargs(canvas, device) + render_pipeline = device.create_render_pipeline(**pipeline_kwargs) -async def setup_triangle_async(canvas, limits=None): - """Async function to setup a viz on the given canvas.""" + return get_draw_function(canvas, device, render_pipeline, asynchronous=False) - adapter = await wgpu.gpu.request_adapter_async(power_preference="high-performance") - device = await adapter.request_device_async(required_limits=limits) - render_pipeline = await get_render_pipeline_async(canvas, device) - draw_function = get_draw_function(canvas, device, render_pipeline) +async def setup_drawing_async(canvas, limits=None): + """Setup to async-draw a triangle on the given canvas. - canvas.request_draw(draw_function) + The given canvas must implement WgpuCanvasInterface, but nothing more. + Returns the draw function. + """ + adapter = await wgpu.gpu.request_adapter_async(power_preference="high-performance") + device = await adapter.request_device_async(required_limits=limits) -def get_render_pipeline_sync(canvas, device): - return device.create_render_pipeline(**get_render_pipeline_kwargs(canvas, device)) + pipeline_kwargs = get_render_pipeline_kwargs(canvas, device) + render_pipeline = await device.create_render_pipeline_async(**pipeline_kwargs) -async def get_render_pipeline_async(canvas, device): - return await device.create_render_pipeline_async( - **get_render_pipeline_kwargs(canvas, device) - ) + return get_draw_function(canvas, device, render_pipeline, asynchronous=True) # %% Functions to create wgpu objects @@ -92,8 +92,8 @@ def get_render_pipeline_kwargs(canvas, device): ) -def get_draw_function(canvas, device, render_pipeline): - def draw_frame(): +def get_draw_function(canvas, device, render_pipeline, *, asynchronous): + def draw_frame_sync(): current_texture = canvas.get_context().get_current_texture() command_encoder = device.create_command_encoder() @@ -115,7 +115,13 @@ def draw_frame(): render_pass.end() device.queue.submit([command_encoder.finish()]) - return draw_frame + async def draw_frame_async(): + draw_frame_sync() # nothing async here + + if asynchronous: + return draw_frame_async + else: + return draw_frame_sync # %% WGSL @@ -161,5 +167,6 @@ def draw_frame(): from wgpu.gui.auto import WgpuCanvas, run canvas = WgpuCanvas(size=(640, 480), title="wgpu triangle example") - setup_triangle(canvas) + draw_frame = setup_drawing_sync(canvas) + canvas.request_draw(draw_frame) run() diff --git a/examples/triangle_glsl.py b/examples/triangle_glsl.py index 0c0ea133..94251753 100644 --- a/examples/triangle_glsl.py +++ b/examples/triangle_glsl.py @@ -45,16 +45,14 @@ # %% The wgpu calls -def setup_triangle(canvas, power_preference="high-performance", limits=None): +def setup_drawing_sync(canvas, power_preference="high-performance", limits=None): """Regular function to setup a viz on the given canvas.""" adapter = wgpu.gpu.request_adapter_sync(power_preference=power_preference) device = adapter.request_device_sync(required_limits=limits) render_pipeline = get_render_pipeline(canvas, device) - draw_function = get_draw_function(canvas, device, render_pipeline) - - canvas.request_draw(draw_function) + return get_draw_function(canvas, device, render_pipeline) def get_render_pipeline(canvas, device): @@ -129,5 +127,6 @@ def draw_frame(): from wgpu.gui.auto import WgpuCanvas, run canvas = WgpuCanvas(size=(640, 480), title="wgpu triangle glsl example") - setup_triangle(canvas) + draw_frame = setup_drawing_sync(canvas) + canvas.request_draw(draw_frame) run() diff --git a/tests/test_gui_glfw.py b/tests/test_gui_glfw.py index 33f8aaaf..d894b197 100644 --- a/tests/test_gui_glfw.py +++ b/tests/test_gui_glfw.py @@ -26,6 +26,9 @@ def setup_module(): def teardown_module(): + from wgpu.gui.glfw import poll_glfw_briefly + + poll_glfw_briefly() pass # Do not glfw.terminate() because other tests may still need glfw diff --git a/tests_mem/test_gui_glfw.py b/tests_mem/test_gui_glfw.py index bbfeb523..c214896c 100644 --- a/tests_mem/test_gui_glfw.py +++ b/tests_mem/test_gui_glfw.py @@ -38,7 +38,7 @@ def test_release_canvas_context(n): # Texture and a TextureView, but these are released in present(), # so we don't see them in the counts. - from wgpu.gui.glfw import WgpuCanvas + from wgpu.gui.glfw import WgpuCanvas, poll_glfw_briefly yield { "ignore": {"CommandBuffer"}, @@ -60,6 +60,8 @@ def test_release_canvas_context(n): loop.run_until_complete(stub_event_loop()) gc.collect() + poll_glfw_briefly() # removes all windows from screen + # Check that the canvas objects are really deleted assert not canvases, f"Still {len(canvases)} canvases" diff --git a/wgpu/gui/glfw.py b/wgpu/gui/glfw.py index 483e2b3f..54d24919 100644 --- a/wgpu/gui/glfw.py +++ b/wgpu/gui/glfw.py @@ -597,6 +597,21 @@ async def keep_glfw_alive(): loop.stop() +def poll_glfw_briefly(poll_time=0.1): + """Briefly poll glfw for a set amount of time. + + Intended to work around the bug that destroyed windows sometimes hang + around if the mainloop exits: https://github.com/glfw/glfw/issues/1766 + + I found that 10ms is enough, but make it 100ms just in case. You should + only run this right after your mainloop stops. + + """ + end_time = time.perf_counter() + poll_time + while time.perf_counter() < end_time: + glfw.wait_events_timeout(end_time - time.perf_counter()) + + def call_later(delay, callback, *args): loop = app.get_loop() loop.call_later(delay, callback, *args) @@ -610,3 +625,4 @@ def run(): app.stop_if_no_more_canvases = True loop.run_forever() app.stop_if_no_more_canvases = False + poll_glfw_briefly()