From 33e1f599e0a075c5e8e919b1201e74fddf2b21d6 Mon Sep 17 00:00:00 2001 From: Paul V Craven Date: Thu, 16 Apr 2026 15:49:10 -0500 Subject: [PATCH 1/3] feat: add pixel_perfect option for 1:1 pixel ratio rendering --- arcade/application.py | 31 +++++++++++++++++++++++++++---- 1 file changed, 27 insertions(+), 4 deletions(-) diff --git a/arcade/application.py b/arcade/application.py index 7f7c46e63..69a819988 100644 --- a/arcade/application.py +++ b/arcade/application.py @@ -175,6 +175,7 @@ def __init__( fixed_rate: float = 1.0 / 60.0, fixed_frame_cap: int | None = None, file_drops: bool = False, + pixel_perfect: bool = False, **kwargs, ) -> None: # In certain environments we can't have antialiasing/MSAA enabled. @@ -182,6 +183,9 @@ def __init__( if os.environ.get("REPL_ID"): antialiasing = False + if pixel_perfect: + pyglet.options.dpi_scaling = "platform" + desired_gl_provider = "opengl" if is_pyodide(): gl_api = "webgl" @@ -199,16 +203,25 @@ def __init__( """Indicates if the window was closed""" self.headless: bool = arcade.headless """If True, the window is running in headless mode.""" + self._pixel_perfect: bool = pixel_perfect + """If True, ignore OS DPI scaling and use a 1:1 pixel ratio.""" config = None # Attempt to make window with antialiasing if gl_api == "opengl" or gl_api == "opengles": + from pyglet.enums import GraphicsAPI + _api_map = { + "opengl": GraphicsAPI.OPENGL, + "opengles": GraphicsAPI.OPENGL_ES_3, + } + _graphics_api = _api_map.get(gl_api, GraphicsAPI.OPENGL) + if antialiasing: try: - config = pyglet.config.OpenGLConfig( + config = pyglet.config.OpenGLUserConfig( major_version=gl_version[0], minor_version=gl_version[1], - opengl_api=gl_api.replace("open", ""), # type: ignore # pending: upstream fix + api=_graphics_api, double_buffer=True, sample_buffers=1, samples=samples, @@ -225,10 +238,10 @@ def __init__( antialiasing = False # If we still don't have a config if not config: - config = pyglet.config.OpenGLConfig( + config = pyglet.config.OpenGLUserConfig( major_version=gl_version[0], minor_version=gl_version[1], - opengl_api=gl_api.replace("open", ""), # type: ignore # pending: upstream fix + api=_graphics_api, double_buffer=True, depth_size=24, stencil_size=8, @@ -891,6 +904,16 @@ def on_draw(self) -> EVENT_HANDLE_STATE: return EVENT_UNHANDLED + def get_pixel_ratio(self) -> float: + """Return the framebuffer/window size ratio. + + When ``pixel_perfect=True``, this always returns ``1.0`` so that + arcade treats the framebuffer as unscaled. + """ + if self._pixel_perfect: + return 1.0 + return super().get_pixel_ratio() + def _on_resize(self, width: int, height: int) -> EVENT_HANDLE_STATE: """ The internal method called when the window is resized. From 3fa3139b47712f31ee4a6076f1a38240315a0d9d Mon Sep 17 00:00:00 2001 From: Paul V Craven Date: Thu, 16 Apr 2026 16:01:27 -0500 Subject: [PATCH 2/3] revert: remove pyglet 3.0.dev2 API naming changes MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Reverts OpenGLConfig→OpenGLUserConfig and opengl_api→api changes that were accidentally introduced while prototyping pixel_perfect. Arcade targets pyglet 3.0.dev1 which uses the original names. Co-Authored-By: Claude Opus 4.6 --- arcade/application.py | 15 ++++----------- 1 file changed, 4 insertions(+), 11 deletions(-) diff --git a/arcade/application.py b/arcade/application.py index 69a819988..32183ee85 100644 --- a/arcade/application.py +++ b/arcade/application.py @@ -209,19 +209,12 @@ def __init__( config = None # Attempt to make window with antialiasing if gl_api == "opengl" or gl_api == "opengles": - from pyglet.enums import GraphicsAPI - _api_map = { - "opengl": GraphicsAPI.OPENGL, - "opengles": GraphicsAPI.OPENGL_ES_3, - } - _graphics_api = _api_map.get(gl_api, GraphicsAPI.OPENGL) - if antialiasing: try: - config = pyglet.config.OpenGLUserConfig( + config = pyglet.config.OpenGLConfig( major_version=gl_version[0], minor_version=gl_version[1], - api=_graphics_api, + opengl_api=gl_api.replace("open", ""), # type: ignore # pending: upstream fix double_buffer=True, sample_buffers=1, samples=samples, @@ -238,10 +231,10 @@ def __init__( antialiasing = False # If we still don't have a config if not config: - config = pyglet.config.OpenGLUserConfig( + config = pyglet.config.OpenGLConfig( major_version=gl_version[0], minor_version=gl_version[1], - api=_graphics_api, + opengl_api=gl_api.replace("open", ""), # type: ignore # pending: upstream fix double_buffer=True, depth_size=24, stencil_size=8, From 5b6377f6ce765f3286025ff34bb4924d64b13b1f Mon Sep 17 00:00:00 2001 From: Paul V Craven Date: Thu, 16 Apr 2026 16:58:14 -0500 Subject: [PATCH 3/3] feat: add pixel_perfect option to Window for 1:1 pixel ratio rendering fix: remove redundant check in camera viewport setter --- arcade/application.py | 5 +++++ arcade/camera/default.py | 2 -- 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/arcade/application.py b/arcade/application.py index 32183ee85..e72554437 100644 --- a/arcade/application.py +++ b/arcade/application.py @@ -144,6 +144,11 @@ class Window(pyglet.window.Window): file_drops: Should the window listen for file drops? If True, the window will dispatch ``on_file_drop`` events when files are dropped onto the window. + pixel_perfect: + If True, ignore OS DPI scaling and use a 1:1 pixel ratio. + The window and framebuffer will be created at exactly the + requested size. The window may appear smaller on HiDPI + displays, but rendering will be pixel-perfect. **kwargs: Further keyword arguments are passed to the pyglet window constructor. This can be used to set advanced options that aren't explicitly handled by Arcade. diff --git a/arcade/camera/default.py b/arcade/camera/default.py index b64399554..d4e30ee18 100644 --- a/arcade/camera/default.py +++ b/arcade/camera/default.py @@ -68,8 +68,6 @@ def viewport(self) -> tuple[int, int, int, int] | None: @viewport.setter def viewport(self, viewport: tuple[int, int, int, int] | None) -> None: - if viewport == self._viewport: - return self._viewport = viewport self._matrix = Mat4.orthogonal_projection( 0, self.width, 0, self.height, DEFAULT_NEAR_ORTHO, DEFAULT_FAR