From 47dd1e6bb5fa85e32441200749808c0154fb56b6 Mon Sep 17 00:00:00 2001 From: samme <435681+samme@users.noreply.github.com> Date: Sat, 24 Oct 2026 04:17:05 +0000 Subject: [PATCH 1/5] Fix stale texture unit cache in WebGL multi-texture mode Keep PIXI.WebGLRenderer.textureArray in sync with texture unit bindings, stop binding the batch's first texture on the active unit, and upload dirty or late-loaded textures before drawing. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- CHANGELOG.md | 8 +++ src/gameobjects/Creature.js | 3 ++ src/gameobjects/Rope.js | 3 ++ src/pixi/renderers/webgl/WebGLRenderer.js | 27 ++++++++++ .../renderers/webgl/shaders/PixiShader.js | 5 ++ .../renderers/webgl/utils/FilterTexture.js | 3 ++ .../webgl/utils/WebGLFastSpriteBatch.js | 40 ++++++++++---- .../webgl/utils/WebGLFilterManager.js | 8 ++- .../renderers/webgl/utils/WebGLSpriteBatch.js | 54 ++++++++++++++----- 9 files changed, 127 insertions(+), 24 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 48001f36a..8e0785846 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,13 @@ # 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. + ## 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/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..d1ecde19e 100644 --- a/src/pixi/renderers/webgl/shaders/PixiShader.js +++ b/src/pixi/renderers/webgl/shaders/PixiShader.js @@ -145,6 +145,9 @@ PIXI.PixiShader.prototype.initMultitexShader = function () gl.activeTexture(gl.TEXTURE0); gl.uniform1iv(this.uSamplerArray, indices); + // every unit now holds the dummy texture + PIXI.WebGLRenderer.invalidateTextureUnit(); + // Begin worst hack eva // /* @@ -344,6 +347,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 +465,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..0676a7662 100644 --- a/src/pixi/renderers/webgl/utils/FilterTexture.js +++ b/src/pixi/renderers/webgl/utils/FilterTexture.js @@ -37,6 +37,9 @@ function _CreateFramebuffer (gl, width, height, scaleMode, textureUnit) var colorBuffer = null; var fbStatus = 0; + // creating the texture and framebuffer rebinds texture units + PIXI.WebGLRenderer.invalidateTextureUnit(); + gl.activeTexture(gl.TEXTURE0 + textureUnit); gl.bindFramebuffer(gl.FRAMEBUFFER, framebuffer); gl.bindRenderbuffer(gl.RENDERBUFFER, depthStencilBuffer); diff --git a/src/pixi/renderers/webgl/utils/WebGLFastSpriteBatch.js b/src/pixi/renderers/webgl/utils/WebGLFastSpriteBatch.js index d596fad85..f296ca68c 100644 --- a/src/pixi/renderers/webgl/utils/WebGLFastSpriteBatch.js +++ b/src/pixi/renderers/webgl/utils/WebGLFastSpriteBatch.js @@ -199,10 +199,18 @@ 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._dirty[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++) { @@ -222,14 +230,28 @@ PIXI.WebGLFastSpriteBatch.prototype.renderSprite = function (sprite) var baseTexture = texture.baseTexture; var gl = this.gl; var textureIndex = sprite.texture.baseTexture.textureIndex; + var dirty = baseTexture._dirty[gl.id]; - if (PIXI.WebGLRenderer.textureArray[textureIndex] != baseTexture && // eslint-disable-line eqeqeq - baseTexture._glTextures[gl.id] && !sprite.texture.baseTexture.skipRender) + if ((dirty || PIXI.WebGLRenderer.textureArray[textureIndex] !== baseTexture) && + (dirty || baseTexture._glTextures[gl.id]) && !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..f979fd196 100644 --- a/src/pixi/renderers/webgl/utils/WebGLSpriteBatch.js +++ b/src/pixi/renderers/webgl/utils/WebGLSpriteBatch.js @@ -248,6 +248,40 @@ 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; + var dirty = baseTexture._dirty[gl.id]; + + if (!dirty && PIXI.WebGLRenderer.textureArray[textureUnit] === baseTexture) + { + return true; + } + + this.flush(); + + if (dirty) + { + 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 +294,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 +477,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 +807,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]); } From 6cb50eae16772724703b05320b49c02afb6f37ed Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sat, 24 Oct 2026 05:19:01 +0000 Subject: [PATCH 2/5] Fix remaining multi-texture binding cases Co-authored-by: samme <435681+samme@users.noreply.github.com> --- src/gameobjects/TileSprite.js | 1 + src/pixi/renderers/webgl/shaders/PixiShader.js | 14 ++++++++++++++ src/pixi/renderers/webgl/utils/FilterTexture.js | 1 + 3 files changed, 16 insertions(+) diff --git a/src/gameobjects/TileSprite.js b/src/gameobjects/TileSprite.js index c7ebbac7a..3ab133012 100644 --- a/src/gameobjects/TileSprite.js +++ b/src/gameobjects/TileSprite.js @@ -344,6 +344,7 @@ Phaser.TileSprite.prototype._renderWebGL = function (renderSession) if (this.tilingTexture.needsUpdate) { this.tilingTexture.baseTexture.textureIndex = this.texture.baseTexture.textureIndex; + renderSession.spriteBatch.flush(); renderSession.renderer.updateTexture(this.tilingTexture.baseTexture); this.tilingTexture.needsUpdate = false; } diff --git a/src/pixi/renderers/webgl/shaders/PixiShader.js b/src/pixi/renderers/webgl/shaders/PixiShader.js index d1ecde19e..38280572a 100644 --- a/src/pixi/renderers/webgl/shaders/PixiShader.js +++ b/src/pixi/renderers/webgl/shaders/PixiShader.js @@ -132,6 +132,7 @@ PIXI.PixiShader.prototype.initMultitexShader = function () var indices = []; // HACK: we bind an empty texture to avoid WebGL warning spam. + var textureArray = PIXI.WebGLRenderer.textureArray.slice(); var tempTexture = gl.createTexture(); gl.activeTexture(gl.TEXTURE0); gl.bindTexture(gl.TEXTURE_2D, tempTexture); @@ -148,6 +149,19 @@ PIXI.PixiShader.prototype.initMultitexShader = function () // every unit now holds the dummy texture PIXI.WebGLRenderer.invalidateTextureUnit(); + for (var i = 0; i < textureArray.length && i < this.MAX_TEXTURES; ++i) + { + var texture = textureArray[i]; + + if (texture && texture._glTextures && texture._glTextures[gl.id]) + { + gl.activeTexture(gl.TEXTURE0 + i); + gl.bindTexture(gl.TEXTURE_2D, texture._glTextures[gl.id]); + PIXI.WebGLRenderer.textureArray[i] = texture; + } + } + gl.activeTexture(gl.TEXTURE0); + // Begin worst hack eva // /* diff --git a/src/pixi/renderers/webgl/utils/FilterTexture.js b/src/pixi/renderers/webgl/utils/FilterTexture.js index 0676a7662..0a449263f 100644 --- a/src/pixi/renderers/webgl/utils/FilterTexture.js +++ b/src/pixi/renderers/webgl/utils/FilterTexture.js @@ -125,6 +125,7 @@ PIXI.FilterTexture.prototype.resize = function (width, height) var gl = this.gl; gl.bindTexture(gl.TEXTURE_2D, this.texture); + PIXI.WebGLRenderer.invalidateTextureUnit(); gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA, width , height , 0, gl.RGBA, gl.UNSIGNED_BYTE, null); // update the stencil buffer width and height From b09736869996f25ffe2607c39aa5c5d3c83cf032 Mon Sep 17 00:00:00 2001 From: samme <435681+samme@users.noreply.github.com> Date: Sat, 24 Oct 2026 17:14:02 +0000 Subject: [PATCH 3/5] Preserve texture unit bindings during TileSprite uploads, shader init and RenderTexture resize - TileSprite: flush before uploading the tiling texture, and upload it on the unit the vertices sample. - PixiShader.initMultitexShader: restore existing unit bindings after the dummy-texture hack instead of clobbering them mid-render. - FilterTexture: restore the previous binding after creating or resizing. - Treat base textures without a GL texture as needing an upload. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- CHANGELOG.md | 1 + src/gameobjects/TileSprite.js | 6 +++++ .../renderers/webgl/shaders/PixiShader.js | 27 ++++++++++++++++--- .../renderers/webgl/utils/FilterTexture.js | 15 ++++++++--- .../webgl/utils/WebGLFastSpriteBatch.js | 7 +++-- .../renderers/webgl/utils/WebGLSpriteBatch.js | 5 +++- 6 files changed, 49 insertions(+), 12 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 8e0785846..fac23551f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,7 @@ - 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 diff --git a/src/gameobjects/TileSprite.js b/src/gameobjects/TileSprite.js index 3ab133012..74e1414e9 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.spriteBatch.flush(); renderSession.renderer.updateTexture(this.tilingTexture.baseTexture); diff --git a/src/pixi/renderers/webgl/shaders/PixiShader.js b/src/pixi/renderers/webgl/shaders/PixiShader.js index 38280572a..5d1c2a137 100644 --- a/src/pixi/renderers/webgl/shaders/PixiShader.js +++ b/src/pixi/renderers/webgl/shaders/PixiShader.js @@ -131,23 +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 textureArray = PIXI.WebGLRenderer.textureArray.slice(); 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); - // every unit now holds the dummy texture - PIXI.WebGLRenderer.invalidateTextureUnit(); + // 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); for (var i = 0; i < textureArray.length && i < this.MAX_TEXTURES; ++i) { diff --git a/src/pixi/renderers/webgl/utils/FilterTexture.js b/src/pixi/renderers/webgl/utils/FilterTexture.js index 0a449263f..e509cf2ec 100644 --- a/src/pixi/renderers/webgl/utils/FilterTexture.js +++ b/src/pixi/renderers/webgl/utils/FilterTexture.js @@ -37,10 +37,11 @@ function _CreateFramebuffer (gl, width, height, scaleMode, textureUnit) var colorBuffer = null; var fbStatus = 0; - // creating the texture and framebuffer rebinds texture units - PIXI.WebGLRenderer.invalidateTextureUnit(); - + // 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); @@ -55,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; } @@ -124,9 +129,13 @@ 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); PIXI.WebGLRenderer.invalidateTextureUnit(); 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 f296ca68c..62202c81e 100644 --- a/src/pixi/renderers/webgl/utils/WebGLFastSpriteBatch.js +++ b/src/pixi/renderers/webgl/utils/WebGLFastSpriteBatch.js @@ -200,7 +200,7 @@ PIXI.WebGLFastSpriteBatch.prototype.render = function (spriteBatch) var textureIndex = this.currentBaseTexture.textureIndex; var gl = this.gl; - if (this.currentBaseTexture._dirty[gl.id]) + 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); @@ -230,10 +230,9 @@ PIXI.WebGLFastSpriteBatch.prototype.renderSprite = function (sprite) var baseTexture = texture.baseTexture; var gl = this.gl; var textureIndex = sprite.texture.baseTexture.textureIndex; - var dirty = baseTexture._dirty[gl.id]; + var dirty = baseTexture._dirty[gl.id] || !baseTexture._glTextures[gl.id]; - if ((dirty || PIXI.WebGLRenderer.textureArray[textureIndex] !== baseTexture) && - (dirty || baseTexture._glTextures[gl.id]) && !baseTexture.skipRender) + if ((dirty || PIXI.WebGLRenderer.textureArray[textureIndex] !== baseTexture) && !baseTexture.skipRender) { this.flush(); diff --git a/src/pixi/renderers/webgl/utils/WebGLSpriteBatch.js b/src/pixi/renderers/webgl/utils/WebGLSpriteBatch.js index f979fd196..3731ceec6 100644 --- a/src/pixi/renderers/webgl/utils/WebGLSpriteBatch.js +++ b/src/pixi/renderers/webgl/utils/WebGLSpriteBatch.js @@ -261,7 +261,7 @@ PIXI.WebGLSpriteBatch.prototype.end = function () PIXI.WebGLSpriteBatch.prototype.bindMultiTexture = function (baseTexture, textureUnit) { var gl = this.gl; - var dirty = baseTexture._dirty[gl.id]; + var dirty = baseTexture._dirty[gl.id] || !baseTexture._glTextures[gl.id]; if (!dirty && PIXI.WebGLRenderer.textureArray[textureUnit] === baseTexture) { @@ -272,6 +272,9 @@ PIXI.WebGLSpriteBatch.prototype.bindMultiTexture = function (baseTexture, textur 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); } From 398c6f84c792c730e9f46542a2baec6d44fcf783 Mon Sep 17 00:00:00 2001 From: samme <435681+samme@users.noreply.github.com> Date: Sat, 24 Oct 2026 17:15:09 +0000 Subject: [PATCH 4/5] Remove duplicate multi-texture binding fixes after merge Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- src/gameobjects/TileSprite.js | 1 - src/pixi/renderers/webgl/shaders/PixiShader.js | 14 -------------- src/pixi/renderers/webgl/utils/FilterTexture.js | 1 - 3 files changed, 16 deletions(-) diff --git a/src/gameobjects/TileSprite.js b/src/gameobjects/TileSprite.js index 74e1414e9..01b1bacf9 100644 --- a/src/gameobjects/TileSprite.js +++ b/src/gameobjects/TileSprite.js @@ -350,7 +350,6 @@ Phaser.TileSprite.prototype._renderWebGL = function (renderSession) } this.tilingTexture.baseTexture.textureIndex = this.texture.baseTexture.textureIndex; - renderSession.spriteBatch.flush(); renderSession.renderer.updateTexture(this.tilingTexture.baseTexture); this.tilingTexture.needsUpdate = false; } diff --git a/src/pixi/renderers/webgl/shaders/PixiShader.js b/src/pixi/renderers/webgl/shaders/PixiShader.js index 5d1c2a137..413533b47 100644 --- a/src/pixi/renderers/webgl/shaders/PixiShader.js +++ b/src/pixi/renderers/webgl/shaders/PixiShader.js @@ -143,7 +143,6 @@ PIXI.PixiShader.prototype.initMultitexShader = function () } // HACK: we bind an empty texture to avoid WebGL warning spam. - var textureArray = PIXI.WebGLRenderer.textureArray.slice(); var tempTexture = gl.createTexture(); gl.activeTexture(gl.TEXTURE0); gl.bindTexture(gl.TEXTURE_2D, tempTexture); @@ -168,19 +167,6 @@ PIXI.PixiShader.prototype.initMultitexShader = function () gl.activeTexture(previousActive); - for (var i = 0; i < textureArray.length && i < this.MAX_TEXTURES; ++i) - { - var texture = textureArray[i]; - - if (texture && texture._glTextures && texture._glTextures[gl.id]) - { - gl.activeTexture(gl.TEXTURE0 + i); - gl.bindTexture(gl.TEXTURE_2D, texture._glTextures[gl.id]); - PIXI.WebGLRenderer.textureArray[i] = texture; - } - } - gl.activeTexture(gl.TEXTURE0); - // Begin worst hack eva // /* diff --git a/src/pixi/renderers/webgl/utils/FilterTexture.js b/src/pixi/renderers/webgl/utils/FilterTexture.js index e509cf2ec..c261c4fcb 100644 --- a/src/pixi/renderers/webgl/utils/FilterTexture.js +++ b/src/pixi/renderers/webgl/utils/FilterTexture.js @@ -133,7 +133,6 @@ PIXI.FilterTexture.prototype.resize = function (width, height) // 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); - PIXI.WebGLRenderer.invalidateTextureUnit(); gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA, width , height , 0, gl.RGBA, gl.UNSIGNED_BYTE, null); gl.bindTexture(gl.TEXTURE_2D, previousTexture); From 57ad1de3380ad4dc8c8ae739d89294b686361e25 Mon Sep 17 00:00:00 2001 From: samme <435681+samme@users.noreply.github.com> Date: Sat, 24 Oct 2026 18:32:09 +0000 Subject: [PATCH 5/5] Treat source-less render targets as never stale in multi-texture binding Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../renderers/webgl/utils/WebGLFastSpriteBatch.js | 12 ++++++++++++ src/pixi/renderers/webgl/utils/WebGLSpriteBatch.js | 7 +++++++ 2 files changed, 19 insertions(+) diff --git a/src/pixi/renderers/webgl/utils/WebGLFastSpriteBatch.js b/src/pixi/renderers/webgl/utils/WebGLFastSpriteBatch.js index 62202c81e..e2f48d687 100644 --- a/src/pixi/renderers/webgl/utils/WebGLFastSpriteBatch.js +++ b/src/pixi/renderers/webgl/utils/WebGLFastSpriteBatch.js @@ -200,6 +200,12 @@ PIXI.WebGLFastSpriteBatch.prototype.render = function (spriteBatch) var textureIndex = this.currentBaseTexture.textureIndex; var gl = this.gl; + 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 @@ -230,6 +236,12 @@ PIXI.WebGLFastSpriteBatch.prototype.renderSprite = function (sprite) var baseTexture = texture.baseTexture; var gl = this.gl; var textureIndex = sprite.texture.baseTexture.textureIndex; + + 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) diff --git a/src/pixi/renderers/webgl/utils/WebGLSpriteBatch.js b/src/pixi/renderers/webgl/utils/WebGLSpriteBatch.js index 3731ceec6..ca3b5ddb2 100644 --- a/src/pixi/renderers/webgl/utils/WebGLSpriteBatch.js +++ b/src/pixi/renderers/webgl/utils/WebGLSpriteBatch.js @@ -261,6 +261,13 @@ PIXI.WebGLSpriteBatch.prototype.end = function () 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)