From 65e30546dde1fe9c9132a4263b28e48df94fda17 Mon Sep 17 00:00:00 2001 From: German Khokhlov Date: Sat, 17 Oct 2026 06:49:53 +0400 Subject: [PATCH] fix(globe): Navara sprites keep a full rewrite when a commit follows it MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit A sprite/point set uploads its instances by update range. When the camera went far enough to move the set's origin, update() rewrote every instance (relative to the new origin) — and a commit later in the same frame cleared the ranges and kept only its own, so the GPU never got the rewrite: the instances not committed again drew offset by the origin move (a military glyph at the orbit pose drew 45 px from its point after the global → city → orbit sequence; the CPU picks, the probe and Cesium had it in place). Ranges now add up until three uploads them (it clears them after the upload). Found by the military side-by-side (11b); flights' sprites had the same exposure since 10b. --- src/globe/navara/movers/moverMesh.js | 4 ++- src/globe/navara/movers/moverMesh.test.mjs | 32 ++++++++++++++++++++++ 2 files changed, 35 insertions(+), 1 deletion(-) diff --git a/src/globe/navara/movers/moverMesh.js b/src/globe/navara/movers/moverMesh.js index 6372e2d05..aeae1d3bd 100644 --- a/src/globe/navara/movers/moverMesh.js +++ b/src/globe/navara/movers/moverMesh.js @@ -262,7 +262,9 @@ export function createMoverMesh( [colorAttribute, 4], [uvAttribute, 4], ]) { - value.clearUpdateRanges(); + // Ranges add up until three uploads them (and clears them): a + // partial commit after a full rewrite in the same frame (an origin + // move) must not drop the rewrite — sprites drew offset by the move. value.addUpdateRange(from * size, (to - from) * size); value.needsUpdate = true; } diff --git a/src/globe/navara/movers/moverMesh.test.mjs b/src/globe/navara/movers/moverMesh.test.mjs index d1c5193dd..d2e3c3afb 100644 --- a/src/globe/navara/movers/moverMesh.test.mjs +++ b/src/globe/navara/movers/moverMesh.test.mjs @@ -112,3 +112,35 @@ test('points have no atlas and draw a disc', () => { assert.equal(mover.uniforms.uGevOutlinePx.value, 1); mover.dispose(); }); + +test('an origin move uploads every instance even when a commit follows in the same frame', () => { + const store = createMoverStore(spriteOptions); + const mover = createMoverMesh(spriteOptions, store); + for (let index = 0; index < 4; index++) + store.set(index, { position: { x: 6_378_137 + index * 10, y: 0, z: 0 } }); + store.setCount(4); + mover.commit(store.dirty()); + store.clean(); + // The GPU took that upload (three clears the ranges after it). + mover.mesh.instanceMatrix.clearUpdateRanges(); + // The camera went far: the origin moves and every instance is rewritten... + assert.equal( + mover.update({ + cameraPosition: { x: 6_378_137 + 2_000_000, y: 0, z: 0 }, + cameraHeight: 2_000_000, + fovDeg: 60, + viewportHeightPx: 800, + }), + true, + ); + // ...then one instance changes before the frame is drawn. + store.set(2, { rotation: 1 }); + mover.commit(store.dirty()); + store.clean(); + const ranges = mover.mesh.instanceMatrix.updateRanges; + const covered = new Set(); + for (const { start, count } of ranges) + for (let i = start; i < start + count; i++) covered.add(i); + for (let i = 0; i < 4 * 16; i++) + assert.ok(covered.has(i), `instance float ${i} is uploaded`); +});