Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 11 additions & 5 deletions src/EffectComposer.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -271,19 +271,25 @@ export const EffectComposer = /* @__PURE__ */ memo(function EffectComposer({
])

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

useFrame(
(_, delta) => {
if (!enabled || !composerState) return
const { composer } = composerState

gl.getSize(glSize)
if (glSize.width !== appliedSizeRef.current.width || glSize.height !== appliedSizeRef.current.height) {
const pixelRatio = gl.getPixelRatio()
const applied = appliedSizeRef.current
if (glSize.width !== applied.width || glSize.height !== applied.height || pixelRatio !== applied.pixelRatio) {
composer.setSize(glSize.width, glSize.height)
appliedSizeRef.current.width = glSize.width
appliedSizeRef.current.height = glSize.height
applied.width = glSize.width
applied.height = glSize.height
applied.pixelRatio = pixelRatio
}

const currentAutoClear = gl.autoClear
Expand Down
69 changes: 68 additions & 1 deletion src/tests/EffectComposer.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ import {
} from 'postprocessing'
import * as React from 'react'
import * as THREE from 'three'
import { afterEach, describe, expect, it, vi } from 'vitest'
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
import { EffectComposer } from '../EffectComposer'
import { ColorAverage } from '../effects/ColorAverage'
import { wrapEffect } from '../wrapEffect'
Expand Down Expand Up @@ -1462,4 +1462,71 @@ describe('EffectComposer', () => {
initializeSpy.mockRestore()
})
})

describe('sizing', () => {
// frameloop is 'never', so frames are advanced by hand. The GL context is mocked, so only
// the sizing check runs, not the render. The test canvas has no layout, so give it a size
const renderer = () => root.render(null).getState().gl

beforeEach(() => renderer().setSize(640, 400, false))

afterEach(() => {
renderer().setPixelRatio(1)
renderer().setSize(0, 0, false)
})

const mountSized = async () => {
const ref = React.createRef<EffectComposerImpl>()
const store = await React.act(async () =>
root.render(
<EffectComposer ref={ref}>
<WrappedEffectA />
</EffectComposer>
)
)
const composer = await waitForComposer(ref)
vi.spyOn(composer, 'render').mockImplementation(() => {})
const frame = () => React.act(async () => store.getState().advance(performance.now()))
await frame()
return { gl: store.getState().gl, composer, frame, setSize: vi.spyOn(composer, 'setSize') }
}

it('does not resize the composer on frames where nothing changed', async () => {
const { frame, setSize } = await mountSized()
await frame()
await frame()
expect(setSize).not.toHaveBeenCalled()
})

it('resizes the composer when only the pixel ratio changes', async () => {
const { gl, composer, frame, setSize } = await mountSized()
const cssSize = gl.getSize(new THREE.Vector2())

// What r3f's setDpr and drei's AdaptiveDpr do: the CSS size stays, the drawing buffer doesn't
gl.setPixelRatio(2)
await frame()

expect(setSize).toHaveBeenCalledTimes(1)
expect(setSize).toHaveBeenCalledWith(cssSize.width, cssSize.height)
expect(composer.inputBuffer.width).toBe(cssSize.width * 2)
expect(composer.inputBuffer.height).toBe(cssSize.height * 2)

await frame()
expect(setSize).toHaveBeenCalledTimes(1)
})

it('shrinks its buffers when the pixel ratio drops', async () => {
// e.g. AdaptiveDpr regressing, or the window moving to a 1x display
renderer().setPixelRatio(2)
const { gl, composer, frame } = await mountSized()
const cssSize = gl.getSize(new THREE.Vector2())
expect(composer.inputBuffer.width).toBe(cssSize.width * 2)

gl.setPixelRatio(1)
await frame()

expect(composer.inputBuffer.width).toBe(cssSize.width)
expect(composer.outputBuffer.width).toBe(cssSize.width)
})
})
})