From 430590045bdd2127996809cfef425155592dd6c3 Mon Sep 17 00:00:00 2001 From: Almar Klein Date: Wed, 15 Dec 2021 14:40:14 +0100 Subject: [PATCH 1/2] Add max_fps to jupyter widget --- wgpu/gui/jupyter.py | 25 ++++++++++++++++++++++--- 1 file changed, 22 insertions(+), 3 deletions(-) diff --git a/wgpu/gui/jupyter.py b/wgpu/gui/jupyter.py index a5f646c1..9613ade3 100644 --- a/wgpu/gui/jupyter.py +++ b/wgpu/gui/jupyter.py @@ -3,6 +3,7 @@ can be used as cell output, or embedded in a ipywidgets gui. """ +import time import weakref import asyncio @@ -19,14 +20,25 @@ class JupyterWgpuCanvas(WgpuOffscreenCanvas, RemoteFrameBuffer): """An ipywidgets widget providing a wgpu canvas. Needs the jupyter_rfb library.""" - def __init__(self, *, size=None, title=None): + def __init__(self, *, size=None, title=None, max_fps=30): super().__init__() + + # Internal variables self._pixel_ratio = 1 self._logical_size = 0, 0 self._is_closed = False + + # Register so this can be display'ed when run() is called + pending_jupyter_canvases.append(weakref.ref(self)) + + # Variables to manage the drawing + self._request_draw_timer_running = False + self._draw_time = 0 + self._max_fps = float(max_fps) + + # Initialize size if size is not None: self.set_logical_size(*size) - pending_jupyter_canvases.append(weakref.ref(self)) # Implementation needed for RemoteFrameBuffer @@ -39,6 +51,8 @@ def handle_event(self, event): self._logical_size = event["width"], event["height"] def get_frame(self): + self._draw_time = time.perf_counter() + self._request_draw_timer_running = False # The _draw_frame_and_present() does the drawing and then calls # present_context.present(), which calls our present() method. # The resuls is either a numpy array or None, and this matches @@ -69,7 +83,12 @@ def is_closed(self): return self._is_closed def _request_draw(self): - RemoteFrameBuffer.request_draw(self) + if not self._request_draw_timer_running: + now = time.perf_counter() + target_time = self._draw_time + 1.0 / self._max_fps + wait_time = max(0, target_time - now) + self._request_draw_timer_running = True + call_later(wait_time, RemoteFrameBuffer.request_draw, self) # Implementation needed for WgpuOffscreenCanvas From 9bb9e5b1bc98be2474892df18f535ed62fddf34f Mon Sep 17 00:00:00 2001 From: Almar Klein Date: Wed, 15 Dec 2021 15:11:13 +0100 Subject: [PATCH 2/2] Put max_fps logic in base class --- wgpu/gui/base.py | 12 +++++++++++- wgpu/gui/glfw.py | 23 ++++++----------------- wgpu/gui/jupyter.py | 17 ++++------------- wgpu/gui/qt.py | 13 ++----------- wgpu/gui/wx.py | 17 ++++++----------- 5 files changed, 29 insertions(+), 53 deletions(-) diff --git a/wgpu/gui/base.py b/wgpu/gui/base.py index 78ba0ae1..52e604b0 100644 --- a/wgpu/gui/base.py +++ b/wgpu/gui/base.py @@ -1,5 +1,6 @@ import os import sys +import time import logging import ctypes.util @@ -77,8 +78,10 @@ class WgpuCanvasBase(WgpuCanvasInterface): subclasses) to use wgpu-py. """ - def __init__(self, *args, **kwargs): + def __init__(self, *args, max_fps=30, **kwargs): super().__init__(*args, **kwargs) + self._last_draw_time = 0 + self._max_fps = float(max_fps) self._err_hashes = {} def draw_frame(self): @@ -100,6 +103,7 @@ def _draw_frame_and_present(self): """Draw the frame and present the result. Errors are logged to the "wgpu" logger. Should be called by the subclass at an appropriate time. """ + self._last_draw_time = time.perf_counter() # Perform the user-defined drawing code. When this errors, # we should report the error and then continue, otherwise we crash. # Returns the result of the context's present() call or None. @@ -113,6 +117,12 @@ def _draw_frame_and_present(self): except Exception as err: self._log_exception("Present error", err) + def _get_draw_wait_time(self): + """Get time (in seconds) to wait until the next draw in order to honour max_fps.""" + now = time.perf_counter() + target_time = self._last_draw_time + 1.0 / self._max_fps + return max(0, target_time - now) + def _log_exception(self, kind, err): """Log the given exception instance, but only log a one-liner for subsequent occurances of the same error to avoid spamming (which diff --git a/wgpu/gui/glfw.py b/wgpu/gui/glfw.py index eab64341..54681068 100644 --- a/wgpu/gui/glfw.py +++ b/wgpu/gui/glfw.py @@ -46,7 +46,8 @@ def update_glfw_canvasses(): canvases = tuple(all_glfw_canvases) for canvas in canvases: if canvas._need_draw: - canvas._perform_draw() + canvas._need_draw = False + canvas._draw_frame_and_present() return len(canvases) @@ -101,8 +102,8 @@ class GlfwWgpuCanvas(WgpuCanvasBase): # See https://www.glfw.org/docs/latest/group__window.html - def __init__(self, *, size=None, title=None, max_fps=30): - super().__init__() + def __init__(self, *, size=None, title=None, **kwargs): + super().__init__(**kwargs) # Handle inputs if not size: @@ -121,13 +122,9 @@ def __init__(self, *, size=None, title=None, max_fps=30): # Create the window (the initial size may not be in logical pixels) self._window = glfw.create_window(int(size[0]), int(size[1]), title, None, None) - # Variables to manage the drawing + # Other internal variables self._need_draw = False self._request_draw_timer_running = False - self._draw_time = 0 - self._max_fps = float(max_fps) - - # Other internal variables self._changing_pixel_ratio = False # Register ourselves @@ -188,11 +185,6 @@ def _mark_ready_for_draw(self): self._need_draw = True # The event loop looks at this flag glfw.post_empty_event() # Awake the event loop, if it's in wait-mode - def _perform_draw(self): - self._need_draw = False - self._draw_time = time.perf_counter() - self._draw_frame_and_present() - def _determine_size(self): # Because the value of get_window_size is in physical-pixels # on some systems and in logical-pixels on other, we use the @@ -286,11 +278,8 @@ def set_logical_size(self, width, height): def _request_draw(self): if not self._request_draw_timer_running: - now = time.perf_counter() - target_time = self._draw_time + 1.0 / self._max_fps - wait_time = max(0, target_time - now) self._request_draw_timer_running = True - call_later(wait_time, self._mark_ready_for_draw) + call_later(self._get_draw_wait_time(), self._mark_ready_for_draw) def close(self): glfw.set_window_should_close(self._window, True) diff --git a/wgpu/gui/jupyter.py b/wgpu/gui/jupyter.py index 9613ade3..2a0260e3 100644 --- a/wgpu/gui/jupyter.py +++ b/wgpu/gui/jupyter.py @@ -3,7 +3,6 @@ can be used as cell output, or embedded in a ipywidgets gui. """ -import time import weakref import asyncio @@ -20,22 +19,18 @@ class JupyterWgpuCanvas(WgpuOffscreenCanvas, RemoteFrameBuffer): """An ipywidgets widget providing a wgpu canvas. Needs the jupyter_rfb library.""" - def __init__(self, *, size=None, title=None, max_fps=30): - super().__init__() + def __init__(self, *, size=None, title=None, **kwargs): + super().__init__(**kwargs) # Internal variables self._pixel_ratio = 1 self._logical_size = 0, 0 self._is_closed = False + self._request_draw_timer_running = False # Register so this can be display'ed when run() is called pending_jupyter_canvases.append(weakref.ref(self)) - # Variables to manage the drawing - self._request_draw_timer_running = False - self._draw_time = 0 - self._max_fps = float(max_fps) - # Initialize size if size is not None: self.set_logical_size(*size) @@ -51,7 +46,6 @@ def handle_event(self, event): self._logical_size = event["width"], event["height"] def get_frame(self): - self._draw_time = time.perf_counter() self._request_draw_timer_running = False # The _draw_frame_and_present() does the drawing and then calls # present_context.present(), which calls our present() method. @@ -84,11 +78,8 @@ def is_closed(self): def _request_draw(self): if not self._request_draw_timer_running: - now = time.perf_counter() - target_time = self._draw_time + 1.0 / self._max_fps - wait_time = max(0, target_time - now) self._request_draw_timer_running = True - call_later(wait_time, RemoteFrameBuffer.request_draw, self) + call_later(self._get_draw_wait_time(), RemoteFrameBuffer.request_draw, self) # Implementation needed for WgpuOffscreenCanvas diff --git a/wgpu/gui/qt.py b/wgpu/gui/qt.py index f868e94c..13ad92cb 100644 --- a/wgpu/gui/qt.py +++ b/wgpu/gui/qt.py @@ -4,7 +4,6 @@ """ import sys -import time import ctypes import importlib @@ -58,7 +57,7 @@ def enable_hidpi(): class QWgpuWidget(WgpuCanvasBase, QtWidgets.QWidget): """A QWidget representing a wgpu canvas that can be embedded in a Qt application.""" - def __init__(self, *args, size=None, title=None, max_fps=30, **kwargs): + def __init__(self, *args, size=None, title=None, **kwargs): super().__init__(*args, **kwargs) if size: @@ -70,10 +69,6 @@ def __init__(self, *args, size=None, title=None, max_fps=30, **kwargs): self.setAttribute(WA_PaintOnScreen, True) self.setAutoFillBackground(False) - # Variables to limit the fps - self._draw_time = 0 - self._max_fps = float(max_fps) - # A timer for limiting fps self._request_draw_timer = QtCore.QTimer() self._request_draw_timer.setTimerType(PreciseTimer) @@ -90,7 +85,6 @@ def paintEngine(self): # noqa: N802 - this is a Qt method return None def paintEvent(self, event): # noqa: N802 - this is a Qt method - self._draw_time = time.perf_counter() self._draw_frame_and_present() # Methods that we add from wgpu (snake_case) @@ -135,10 +129,7 @@ def set_logical_size(self, width, height): def _request_draw(self): if not self._request_draw_timer.isActive(): - now = time.perf_counter() - target_time = self._draw_time + 1.0 / self._max_fps - wait_time = max(0, target_time - now) - self._request_draw_timer.start(wait_time * 1000) + self._request_draw_timer.start(self._get_draw_wait_time() * 1000) def close(self): super().close() diff --git a/wgpu/gui/wx.py b/wgpu/gui/wx.py index ec47a2a9..8666a2ec 100644 --- a/wgpu/gui/wx.py +++ b/wgpu/gui/wx.py @@ -3,7 +3,6 @@ can be used as a standalone window or in a larger GUI. """ -import time import ctypes from .base import WgpuCanvasBase @@ -38,15 +37,13 @@ def Notify(self, *args): # noqa: N802 class WxWgpuWindow(WgpuCanvasBase, wx.Window): """A wx Window representing a wgpu canvas that can be embedded in a wx application.""" - def __init__(self, *args, max_fps=30, **kwargs): + def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) - # Variables to limit the fps - self._draw_time = 0 - self._max_fps = float(max_fps) + # A timer for limiting fps self._request_draw_timer = TimerWithCallback(self.Refresh) - # We also keep a timer to prevent draws during a resize. This prevents + # We keep a timer to prevent draws during a resize. This prevents # issues with mismatching present sizes during resizing (on Linux). self._resize_timer = TimerWithCallback(self._on_resize_done) self._draw_lock = False @@ -56,7 +53,6 @@ def __init__(self, *args, max_fps=30, **kwargs): self.Bind(wx.EVT_SIZE, self._on_resize) def on_paint(self, event): - self._draw_time = time.perf_counter() dc = wx.PaintDC(self) # needed for wx if not self._draw_lock: self._draw_frame_and_present() @@ -101,10 +97,9 @@ def _request_draw(self): # Despite the FPS limiting the delayed call to refresh solves # that drawing only happens when the mouse is down, see #209. if not self._request_draw_timer.IsRunning(): - now = time.perf_counter() - target_time = self._draw_time + 1.0 / self._max_fps - wait_time = max(0, target_time - now) - self._request_draw_timer.Start(wait_time * 1000, wx.TIMER_ONE_SHOT) + self._request_draw_timer.Start( + self._get_draw_wait_time() * 1000, wx.TIMER_ONE_SHOT + ) def close(self): self.Hide()