From be395f0f5260e1eb70e0bba86b2607dbeb30a3de Mon Sep 17 00:00:00 2001 From: German Khokhlov Date: Tue, 10 Nov 2026 22:31:20 +0400 Subject: [PATCH] Finish traffic surface sampling with the camera still Road samples that were not ready, or left over when a pass ran out of its slice, waited for the next camera move; with the camera at rest they stayed pending and part of the network never settled on the ground. The flow now requests the frame it needs and continues the unresolved samples within the same 12 ms slice and station budget. --- src/layers/traffic/flow.js | 17 ++++--- src/layers/traffic/handoff.test.mjs | 75 +++++++++++++++++++++++++++++ 2 files changed, 86 insertions(+), 6 deletions(-) diff --git a/src/layers/traffic/flow.js b/src/layers/traffic/flow.js index 5f717ec43..fd9a9cec6 100644 --- a/src/layers/traffic/flow.js +++ b/src/layers/traffic/flow.js @@ -538,6 +538,7 @@ export function createFlow({ state: layerState, services, parts, source }) { current() && performance.now() < deadline ) { + engine.render.request('traffic-surface'); await new Promise((resolve) => setTimeout(resolve, 100)); if (!current()) return false; prepared = await prepareRoadSurfaces( @@ -555,10 +556,9 @@ export function createFlow({ state: layerState, services, parts, source }) { if ( current() && !preview && - !label.includes('refined') && - (!trafficSurfaceReady(engine) || + ((!refining && !trafficSurfaceReady(engine)) || prepared.pending.length || - ready.some((road) => road.surfaceSettled === false)) + (!refining && ready.some((road) => road.surfaceSettled === false))) ) { layerState._surfaceRefineRemove?.(); const stop = () => { @@ -572,17 +572,21 @@ export function createFlow({ state: layerState, services, parts, source }) { stop(); return; } - if (!trafficSurfaceReady(engine)) return; + if (!trafficSurfaceReady(engine)) { + engine.render.request('traffic-surface'); + return; + } stop(); // Revalidate only local samples taken at a coarser rendered LOD. This - // background upgrade never holds road acquisition or first paint. + // background upgrade never holds road acquisition or first paint; + // missing samples keep retrying while refined heights stay cached. layerState._surfaceRefining = true; applyFlowThenRender( roads, clamped, generation, altitude, - `${label} refined`, + refining ? label : `${label} refined`, trace, false, signal, @@ -598,6 +602,7 @@ export function createFlow({ state: layerState, services, parts, source }) { }); const timer = setTimeout(stop, 10000); layerState._surfaceRefineRemove = stop; + engine.render.request('traffic-surface'); } return current() && ready.length > 0; } diff --git a/src/layers/traffic/handoff.test.mjs b/src/layers/traffic/handoff.test.mjs index daaaa547f..8e5e53b21 100644 --- a/src/layers/traffic/handoff.test.mjs +++ b/src/layers/traffic/handoff.test.mjs @@ -166,6 +166,81 @@ test('the handoff pass on a loading globe refines once its surface is ready', as ); }); +test('a still camera drains pending surface samples across bounded passes using requested frames', async (t) => { + const surface = incomingGlobe(t); + surface.ready = true; + surface.frame(); + const { engine } = surface; + const { state, flow } = fixture(surface); + t.after(() => { + state._activeFetchAbort.abort(); + state._surfaceRefineRemove?.(); + }); + const roads = Array.from({ length: 20 }, (_, i) => road(i * 0.0001, GROUND)); + let time = 0; + let available = 8; + let samples = 0; + let samplesAtYield; + let frames = 0; + const measured = new Set(); + t.mock.method(performance, 'now', () => time); + engine.fake.script('terrainHeight', (point) => { + samples++; + if (samples === 1) setTimeout(() => (samplesAtYield = samples), 0); + time += 3; + const key = `${point.longitude},${point.latitude}`; + if (!measured.has(key)) { + if (!available) return undefined; + available--; + measured.add(key); + } + return GROUND; + }); + let cameraChanges = 0; + t.after(engine.camera.changed.addEventListener(() => cameraChanges++)); + t.after( + engine.frames.preRender.addEventListener(() => { + available = 8; + surface.ready = ++frames > 1; + }), + ); + const requests = engine.fake.renderRequests; + await flow.applyFlowThenRender( + roads, + { ...bounds, coverage }, + 1, + 1800, + 'Loaded full', + ); + assert.ok(samples * 3 > 12, 'the samples exceed one surface slice'); + assert.equal(samplesAtYield, 4, 'sampling yields at the 12 ms budget'); + assert.ok(state._surfacePending > 0, 'some streets are not loaded yet'); + let rendered = requests; + await settle(() => { + if (engine.fake.renderRequests > rendered) { + rendered = engine.fake.renderRequests; + surface.frame(); + } + return !state._surfacePending && !state._surfaceRefining; + }); + assert.equal(state._surfacePending, 0); + assert.equal(cameraChanges, 0, 'no camera trigger resumes the work'); + assert.ok(rendered - requests > 1, 'missing heights need further frames'); + assert.equal(state._surfaceRefineRemove, null); + for (const r of roads) { + assert.equal(r.surfaceReady, true); + assert.ok( + r.waypoints.every( + (point) => Math.abs(height(point) - GROUND - DOT_HEIGHT_OFFSET) < 0.01, + ), + ); + } + const finished = engine.fake.renderRequests; + surface.frame(); + await nextTask(); + assert.equal(engine.fake.renderRequests, finished, 'settled work is idle'); +}); + test('resuming the viewport republishes the retained roads as a pass that can still refine', () => { const labels = []; const state = createState({ services: {} });