diff --git a/CHANGELOG.md b/CHANGELOG.md index 48001f36a..fac23551f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,14 @@ # Change Log +## Unreleased + +### Bug Fixes + +- Fixed sprites drawing with the wrong texture in WebGL multi-texture mode, because the texture unit cache (`PIXI.WebGLRenderer.textureArray`) went stale after other code rebound texture units. +- Fixed late-loaded and changed textures (e.g., Text, BitmapData) rendering blank or stale in WebGL multi-texture mode. +- Fixed WebGL feedback-loop errors when rendering filters in multi-texture mode. +- Fixed WebGL multi-texture mode losing texture bindings when a TileSprite texture was uploaded, when a shader was first used mid-render, or when a RenderTexture was created or resized. + ## Version 2.21.0 - 23 October 2026 ### New Features diff --git a/src/gameobjects/Creature.js b/src/gameobjects/Creature.js index 40f852b46..77dc8882b 100644 --- a/src/gameobjects/Creature.js +++ b/src/gameobjects/Creature.js @@ -492,6 +492,9 @@ Phaser.Creature.prototype._renderCreature = function (renderSession) gl.bufferData(gl.ELEMENT_ARRAY_BUFFER, this.indices, gl.STATIC_DRAW); } + // unit 0 was rebound above, outside the sprite batch + PIXI.WebGLRenderer.invalidateTextureUnit(0); + gl.drawElements(gl.TRIANGLES, this.indices.length, gl.UNSIGNED_SHORT, 0); }; diff --git a/src/gameobjects/Rope.js b/src/gameobjects/Rope.js index 98bf7a297..2dbd0899c 100644 --- a/src/gameobjects/Rope.js +++ b/src/gameobjects/Rope.js @@ -447,6 +447,9 @@ Phaser.Rope.prototype._renderStrip = function (renderSession) gl.bufferData(gl.ELEMENT_ARRAY_BUFFER, this.indices, gl.STATIC_DRAW); } + // unit 0 was rebound above, outside the sprite batch + PIXI.WebGLRenderer.invalidateTextureUnit(0); + gl.drawElements(drawMode, this.indices.length, gl.UNSIGNED_SHORT, 0); }; diff --git a/src/gameobjects/TileSprite.js b/src/gameobjects/TileSprite.js index c7ebbac7a..01b1bacf9 100644 --- a/src/gameobjects/TileSprite.js +++ b/src/gameobjects/TileSprite.js @@ -343,6 +343,12 @@ Phaser.TileSprite.prototype._renderWebGL = function (renderSession) { if (this.tilingTexture.needsUpdate) { + if (PIXI._enableMultiTextureToggle) + { + // the upload binds on a texture unit that pending sprites may be using + renderSession.spriteBatch.flush(); + } + this.tilingTexture.baseTexture.textureIndex = this.texture.baseTexture.textureIndex; renderSession.renderer.updateTexture(this.tilingTexture.baseTexture); this.tilingTexture.needsUpdate = false; diff --git a/src/pixi/renderers/webgl/WebGLRenderer.js b/src/pixi/renderers/webgl/WebGLRenderer.js index 97dcff4d3..23f066bde 100644 --- a/src/pixi/renderers/webgl/WebGLRenderer.js +++ b/src/pixi/renderers/webgl/WebGLRenderer.js @@ -268,6 +268,9 @@ PIXI.WebGLRenderer.prototype.initContext = function () this.blendModeManager.setContext(gl); this.stencilManager.setContext(gl); + // a new context starts with nothing bound, and the setContext() calls above bind dummy textures + PIXI.WebGLRenderer.invalidateTextureUnit(); + this.renderSession.gl = this.gl; // now resize and we are good to go! @@ -511,6 +514,7 @@ PIXI.WebGLRenderer.prototype.updateCompressedTexture = function (texture) gl.activeTexture(gl.TEXTURE0 + texture.textureIndex); gl.bindTexture(gl.TEXTURE_2D, texture._glTextures[gl.id]); + PIXI.WebGLRenderer.textureArray[texture.textureIndex] = texture; gl.compressedTexImage2D( gl.TEXTURE_2D, @@ -575,6 +579,7 @@ PIXI.WebGLRenderer.prototype.updateTexture = function (texture) gl.activeTexture(gl.TEXTURE0 + texture.textureIndex); gl.bindTexture(gl.TEXTURE_2D, texture._glTextures[gl.id]); + PIXI.WebGLRenderer.textureArray[texture.textureIndex] = texture; gl.pixelStorei(gl.UNPACK_PREMULTIPLY_ALPHA_WEBGL, texture.premultipliedAlpha); @@ -686,4 +691,26 @@ PIXI.enableMultiTexture = function () PIXI._enableMultiTextureToggle = true; }; +/** + * Records which base texture is bound to each texture unit (multi-texture mode only). + * Shared by all renderers. + */ PIXI.WebGLRenderer.textureArray = []; + +/** + * Forgets what is bound to a texture unit (or to all units), after code outside the sprite batch has rebound it. + * + * @method PIXI.WebGLRenderer.invalidateTextureUnit + * @param {number} [unit] - The texture unit to clear. If omitted, all units are cleared. + */ +PIXI.WebGLRenderer.invalidateTextureUnit = function (unit) +{ + if (typeof unit === 'number') + { + PIXI.WebGLRenderer.textureArray[unit] = null; + } + else + { + PIXI.WebGLRenderer.textureArray.length = 0; + } +}; diff --git a/src/pixi/renderers/webgl/shaders/PixiShader.js b/src/pixi/renderers/webgl/shaders/PixiShader.js index 142019a4e..413533b47 100644 --- a/src/pixi/renderers/webgl/shaders/PixiShader.js +++ b/src/pixi/renderers/webgl/shaders/PixiShader.js @@ -131,20 +131,42 @@ PIXI.PixiShader.prototype.initMultitexShader = function () var indices = []; + // This can run in the middle of a render (first use of a shader), so remember what is bound and put it back. + var previousActive = gl.getParameter(gl.ACTIVE_TEXTURE); + var previousBindings = []; + var i; + + for (i = 0; i < this.MAX_TEXTURES; ++i) + { + gl.activeTexture(gl.TEXTURE0 + i); + previousBindings.push(gl.getParameter(gl.TEXTURE_BINDING_2D)); + } + // HACK: we bind an empty texture to avoid WebGL warning spam. var tempTexture = gl.createTexture(); gl.activeTexture(gl.TEXTURE0); gl.bindTexture(gl.TEXTURE_2D, tempTexture); gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGB, 1, 1, 0, gl.RGB, gl.UNSIGNED_BYTE, null); - for (var i = 0; i < this.MAX_TEXTURES; ++i) + for (i = 0; i < this.MAX_TEXTURES; ++i) { gl.activeTexture(gl.TEXTURE0 + i); gl.bindTexture(gl.TEXTURE_2D, tempTexture); indices.push(i); } - gl.activeTexture(gl.TEXTURE0); gl.uniform1iv(this.uSamplerArray, indices); + // Units that held nothing keep the dummy texture. + for (i = 0; i < this.MAX_TEXTURES; ++i) + { + if (previousBindings[i]) + { + gl.activeTexture(gl.TEXTURE0 + i); + gl.bindTexture(gl.TEXTURE_2D, previousBindings[i]); + } + } + + gl.activeTexture(previousActive); + // Begin worst hack eva // /* @@ -344,6 +366,7 @@ PIXI.PixiShader.prototype.initSampler2D = function (uniform) // No need to do string manipulation for this. gl.activeTexture(gl.TEXTURE0 + this.textureCount); gl.bindTexture(gl.TEXTURE_2D, uniform.value.baseTexture._glTextures[gl.id]); + PIXI.WebGLRenderer.invalidateTextureUnit(this.textureCount); // Extended texture data if (uniform.textureData) @@ -461,6 +484,7 @@ PIXI.PixiShader.prototype.syncUniforms = function () } // gl.bindTexture(gl.TEXTURE_2D, uniform.value.baseTexture._glTextures[gl.id] || PIXI.createWebGLTexture( uniform.value.baseTexture, gl)); + PIXI.WebGLRenderer.invalidateTextureUnit(this.textureCount); gl.uniform1i(uniform.uniformLocation, this.textureCount); this.textureCount++; } diff --git a/src/pixi/renderers/webgl/utils/FilterTexture.js b/src/pixi/renderers/webgl/utils/FilterTexture.js index 53eebb868..c261c4fcb 100644 --- a/src/pixi/renderers/webgl/utils/FilterTexture.js +++ b/src/pixi/renderers/webgl/utils/FilterTexture.js @@ -37,7 +37,11 @@ function _CreateFramebuffer (gl, width, height, scaleMode, textureUnit) var colorBuffer = null; var fbStatus = 0; + // Creating the texture binds on this unit, which may hold a texture that is still waiting to be drawn. + var previousActive = gl.getParameter(gl.ACTIVE_TEXTURE); gl.activeTexture(gl.TEXTURE0 + textureUnit); + var previousTexture = gl.getParameter(gl.TEXTURE_BINDING_2D); + gl.bindFramebuffer(gl.FRAMEBUFFER, framebuffer); gl.bindRenderbuffer(gl.RENDERBUFFER, depthStencilBuffer); gl.framebufferRenderbuffer(gl.FRAMEBUFFER, gl.DEPTH_STENCIL_ATTACHMENT, gl.RENDERBUFFER, this.renderBuffer); @@ -52,6 +56,10 @@ function _CreateFramebuffer (gl, width, height, scaleMode, textureUnit) framebuffer.height = height; framebuffer.targetTexture = colorBuffer; framebuffer.renderBuffer = depthStencilBuffer; + + gl.bindTexture(gl.TEXTURE_2D, previousTexture); + gl.activeTexture(previousActive); + return framebuffer; } @@ -121,8 +129,12 @@ PIXI.FilterTexture.prototype.resize = function (width, height) this.height = height; var gl = this.gl; + + // resizing can happen mid-render, so leave the active unit's binding as it was + var previousTexture = gl.getParameter(gl.TEXTURE_BINDING_2D); gl.bindTexture(gl.TEXTURE_2D, this.texture); gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA, width , height , 0, gl.RGBA, gl.UNSIGNED_BYTE, null); + gl.bindTexture(gl.TEXTURE_2D, previousTexture); // update the stencil buffer width and height gl.bindRenderbuffer(gl.RENDERBUFFER, this.renderBuffer); diff --git a/src/pixi/renderers/webgl/utils/WebGLFastSpriteBatch.js b/src/pixi/renderers/webgl/utils/WebGLFastSpriteBatch.js index d596fad85..e2f48d687 100644 --- a/src/pixi/renderers/webgl/utils/WebGLFastSpriteBatch.js +++ b/src/pixi/renderers/webgl/utils/WebGLFastSpriteBatch.js @@ -199,10 +199,24 @@ PIXI.WebGLFastSpriteBatch.prototype.render = function (spriteBatch) var textureIndex = this.currentBaseTexture.textureIndex; var gl = this.gl; - - gl.activeTexture(gl.TEXTURE0 + textureIndex); - gl.bindTexture(gl.TEXTURE_2D, this.currentBaseTexture._glTextures[gl.id]); - PIXI.WebGLRenderer.textureArray[textureIndex] = this.currentBaseTexture; + + if (!this.currentBaseTexture.source && this.currentBaseTexture._glTextures[gl.id]) + { + // a render target (RenderTexture) has no source to upload, so it is never stale + this.currentBaseTexture._dirty[gl.id] = false; + } + + if (this.currentBaseTexture._dirty[gl.id] || !this.currentBaseTexture._glTextures[gl.id]) + { + // binds on the texture's own unit and updates the texture unit cache + this.renderSession.renderer.updateTexture(this.currentBaseTexture); + } + else + { + gl.activeTexture(gl.TEXTURE0 + textureIndex); + gl.bindTexture(gl.TEXTURE_2D, this.currentBaseTexture._glTextures[gl.id]); + PIXI.WebGLRenderer.textureArray[textureIndex] = this.currentBaseTexture; + } for(var i = 0,j = children.length; i < j; i++) { @@ -223,13 +237,32 @@ PIXI.WebGLFastSpriteBatch.prototype.renderSprite = function (sprite) var gl = this.gl; var textureIndex = sprite.texture.baseTexture.textureIndex; - if (PIXI.WebGLRenderer.textureArray[textureIndex] != baseTexture && // eslint-disable-line eqeqeq - baseTexture._glTextures[gl.id] && !sprite.texture.baseTexture.skipRender) + if (!baseTexture.source && baseTexture._glTextures[gl.id]) + { + baseTexture._dirty[gl.id] = false; + } + + var dirty = baseTexture._dirty[gl.id] || !baseTexture._glTextures[gl.id]; + + if ((dirty || PIXI.WebGLRenderer.textureArray[textureIndex] !== baseTexture) && !baseTexture.skipRender) { this.flush(); - gl.activeTexture(gl.TEXTURE0 + textureIndex); - gl.bindTexture(gl.TEXTURE_2D, baseTexture._glTextures[gl.id]); - PIXI.WebGLRenderer.textureArray[textureIndex] = baseTexture; + + if (dirty) + { + // binds on the texture's own unit and updates the texture unit cache + if (!this.renderSession.renderer.updateTexture(baseTexture)) + { + return; + } + } + else + { + gl.activeTexture(gl.TEXTURE0 + textureIndex); + gl.bindTexture(gl.TEXTURE_2D, baseTexture._glTextures[gl.id]); + PIXI.WebGLRenderer.textureArray[textureIndex] = baseTexture; + } + if(!sprite.texture._uvs) { return; } } diff --git a/src/pixi/renderers/webgl/utils/WebGLFilterManager.js b/src/pixi/renderers/webgl/utils/WebGLFilterManager.js index 220014be9..4927e2596 100644 --- a/src/pixi/renderers/webgl/utils/WebGLFilterManager.js +++ b/src/pixi/renderers/webgl/utils/WebGLFilterManager.js @@ -93,6 +93,9 @@ PIXI.WebGLFilterManager.prototype.pushFilter = function (filterBlock) this.offsetX += filterBlock._filterArea.x; this.offsetY += filterBlock._filterArea.y; + // FilterTexture creation and resizing bind on the active unit, so use unit 0 and leave it unbound. + gl.activeTexture(gl.TEXTURE0); + var texture = this.texturePool.pop(); if(!texture) { @@ -103,7 +106,8 @@ PIXI.WebGLFilterManager.prototype.pushFilter = function (filterBlock) texture.resize(this.width * this.renderSession.resolution, this.height * this.renderSession.resolution); } - gl.bindTexture(gl.TEXTURE_2D, texture.texture); + gl.bindTexture(gl.TEXTURE_2D, null); + PIXI.WebGLRenderer.invalidateTextureUnit(); var filterArea = filterBlock._filterArea;// filterBlock.target.getBounds();///filterBlock.target.filterArea; @@ -210,6 +214,7 @@ PIXI.WebGLFilterManager.prototype.popFilter = function () // set texture gl.activeTexture(gl.TEXTURE0); gl.bindTexture(gl.TEXTURE_2D, inputTexture.texture); + PIXI.WebGLRenderer.invalidateTextureUnit(); /* * draw texture.. @@ -315,6 +320,7 @@ PIXI.WebGLFilterManager.prototype.popFilter = function () // set texture gl.activeTexture(gl.TEXTURE0); gl.bindTexture(gl.TEXTURE_2D, texture.texture); + PIXI.WebGLRenderer.invalidateTextureUnit(); // >>> modify by nextht if (this.renderSession.stencilManager) diff --git a/src/pixi/renderers/webgl/utils/WebGLSpriteBatch.js b/src/pixi/renderers/webgl/utils/WebGLSpriteBatch.js index 69aff5c75..ca3b5ddb2 100644 --- a/src/pixi/renderers/webgl/utils/WebGLSpriteBatch.js +++ b/src/pixi/renderers/webgl/utils/WebGLSpriteBatch.js @@ -248,6 +248,50 @@ PIXI.WebGLSpriteBatch.prototype.end = function () this.flush(); }; +/** + * Multi-texture mode: makes sure the base texture is uploaded if dirty and bound to the given texture unit. + * Flushes the batch first if the unit binding or contents will change. + * + * @method PIXI.WebGLSpriteBatch#bindMultiTexture + * @private + * @param {PIXI.BaseTexture} baseTexture + * @param {number} textureUnit + * @return {boolean} False if the texture is dirty but can't be uploaded yet (not loaded), so the sprite should be skipped. + */ +PIXI.WebGLSpriteBatch.prototype.bindMultiTexture = function (baseTexture, textureUnit) +{ + var gl = this.gl; + + if (!baseTexture.source && baseTexture._glTextures[gl.id]) + { + // a render target (RenderTexture) has no source to upload, so it is never stale + baseTexture._dirty[gl.id] = false; + } + + var dirty = baseTexture._dirty[gl.id] || !baseTexture._glTextures[gl.id]; + + if (!dirty && PIXI.WebGLRenderer.textureArray[textureUnit] === baseTexture) + { + return true; + } + + this.flush(); + + if (dirty) + { + // the upload binds on the base texture's own unit, which has to be the one the vertices sample + baseTexture.textureIndex = textureUnit; + + return this.renderSession.renderer.updateTexture(baseTexture); + } + + gl.activeTexture(gl.TEXTURE0 + textureUnit); + gl.bindTexture(gl.TEXTURE_2D, baseTexture._glTextures[gl.id]); + PIXI.WebGLRenderer.textureArray[textureUnit] = baseTexture; + + return true; +}; + /** * @method PIXI.WebGLSpriteBatch#render * @param sprite {Sprite} the sprite to render when using this spritebatch @@ -260,13 +304,9 @@ PIXI.WebGLSpriteBatch.prototype.render = function (sprite, matrix) if (PIXI._enableMultiTextureToggle) { var baseTexture = texture.baseTexture; - var gl = this.gl; - if (PIXI.WebGLRenderer.textureArray[baseTexture.textureIndex] != baseTexture) // eslint-disable-line eqeqeq + if (!this.bindMultiTexture(baseTexture, baseTexture.textureIndex)) { - this.flush(); - gl.activeTexture(gl.TEXTURE0 + baseTexture.textureIndex); - gl.bindTexture(gl.TEXTURE_2D, baseTexture._glTextures[gl.id]); - PIXI.WebGLRenderer.textureArray[baseTexture.textureIndex] = baseTexture; + return; } } @@ -447,14 +487,9 @@ PIXI.WebGLSpriteBatch.prototype.renderTilingSprite = function (sprite) if (PIXI._enableMultiTextureToggle) { - var baseTexture = texture.baseTexture; - var gl = this.gl; - if (PIXI.WebGLRenderer.textureArray[textureIndex] != baseTexture) // eslint-disable-line eqeqeq + if (!this.bindMultiTexture(texture.baseTexture, textureIndex)) { - this.flush(); - gl.activeTexture(gl.TEXTURE0 + textureIndex); - gl.bindTexture(gl.TEXTURE_2D, baseTexture._glTextures[gl.id]); - PIXI.WebGLRenderer.textureArray[textureIndex] = baseTexture; + return; } } @@ -782,8 +817,9 @@ PIXI.WebGLSpriteBatch.prototype.renderBatch = function (texture, size, startInde return; } } - else + else if (!PIXI._enableMultiTextureToggle) { + // in multi-texture mode the batch's textures are already bound on their own units gl.bindTexture(gl.TEXTURE_2D, texture._glTextures[gl.id]); }