Skip to content

SparkRenderer sorting upload sets UNPACK_FLIP_Y_WEBGL directly, desyncing three.js WebGLState cache (other textures upload vertically mirrored) #388

Description

@sebgoubier

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:

  1. three uploads some texture with flipY: true → GL state UNPACK_FLIP_Y = true, cache = true.
  2. Spark's sort upload sets gl.pixelStorei(UNPACK_FLIP_Y, false) directly → GL = false, three's cache still says true.
  3. 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.
  4. 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.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions