Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 11 additions & 1 deletion wgpu/gui/base.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import os
import sys
import time
import logging
import ctypes.util

Expand Down Expand Up @@ -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):
Expand All @@ -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.
Expand All @@ -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
Expand Down
23 changes: 6 additions & 17 deletions wgpu/gui/glfw.py
Original file line number Diff line number Diff line change
Expand Up @@ -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)


Expand Down Expand Up @@ -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:
Expand All @@ -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
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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)
Expand Down
18 changes: 14 additions & 4 deletions wgpu/gui/jupyter.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,14 +19,21 @@
class JupyterWgpuCanvas(WgpuOffscreenCanvas, RemoteFrameBuffer):
"""An ipywidgets widget providing a wgpu canvas. Needs the jupyter_rfb library."""

def __init__(self, *, size=None, title=None):
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))

# Initialize size
if size is not None:
self.set_logical_size(*size)
pending_jupyter_canvases.append(weakref.ref(self))

# Implementation needed for RemoteFrameBuffer

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

def get_frame(self):
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
Expand Down Expand Up @@ -69,7 +77,9 @@ def is_closed(self):
return self._is_closed

def _request_draw(self):
RemoteFrameBuffer.request_draw(self)
if not self._request_draw_timer_running:
self._request_draw_timer_running = True
call_later(self._get_draw_wait_time(), RemoteFrameBuffer.request_draw, self)

# Implementation needed for WgpuOffscreenCanvas

Expand Down
13 changes: 2 additions & 11 deletions wgpu/gui/qt.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,6 @@
"""

import sys
import time
import ctypes
import importlib

Expand Down Expand Up @@ -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:
Expand All @@ -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)
Expand All @@ -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)
Expand Down Expand Up @@ -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()
Expand Down
17 changes: 6 additions & 11 deletions wgpu/gui/wx.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,6 @@
can be used as a standalone window or in a larger GUI.
"""

import time
import ctypes

from .base import WgpuCanvasBase
Expand Down Expand Up @@ -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
Expand All @@ -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()
Expand Down Expand Up @@ -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()
Expand Down