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: {} });