Skip to content

Commit fdd6c28

Browse files
authored
Generic implementation of max fps (#219)
* Add max_fps to jupyter widget * Put max_fps logic in base class
1 parent bdca918 commit fdd6c28

File tree

5 files changed

+39
-44
lines changed

5 files changed

+39
-44
lines changed

wgpu/gui/base.py

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import os
22
import sys
3+
import time
34
import logging
45
import ctypes.util
56

@@ -77,8 +78,10 @@ class WgpuCanvasBase(WgpuCanvasInterface):
7778
subclasses) to use wgpu-py.
7879
"""
7980

80-
def __init__(self, *args, **kwargs):
81+
def __init__(self, *args, max_fps=30, **kwargs):
8182
super().__init__(*args, **kwargs)
83+
self._last_draw_time = 0
84+
self._max_fps = float(max_fps)
8285
self._err_hashes = {}
8386

8487
def draw_frame(self):
@@ -100,6 +103,7 @@ def _draw_frame_and_present(self):
100103
"""Draw the frame and present the result. Errors are logged to the
101104
"wgpu" logger. Should be called by the subclass at an appropriate time.
102105
"""
106+
self._last_draw_time = time.perf_counter()
103107
# Perform the user-defined drawing code. When this errors,
104108
# we should report the error and then continue, otherwise we crash.
105109
# Returns the result of the context's present() call or None.
@@ -113,6 +117,12 @@ def _draw_frame_and_present(self):
113117
except Exception as err:
114118
self._log_exception("Present error", err)
115119

120+
def _get_draw_wait_time(self):
121+
"""Get time (in seconds) to wait until the next draw in order to honour max_fps."""
122+
now = time.perf_counter()
123+
target_time = self._last_draw_time + 1.0 / self._max_fps
124+
return max(0, target_time - now)
125+
116126
def _log_exception(self, kind, err):
117127
"""Log the given exception instance, but only log a one-liner for
118128
subsequent occurances of the same error to avoid spamming (which

wgpu/gui/glfw.py

Lines changed: 6 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -46,7 +46,8 @@ def update_glfw_canvasses():
4646
canvases = tuple(all_glfw_canvases)
4747
for canvas in canvases:
4848
if canvas._need_draw:
49-
canvas._perform_draw()
49+
canvas._need_draw = False
50+
canvas._draw_frame_and_present()
5051
return len(canvases)
5152

5253

@@ -101,8 +102,8 @@ class GlfwWgpuCanvas(WgpuCanvasBase):
101102

102103
# See https://www.glfw.org/docs/latest/group__window.html
103104

104-
def __init__(self, *, size=None, title=None, max_fps=30):
105-
super().__init__()
105+
def __init__(self, *, size=None, title=None, **kwargs):
106+
super().__init__(**kwargs)
106107

107108
# Handle inputs
108109
if not size:
@@ -121,13 +122,9 @@ def __init__(self, *, size=None, title=None, max_fps=30):
121122
# Create the window (the initial size may not be in logical pixels)
122123
self._window = glfw.create_window(int(size[0]), int(size[1]), title, None, None)
123124

124-
# Variables to manage the drawing
125+
# Other internal variables
125126
self._need_draw = False
126127
self._request_draw_timer_running = False
127-
self._draw_time = 0
128-
self._max_fps = float(max_fps)
129-
130-
# Other internal variables
131128
self._changing_pixel_ratio = False
132129

133130
# Register ourselves
@@ -188,11 +185,6 @@ def _mark_ready_for_draw(self):
188185
self._need_draw = True # The event loop looks at this flag
189186
glfw.post_empty_event() # Awake the event loop, if it's in wait-mode
190187

191-
def _perform_draw(self):
192-
self._need_draw = False
193-
self._draw_time = time.perf_counter()
194-
self._draw_frame_and_present()
195-
196188
def _determine_size(self):
197189
# Because the value of get_window_size is in physical-pixels
198190
# on some systems and in logical-pixels on other, we use the
@@ -286,11 +278,8 @@ def set_logical_size(self, width, height):
286278

287279
def _request_draw(self):
288280
if not self._request_draw_timer_running:
289-
now = time.perf_counter()
290-
target_time = self._draw_time + 1.0 / self._max_fps
291-
wait_time = max(0, target_time - now)
292281
self._request_draw_timer_running = True
293-
call_later(wait_time, self._mark_ready_for_draw)
282+
call_later(self._get_draw_wait_time(), self._mark_ready_for_draw)
294283

295284
def close(self):
296285
glfw.set_window_should_close(self._window, True)

wgpu/gui/jupyter.py

Lines changed: 14 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -19,14 +19,21 @@
1919
class JupyterWgpuCanvas(WgpuOffscreenCanvas, RemoteFrameBuffer):
2020
"""An ipywidgets widget providing a wgpu canvas. Needs the jupyter_rfb library."""
2121

22-
def __init__(self, *, size=None, title=None):
23-
super().__init__()
22+
def __init__(self, *, size=None, title=None, **kwargs):
23+
super().__init__(**kwargs)
24+
25+
# Internal variables
2426
self._pixel_ratio = 1
2527
self._logical_size = 0, 0
2628
self._is_closed = False
29+
self._request_draw_timer_running = False
30+
31+
# Register so this can be display'ed when run() is called
32+
pending_jupyter_canvases.append(weakref.ref(self))
33+
34+
# Initialize size
2735
if size is not None:
2836
self.set_logical_size(*size)
29-
pending_jupyter_canvases.append(weakref.ref(self))
3037

3138
# Implementation needed for RemoteFrameBuffer
3239

@@ -39,6 +46,7 @@ def handle_event(self, event):
3946
self._logical_size = event["width"], event["height"]
4047

4148
def get_frame(self):
49+
self._request_draw_timer_running = False
4250
# The _draw_frame_and_present() does the drawing and then calls
4351
# present_context.present(), which calls our present() method.
4452
# The resuls is either a numpy array or None, and this matches
@@ -69,7 +77,9 @@ def is_closed(self):
6977
return self._is_closed
7078

7179
def _request_draw(self):
72-
RemoteFrameBuffer.request_draw(self)
80+
if not self._request_draw_timer_running:
81+
self._request_draw_timer_running = True
82+
call_later(self._get_draw_wait_time(), RemoteFrameBuffer.request_draw, self)
7383

7484
# Implementation needed for WgpuOffscreenCanvas
7585

wgpu/gui/qt.py

Lines changed: 2 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,6 @@
44
"""
55

66
import sys
7-
import time
87
import ctypes
98
import importlib
109

@@ -58,7 +57,7 @@ def enable_hidpi():
5857
class QWgpuWidget(WgpuCanvasBase, QtWidgets.QWidget):
5958
"""A QWidget representing a wgpu canvas that can be embedded in a Qt application."""
6059

61-
def __init__(self, *args, size=None, title=None, max_fps=30, **kwargs):
60+
def __init__(self, *args, size=None, title=None, **kwargs):
6261
super().__init__(*args, **kwargs)
6362

6463
if size:
@@ -70,10 +69,6 @@ def __init__(self, *args, size=None, title=None, max_fps=30, **kwargs):
7069
self.setAttribute(WA_PaintOnScreen, True)
7170
self.setAutoFillBackground(False)
7271

73-
# Variables to limit the fps
74-
self._draw_time = 0
75-
self._max_fps = float(max_fps)
76-
7772
# A timer for limiting fps
7873
self._request_draw_timer = QtCore.QTimer()
7974
self._request_draw_timer.setTimerType(PreciseTimer)
@@ -90,7 +85,6 @@ def paintEngine(self): # noqa: N802 - this is a Qt method
9085
return None
9186

9287
def paintEvent(self, event): # noqa: N802 - this is a Qt method
93-
self._draw_time = time.perf_counter()
9488
self._draw_frame_and_present()
9589

9690
# Methods that we add from wgpu (snake_case)
@@ -135,10 +129,7 @@ def set_logical_size(self, width, height):
135129

136130
def _request_draw(self):
137131
if not self._request_draw_timer.isActive():
138-
now = time.perf_counter()
139-
target_time = self._draw_time + 1.0 / self._max_fps
140-
wait_time = max(0, target_time - now)
141-
self._request_draw_timer.start(wait_time * 1000)
132+
self._request_draw_timer.start(self._get_draw_wait_time() * 1000)
142133

143134
def close(self):
144135
super().close()

wgpu/gui/wx.py

Lines changed: 6 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,6 @@
33
can be used as a standalone window or in a larger GUI.
44
"""
55

6-
import time
76
import ctypes
87

98
from .base import WgpuCanvasBase
@@ -38,15 +37,13 @@ def Notify(self, *args): # noqa: N802
3837
class WxWgpuWindow(WgpuCanvasBase, wx.Window):
3938
"""A wx Window representing a wgpu canvas that can be embedded in a wx application."""
4039

41-
def __init__(self, *args, max_fps=30, **kwargs):
40+
def __init__(self, *args, **kwargs):
4241
super().__init__(*args, **kwargs)
4342

44-
# Variables to limit the fps
45-
self._draw_time = 0
46-
self._max_fps = float(max_fps)
43+
# A timer for limiting fps
4744
self._request_draw_timer = TimerWithCallback(self.Refresh)
4845

49-
# We also keep a timer to prevent draws during a resize. This prevents
46+
# We keep a timer to prevent draws during a resize. This prevents
5047
# issues with mismatching present sizes during resizing (on Linux).
5148
self._resize_timer = TimerWithCallback(self._on_resize_done)
5249
self._draw_lock = False
@@ -56,7 +53,6 @@ def __init__(self, *args, max_fps=30, **kwargs):
5653
self.Bind(wx.EVT_SIZE, self._on_resize)
5754

5855
def on_paint(self, event):
59-
self._draw_time = time.perf_counter()
6056
dc = wx.PaintDC(self) # needed for wx
6157
if not self._draw_lock:
6258
self._draw_frame_and_present()
@@ -101,10 +97,9 @@ def _request_draw(self):
10197
# Despite the FPS limiting the delayed call to refresh solves
10298
# that drawing only happens when the mouse is down, see #209.
10399
if not self._request_draw_timer.IsRunning():
104-
now = time.perf_counter()
105-
target_time = self._draw_time + 1.0 / self._max_fps
106-
wait_time = max(0, target_time - now)
107-
self._request_draw_timer.Start(wait_time * 1000, wx.TIMER_ONE_SHOT)
100+
self._request_draw_timer.Start(
101+
self._get_draw_wait_time() * 1000, wx.TIMER_ONE_SHOT
102+
)
108103

109104
def close(self):
110105
self.Hide()

0 commit comments

Comments
 (0)