Skip to content

Commit

Permalink
fix leaking geometries and materials
Browse files Browse the repository at this point in the history
  • Loading branch information
bbohlender committed Oct 30, 2024
1 parent 02e6afd commit 55bd728
Show file tree
Hide file tree
Showing 11 changed files with 292 additions and 248 deletions.
423 changes: 217 additions & 206 deletions examples/uikit/src/App.tsx

Large diffs are not rendered by default.

3 changes: 3 additions & 0 deletions packages/react/src/suspending.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,9 @@ export type SuspendingImageProperties = ImageProperties & {
children?: ReactNode
}

/**
* be aware that this component does not dispose the loaded texture
*/
export const SuspendingImage: (
props: SuspendingImageProperties & RefAttributes<ComponentInternals<Omit<ImageProperties, 'src'>>>,
) => ReactNode = forwardRef(({ src, ...props }, ref) => {
Expand Down
1 change: 1 addition & 0 deletions packages/react/src/video.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -81,6 +81,7 @@ export const Video: (props: VideoProperties & RefAttributes<VideoInternals>) =>
useEffect(() => {
const videoTexture = new VideoTexture(element)
videoTexture.colorSpace = SRGBColorSpace
videoTexture.needsUpdate = true
texture.value = videoTexture
return () => videoTexture.dispose()
}, [texture, element])
Expand Down
10 changes: 9 additions & 1 deletion packages/uikit/src/components/icon.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { Signal, effect, signal } from '@preact/signals-core'
import { Color, Group, Mesh, MeshBasicMaterial, Plane, ShapeGeometry } from 'three'
import { BufferGeometry, Color, Group, Material, Mesh, MeshBasicMaterial, Plane, ShapeGeometry } from 'three'
import { Listeners } from '../index.js'
import { Object3DRef, ParentContext } from '../context.js'
import { FlexNode, FlexNodeState, YogaProperties, createFlexNodeState } from '../flex/index.js'
Expand Down Expand Up @@ -221,6 +221,14 @@ function createIconGroup(
group.updateMatrix()
parentContext.root.requestRender()
}),
() => () =>
group.children.forEach((child) => {
if (!(child instanceof Mesh)) {
return
}
;(child.geometry as BufferGeometry).dispose()
;(child.material as Material).dispose()
}),
() =>
effect(() => {
group.visible = isVisible.value
Expand Down
80 changes: 40 additions & 40 deletions packages/uikit/src/components/image.ts
Original file line number Diff line number Diff line change
Expand Up @@ -268,48 +268,44 @@ function createImageMesh(
mesh.spherecast = makeClippedCast(mesh, makePanelSpherecast(mesh), root.object, parentContext.clippingRect, orderInfo)
setupRenderOrder(mesh, root, orderInfo)
const objectFit = computedInheritableProperty(propertiesSignal, 'objectFit', defaultImageFit)
initializers.push(() =>
effect(() => {
const texture = textureSignal.value
if (texture == null || flexState.size.value == null || flexState.borderInset.value == null) {
return
}
texture.matrix.identity()
root.requestRender()

if (objectFit.value === 'fill' || texture == null) {
transformInsideBorder(flexState.borderInset, flexState.size, texture)
return
}
initializers.push(
() =>
effect(() => {
const texture = textureSignal.value
if (texture == null || flexState.size.value == null || flexState.borderInset.value == null) {
return
}
texture.matrix.identity()
root.requestRender()

const { width: textureWidth, height: textureHeight } = texture.source.data as { width: number; height: number }
const textureRatio = textureWidth / textureHeight

const [width, height] = flexState.size.value
const [top, right, bottom, left] = flexState.borderInset.value
const boundsRatioValue = (width - left - right) / (height - top - bottom)

if (textureRatio > boundsRatioValue) {
texture.matrix
.translate(-(0.5 * (boundsRatioValue - textureRatio)) / boundsRatioValue, 0)
.scale(boundsRatioValue / textureRatio, 1)
} else {
texture.matrix
.translate(0, -(0.5 * (textureRatio - boundsRatioValue)) / textureRatio)
.scale(1, textureRatio / boundsRatioValue)
}
transformInsideBorder(flexState.borderInset, flexState.size, texture)
}),
)
if (objectFit.value === 'fill' || texture == null) {
transformInsideBorder(flexState.borderInset, flexState.size, texture)
return
}

initializers.push(() =>
effect(() => {
mesh.visible = isMeshVisible.value
parentContext.root.requestRender()
}),
)
const { width: textureWidth, height: textureHeight } = texture.source.data as { width: number; height: number }
const textureRatio = textureWidth / textureHeight

initializers.push(
const [width, height] = flexState.size.value
const [top, right, bottom, left] = flexState.borderInset.value
const boundsRatioValue = (width - left - right) / (height - top - bottom)

if (textureRatio > boundsRatioValue) {
texture.matrix
.translate(-(0.5 * (boundsRatioValue - textureRatio)) / boundsRatioValue, 0)
.scale(boundsRatioValue / textureRatio, 1)
} else {
texture.matrix
.translate(0, -(0.5 * (textureRatio - boundsRatioValue)) / textureRatio)
.scale(1, textureRatio / boundsRatioValue)
}
transformInsideBorder(flexState.borderInset, flexState.size, texture)
}),
() =>
effect(() => {
mesh.visible = isMeshVisible.value
parentContext.root.requestRender()
}),
() =>
effect(() => {
const map = textureSignal.value ?? null
Expand Down Expand Up @@ -405,10 +401,14 @@ function setupImageMaterials(
const material = createPanelMaterial(panelMaterialClass.value, info)
material.clippingPlanes = clippingPlanes
target.material = material
return effect(() => {
const cleanupDepthTestEffect = effect(() => {
material.depthTest = root.depthTest.value
root.requestRender()
})
return () => {
cleanupDepthTestEffect()
material.dispose()
}
}),
effect(() => {
target.renderOrder = root.renderOrder.value
Expand Down
6 changes: 6 additions & 0 deletions packages/uikit/src/panel/instanced-panel-group.ts
Original file line number Diff line number Diff line change
Expand Up @@ -325,6 +325,12 @@ export class InstancedPanelGroup {

destroy() {
clearTimeout(this.nextUpdateTimeoutRef)
if (this.mesh == null) {
return
}
this.object.current?.remove(this.mesh)
this.mesh?.dispose()
this.instanceMaterial.dispose()
}
}

Expand Down
1 change: 1 addition & 0 deletions packages/uikit/src/panel/instanced-panel-mesh.ts
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,7 @@ export class InstancedPanelMesh extends Mesh {

dispose() {
this.dispatchEvent({ type: 'dispose' as keyof Object3DEventMap })
this.geometry.dispose()
}

copy(): this {
Expand Down
10 changes: 10 additions & 0 deletions packages/uikit/src/text/render/instanced-glyph-group.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ export class GlyphGroupManager {
root.onFrameSet.add(onFrame)
return () => root.onFrameSet.delete(onFrame)
},
() => () => this.traverse((group) => group.destroy()),
() =>
effect(() => {
const ro = renderOrder.value
Expand Down Expand Up @@ -276,6 +277,15 @@ export class InstancedGlyphGroup {
this.mesh.count = this.glyphs.length
this.object.current?.add(this.mesh)
}

destroy() {
if (this.mesh == null) {
return
}
this.object.current?.remove(this.mesh)
this.mesh.dispose()
this.instanceMaterial.dispose()
}
}

function copyBuffer(
Expand Down
1 change: 1 addition & 0 deletions packages/uikit/src/text/render/instanced-glyph-mesh.ts
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ export class InstancedGlyphMesh extends Mesh {

dispose() {
this.dispatchEvent({ type: 'dispose' as keyof Object3DEventMap })
this.geometry.dispose()
}

//functions not needed because intersection (and morphing) is intenionally disabled
Expand Down
4 changes: 3 additions & 1 deletion packages/uikit/src/vanilla/custom.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ export class CustomContainer<T = {}> extends Component<T> {
private readonly defaultPropertiesSignal: Signal<AllOptionalProperties | undefined>
private readonly parentContextSignal = createParentContextSignal()
private readonly unsubscribe: () => void
private readonly material = new MeshBasicMaterial()

constructor(properties?: CustomContainerProperties, defaultProperties?: AllOptionalProperties) {
super()
Expand All @@ -22,7 +23,7 @@ export class CustomContainer<T = {}> extends Component<T> {
this.propertiesSignal = signal(properties)
this.defaultPropertiesSignal = signal(defaultProperties)

const mesh = new Mesh(panelGeometry, new MeshBasicMaterial())
const mesh = new Mesh(panelGeometry, this.material)
super.add(mesh)

this.unsubscribe = effect(() => {
Expand Down Expand Up @@ -79,5 +80,6 @@ export class CustomContainer<T = {}> extends Component<T> {
destroy() {
this.parent?.remove(this)
this.unsubscribe()
this.material.dispose()
}
}
1 change: 1 addition & 0 deletions packages/uikit/src/vanilla/video.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ export class Video<T = {}> extends Image<T> {
const element = props.src instanceof HTMLVideoElement ? props.src : document.createElement('video')
updateVideoElement(element, props)
const texture = new VideoTexture(element)
texture.needsUpdate = true
const aspectRatio = signal<number>(1)
super({ aspectRatio, ...props, src: texture }, defaultProperties)

Expand Down

0 comments on commit 55bd728

Please sign in to comment.