mirror of
https://github.com/bilawalsidhu/gods-eye-view.git
synced 2026-11-07 20:04:00 +00:00
Settle the application's renderer through the shared Cesium ports
whenSettled moves from the conformance renderer into the shared Cesium ports, unchanged but for what it waits for, and the application's renderer gains it. A hidden globe loads nothing, so it no longer holds settling up: the application hides its globe under photoreal tiles. Shown 3D tilesets in the scene must have loaded what is in view, named 'tiles' while they have not. Disposing the ports ends the watches under way, which fail as destroyed, as the conformance renderer's destroy did. It waits for the globe, the scene's tiles and what the draw sets hold, not for what layers still draw through the viewer themselves.
This commit is contained in:
1 parent
2bb0b1e367
commit
6ffdf30795
5 files changed
+212
-147
No files matched your search
@@ -7,10 +7,11 @@
|
||||
* The renderer it makes is transitional. Its ports are the shared Cesium
|
||||
* ports (`ports.js`), the ones the conformance renderer is checked with,
|
||||
* less the members that would not be true of the application yet: the
|
||||
* globe (it draws photoreal tiles the port does not know) and
|
||||
* `whenSettled` (it would not wait for them). Those arrive with the layer
|
||||
* families that need them (phase 2 of the renderer plan);
|
||||
* `checkRendererShape` lists what is still missing.
|
||||
* globe (it draws photoreal tiles the port does not know). Those arrive
|
||||
* with the layer families that need them (phase 2 of the renderer plan);
|
||||
* `checkRendererShape` lists what is still missing. `whenSettled` waits
|
||||
* for the globe, the scene's 3D tiles and what the draw sets hold, not for
|
||||
* what layers still draw through the viewer themselves.
|
||||
* Until a family is ported, its code reaches the Cesium viewer through
|
||||
* `cesiumViewerOf`, never through a renderer member, so no engine type
|
||||
* crosses the contract and each call marks code still to port. Ported code
|
||||
@@ -27,8 +28,7 @@ import { cesiumCapabilities, createCesiumPorts } from './ports.js';
|
||||
* @typedef {Pick<import('../contract/renderer').Renderer,
|
||||
* 'id' | 'contractVersion' | 'capabilities' | 'canvas' | 'frame' | 'draw'
|
||||
* | 'camera' | 'query' | 'input' | 'effects' | 'quality' | 'capture'
|
||||
* | 'credits'
|
||||
* | 'on' | 'resize' | 'destroy'>}
|
||||
* | 'credits' | 'whenSettled' | 'on' | 'resize' | 'destroy'>}
|
||||
* TransitionalRenderer
|
||||
*/
|
||||
|
||||
@@ -118,6 +118,7 @@ export function createCesiumRendererPlugin({
|
||||
quality: ports.quality,
|
||||
capture: ports.capture,
|
||||
credits: ports.credits,
|
||||
whenSettled: ports.whenSettled,
|
||||
on: ports.on,
|
||||
resize() {
|
||||
viewer.resize();
|
||||
|
||||
@@ -321,5 +321,4 @@ const EXPECTED_MISSING = [
|
||||
'renderer.globe',
|
||||
'renderer.readPixels',
|
||||
'renderer.releaseUnused',
|
||||
'renderer.whenSettled',
|
||||
].map((member) => `${member} is missing`);
|
||||
+146
-4
@@ -24,6 +24,7 @@ import {
|
||||
billboardLoadError,
|
||||
lastRenderTime,
|
||||
lastSeenCamera,
|
||||
onEngineWorkCompleted,
|
||||
renderPending,
|
||||
} from './internals.js';
|
||||
|
||||
@@ -1147,7 +1148,7 @@ export function createCesiumPorts(widget, { capabilities }) {
|
||||
);
|
||||
}
|
||||
|
||||
/** The live model sets, for a renderer's own `whenSettled`. */
|
||||
/** The live model sets, for `whenSettled`. */
|
||||
const modelSets = () =>
|
||||
[...sets.values()].filter((r) => r.kind === 'models' && r.host);
|
||||
|
||||
@@ -1170,6 +1171,148 @@ export function createCesiumPorts(widget, { capabilities }) {
|
||||
return loading ? 'loading' : 'settled';
|
||||
}
|
||||
|
||||
/** A hidden globe loads nothing, so it is never what is still loading. */
|
||||
const globeLoaded = () =>
|
||||
!scene.globe?.show || scene.globe.tilesLoaded !== false;
|
||||
/** Whether every shown 3D tileset has loaded what is in view. */
|
||||
const tilesLoaded = () =>
|
||||
tilesets().every((tileset) => !tileset.show || tileset.tilesLoaded);
|
||||
|
||||
// --- Settling --------------------------------------------------------------
|
||||
|
||||
// Idle watches in progress; disposal ends them rather than leave them
|
||||
// waiting on a scene that will not tick again.
|
||||
const watching = new Set();
|
||||
|
||||
/** How long the loop must stay idle, drawing nothing, to be settled. */
|
||||
const IDLE_MS = 100;
|
||||
/** How long without a tick a loop counts as stopped. */
|
||||
const STALL_MS = 250;
|
||||
|
||||
/**
|
||||
* Watches the loop, without asking for frames, for at least two whole
|
||||
* ticks and `IDLE_MS`. Resolves `'idle'` if in that time it drew nothing
|
||||
* of its own accord and `busy()` stayed false; `'busy'` as soon as it did;
|
||||
* `'ended'` if the ports are disposed or `end` passes first. A draw while
|
||||
* a hold keeps the loop drawing every frame is the hold's, not the
|
||||
* scene's own; `busy` is asked at each tick's start, before Cesium decides
|
||||
* whether it draws.
|
||||
* @param {number} end
|
||||
* @param {() => boolean} busy Whether work landed that will draw again.
|
||||
*/
|
||||
function watchIdle(end, busy) {
|
||||
return new Promise((resolve) => {
|
||||
const from = performance.now();
|
||||
let ticks = 0;
|
||||
const offDraw = scene.postRender.addEventListener(() => {
|
||||
if (scene.requestRenderMode) finish('busy');
|
||||
});
|
||||
// Each preUpdate also ends the tick before it.
|
||||
const offTick = scene.preUpdate.addEventListener(() => {
|
||||
if (busy()) return finish('busy');
|
||||
ticks += 1;
|
||||
if (ticks > 2 && performance.now() - from >= IDLE_MS) finish('idle');
|
||||
});
|
||||
const timer = setTimeout(
|
||||
() => finish('ended'),
|
||||
Math.max(0, end - performance.now()),
|
||||
);
|
||||
watching.add(finish);
|
||||
function finish(outcome) {
|
||||
watching.delete(finish);
|
||||
offDraw();
|
||||
offTick();
|
||||
clearTimeout(timer);
|
||||
resolve(outcome);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
async function whenSettled({ timeoutMs = 20000 } = {}) {
|
||||
const end = performance.now() + timeoutMs;
|
||||
// Cesium redraws on its own after each request or worker task it
|
||||
// completes (a terrain picker build after the camera moved, say), which
|
||||
// no loading flag shows; settled means that has stopped too. Every
|
||||
// scene in the page redraws for any completion, so another scene's work
|
||||
// keeps this one from settling: it is drawing.
|
||||
let completed = 0;
|
||||
// Whether the last look found the engine still at work.
|
||||
let busy = false;
|
||||
// When the loop last ticked: one that stopped (suspended, say) is named
|
||||
// in a timeout, not one that merely ran out of time mid-frame.
|
||||
let lastTick = performance.now();
|
||||
const offTick = scene.preUpdate.addEventListener(
|
||||
() => (lastTick = performance.now()),
|
||||
);
|
||||
const off = onEngineWorkCompleted(Cesium, () => (completed += 1));
|
||||
try {
|
||||
let quiet = 0;
|
||||
while (performance.now() < end) {
|
||||
completed = 0;
|
||||
if (disposed) return { state: 'failed', blockers: ['destroyed'] };
|
||||
// Bounded by the time left: a suspended loop draws nothing.
|
||||
const drawn = await nextPostRender({
|
||||
timeoutMs: Math.max(0, end - performance.now()),
|
||||
}).then(
|
||||
() => true,
|
||||
() => false,
|
||||
);
|
||||
if (disposed) return { state: 'failed', blockers: ['destroyed'] };
|
||||
if (!drawn) break;
|
||||
if (modelSets().some((r) => r.host.failed > 0))
|
||||
return { state: 'failed', blockers: ['models'] };
|
||||
const icons = iconState();
|
||||
if (icons === 'failed') return { state: 'failed', blockers: ['icons'] };
|
||||
quiet = loaded() && !idleBlocked() ? quiet + 1 : 0;
|
||||
if (quiet < 3) continue;
|
||||
// Then the loop left alone must draw nothing by itself for a while:
|
||||
// a worker's first task can take tens of milliseconds, and what is
|
||||
// in flight cannot be counted. Something that starts loading
|
||||
// meanwhile ends the watch too; the next pass sees it, or its
|
||||
// failure.
|
||||
const outcome = await watchIdle(end, () => !loaded() || idleBlocked());
|
||||
if (outcome === 'idle') return { state: 'settled', blockers: [] };
|
||||
if (outcome !== 'busy') break;
|
||||
// Work or a draw, unless it was something loading.
|
||||
busy = loaded();
|
||||
quiet = 0;
|
||||
}
|
||||
if (disposed) return { state: 'failed', blockers: ['destroyed'] };
|
||||
const blockers = [];
|
||||
if (!globeLoaded()) blockers.push('globe');
|
||||
if (!tilesLoaded()) blockers.push('tiles');
|
||||
if (modelSets().some((r) => r.host.loading > 0)) blockers.push('models');
|
||||
if (iconState() !== 'settled') blockers.push('icons');
|
||||
if (busy) blockers.push('redraw');
|
||||
if (performance.now() - lastTick >= STALL_MS) blockers.push('frames');
|
||||
return { state: 'timeout', blockers };
|
||||
} finally {
|
||||
off();
|
||||
offTick();
|
||||
}
|
||||
|
||||
/** Whether what is in view has loaded, nothing failing. */
|
||||
function loaded() {
|
||||
const icons = iconState();
|
||||
return (
|
||||
globeLoaded() &&
|
||||
tilesLoaded() &&
|
||||
icons === 'settled' &&
|
||||
!modelSets().some((r) => r.host.loading > 0 || r.host.failed > 0)
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Work since the last look that will draw again, or a draw queued (one a
|
||||
* hold makes anyway is the hold's).
|
||||
*/
|
||||
function idleBlocked() {
|
||||
busy = completed > 0 || (scene.requestRenderMode && renderPending(scene));
|
||||
completed = 0;
|
||||
return busy;
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
/** The renderer's own events: `error`, `contextLost`, `contextRestored`. */
|
||||
on(event, cb) {
|
||||
@@ -1184,12 +1327,11 @@ export function createCesiumPorts(widget, { capabilities }) {
|
||||
effects: effectsPart.port,
|
||||
quality: qualityPart.port,
|
||||
capture,
|
||||
modelSets,
|
||||
iconState,
|
||||
nextPostRender,
|
||||
whenSettled,
|
||||
/** Detach from the scene; the widget's owner destroys the widget. */
|
||||
dispose() {
|
||||
disposed = true;
|
||||
for (const finish of [...watching]) finish('ended');
|
||||
// The tick under way is no longer seen through: it is not claimed.
|
||||
settleTick(false);
|
||||
for (const off of removers) off();
|
||||
|
||||
@@ -712,3 +712,61 @@ test('setTime moves scene time and draws it, leaving the clock running or not',
|
||||
assert.deepEqual(ports.frame.diagnostics().pendingReasons, ['time']);
|
||||
ports.dispose();
|
||||
});
|
||||
|
||||
test('whenSettled waits for shown 3D tiles, not for a hidden globe', async () => {
|
||||
const { widget, scene, raise } = tickingWidget();
|
||||
const list = [];
|
||||
scene.primitives = listedPrimitives(list);
|
||||
scene.globe = { show: false, tilesLoaded: false };
|
||||
// Each asked-for frame is drawn soon after.
|
||||
scene.requestRender = () => setTimeout(() => raise('postRender'), 1);
|
||||
const ports = createCesiumPorts(widget, { capabilities: all });
|
||||
const tileset = Object.create(Cesium.Cesium3DTileset.prototype);
|
||||
Object.defineProperty(tileset, 'tilesLoaded', { value: false });
|
||||
tileset.show = true;
|
||||
list.push(tileset);
|
||||
const result = await ports.whenSettled({ timeoutMs: 60 });
|
||||
assert.equal(result.state, 'timeout');
|
||||
assert.ok(result.blockers.includes('tiles'), result.blockers.join());
|
||||
assert.ok(!result.blockers.includes('globe'), 'a hidden globe loads nothing');
|
||||
tileset.show = false;
|
||||
scene.globe.show = true;
|
||||
const shown = await ports.whenSettled({ timeoutMs: 60 });
|
||||
assert.ok(!shown.blockers.includes('tiles'), 'hidden tiles load nothing');
|
||||
assert.ok(shown.blockers.includes('globe'), 'a shown globe still loading');
|
||||
ports.dispose();
|
||||
});
|
||||
|
||||
test('whenSettled under way fails as destroyed once the ports are disposed', async () => {
|
||||
const { widget, scene, raise } = tickingWidget();
|
||||
scene.primitives = listedPrimitives([]);
|
||||
scene.requestRender = () => setTimeout(() => raise('postRender'), 1);
|
||||
const ports = createCesiumPorts(widget, { capabilities: all });
|
||||
const pending = ports.whenSettled({ timeoutMs: 5000 });
|
||||
setTimeout(() => ports.dispose(), 5);
|
||||
assert.deepEqual(await pending, { state: 'failed', blockers: ['destroyed'] });
|
||||
});
|
||||
|
||||
test('whenSettled settles once shown tiles have loaded under a hidden globe', async () => {
|
||||
const { widget, scene, raise } = tickingWidget();
|
||||
const list = [];
|
||||
scene.primitives = listedPrimitives(list);
|
||||
scene.globe = { show: false, tilesLoaded: false };
|
||||
scene.requestRender = () => setTimeout(() => raise('postRender'), 1);
|
||||
const tileset = Object.create(Cesium.Cesium3DTileset.prototype);
|
||||
Object.defineProperty(tileset, 'tilesLoaded', { value: true });
|
||||
tileset.show = true;
|
||||
list.push(tileset);
|
||||
const ports = createCesiumPorts(widget, { capabilities: all });
|
||||
// The loop ticks without drawing.
|
||||
const ticker = setInterval(() => raise('preUpdate'), 5);
|
||||
try {
|
||||
assert.deepEqual(await ports.whenSettled({ timeoutMs: 2000 }), {
|
||||
state: 'settled',
|
||||
blockers: [],
|
||||
});
|
||||
} finally {
|
||||
clearInterval(ticker);
|
||||
ports.dispose();
|
||||
}
|
||||
});
|
||||
@@ -18,7 +18,6 @@
|
||||
*/
|
||||
import * as Cesium from 'cesium';
|
||||
import { createCesiumInput } from './input.js';
|
||||
import { onEngineWorkCompleted, renderPending } from './internals.js';
|
||||
import { cesiumCapabilities, createCesiumPorts } from './ports.js';
|
||||
|
||||
export { toCartesian, toColor, toImage } from './ports.js';
|
||||
@@ -125,138 +124,6 @@ export async function createCesiumSceneRenderer({
|
||||
|
||||
// --- Renderer --------------------------------------------------------------
|
||||
|
||||
let destroyed = false;
|
||||
// Idle watches in progress; destroy ends them rather than leave them
|
||||
// waiting on a scene that will not tick again.
|
||||
const watching = new Set();
|
||||
|
||||
/** How long the loop must stay idle, drawing nothing, to be settled. */
|
||||
const IDLE_MS = 100;
|
||||
/** How long without a tick a loop counts as stopped. */
|
||||
const STALL_MS = 250;
|
||||
|
||||
/**
|
||||
* Watches the loop, without asking for frames, for at least two whole
|
||||
* ticks and `IDLE_MS`. Resolves `'idle'` if in that time it drew nothing
|
||||
* of its own accord and `busy()` stayed false; `'busy'` as soon as it did;
|
||||
* `'ended'` if the renderer is destroyed or `end` passes first. A draw while a hold keeps
|
||||
* the loop drawing every frame is the hold's, not the scene's own; `busy`
|
||||
* is asked at each tick's start, before Cesium decides whether it draws.
|
||||
* @param {number} end
|
||||
* @param {() => boolean} busy Whether work landed that will draw again.
|
||||
*/
|
||||
function watchIdle(end, busy) {
|
||||
return new Promise((resolve) => {
|
||||
const from = performance.now();
|
||||
let ticks = 0;
|
||||
const offDraw = scene.postRender.addEventListener(() => {
|
||||
if (scene.requestRenderMode) finish('busy');
|
||||
});
|
||||
// Each preUpdate also ends the tick before it.
|
||||
const offTick = scene.preUpdate.addEventListener(() => {
|
||||
if (busy()) return finish('busy');
|
||||
ticks += 1;
|
||||
if (ticks > 2 && performance.now() - from >= IDLE_MS) finish('idle');
|
||||
});
|
||||
const timer = setTimeout(
|
||||
() => finish('ended'),
|
||||
Math.max(0, end - performance.now()),
|
||||
);
|
||||
watching.add(finish);
|
||||
function finish(outcome) {
|
||||
watching.delete(finish);
|
||||
offDraw();
|
||||
offTick();
|
||||
clearTimeout(timer);
|
||||
resolve(outcome);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
async function whenSettled({ timeoutMs = 20000 } = {}) {
|
||||
const models = ports.modelSets;
|
||||
const end = performance.now() + timeoutMs;
|
||||
// Cesium redraws on its own after each request or worker task it
|
||||
// completes (a terrain picker build after the camera moved, say), which
|
||||
// no loading flag shows; settled means that has stopped too. Every
|
||||
// scene in the page redraws for any completion, so another scene's work
|
||||
// keeps this one from settling: it is drawing.
|
||||
let completed = 0;
|
||||
// Whether the last look found the engine still at work.
|
||||
let busy = false;
|
||||
// When the loop last ticked: one that stopped (suspended, say) is named
|
||||
// in a timeout, not one that merely ran out of time mid-frame.
|
||||
let lastTick = performance.now();
|
||||
const offTick = scene.preUpdate.addEventListener(
|
||||
() => (lastTick = performance.now()),
|
||||
);
|
||||
const off = onEngineWorkCompleted(Cesium, () => (completed += 1));
|
||||
try {
|
||||
let quiet = 0;
|
||||
while (performance.now() < end) {
|
||||
completed = 0;
|
||||
if (destroyed) return { state: 'failed', blockers: ['destroyed'] };
|
||||
// Bounded by the time left: a suspended loop draws nothing.
|
||||
const drawn = await ports
|
||||
.nextPostRender({ timeoutMs: Math.max(0, end - performance.now()) })
|
||||
.then(
|
||||
() => true,
|
||||
() => false,
|
||||
);
|
||||
if (destroyed) return { state: 'failed', blockers: ['destroyed'] };
|
||||
if (!drawn) break;
|
||||
if (models().some((r) => r.host.failed > 0))
|
||||
return { state: 'failed', blockers: ['models'] };
|
||||
const icons = ports.iconState();
|
||||
if (icons === 'failed') return { state: 'failed', blockers: ['icons'] };
|
||||
quiet = loaded() && !idleBlocked() ? quiet + 1 : 0;
|
||||
if (quiet < 3) continue;
|
||||
// Then the loop left alone must draw nothing by itself for a while:
|
||||
// a worker's first task can take tens of milliseconds, and what is
|
||||
// in flight cannot be counted. Something that starts loading
|
||||
// meanwhile ends the watch too; the next pass sees it, or its
|
||||
// failure.
|
||||
const outcome = await watchIdle(end, () => !loaded() || idleBlocked());
|
||||
if (outcome === 'idle') return { state: 'settled', blockers: [] };
|
||||
if (outcome !== 'busy') break;
|
||||
// Work or a draw, unless it was something loading.
|
||||
busy = loaded();
|
||||
quiet = 0;
|
||||
}
|
||||
if (destroyed) return { state: 'failed', blockers: ['destroyed'] };
|
||||
const blockers = [];
|
||||
if (!scene.globe.tilesLoaded) blockers.push('globe');
|
||||
if (models().some((r) => r.host.loading > 0)) blockers.push('models');
|
||||
if (ports.iconState() !== 'settled') blockers.push('icons');
|
||||
if (busy) blockers.push('redraw');
|
||||
if (performance.now() - lastTick >= STALL_MS) blockers.push('frames');
|
||||
return { state: 'timeout', blockers };
|
||||
} finally {
|
||||
off();
|
||||
offTick();
|
||||
}
|
||||
|
||||
/** Whether what is in view has loaded, nothing failing. */
|
||||
function loaded() {
|
||||
const icons = ports.iconState();
|
||||
return (
|
||||
scene.globe.tilesLoaded &&
|
||||
icons === 'settled' &&
|
||||
!models().some((r) => r.host.loading > 0 || r.host.failed > 0)
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Work since the last look that will draw again, or a draw queued (one a
|
||||
* hold makes anyway is the hold's).
|
||||
*/
|
||||
function idleBlocked() {
|
||||
busy = completed > 0 || (scene.requestRenderMode && renderPending(scene));
|
||||
completed = 0;
|
||||
return busy;
|
||||
}
|
||||
}
|
||||
|
||||
const renderer = {
|
||||
id: 'cesium',
|
||||
contractVersion: '0',
|
||||
@@ -277,12 +144,10 @@ export async function createCesiumSceneRenderer({
|
||||
quality: ports.quality,
|
||||
capture: ports.capture,
|
||||
credits: ports.credits,
|
||||
whenSettled,
|
||||
whenSettled: ports.whenSettled,
|
||||
resize: () => widget.resize(),
|
||||
on: ports.on,
|
||||
destroy() {
|
||||
destroyed = true;
|
||||
for (const finish of [...watching]) finish('ended');
|
||||
input.dispose();
|
||||
ports.dispose();
|
||||
widget.destroy();
|
||||
|
||||
Reference in new issue
Block a user