Repository navigation
Fix drawing on scaled (HiDPI) displays - #2926
Merged
Merged
Conversation
pyglet draws text (and any pyglet batch) with the window camera's viewport and scissor, which it reads from Window.camera, Arcade's DefaultProjector, and applies in framebuffer pixels. DefaultProjector gave them in window units. On a scaled display in pyglet's "stretch" mode, which Arcade uses by default, text was drawn too small, and the too small viewport and scissor stayed set: from the second frame on, the whole game was drawn in the bottom left corner of the window. GUI text was drawn outside its widgets. - DefaultProjector.viewport and get_group_scissor_area() now return the active framebuffer's viewport and scissor in pixels, which is what Arcade already set, whichever camera set it. - DefaultProjector compares and falls back to the framebuffer's size in window units, so a viewport covering the window is stored as None. - Window.get_pixel_ratio() returns the framebuffer to window size ratio. pyglet 3's version returns the display scale, which differs in "platform" DPI mode, so the test suite drew everything scaled up on a scaled Windows display, and three render tests failed there. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This was referenced Oct 6, 2026
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Fixes drawing on scaled (HiDPI) displays, such as Windows set to 125% or 150%. I found it while working on #2923, where sprites came out 1.25× too big in tests on my 125% display.
Bug 1: after the first frame with text, the whole game is drawn too small
This is in Arcade's default DPI mode, pyglet's "stretch", so it affects real games.
When pyglet draws text (or any pyglet batch), it sets the viewport and scissor from
Window.camera, which is Arcade'sDefaultProjector. pyglet applies them in framebuffer pixels (pyglet/graphics/draw.py,apply_viewportandapply_scissor), butDefaultProjectorgave them in window units. On a 200×100 window with a 250×125 framebuffer:I measured these in Arcade's real event loop (
arcade.run()). The window is 200×100 logical, 250×125 pixels:development, frame 1development, frames 2+Camera2DA
UILabelwith a background, at 125%:developmentBug 2:
get_pixel_ratio()returns the display scaleIn pyglet 3,
Window.get_pixel_ratio()is deprecated and returnsWindow.scale, which is DPI / 96. Arcade uses it as framebuffer size ÷ window size. The two differ in pyglet's "platform" DPI mode: on Windows at 125% the framebuffer is the window's size, but the ratio is still 1.25. Arcade then made the screen viewport 1.25× the framebuffer, so everything on screen was drawn scaled up and cut off at the right and top.The test suite runs in "platform" mode (
ARCADE_TEST). That's whytest_draw_primitives,test_render_sprite_solid_pixelsandtest_get_imagealways failed on my machine but passed in CI.pixel_perfect=Truealready forces the ratio to 1.0, so it wasn't affected.The fix
Window.get_pixel_ratio()returns framebuffer width ÷get_size()width. This doesn't use_width, because in stretch mode pyglet stores the physical width there. The pyodide branch is unchanged. On macOS Retina, the result is the same as before (2.0).DefaultProjector.viewportreturns the active framebuffer's viewport in pixels, which is what Arcade has already set, whichever camera set it. pyglet then re-applies the same viewport instead of changing it.get_group_scissor_area()returns pixels too. Nothing in Arcade readsDefaultProjector.viewport; Arcade usesget_current_viewport(),widthandheight, which stay in window units.DefaultProjector.update_viewport(),width,heightandget_current_viewport()compare with, and fall back to, the framebuffer's size in window units. Before, they mixed in pixels. The fallback to pixels only happened to be avoided because the comparison never matched on a scaled screen.Offscreen framebuffers aren't affected, since they have their own pixel size, and a test checks this.
Tests
New
tests/unit/window/test_hidpi.py. CI runs at a ratio of 1, so the tests make the window report a framebuffer twice its size, like stretch mode on a 200% display:Camera2D.All 7 fail on
development. The full suite passes on my 125% display for the first time: 1581 passed, no warnings, including the 3 render tests that used to fail only there. Ruff is clean except for one over-long docstring line in_dispatch_frame, which was already there. mypy has no new errors.The changelog has an entry under Unreleased → Fixes.
🤖 Generated with Claude Code