Summary
SparkRenderer uploads its sorting/ordering texture with a direct gl.pixelStorei call:
https://github.com/sparkjsdev/spark/blob/main/src/SparkRenderer.ts#L1641
renderer.state.activeTexture(gl.TEXTURE0);
renderer.state.bindTexture(gl.TEXTURE_2D, glTexture);
gl.bindBuffer(gl.PIXEL_UNPACK_BUFFER, null);
gl.pixelStorei(gl.UNPACK_FLIP_Y_WEBGL, false); // <-- direct GL call
gl.texSubImage2D(...)
Recent three.js versions (observed with r184) cache pixel-store state in WebGLState.pixelStorei() and skip the redundant GL call when the cached value matches. Since Spark bypasses renderer.state here (while correctly using it for activeTexture/bindTexture just above), three's cache silently goes stale:
- three uploads some texture with
flipY: true → GL state UNPACK_FLIP_Y = true, cache = true.
- Spark's sort upload sets
gl.pixelStorei(UNPACK_FLIP_Y, false) directly → GL = false, three's cache still says true.
- three uploads the next
flipY: true texture (e.g. a CanvasTexture): state.pixelStorei(UNPACK_FLIP_Y, true) compares against the stale cache, considers it a no-op, and skips the GL call → the texture is uploaded without the flip and renders vertically mirrored.
- The cache resyncs on the next
flipY: false upload, so the corruption is intermittent — only textures whose first upload happens to land between a Spark sort and the next cache resync are affected.
Real-world impact
We hit this in a scene combining Spark splats with NASA's 3DTilesRendererJS ImageOverlayPlugin (satellite imagery draped over Cesium World Terrain). The overlay's composited CanvasTextures (flipY: true) intermittently uploaded un-flipped, producing vertically mirrored ground tiles whose content jumped around when the LOD refined — extremely confusing to debug, since texture data, UVs and texture.flipY were all correct; only the GPU upload was wrong. In one session 33 of 262 uploaded overlay tiles were mirrored.
src/pcsogs.ts L294 has the same pattern (gl.pixelStorei(gl.UNPACK_FLIP_Y_WEBGL, true) on a raw-GL texture path) and can desync the cache in the opposite direction.
Suggested fix
Either route the call through three's cache so it stays coherent:
renderer.state.pixelStorei(gl.UNPACK_FLIP_Y_WEBGL, false);
(available on recent three; falls back gracefully) — or save/restore the previous value around the texSubImage2D, or call renderer.state.reset() after raw-GL work.
Workaround we use
Wrapping driveSort and resyncing the cache afterwards fixes it reliably:
const orig = spark.driveSort.bind(spark);
spark.driveSort = async (...args) => {
try { return await orig(...args); }
finally { renderer.state.pixelStorei(0x9240 /* UNPACK_FLIP_Y_WEBGL */, false); }
};
Spark 2.1.0, three r184, Chrome/Windows.
Summary
SparkRendereruploads its sorting/ordering texture with a directgl.pixelStoreicall:https://github.com/sparkjsdev/spark/blob/main/src/SparkRenderer.ts#L1641
Recent three.js versions (observed with r184) cache pixel-store state in
WebGLState.pixelStorei()and skip the redundant GL call when the cached value matches. Since Spark bypassesrenderer.statehere (while correctly using it foractiveTexture/bindTexturejust above), three's cache silently goes stale:flipY: true→ GL stateUNPACK_FLIP_Y = true, cache =true.gl.pixelStorei(UNPACK_FLIP_Y, false)directly → GL =false, three's cache still saystrue.flipY: truetexture (e.g. aCanvasTexture):state.pixelStorei(UNPACK_FLIP_Y, true)compares against the stale cache, considers it a no-op, and skips the GL call → the texture is uploaded without the flip and renders vertically mirrored.flipY: falseupload, so the corruption is intermittent — only textures whose first upload happens to land between a Spark sort and the next cache resync are affected.Real-world impact
We hit this in a scene combining Spark splats with NASA's
3DTilesRendererJSImageOverlayPlugin(satellite imagery draped over Cesium World Terrain). The overlay's compositedCanvasTextures (flipY: true) intermittently uploaded un-flipped, producing vertically mirrored ground tiles whose content jumped around when the LOD refined — extremely confusing to debug, since texture data, UVs andtexture.flipYwere all correct; only the GPU upload was wrong. In one session 33 of 262 uploaded overlay tiles were mirrored.src/pcsogs.tsL294 has the same pattern (gl.pixelStorei(gl.UNPACK_FLIP_Y_WEBGL, true)on a raw-GL texture path) and can desync the cache in the opposite direction.Suggested fix
Either route the call through three's cache so it stays coherent:
(available on recent three; falls back gracefully) — or save/restore the previous value around the
texSubImage2D, or callrenderer.state.reset()after raw-GL work.Workaround we use
Wrapping
driveSortand resyncing the cache afterwards fixes it reliably:Spark 2.1.0, three r184, Chrome/Windows.