From b34ac1786f0acca331fb96cb222fa53b491f00ed Mon Sep 17 00:00:00 2001 From: Darren Eberly Date: Sun, 3 May 2026 20:52:39 -0400 Subject: [PATCH] Handle pixel scaling properly in WebGL backend --- arcade/application.py | 18 +++++++++++++++++ arcade/gl/backends/webgl/framebuffer.py | 27 +++++++++++++++++-------- 2 files changed, 37 insertions(+), 8 deletions(-) diff --git a/arcade/application.py b/arcade/application.py index 9da771ea7..4dbf6190b 100644 --- a/arcade/application.py +++ b/arcade/application.py @@ -968,6 +968,16 @@ def get_pixel_ratio(self) -> float: """ if self._pixel_perfect: return 1.0 + if is_pyodide: + # Pyglet's emscripten window caches devicePixelRatio at init, but the + # actual canvas drawing buffer is sized via getBoundingClientRect() + # which can be sub-pixel less than logical_size * devicePixelRatio. + # Returning fb_size / logical_size matches the canvas exactly, so + # full-canvas viewport round-trips through Camera2D don't leave a + # 1-2 pixel gap on the top/right edges. + log_w = self._width + if log_w: + return self.get_framebuffer_size()[0] / log_w return super().get_pixel_ratio() def _on_resize(self, width: int, height: int) -> EVENT_HANDLE_STATE: @@ -1038,6 +1048,14 @@ def set_size(self, width: int, height: int) -> None: def get_size(self) -> tuple[int, int]: """Get the size of the window.""" + if is_pyodide: + # Pyglet's emscripten window returns the canvas drawing-buffer + # size (physical, DPI-scaled pixels) from get_size(); desktop + # pyglet returns logical pixels. Return logical pixels here so + # viewport math in show_view/_on_resize stays consistent across + # backends and Camera2D doesn't render into a sub-region of the + # canvas on HiDPI displays. + return self._width, self._height return super().get_size() def get_location(self) -> tuple[int, int]: diff --git a/arcade/gl/backends/webgl/framebuffer.py b/arcade/gl/backends/webgl/framebuffer.py index a4d30975b..f7f338d7a 100644 --- a/arcade/gl/backends/webgl/framebuffer.py +++ b/arcade/gl/backends/webgl/framebuffer.py @@ -256,15 +256,20 @@ def __init__(self, ctx: WebGLContext): @DefaultFrameBuffer.viewport.setter def viewport(self, value: tuple[int, int, int, int]): - # This is very similar to the OpenGL backend setter - # WebGL backend doesn't need to handle pixel scaling for the - # default framebuffer like desktop does, the browser does that - # for us. However we need a separate implementation for the - # function because of ABC + # Pyglet sizes the canvas drawing buffer at physical pixels + # (canvas.width = logical_width * devicePixelRatio), so we apply + # the same pixel-ratio multiply as the OpenGL backend to keep the + # default framebuffer's get/set symmetric in logical pixels. if not isinstance(value, tuple) or len(value) != 4: - raise ValueError("viewport shouldbe a 4-component tuple") + raise ValueError("viewport should be a 4-component tuple") - self._viewport = value + ratio = self.ctx.window.get_pixel_ratio() + self._viewport = ( + int(value[0] * ratio), + int(value[1] * ratio), + int(value[2] * ratio), + int(value[3] * ratio), + ) if self._ctx.active_framebuffer == self: self._ctx._gl.viewport(*self._viewport) @@ -280,6 +285,12 @@ def scissor(self, value): if self._ctx.active_framebuffer == self: self._ctx._gl.scissor(*self._viewport) else: - self._scissor = value + ratio = self.ctx.window.get_pixel_ratio() + self._scissor = ( + int(value[0] * ratio), + int(value[1] * ratio), + int(value[2] * ratio), + int(value[3] * ratio), + ) if self._ctx.active_framebuffer == self: self._ctx._gl.scissor(*self._scissor)