Repository navigation
feat: add maxPixelRatio prop to Application - #361
Conversation
Add a `maxPixelRatio` prop to `Application` (and `ApplicationWithoutCanvas`)
that sets `app.graphicsDevice.maxPixelRatio`. The engine caps the ratio at
1 by default, so apps render at CSS resolution and look soft on high-DPI
phones. Raising it from a component meant mutating `useApp().graphicsDevice`
in an effect, which the `react-hooks/immutability` lint rule rejects.
Like `fillMode` and `resolutionMode`, the prop is applied in its own effect
once the app exists, and re-applied when it changes. That effect runs before
the fill and resolution one, so the canvas resize it triggers already uses
the new ratio. When the prop is omitted the engine default is left alone,
and removing the prop restores it. Values must be finite numbers greater
than 0; anything else warns and keeps the default.
Once this ships, the `web-lens` starter in create-playcanvas can drop its
`useEffect` workaround (behind `eslint-disable-next-line
react-hooks/immutability`) and use the prop instead, for example
`<Application maxPixelRatio={Math.min(window.devicePixelRatio, 2)}>`.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
🦋 Changeset detectedLatest commit: a50757a The changes in this PR will be included in the next version bump. This PR includes changesets to release 2 packages
Not sure what this means? Click here to learn what changesets are. Click here if you're a maintainer who wants to add another changeset to this PR |
commit: |
abstrakt8
left a comment
There was a problem hiding this comment.
Automated review by Claude Opus and GPT-6 Astra.
Thanks, this looks like the right shape to me. A top-level prop fits with fillMode/resolutionMode and <pc-app max-pixel-ratio>, and graphicsDeviceOptions isn't the place for it, since those are creation options and changing them recreates the app. A few things I'd tighten before merging:
- Clamp to
devicePixelRatiointernally. The engine clamps inresizeCanvas, but the picker divides by the rawmaxPixelRatio(picker.tsx). SomaxPixelRatio={3}on a 2x screen would offset picks. I'd rather enforce the limit in code than in the docs. - Re-apply after device restore. On WebGPU,
initDeviceCapsresets the ratio to 1 when the device is restored, and the effect won't re-run because its deps don't change. Adevicerestoredlistener, removed in cleanup, would cover that. - Make the doc example SSR-safe.
Math.min(window.devicePixelRatio, 2)runs during render. With an internal clamp, the example can just bemaxPixelRatio={2}. - Optional: follow DPR changes, for example when the window moves to another monitor, via a
matchMediaresolution listener like web-components uses. - Tests: it would be good to also assert the actual canvas size, and the re-apply after restore.
Separately, it might be worth an issue to discuss changing the default to match web-components (full DPR) in the next major. That's a performance trade-off, so it's out of scope here.
Address review on #361: - Hand the graphics device the ratio in effect, the smaller of the prop and `window.devicePixelRatio`, rather than the raw cap. The picker divides by `maxPixelRatio`, so a cap above the display's ratio offset picks. With the clamp, `Infinity` is accepted and renders at the display's full resolution. - Apply the ratio again on `devicerestored`, since WebGPU resets it to 1 when it restores a lost device. WebGPU fires that event from engine 2.19. - Follow changes to the display's ratio with a `matchMedia` resolution query, as `<pc-app>` does, so moving the window to another monitor updates it. - Make the doc example SSR-safe: `maxPixelRatio={2}`. - Move the device logic into `utils/max-pixel-ratio.ts`. The effect no longer needs an eslint-disable. - Test the canvas size, the clamp, the restore and the display change. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
abstrakt8
left a comment
There was a problem hiding this comment.
Automated review by Claude Opus and GPT-6 Astra.
Thanks, a50757a addresses all the points: the clamp to the display ratio, re-applying on devicerestored, following display changes, the SSR-safe example and the tests. Approving.
|
Will run some real device testing, then merge it in |
|
Verified on real devices at a50757a:
Not tested: iOS Safari, Firefox, and WebGPU on Android (the engine disables it on that GPU). |
Summary
Adds a
maxPixelRatioprop toApplication(andApplicationWithoutCanvas) that caps the pixel ratio the canvas renders at:The engine caps the pixel ratio at 1 by default (
Math.min(1, devicePixelRatio)on WebGL2,1on WebGPU), so apps render at CSS resolution and look soft on high-DPI phones. Raising the cap from a component meant mutatinguseApp().graphicsDevicein an effect. Thereact-hooks/immutabilitylint rule rejects that, so every app needed aneslint-disablefor it.@playcanvas/web-componentsalready exposes this setting as<pc-app max-pixel-ratio>, and this prop works the same way.Change
window.devicePixelRatio. The engine takes that minimum itself when it sizes the canvas, but it also readsmaxPixelRatioback elsewhere (the picker, UI element drags and XR). The picker divides by it, so an unclamped cap above the display's ratio would offset picks.Infinityrenders at the display's full resolution.matchMediaresolution query, as<pc-app>uses, applies it again when the window moves to another monitor or the page is zoomed.devicerestored, because WebGPU resets the ratio to 1 when it restores a lost device. WebGPU fires that event from engine 2.19, so on older engines a WebGPU restore still drops the ratio back to 1.fillModeandresolutionMode, the prop is applied in its own effect once the app exists, and re-applied when it changes. That effect runs just before theirs, so the canvas resize their calls trigger already uses the new ratio.0, negatives,NaN) logs a warning and keeps the default.utils/max-pixel-ratio.ts, so the effect needs no eslint-disable.@playcanvas/react.Testing
Application.test.tsx, on a stubbed 3x display, cover:4andInfinityto the display's ratiodevicerestored, and not once the prop is removed0,-1andNaNpnpm test: 156 passed, 16 skipped (13 more passing thanmain).pnpm run lint: no new errors or warnings.pnpm run build:libandpnpm run build:blockspass.Once this is released, the upcoming
web-lensstarter in create-playcanvas can use this prop instead of itsuseEffectworkaround.🤖 Generated with Claude Code