Skip to content

Commit ef65ef3

Browse files
authored
Merge pull request #381 from tonyblu331/fix/280-composer-pixel-ratio
fix(EffectComposer): resize the composer when the pixel ratio changes
2 parents 3da552a + 98a7111 commit ef65ef3

2 files changed

Lines changed: 79 additions & 6 deletions

File tree

‎src/EffectComposer.tsx‎

Lines changed: 11 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -271,19 +271,25 @@ export const EffectComposer = /* @__PURE__ */ memo(function EffectComposer({
271271
])
272272

273273
// Last size actually applied to the composer, so the check below is a
274-
// cheap no-op on frames where nothing changed.
275-
const appliedSizeRef = useRef({ width: -1, height: -1 })
274+
// cheap no-op on frames where nothing changed. The pixel ratio is part of
275+
// it: setDpr, AdaptiveDpr or moving to another display change the drawing
276+
// buffer without changing gl.getSize(), and composer.setSize() sizes its
277+
// buffers and passes from the drawing buffer.
278+
const appliedSizeRef = useRef({ width: -1, height: -1, pixelRatio: -1 })
276279

277280
useFrame(
278281
(_, delta) => {
279282
if (!enabled || !composerState) return
280283
const { composer } = composerState
281284

282285
gl.getSize(glSize)
283-
if (glSize.width !== appliedSizeRef.current.width || glSize.height !== appliedSizeRef.current.height) {
286+
const pixelRatio = gl.getPixelRatio()
287+
const applied = appliedSizeRef.current
288+
if (glSize.width !== applied.width || glSize.height !== applied.height || pixelRatio !== applied.pixelRatio) {
284289
composer.setSize(glSize.width, glSize.height)
285-
appliedSizeRef.current.width = glSize.width
286-
appliedSizeRef.current.height = glSize.height
290+
applied.width = glSize.width
291+
applied.height = glSize.height
292+
applied.pixelRatio = pixelRatio
287293
}
288294

289295
const currentAutoClear = gl.autoClear

‎src/tests/EffectComposer.test.tsx‎

Lines changed: 68 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ import {
1111
} from 'postprocessing'
1212
import * as React from 'react'
1313
import * as THREE from 'three'
14-
import { afterEach, describe, expect, it, vi } from 'vitest'
14+
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
1515
import { EffectComposer } from '../EffectComposer'
1616
import { ColorAverage } from '../effects/ColorAverage'
1717
import { wrapEffect } from '../wrapEffect'
@@ -1462,4 +1462,71 @@ describe('EffectComposer', () => {
14621462
initializeSpy.mockRestore()
14631463
})
14641464
})
1465+
1466+
describe('sizing', () => {
1467+
// frameloop is 'never', so frames are advanced by hand. The GL context is mocked, so only
1468+
// the sizing check runs, not the render. The test canvas has no layout, so give it a size
1469+
const renderer = () => root.render(null).getState().gl
1470+
1471+
beforeEach(() => renderer().setSize(640, 400, false))
1472+
1473+
afterEach(() => {
1474+
renderer().setPixelRatio(1)
1475+
renderer().setSize(0, 0, false)
1476+
})
1477+
1478+
const mountSized = async () => {
1479+
const ref = React.createRef<EffectComposerImpl>()
1480+
const store = await React.act(async () =>
1481+
root.render(
1482+
<EffectComposer ref={ref}>
1483+
<WrappedEffectA />
1484+
</EffectComposer>
1485+
)
1486+
)
1487+
const composer = await waitForComposer(ref)
1488+
vi.spyOn(composer, 'render').mockImplementation(() => {})
1489+
const frame = () => React.act(async () => store.getState().advance(performance.now()))
1490+
await frame()
1491+
return { gl: store.getState().gl, composer, frame, setSize: vi.spyOn(composer, 'setSize') }
1492+
}
1493+
1494+
it('does not resize the composer on frames where nothing changed', async () => {
1495+
const { frame, setSize } = await mountSized()
1496+
await frame()
1497+
await frame()
1498+
expect(setSize).not.toHaveBeenCalled()
1499+
})
1500+
1501+
it('resizes the composer when only the pixel ratio changes', async () => {
1502+
const { gl, composer, frame, setSize } = await mountSized()
1503+
const cssSize = gl.getSize(new THREE.Vector2())
1504+
1505+
// What r3f's setDpr and drei's AdaptiveDpr do: the CSS size stays, the drawing buffer doesn't
1506+
gl.setPixelRatio(2)
1507+
await frame()
1508+
1509+
expect(setSize).toHaveBeenCalledTimes(1)
1510+
expect(setSize).toHaveBeenCalledWith(cssSize.width, cssSize.height)
1511+
expect(composer.inputBuffer.width).toBe(cssSize.width * 2)
1512+
expect(composer.inputBuffer.height).toBe(cssSize.height * 2)
1513+
1514+
await frame()
1515+
expect(setSize).toHaveBeenCalledTimes(1)
1516+
})
1517+
1518+
it('shrinks its buffers when the pixel ratio drops', async () => {
1519+
// e.g. AdaptiveDpr regressing, or the window moving to a 1x display
1520+
renderer().setPixelRatio(2)
1521+
const { gl, composer, frame } = await mountSized()
1522+
const cssSize = gl.getSize(new THREE.Vector2())
1523+
expect(composer.inputBuffer.width).toBe(cssSize.width * 2)
1524+
1525+
gl.setPixelRatio(1)
1526+
await frame()
1527+
1528+
expect(composer.inputBuffer.width).toBe(cssSize.width)
1529+
expect(composer.outputBuffer.width).toBe(cssSize.width)
1530+
})
1531+
})
14651532
})

0 commit comments

Comments
 (0)