-
-
Notifications
You must be signed in to change notification settings - Fork 36.2k
TSL: Fix onDispose listener not being removed in Sampler
#31868
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Conversation
📦 Bundle sizeFull ESM build, minified and gzipped.
🌳 Bundle size after tree-shakingMinimal build including a renderer, camera, empty scene, and dependencies.
|
src/renderers/common/Sampler.js
Outdated
| */ | ||
| _onTextureDispose() { | ||
|
|
||
| this._texture = null; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'm afraid this does not work. When EventDispatcher executes the callback, this will point to the texture, not the Sampler class.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Oh, that's correct. It needs binding or to be an arrow function.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Now the version is restored before #31751, introducing a different bug.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Does _onTextureDispose have to be recreated when cloned?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
A sampler binding always has a reference to a texture. Before #31751, disposing textures can lead to sampler bindings which point to a destroyed textures. The texture is of course not rendered correctly and WebGPU warnings are displayed.
#31751 made sure that cloned samplers have correct dispose handlers by creating them inline. The issue is the removal is not implemented correctly since it refers to the wrong dispose handler. I think we can fix this issue by keeping track of the current dispose handler in a sampler property and use it in removeEventListener().
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'm not sure anymore if we need to create dispose inline. Can you please make a test with the fiddles from #31747 (comment) and verify if the error pops up again.
Maybe just adding clone() was already sufficient to fix the issue.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'm seeing only the following error:
Uncaught TypeError: Cannot read properties of undefined (reading 'call')
at Texture.dispatchEvent (EventDispatcher.js:118:1)
at Texture.dispose (Texture.js:644:1)
This should be caused by this.texture = texture in the constructor before populating _onTextureDispose, which needs to be moved before that line.
| * @private | ||
| * @type {?Texture} | ||
| */ | ||
| this._texture = null; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Since texture is compared with the strict equality operator in set texture(), we need to initialize it with null before setting it via the setter.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Would this version work:
import Binding from './Binding.js';
/**
* Represents a sampler binding type.
*
* @private
* @augments Binding
*/
class Sampler extends Binding {
/**
* Constructs a new sampler.
*
* @param {string} name - The samplers's name.
* @param {?Texture} texture - The texture this binding is referring to.
*/
constructor( name, texture ) {
super( name );
/**
* The texture the sampler is referring to.
*
* @type {?Texture}
*/
this.texture = texture;
/**
* The binding's version.
*
* @type {number}
*/
this.version = texture ? texture.version : 0;
/**
* The binding's generation which is an additional version
* qualifier.
*
* @type {?number}
* @default null
*/
this.generation = null;
/**
* This flag can be used for type testing.
*
* @type {boolean}
* @readonly
* @default true
*/
this.isSampler = true;
/**
* An event listener which is added to {@link texture}'s dispose event.
*
* @private
* @type {Function}
*/
this._currentDispose = null;
}
/**
* Sets the texture of this sampler.
* @param {?Texture} value - The texture to set.
*/
set texture( value ) {
if ( this._texture === value ) return;
if ( this._texture ) {
this._texture.removeEventListener( 'dispose', this._currentDispose );
this._currentDispose = null;
}
this._texture = value;
this.generation = null;
this.version = 0;
if ( this._texture ) {
this._currentDispose = () => {
this._texture = null;
this.generation = null;
this.version = 0;
};
this._texture.addEventListener( 'dispose', this._currentDispose );
}
}
/**
* Gets the texture of this sampler.
* @return {?Texture} The texture.
*/
get texture() {
return this._texture;
}
/**
* Updates the binding.
*
* @return {boolean} Whether the texture has been updated and must be
* uploaded to the GPU.
*/
update() {
const { texture, version } = this;
if ( version !== texture.version ) {
this.version = texture.version;
return true;
}
return false;
}
clone() {
const clonedSampler = super.clone();
// fix dispose handler for cloned instances
// TODO: Find better solution, see #31747
clonedSampler._texture = null;
clonedSampler.texture = this.texture;
return clonedSampler;
}
}
export default Sampler;There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yes, your code does basically the same thing as the changes I made. The only difference is whether to lazily initialize the dispose listener.
But your _currentDispose is recreated on every call (though it removes the previous one correctly). Is it really necessary?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
No, you version looks cleaner. Let's go with it.
Like mentioned in the comments, the entire module needs some refactoring since the binding/texture handling is quite fragile and hard to follow right now.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I mean, doesn't it work just lazily initialize the listener like the following:
// Note that the strict equality operator doesn't work here because the assignment
// via the setter occurs before `_currentDispose` is initialized with `null`.
if ( this._currentDispose == null ) {
this._currentDispose = () => {
this._texture = null;
this.generation = null;
this.version = 0;
};
}There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Oh, did I merge too soon? If so, please make a new PR with your suggestion.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I mean, doesn't it work just lazily initialize the listener like the following:
I'm okay with it as long as the use case from #31747 (comment) does not break again.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Oh, did I merge too soon?
No, our messages crossed. 😓
The code in clone() and the timing issue in the texture setter are hard to notice, though.
Fixes: #31866
Description
This PR fixes that the event listeners on textures are not removed and grow indefinitely. Heap snapshots of
webgpu_cubemap_dynamicno longer contain a large array ofonDisposelisteners.