Skip to content

Commit 64caf4f

Browse files
authored
Proper update/draw rate loop handling with Pyglet 3 (#2845)
1 parent ce9dd7d commit 64caf4f

4 files changed

Lines changed: 59 additions & 12 deletions

File tree

arcade/application.py

Lines changed: 55 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@
1515

1616
from arcade.utils import is_pyodide
1717

18-
if is_pyodide():
18+
if is_pyodide:
1919
pyglet.options.backend = "webgl"
2020

2121
import pyglet.config
@@ -193,7 +193,7 @@ def __init__(
193193
pyglet.options.dpi_scaling = "platform"
194194

195195
desired_gl_provider = "opengl"
196-
if is_pyodide():
196+
if is_pyodide:
197197
gl_api = "webgl"
198198

199199
if gl_api == "webgl":
@@ -304,8 +304,9 @@ def __init__(
304304
assert update_rate <= draw_rate, (
305305
"An arcade window's draw rate cannot be faster than its update rate"
306306
)
307-
self._draw_rate = max(update_rate, draw_rate)
307+
self._draw_rate = min(update_rate, draw_rate)
308308
self._accumulated_draw_time: float = 0.0
309+
self._accumulated_update_time: float = 0.0
309310

310311
# Fixed rate cannot be changed post initialization as this throws off physics sims.
311312
# If more time resolution is needed in fixed updates, devs can do 'sub-stepping'.
@@ -578,10 +579,22 @@ def _dispatch_frame(self, delta_time: float) -> None:
578579
The modulus on the accumulated draw time means that when the update rate is greater
579580
than the draw rate no time is lost.
580581
582+
This method is entirely skipped when running in pyodide, this is because the event loop
583+
is driven by requestAnimationFrame in the browser, which adds some unique limitations and
584+
considerations around Arcade's event loop handling. In pyglet, the draw() function of the
585+
window is called directly during the requestAnimationFrame loop, so Arcade handles special
586+
control of the update/draw timing directly in that function. Arcade's version of this function
587+
is never called on desktop, because this function is called instead, and this calls directly
588+
to the superclass's implementation.
589+
581590
Args:
582591
delta_time: The amount of time since the last update.
583592
"""
593+
if is_pyodide:
594+
return
595+
584596
self._dispatch_updates(delta_time)
597+
585598
self._accumulated_draw_time += delta_time
586599

587600
if self._draw_rate <= self._accumulated_draw_time:
@@ -592,7 +605,7 @@ def _dispatch_frame(self, delta_time: float) -> None:
592605

593606
# In case the window close in on_update, on_fixed_update or input callbacks
594607
if not self.closed:
595-
self.draw(self._accumulated_draw_time)
608+
super().draw(self._accumulated_draw_time)
596609
self._accumulated_draw_time %= self._draw_rate
597610

598611
def _dispatch_updates(self, delta_time: float) -> None:
@@ -617,6 +630,44 @@ def _dispatch_updates(self, delta_time: float) -> None:
617630
fixed_count += 1
618631
self.dispatch_event("on_update", GLOBAL_CLOCK.delta_time)
619632

633+
def draw(self, dt: float) -> None:
634+
"""
635+
Render a frame.
636+
637+
On desktop this is driven by arcade's clock-scheduled
638+
:meth:`_dispatch_frame`, which calls the super version of this method direclty.
639+
This implementation is only called when using Pyglet's pyodide backend as part of it's
640+
requestAnimationFrame loop.
641+
642+
The loop rate in a browser is tied inherently to the requestAnimationFrame speed, which
643+
is tied to the monitor's refresh rate, so basically the Arcade loop can never be called
644+
faster than the monitor refresh rate in a browser. This method does some special handling
645+
of the update rate to make the updates happen multiple times per loop to achieve the target
646+
update rate if it is higher than the refresh rate.
647+
648+
It does not bypass the refresh rate for draw rate, because the framebuffer will never drawn
649+
faster to the canvas than that anyways, so us running it faster than that is pointless.
650+
"""
651+
self._accumulated_update_time += dt
652+
while self._accumulated_update_time >= self._update_rate:
653+
GLOBAL_CLOCK.tick(self._update_rate)
654+
fixed_count = 0
655+
while GLOBAL_FIXED_CLOCK.accumulated >= self._fixed_rate and (
656+
self._fixed_frame_cap is None or fixed_count <= self._fixed_frame_cap
657+
):
658+
GLOBAL_FIXED_CLOCK.tick(self._fixed_rate)
659+
self.dispatch_event("on_fixed_update", self._fixed_rate)
660+
fixed_count += 1
661+
662+
self.dispatch_event("on_update", GLOBAL_CLOCK.delta_time)
663+
self._accumulated_update_time -= self._update_rate
664+
665+
self._accumulated_draw_time += dt
666+
if self._accumulated_draw_time < self._draw_rate:
667+
return
668+
self._accumulated_draw_time %= self._draw_rate
669+
super().draw(dt)
670+
620671
def flip(self) -> None:
621672
"""
622673
Present the rendered content to the screen.

arcade/sound.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@
1313

1414
if os.environ.get("ARCADE_SOUND_BACKENDS"):
1515
pyglet.options.audio = tuple(v.strip() for v in os.environ["ARCADE_SOUND_BACKENDS"].split(","))
16-
elif is_pyodide():
16+
elif is_pyodide:
1717
# Pyglet will also detect Pyodide and auto select the driver for it
1818
# but the driver tuple needs to be empty for that to happen
1919
pyglet.options.audio = ()

arcade/utils.py

Lines changed: 2 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,8 @@
3131
_T = TypeVar("_T")
3232
_TType = TypeVar("_TType", bound=type)
3333

34+
is_pyodide = True if sys.platform == "emscripten" else False
35+
3436

3537
class Chain(Generic[_T]):
3638
"""A reusable OOP version of :py:class:`itertools.chain`.
@@ -255,12 +257,6 @@ def __deepcopy__(self, memo): # noqa
255257
return decorated_type
256258

257259

258-
def is_pyodide() -> bool:
259-
if sys.platform == "emscripten":
260-
return True
261-
return False
262-
263-
264260
def is_raspberry_pi() -> bool:
265261
"""Determine if the host is a raspberry pi."""
266262
return get_raspberry_pi_info()[0]

webplayground/server.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@
2020
here = Path(__file__).parent.resolve()
2121

2222
path_arcade = Path("../")
23-
arcade_wheel_filename = "arcade-4.0.0.dev3-py3-none-any.whl"
23+
arcade_wheel_filename = "arcade-4.0.0.dev4-py3-none-any.whl"
2424
path_arcade_wheel = path_arcade / "dist" / arcade_wheel_filename
2525

2626
# Directory for local test scripts

0 commit comments

Comments
 (0)