From 8757d24ba79099f4bd3d5c5d85023335f69d8f20 Mon Sep 17 00:00:00 2001 From: Davide Cristini Date: Thu, 28 Mar 2024 18:27:44 +0100 Subject: [PATCH] fix: WIP fix for memory leak --- .storybook/stories/MemoryLeak.stories.tsx | 118 ++++++++++++++++++++ package.json | 2 +- src/EffectComposer.tsx | 130 ++++++++++++---------- src/effects/LensFlare.tsx | 8 +- yarn.lock | 8 +- 5 files changed, 197 insertions(+), 69 deletions(-) create mode 100644 .storybook/stories/MemoryLeak.stories.tsx diff --git a/.storybook/stories/MemoryLeak.stories.tsx b/.storybook/stories/MemoryLeak.stories.tsx new file mode 100644 index 0000000..341aebc --- /dev/null +++ b/.storybook/stories/MemoryLeak.stories.tsx @@ -0,0 +1,118 @@ +import { Box, Stats, useTexture } from '@react-three/drei' +import type { Meta, StoryObj } from '@storybook/react' +import React, { useEffect, useRef, useState } from 'react' +import * as THREE from 'three' +import { BackSide } from 'three' + +import { useFrame, useThree } from '@react-three/fiber' +import { EffectComposer, LensFlare } from '../../src' +import { Setup } from '../Setup' + +const meta = { + title: 'MemoryLeak', + component: LensFlare, + decorators: [ + (Story) => ( + + + {Story()} + + ), + ], +} satisfies Meta + +export default meta +type Story = StoryObj + +export const WithPostprocessing: Story = { + render: (args) => ( + <> + + + + + + + + + + + + + + + ), + args: {}, +} + +export const WithoutPostprocessing: Story = { + render: (args) => ( + <> + + + + + + + + + + + ), + args: {}, +} + +function CameraSwitcher() { + const { camera, set } = useThree() + const camRef = useRef(new THREE.OrthographicCamera()) + const camDef = useRef(camera) + + const keySPressedCount = useKeyPressedCount('c') + + const switchCamera = () => { + const newcam = camera === camDef.current ? camRef.current : camDef.current + set(() => ({ camera: newcam })) + + // log memory usage + // if ('memory' in performance) { + // console.log(((performance as any).memory.usedJSHeapSize / 1024 / 1024).toFixed(0)) + // } + } + + useFrame(() => { + if (keySPressedCount % 2 === 1) { + switchCamera() + } + }) + + return null +} + +function useKeyPressedCount(key: string) { + const [count, setCount] = useState(0) + + useEffect(() => { + const handler = (e: KeyboardEvent) => { + if (e.key === key) { + setCount((prev) => prev + 1) + } + } + + window.addEventListener('keydown', handler) + + return () => window.removeEventListener('keydown', handler) + }, []) + + return count +} + +function SkyBox() { + const texture = useTexture('digital_painting_golden_hour_sunset.jpg') + + return ( + + + + + ) +} diff --git a/package.json b/package.json index 1fc7f28..9e8c28c 100644 --- a/package.json +++ b/package.json @@ -49,7 +49,7 @@ "buffer": "^6.0.3", "maath": "^0.6.0", "n8ao": "^1.6.6", - "postprocessing": "^6.32.1", + "postprocessing": "^6.35.2", "three-stdlib": "^2.23.4" }, "devDependencies": { diff --git a/src/EffectComposer.tsx b/src/EffectComposer.tsx index cc3db74..2cba122 100644 --- a/src/EffectComposer.tsx +++ b/src/EffectComposer.tsx @@ -1,14 +1,6 @@ import type { TextureDataType } from 'three' import { HalfFloatType, NoToneMapping } from 'three' -import React, { - forwardRef, - useMemo, - useEffect, - useLayoutEffect, - createContext, - useRef, - useImperativeHandle, -} from 'react' +import React, { forwardRef, useMemo, useEffect, createContext, useRef, useImperativeHandle } from 'react' import { useThree, useFrame, useInstanceHandle } from '@react-three/fiber' import { EffectComposer as EffectComposerImpl, @@ -32,7 +24,7 @@ export const EffectComposerContext = createContext<{ resolutionScale?: number }>(null!) -export type EffectComposerProps = { +export type EffectComposerProps = { enabled?: boolean children: JSX.Element | JSX.Element[] depthBuffer?: boolean @@ -74,10 +66,18 @@ export const EffectComposer = React.memo( const scene = _scene || defaultScene const camera = _camera || defaultCamera - const [composer, normalPass, downSamplingPass] = useMemo(() => { + const composer = useRef() + const normalPass = useRef() + const downSamplingPass = useRef() + + const group = useRef(null) + const instance = useInstanceHandle(group) + + useEffect(() => { const webGL2Available = isWebGL2Available() + // Initialize composer - const effectComposer = new EffectComposerImpl(gl, { + composer.current = new EffectComposerImpl(gl, { depthBuffer, stencilBuffer, multisampling: multisampling > 0 && webGL2Available ? multisampling : 0, @@ -85,55 +85,26 @@ export const EffectComposer = React.memo( }) // Add render pass - effectComposer.addPass(new RenderPass(scene, camera)) + composer.current.addPass(new RenderPass(scene, camera)) // Create normal pass - let downSamplingPass = null - let normalPass = null if (enableNormalPass) { - normalPass = new NormalPass(scene, camera) - normalPass.enabled = false - effectComposer.addPass(normalPass) + normalPass.current = new NormalPass(scene, camera) + normalPass.current.enabled = false + composer.current.addPass(normalPass.current) if (resolutionScale !== undefined && webGL2Available) { - downSamplingPass = new DepthDownsamplingPass({ normalBuffer: normalPass.texture, resolutionScale }) - downSamplingPass.enabled = false - effectComposer.addPass(downSamplingPass) + downSamplingPass.current = new DepthDownsamplingPass({ + normalBuffer: normalPass.current.texture, + resolutionScale, + }) + downSamplingPass.current.enabled = false + composer.current.addPass(downSamplingPass.current) } } - return [effectComposer, normalPass, downSamplingPass] - }, [ - camera, - gl, - depthBuffer, - stencilBuffer, - multisampling, - frameBufferType, - scene, - enableNormalPass, - resolutionScale, - ]) - - useEffect(() => composer?.setSize(size.width, size.height), [composer, size]) - useFrame( - (_, delta) => { - if (enabled) { - const currentAutoClear = gl.autoClear - gl.autoClear = autoClear - if (stencilBuffer && !autoClear) gl.clearStencil() - composer.render(delta) - gl.autoClear = currentAutoClear - } - }, - enabled ? renderPriority : 0 - ) - - const group = useRef(null) - const instance = useInstanceHandle(group) - useLayoutEffect(() => { const passes: Pass[] = [] - if (group.current && instance.current && composer) { + if (group.current && instance.current && composer.current) { const children = instance.current.objects as unknown[] for (let i = 0; i < children.length; i++) { @@ -158,18 +129,50 @@ export const EffectComposer = React.memo( } } - for (const pass of passes) composer?.addPass(pass) + for (const pass of passes) composer.current?.addPass(pass) - if (normalPass) normalPass.enabled = true - if (downSamplingPass) downSamplingPass.enabled = true + if (normalPass.current) normalPass.current.enabled = true + if (downSamplingPass.current) downSamplingPass.current.enabled = true } return () => { - for (const pass of passes) composer?.removePass(pass) - if (normalPass) normalPass.enabled = false - if (downSamplingPass) downSamplingPass.enabled = false + for (const pass of passes) composer.current?.removePass(pass) + if (normalPass.current) normalPass.current.enabled = false + if (downSamplingPass.current) downSamplingPass.current.enabled = false + normalPass.current?.dispose() + downSamplingPass.current?.dispose() + composer.current?.dispose() + + composer.current = undefined + normalPass.current = undefined + downSamplingPass.current = undefined } - }, [composer, children, camera, normalPass, downSamplingPass, instance]) + }, [ + camera, + gl, + depthBuffer, + stencilBuffer, + multisampling, + frameBufferType, + scene, + enableNormalPass, + resolutionScale, + instance, + ]) + + useEffect(() => composer.current?.setSize(size.width, size.height), [size]) + useFrame( + (_, delta) => { + if (enabled) { + const currentAutoClear = gl.autoClear + gl.autoClear = autoClear + if (stencilBuffer && !autoClear) gl.clearStencil() + composer.current?.render(delta) + gl.autoClear = currentAutoClear + } + }, + enabled ? renderPriority : 0 + ) // Disable tone mapping because threejs disallows tonemapping on render targets useEffect(() => { @@ -182,7 +185,14 @@ export const EffectComposer = React.memo( // Memoize state, otherwise it would trigger all consumers on every render const state = useMemo( - () => ({ composer, normalPass, downSamplingPass, resolutionScale, camera, scene }), + () => ({ + composer: composer.current!, + normalPass: normalPass.current!, + downSamplingPass: downSamplingPass.current!, + resolutionScale, + camera, + scene, + }), [composer, normalPass, downSamplingPass, resolutionScale, camera, scene] ) diff --git a/src/effects/LensFlare.tsx b/src/effects/LensFlare.tsx index ee5bac6..62c9036 100644 --- a/src/effects/LensFlare.tsx +++ b/src/effects/LensFlare.tsx @@ -47,13 +47,13 @@ const LensFlareShader = { float rShp(vec2 p, int N){float f;float a=atan(p.x,p.y)+.2;float b=6.28319/float(N);f=smoothstep(.5,.51, cos(floor(.5+a/b)*b-a)*length(p.xy)* 2.0 -ghostScale);return f;} vec3 drC(vec2 p, float zsi, float dCy, vec3 clr, vec3 clr2, float ams2, vec2 esom){float l = length(p + esom*(ams2*2.))+zsi/2.;float l2 = length(p + esom*(ams2*4.))+zsi/3.;float c = max(0.01-pow(length(p + esom*ams2), zsi*ghostScale), 0.0)*10.;float c1 = max(0.001-pow(l-0.3, 1./40.)+sin(l*20.), 0.0)*3.;float c2 = max(0.09/pow(length(p-esom*ams2/.5)*1., .95), 0.0)/20.;float s = max(0.02-pow(rShp(p*5. + esom*ams2*5. + dCy, 6) , 1.), 0.0)*1.5;clr = cos(vec3(0.44, .24, .2)*8. + ams2*4.)*0.5+.5;vec3 f = c*clr;f += c1*clr;f += c2*clr;f += s*clr;return f-0.01;} vec4 geLC(float x){return vec4(vec3(mix(mix(mix(mix(mix(mix(mix(mix(mix(mix(mix(mix(mix(mix(mix(vec3(0., 0., 0.),vec3(0., 0., 0.), smoothstep(0.0, 0.063, x)),vec3(0., 0., 0.), smoothstep(0.063, 0.125, x)),vec3(0.0, 0., 0.), smoothstep(0.125, 0.188, x)),vec3(0.188, 0.131, 0.116), smoothstep(0.188, 0.227, x)),vec3(0.31, 0.204, 0.537), smoothstep(0.227, 0.251, x)),vec3(0.192, 0.106, 0.286), smoothstep(0.251, 0.314, x)),vec3(0.102, 0.008, 0.341), smoothstep(0.314, 0.392, x)),vec3(0.086, 0.0, 0.141), smoothstep(0.392, 0.502, x)),vec3(1.0, 0.31, 0.0), smoothstep(0.502, 0.604, x)),vec3(.1, 0.1, 0.1), smoothstep(0.604, 0.643, x)),vec3(1.0, 0.929, 0.0), smoothstep(0.643, 0.761, x)),vec3(1.0, 0.086, 0.424), smoothstep(0.761, 0.847, x)),vec3(1.0, 0.49, 0.0), smoothstep(0.847, 0.89, x)),vec3(0.945, 0.275, 0.475), smoothstep(0.89, 0.941, x)),vec3(0.251, 0.275, 0.796), smoothstep(0.941, 1.0, x))),1.0);} - float diTN(vec2 p){vec2 f = fract(p);f = (f * f) * (3.0 - (2.0 * f));float n = dot(floor(p), vec2(1.0, 157.0));vec4 a = fract(sin(vec4(n + 0.0, n + 1.0, n + 157.0, n + 158.0)) * 43758.5453123);return mix(mix(a.x, a.y, f.x), mix(a.z, a.w, f.x), f.y);} - float fbm(vec2 p){const mat2 m = mat2(0.80, -0.60, 0.60, 0.80);float f = 0.0;f += 0.5000*diTN(p); p = m*p*2.02;f += 0.2500*diTN(p); p = m*p*2.03;f += 0.1250*diTN(p); p = m*p*2.01;f += 0.0625*diTN(p);return f/0.9375;} + float diTN(vec2 p){vec2 f = fract(p);f = (f * f) * (3.0 - (2.0 * f));float n = dot(floor(p), vec2(1.0, 157.0));vec4 a = fract(sin(vec4(n + 0.0, n + 1.0, n + 157.0, n + 158.0)) * 43758.5453123);return mix(mix(a.x, a.y, f.x), mix(a.z, a.w, f.x), f.y);} + float fbm(vec2 p){const mat2 m = mat2(0.80, -0.60, 0.60, 0.80);float f = 0.0;f += 0.5000*diTN(p); p = m*p*2.02;f += 0.2500*diTN(p); p = m*p*2.03;f += 0.1250*diTN(p); p = m*p*2.01;f += 0.0625*diTN(p);return f/0.9375;} vec4 geLS(vec2 p){vec2 pp = (p - vec2(0.5)) * 2.0;float a = atan(pp.y, pp.x);vec4 cp = vec4(sin(a * 1.0), length(pp), sin(a * 13.0), sin(a * 53.0));float d = sin(clamp(pow(length(vec2(0.5) - p) * 0.5 + haloScale /2., 5.0), 0.0, 1.0) * 3.14159);vec3 c = vec3(d) * vec3(fbm(cp.xy * 16.0) * fbm(cp.zw * 9.0) * max(max(max(max(0.5, sin(a * 1.0)), sin(a * 3.0) * 0.8), sin(a * 7.0) * 0.8), sin(a * 9.0) * 10.6));c *= vec3(mix(2.0, (sin(length(pp.xy) * 256.0) * 0.5) + 0.5, sin((clamp((length(pp.xy) - 0.875) / 0.1, 0.0, 1.0) + 0.0) * 2.0 * 3.14159) * 1.5) + 0.5) * 0.3275;return vec4(vec3(c * 1.0), d);} vec4 geLD(vec2 p){p.xy += vec2(fbm(p.yx * 3.0), fbm(p.yx * 2.0)) * 0.0825;vec3 o = vec3(mix(0.125, 0.25, max(max(smoothstep(0.1, 0.0, length(p - vec2(0.25))),smoothstep(0.4, 0.0, length(p - vec2(0.75)))),smoothstep(0.8, 0.0, length(p - vec2(0.875, 0.125))))));o += vec3(max(fbm(p * 1.0) - 0.5, 0.0)) * 0.5;o += vec3(max(fbm(p * 2.0) - 0.5, 0.0)) * 0.5;o += vec3(max(fbm(p * 4.0) - 0.5, 0.0)) * 0.25;o += vec3(max(fbm(p * 8.0) - 0.75, 0.0)) * 1.0;o += vec3(max(fbm(p * 16.0) - 0.75, 0.0)) * 0.75;o += vec3(max(fbm(p * 64.0) - 0.75, 0.0)) * 0.5;return vec4(clamp(o, vec3(0.15), vec3(1.0)), 1.0);} vec4 txL(sampler2D tex, vec2 xtC){if(((xtC.x < 0.) || (xtC.y < 0.)) || ((xtC.x > 1.) || (xtC.y > 1.))){return vec4(0.0);}else{return texture(tex, xtC); }} vec4 txD(sampler2D tex, vec2 xtC, vec2 dir, vec3 ditn) {return vec4(txL(tex, (xtC + (dir * ditn.r))).r,txL(tex, (xtC + (dir * ditn.g))).g,txL(tex, (xtC + (dir * ditn.b))).b,1.0);} - vec4 strB(){vec2 aspXtc = vec2(1.0) - (((vxtC - vec2(0.5)) * vec2(1.0)) + vec2(0.5)); vec2 xtC = vec2(1.0) - vxtC; vec2 ghvc = (vec2(0.5) - xtC) * 0.3 - lensPosition; vec2 ghNm = normalize(ghvc * vec2(1.0)) * vec2(1.0);vec2 haloVec = normalize(ghvc) * 0.6;vec2 hlNm = ghNm * 0.6;vec2 texelSize = vec2(1.0) / vec2(iResolution.xy);vec3 ditn = vec3(-(texelSize.x * 1.5), 0.2, texelSize.x * 1.5);vec4 c = vec4(0.0);for (int i = 0; i < 8; i++) {vec2 offset = xtC + (ghvc * float(i));c += txD(lensDirtTexture, offset, ghNm, ditn) * pow(max(0.0, 1.0 - (length(vec2(0.5) - offset) / length(vec2(0.5)))), 10.0);}vec2 uyTrz = xtC + hlNm; return (c * geLC((length(vec2(0.5) - aspXtc) / length(vec2(haloScale))))) +(txD(lensDirtTexture, uyTrz, ghNm, ditn) * pow(max(0.0, 1.0 - (length(vec2(0.5) - uyTrz) / length(vec2(0.5)))), 10.0));} + vec4 strB(){vec2 aspXtc = vec2(1.0) - (((vxtC - vec2(0.5)) * vec2(1.0)) + vec2(0.5)); vec2 xtC = vec2(1.0) - vxtC; vec2 ghvc = (vec2(0.5) - xtC) * 0.3 - lensPosition; vec2 ghNm = normalize(ghvc * vec2(1.0)) * vec2(1.0);vec2 haloVec = normalize(ghvc) * 0.6;vec2 hlNm = ghNm * 0.6;vec2 texelSize = vec2(1.0) / vec2(iResolution.xy);vec3 ditn = vec3(-(texelSize.x * 1.5), 0.2, texelSize.x * 1.5);vec4 c = vec4(0.0);for (int i = 0; i < 8; i++) {vec2 offset = xtC + (ghvc * float(i));c += txD(lensDirtTexture, offset, ghNm, ditn) * pow(max(0.0, 1.0 - (length(vec2(0.5) - offset) / length(vec2(0.5)))), 10.0);}vec2 uyTrz = xtC + hlNm; return (c * geLC((length(vec2(0.5) - aspXtc) / length(vec2(haloScale))))) +(txD(lensDirtTexture, uyTrz, ghNm, ditn) * pow(max(0.0, 1.0 - (length(vec2(0.5) - uyTrz) / length(vec2(0.5)))), 10.0));} void mainImage(vec4 v,vec2 r,out vec4 i){vec2 g=r-.5;g.y*=iResolution.y/iResolution.x;vec2 l=lensPosition*.5;l.y*=iResolution.y/iResolution.x;vec3 f=mLs(g,l)*20.*colorGain/256.;if(aditionalStreaks){vec3 o=vec3(.9,.2,.1),p=vec3(.3,.1,.9);for(float n=0.;n<10.;n++)f+=drC(g,pow(rnd(n*2e3)*2.8,.1)+1.41,0.,o+n,p+n,rnd(n*20.)*3.+.2-.5,lensPosition);}if(secondaryGhosts){vec3 n=vec3(0);n+=rHx(g,-lensPosition*.25,ghostScale*1.4,vec3(.25,.35,0));n+=rHx(g,lensPosition*.25,ghostScale*.5,vec3(1,.5,.5));n+=rHx(g,lensPosition*.1,ghostScale*1.6,vec3(1));n+=rHx(g,lensPosition*1.8,ghostScale*2.,vec3(0,.5,.75));n+=rHx(g,lensPosition*1.25,ghostScale*.8,vec3(1,1,.5));n+=rHx(g,-lensPosition*1.25,ghostScale*5.,vec3(.5,.5,.25));n+=fpow(1.-abs(distance(lensPosition*.8,g)-.7),.985)*colorGain/2100.;f+=n;}if(starBurst){vxtC=g+.5;vec4 n=geLD(g);float o=1.-clamp(0.5,0.,.5)*2.;n+=mix(n,pow(n*2.,vec4(2))*.5,o);float s=(g.x+g.y)*(1./6.);vec2 d=mat2(cos(s),-sin(s),sin(s),cos(s))*vxtC;n+=geLS(d)*2.;f+=clamp(n.xyz*strB().xyz,.01,1.);}i=enabled?vec4(mix(f,vec3(0),opacity)+v.xyz,v.w):vec4(v);} `, } @@ -183,6 +183,6 @@ export const LensFlare = forwardRef( } }, [effect, viewport]) - return + return } ) diff --git a/yarn.lock b/yarn.lock index de8cf38..95d6497 100644 --- a/yarn.lock +++ b/yarn.lock @@ -8394,10 +8394,10 @@ postcss@^8.4.23: picocolors "^1.0.0" source-map-js "^1.0.2" -postprocessing@^6.32.1: - version "6.32.1" - resolved "https://registry.yarnpkg.com/postprocessing/-/postprocessing-6.32.1.tgz#a91fa4101246620e12113cded7028d9e4b504845" - integrity sha512-GiUv5vN/QCWnPJ3DdYPYn/4V1amps94T/0jFPSUL40KfaLCkfE9yPudzTtJJQjs168QNpwkmnvYF9RcP3HiAWA== +postprocessing@^6.35.2: + version "6.35.2" + resolved "https://registry.yarnpkg.com/postprocessing/-/postprocessing-6.35.2.tgz#7a7b42f7d3cc21cd2fded2505af645bf62276716" + integrity sha512-yGmidrVzA1dSEmExYGgWOGcRvyOVahvurNo9iuzOonRCY6f1hnJe6/HMVSnKV9ppjLtCTqzZOI9iz8CACkmijw== potpack@^1.0.1: version "1.0.2"