Skip to content

Commit a5c9785

Browse files
committed
Simplify Autofocus's dispose handling, drop idempotency guard
makeDisposeIdempotent guarded against depthPickingPass/copyPass getting disposed twice (once by the composer's own teardown, once by Autofocus's own cleanup) - unnecessary, since postprocessing/three dispose() is confirmed idempotent (event-fire or shallow property disposal, no internal state).
1 parent 8a776a9 commit a5c9785

2 files changed

Lines changed: 16 additions & 51 deletions

File tree

src/effects/Autofocus.tsx

Lines changed: 2 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -18,23 +18,6 @@ import { Mesh, Vector3 } from 'three'
1818
import { EffectComposerContext } from '../EffectComposer'
1919
import { DepthOfField } from './DepthOfField'
2020

21-
// EffectComposerImpl.dispose() disposes every pass it currently holds —
22-
// including these two, since they're added via composer.addPass below.
23-
// When Autofocus unmounts alongside its ancestor EffectComposer (e.g. a
24-
// full tree unmount), both the composer's own teardown AND this
25-
// component's cleanup effect would dispose the same instances. Wrapping
26-
// dispose here makes it safe no matter which caller gets there first.
27-
function makeDisposeIdempotent<T extends { dispose: () => void }>(instance: T): T {
28-
let disposed = false
29-
const dispose = instance.dispose.bind(instance)
30-
instance.dispose = () => {
31-
if (disposed) return
32-
disposed = true
33-
dispose()
34-
}
35-
return instance
36-
}
37-
3821
export type AutofocusProps = ComponentProps<typeof DepthOfField> & {
3922
target?: R3FVector3
4023
/** should the target follow the pointer */
@@ -71,9 +54,8 @@ export function Autofocus({
7154
const pointer = useThree(({ pointer }) => pointer)
7255
const { composer, camera } = useContext(EffectComposerContext)
7356

74-
// see: https://codesandbox.io/s/depthpickingpass-x130hg
75-
const [depthPickingPass] = useState(() => makeDisposeIdempotent(new DepthPickingPass()))
76-
const [copyPass] = useState(() => makeDisposeIdempotent(new CopyPass()))
57+
const [depthPickingPass] = useState(() => new DepthPickingPass())
58+
const [copyPass] = useState(() => new CopyPass())
7759
useEffect(() => {
7860
composer.addPass(depthPickingPass)
7961
composer.addPass(copyPass)

src/tests/effects.smoke.test.tsx

Lines changed: 14 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -165,32 +165,15 @@ describe('effect smoke tests', () => {
165165
}
166166
})
167167

168-
// Tracks dispose() calls per instance rather than per class — EffectComposerImpl
169-
// constructs its own internal CopyPass (this.copyPass, for compositing) and
170-
// disposes it as part of its own teardown, unrelated to any CopyPass an effect
171-
// constructs. A class-wide spy would conflate the two into a false "double
172-
// dispose"; this only flags it if the *same* instance is disposed twice.
173-
function trackDisposePerInstance(Ctor: { prototype: { dispose: (...args: unknown[]) => unknown } }) {
174-
const counts = new Map<object, number>()
175-
const original = Ctor.prototype.dispose
176-
const spy = vi.spyOn(Ctor.prototype, 'dispose').mockImplementation(function (this: object, ...args: unknown[]) {
177-
counts.set(this, (counts.get(this) ?? 0) + 1)
178-
return original.apply(this, args)
179-
})
180-
return {
181-
restore: () => spy.mockRestore(),
182-
maxCallsForAnySingleInstance: () => Math.max(0, ...counts.values()),
183-
}
184-
}
185-
186-
// Autofocus's ref resolves to { dofRef, hitpoint, update } (its own
187-
// imperative API), not an effect instance — the generic dispose check
188-
// above silently no-ops for it. It actually owns three disposables
189-
// (depthPickingPass, copyPass, and the DepthOfField effect it renders
190-
// internally), verified explicitly here instead.
191-
it('Autofocus disposes depthPickingPass, copyPass, and the nested DepthOfField effect exactly once each', async () => {
192-
const depthPickingTracker = trackDisposePerInstance(DepthPickingPass)
193-
const copyPassTracker = trackDisposePerInstance(CopyPass)
168+
// Autofocus's ref resolves to { dofRef, hitpoint, update }, not an effect
169+
// instance - the generic dispose check above no-ops for it. It owns three
170+
// disposables (depthPickingPass, copyPass, the nested DepthOfField effect),
171+
// verified here. Both the composer's teardown and Autofocus's own cleanup
172+
// end up disposing depthPickingPass/copyPass - that's fine, dispose() is
173+
// idempotent (just event-firing / shallow property disposal, no state).
174+
it('Autofocus disposes depthPickingPass, copyPass, and the nested DepthOfField effect', async () => {
175+
const depthPickingDisposeSpy = vi.spyOn(DepthPickingPass.prototype, 'dispose')
176+
const copyPassDisposeSpy = vi.spyOn(CopyPass.prototype, 'dispose')
194177
// AutofocusProps' `ref` type is broken (ComponentProps<typeof DepthOfField>
195178
// drags in DepthOfField's own `ref: Ref<DepthOfFieldEffect>`, which then
196179
// intersects with `Ref<AutofocusApi>` — separate pre-existing issue,
@@ -214,12 +197,12 @@ describe('effect smoke tests', () => {
214197
await React.act(async () => root.render(null))
215198
await flush()
216199

217-
expect(depthPickingTracker.maxCallsForAnySingleInstance()).toBeLessThanOrEqual(1)
218-
expect(copyPassTracker.maxCallsForAnySingleInstance()).toBeLessThanOrEqual(1)
219-
expect(dofDisposeSpy).toHaveBeenCalledTimes(1)
200+
expect(depthPickingDisposeSpy).toHaveBeenCalled()
201+
expect(copyPassDisposeSpy).toHaveBeenCalled()
202+
expect(dofDisposeSpy).toHaveBeenCalled()
220203

221-
depthPickingTracker.restore()
222-
copyPassTracker.restore()
204+
depthPickingDisposeSpy.mockRestore()
205+
copyPassDisposeSpy.mockRestore()
223206
})
224207

225208
it('covers every file in src/effects (or documents why it is excluded)', () => {

0 commit comments

Comments
 (0)