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
59 changes: 55 additions & 4 deletions arcade/application.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@

from arcade.utils import is_pyodide

if is_pyodide():
if is_pyodide:
pyglet.options.backend = "webgl"

import pyglet.config
Expand Down Expand Up @@ -193,7 +193,7 @@ def __init__(
pyglet.options.dpi_scaling = "platform"

desired_gl_provider = "opengl"
if is_pyodide():
if is_pyodide:
gl_api = "webgl"

if gl_api == "webgl":
Expand Down Expand Up @@ -304,8 +304,9 @@ def __init__(
assert update_rate <= draw_rate, (
"An arcade window's draw rate cannot be faster than its update rate"
)
self._draw_rate = max(update_rate, draw_rate)
self._draw_rate = min(update_rate, draw_rate)
self._accumulated_draw_time: float = 0.0
self._accumulated_update_time: float = 0.0

# Fixed rate cannot be changed post initialization as this throws off physics sims.
# If more time resolution is needed in fixed updates, devs can do 'sub-stepping'.
Expand Down Expand Up @@ -578,10 +579,22 @@ def _dispatch_frame(self, delta_time: float) -> None:
The modulus on the accumulated draw time means that when the update rate is greater
than the draw rate no time is lost.

This method is entirely skipped when running in pyodide, this is because the event loop
is driven by requestAnimationFrame in the browser, which adds some unique limitations and
considerations around Arcade's event loop handling. In pyglet, the draw() function of the
window is called directly during the requestAnimationFrame loop, so Arcade handles special
control of the update/draw timing directly in that function. Arcade's version of this function
is never called on desktop, because this function is called instead, and this calls directly
to the superclass's implementation.

Args:
delta_time: The amount of time since the last update.
"""
if is_pyodide:
return

self._dispatch_updates(delta_time)

self._accumulated_draw_time += delta_time

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

# In case the window close in on_update, on_fixed_update or input callbacks
if not self.closed:
self.draw(self._accumulated_draw_time)
super().draw(self._accumulated_draw_time)
self._accumulated_draw_time %= self._draw_rate

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

def draw(self, dt: float) -> None:
"""
Render a frame.

On desktop this is driven by arcade's clock-scheduled
:meth:`_dispatch_frame`, which calls the super version of this method direclty.
This implementation is only called when using Pyglet's pyodide backend as part of it's
requestAnimationFrame loop.

The loop rate in a browser is tied inherently to the requestAnimationFrame speed, which
is tied to the monitor's refresh rate, so basically the Arcade loop can never be called
faster than the monitor refresh rate in a browser. This method does some special handling
of the update rate to make the updates happen multiple times per loop to achieve the target
update rate if it is higher than the refresh rate.

It does not bypass the refresh rate for draw rate, because the framebuffer will never drawn
faster to the canvas than that anyways, so us running it faster than that is pointless.
"""
self._accumulated_update_time += dt
while self._accumulated_update_time >= self._update_rate:
GLOBAL_CLOCK.tick(self._update_rate)
fixed_count = 0
while GLOBAL_FIXED_CLOCK.accumulated >= self._fixed_rate and (
self._fixed_frame_cap is None or fixed_count <= self._fixed_frame_cap
):
GLOBAL_FIXED_CLOCK.tick(self._fixed_rate)
self.dispatch_event("on_fixed_update", self._fixed_rate)
fixed_count += 1

self.dispatch_event("on_update", GLOBAL_CLOCK.delta_time)
self._accumulated_update_time -= self._update_rate

self._accumulated_draw_time += dt
if self._accumulated_draw_time < self._draw_rate:
return
self._accumulated_draw_time %= self._draw_rate
super().draw(dt)

def flip(self) -> None:
"""
Present the rendered content to the screen.
Expand Down
2 changes: 1 addition & 1 deletion arcade/sound.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@

if os.environ.get("ARCADE_SOUND_BACKENDS"):
pyglet.options.audio = tuple(v.strip() for v in os.environ["ARCADE_SOUND_BACKENDS"].split(","))
elif is_pyodide():
elif is_pyodide:
# Pyglet will also detect Pyodide and auto select the driver for it
# but the driver tuple needs to be empty for that to happen
pyglet.options.audio = ()
Expand Down
8 changes: 2 additions & 6 deletions arcade/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,8 @@
_T = TypeVar("_T")
_TType = TypeVar("_TType", bound=type)

is_pyodide = True if sys.platform == "emscripten" else False


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


def is_pyodide() -> bool:
if sys.platform == "emscripten":
return True
return False


def is_raspberry_pi() -> bool:
"""Determine if the host is a raspberry pi."""
return get_raspberry_pi_info()[0]
Expand Down
2 changes: 1 addition & 1 deletion webplayground/server.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@
here = Path(__file__).parent.resolve()

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

# Directory for local test scripts
Expand Down
Loading