mirror of
https://github.com/bilawalsidhu/gods-eye-view.git
synced 2026-11-07 20:04:00 +00:00
Run the conformance logic suite on the Cesium renderer
The conformance page can now run the logic checks against the Cesium
scene renderer as well as the stub. CESIUM_CHECKS lists the checks of
the ports Cesium already has; each phase 2 migration adds its own, so
the suite shows how far the renderer is through the contract. All eight
listed checks pass in the browser, and the stub still passes its 69.
A harness frame is one tick of the render loop, observed through
frame.on('preUpdate'). It asks for no frame itself, so a check's own
requestRender is what makes a tick draw, and a loop that stops fails
the check after five seconds instead of hanging the run. The harness
frees each WebGL context on destroy.
FrameTime.dt is now measured rather than fixed at 1/60, and renderOnce
honours timeoutMs. A scene renderer made without the models capability
has no draw.models.
This commit is contained in:
1 parent
a5143f4467
commit
92fd2b0c2e
6 files changed
+170
-16
No files matched your search
@@ -99,6 +99,7 @@
|
||||
</select></label
|
||||
>
|
||||
<button id="logic">Run logic suite (stub)</button>
|
||||
<button id="logicCesium">Run logic suite (cesium)</button>
|
||||
<button id="check">Check scenes</button>
|
||||
<button id="record">Record references</button>
|
||||
<span id="status"></span>
|
||||
|
||||
+19
-4
@@ -20,6 +20,10 @@ import {
|
||||
import { createStubHarness } from '../../src/render/stub/harness.js';
|
||||
import { toEcef } from '../../src/render/stub/math.js';
|
||||
import { createCesiumSceneRenderer } from '../../src/render/cesium/sceneRenderer.js';
|
||||
import {
|
||||
CESIUM_CHECKS,
|
||||
createCesiumHarness,
|
||||
} from '../../src/render/cesium/harness.js';
|
||||
|
||||
const REFERENCES = '/src/render/conformance/goldens/visual/';
|
||||
const SAVE = '/__conformance/references/';
|
||||
@@ -31,9 +35,19 @@ const RENDERERS = {
|
||||
cesium: () => createCesiumSceneRenderer({ container: $('stage') }),
|
||||
};
|
||||
|
||||
async function runLogic() {
|
||||
status('running the logic suite…');
|
||||
const report = await runConformance(createStubHarness('stub (browser)'));
|
||||
const HARNESSES = {
|
||||
stub: () => [createStubHarness('stub (browser)'), {}],
|
||||
cesium: () => [
|
||||
createCesiumHarness({ model: ctx.assets.model }),
|
||||
{ only: (id) => CESIUM_CHECKS.has(id) },
|
||||
],
|
||||
};
|
||||
|
||||
/** Runs the logic suite on `which`; `all` runs every check, listed or not. */
|
||||
async function runLogic(which = 'stub', { all = false } = {}) {
|
||||
status(`running the logic suite on ${which}…`);
|
||||
const [harness, options] = HARNESSES[which]();
|
||||
const report = await runConformance(harness, all ? {} : options);
|
||||
$('logicResults').innerHTML =
|
||||
`<p>${report.harness}: <b class="pass">${report.passed} passed</b>, <b class="fail">${report.failed} failed</b>, ${report.skipped} skipped</p>
|
||||
<table>${report.results
|
||||
@@ -209,7 +223,8 @@ async function runScenes({ record = false, only } = {}) {
|
||||
return results;
|
||||
}
|
||||
|
||||
$('logic').onclick = () => runLogic();
|
||||
$('logic').onclick = () => runLogic('stub');
|
||||
$('logicCesium').onclick = () => runLogic('cesium');
|
||||
$('check').onclick = () => runScenes();
|
||||
$('record').onclick = () => runScenes({ record: true });
|
||||
window.conformance = { runLogic, runScenes };
|
||||
@@ -145,6 +145,15 @@ test('work pending at destroy settles and lets go of the scene', async (t) => {
|
||||
assert.equal(viewers[0].listeners.size, 0, 'no one-shot render stays');
|
||||
await renderer.frame.renderOnce();
|
||||
|
||||
const waiting = await plugin.create({ container: {} });
|
||||
await assert.rejects(
|
||||
waiting.frame.renderOnce({ timeoutMs: 5 }),
|
||||
/timed out/,
|
||||
'a frame that never comes times out when asked to',
|
||||
);
|
||||
assert.equal(viewers[1].listeners.size, 4, 'and lets go of its listener');
|
||||
waiting.destroy();
|
||||
|
||||
// A model that finishes loading after destroy is destroyed, not added.
|
||||
const load = Promise.withResolvers();
|
||||
const saved = Cesium.Model.fromGltfAsync;
|
||||
|
||||
@@ -0,0 +1,93 @@
|
||||
/**
|
||||
* @module render/cesium/harness
|
||||
* @description The Cesium renderer as the conformance suite's renderer under
|
||||
* test, in the `/dev/conformance/` page.
|
||||
*
|
||||
* `CESIUM_CHECKS` lists the checks of the ports the Cesium renderer has.
|
||||
* Each migration adds the checks of the members it brings, so the suite
|
||||
* shows how far the renderer is through the contract; every other check is
|
||||
* left out of the run rather than failed.
|
||||
*/
|
||||
import { DEFAULT_TOLERANCE } from '../conformance/index.js';
|
||||
import { toEcef } from '../stub/math.js';
|
||||
import { createCesiumSceneRenderer } from './sceneRenderer.js';
|
||||
|
||||
/** @type {ReadonlySet<string>} */
|
||||
export const CESIUM_CHECKS = new Set([
|
||||
'draw.ids',
|
||||
'draw.models',
|
||||
'draw.sets',
|
||||
'frame.order',
|
||||
'frame.renderOnce',
|
||||
'frame.time',
|
||||
'globe.sky',
|
||||
'renderer.whenSettled',
|
||||
]);
|
||||
|
||||
/** A tick fails the check rather than hang it when the loop has stopped. */
|
||||
const TICK_TIMEOUT_MS = 5000;
|
||||
|
||||
/**
|
||||
* One tick of the render loop, through the contract: it resolves once the
|
||||
* next tick (drawn or not) has run, so loads and timers progress between
|
||||
* ticks. It asks for no frame itself, so a check's own `requestRender` is
|
||||
* what makes a tick draw.
|
||||
*/
|
||||
function nextTick(r) {
|
||||
return new Promise((resolve, reject) => {
|
||||
const timer = setTimeout(() => {
|
||||
off();
|
||||
reject(new Error('the render loop did not tick'));
|
||||
}, TICK_TIMEOUT_MS);
|
||||
const off = r.frame.on('preUpdate', () => {
|
||||
off();
|
||||
clearTimeout(timer);
|
||||
// The rest of the tick (render, postRender) runs in the same task.
|
||||
setTimeout(resolve, 0);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {{name?: string, model?: string}} [options] `model`: a glTF URL
|
||||
* for the model checks.
|
||||
* @returns {import('../conformance/index.js').ConformanceHarness}
|
||||
*/
|
||||
export function createCesiumHarness({ name = 'cesium', model } = {}) {
|
||||
return {
|
||||
name,
|
||||
async create({ viewport, capabilities }) {
|
||||
const container = document.createElement('div');
|
||||
container.style.cssText = `position:fixed;left:0;top:0;width:${viewport.width}px;height:${viewport.height}px`;
|
||||
document.body.append(container);
|
||||
let renderer;
|
||||
try {
|
||||
renderer = await createCesiumSceneRenderer({ container, capabilities });
|
||||
} catch (error) {
|
||||
container.remove();
|
||||
throw error;
|
||||
}
|
||||
const destroy = renderer.destroy;
|
||||
const { canvas } = renderer;
|
||||
renderer.destroy = () => {
|
||||
try {
|
||||
destroy();
|
||||
} finally {
|
||||
// Free the context now: a full run makes more widgets than the
|
||||
// browser keeps contexts for.
|
||||
(canvas.getContext('webgl2') ?? canvas.getContext('webgl'))
|
||||
?.getExtension('WEBGL_lose_context')
|
||||
?.loseContext();
|
||||
container.remove();
|
||||
}
|
||||
};
|
||||
return renderer;
|
||||
},
|
||||
frame: nextTick,
|
||||
now: () => performance.now(),
|
||||
toEcef,
|
||||
tolerance: DEFAULT_TOLERANCE,
|
||||
flightPaths: 'endpoints',
|
||||
...(model ? { assets: { model } } : {}),
|
||||
};
|
||||
}
|
||||
@@ -54,6 +54,16 @@ export function createCesiumPorts(widget) {
|
||||
|
||||
const frameListeners = { preUpdate: [], preRender: [], postRender: [] };
|
||||
let frames = 0;
|
||||
// Every tick raises preUpdate, drawn or not; the phases of a tick share
|
||||
// its clock reading. The first tick counts from when the loop was made.
|
||||
let tickNow = performance.now();
|
||||
let dt = 0;
|
||||
const tick = () => {
|
||||
const now = performance.now();
|
||||
dt = (now - tickNow) / 1000;
|
||||
tickNow = now;
|
||||
frames += 1;
|
||||
};
|
||||
const onPhase = (phase) => (_scene, time) => {
|
||||
if (frameListeners[phase].length === 0) return;
|
||||
const t = {
|
||||
@@ -61,15 +71,15 @@ export function createCesiumPorts(widget) {
|
||||
time: Cesium.JulianDate.toDate(
|
||||
time ?? widget.clock.currentTime,
|
||||
).getTime(),
|
||||
now: performance.now(),
|
||||
dt: 1 / 60,
|
||||
now: tickNow,
|
||||
dt,
|
||||
rendering: phase !== 'preUpdate',
|
||||
};
|
||||
for (const cb of frameListeners[phase].slice()) cb(t);
|
||||
};
|
||||
let disposed = false;
|
||||
const removers = [
|
||||
scene.preUpdate.addEventListener(() => (frames += 1)),
|
||||
scene.preUpdate.addEventListener(tick),
|
||||
scene.preUpdate.addEventListener(onPhase('preUpdate')),
|
||||
scene.preRender.addEventListener(onPhase('preRender')),
|
||||
scene.postRender.addEventListener(onPhase('postRender')),
|
||||
@@ -78,16 +88,26 @@ export function createCesiumPorts(widget) {
|
||||
// Pending one-shot renders; disposal settles them rather than leave them
|
||||
// attached to a scene that will not render again.
|
||||
const pendingRenders = new Set();
|
||||
function nextPostRender() {
|
||||
/** Resolves after the next drawn frame; rejects after `timeoutMs`, if given. */
|
||||
function nextPostRender({ timeoutMs } = {}) {
|
||||
if (disposed) return Promise.resolve();
|
||||
return new Promise((resolve) => {
|
||||
const done = () => {
|
||||
return new Promise((resolve, reject) => {
|
||||
let timer;
|
||||
const settle = (error) => {
|
||||
off();
|
||||
clearTimeout(timer);
|
||||
pendingRenders.delete(done);
|
||||
resolve();
|
||||
if (error) reject(error);
|
||||
else resolve();
|
||||
};
|
||||
const done = () => settle();
|
||||
const off = scene.postRender.addEventListener(done);
|
||||
pendingRenders.add(done);
|
||||
if (timeoutMs !== undefined)
|
||||
timer = setTimeout(
|
||||
() => settle(new Error('renderOnce timed out')),
|
||||
timeoutMs,
|
||||
);
|
||||
scene.requestRender();
|
||||
});
|
||||
}
|
||||
@@ -102,7 +122,7 @@ export function createCesiumPorts(widget) {
|
||||
},
|
||||
requestRender: () => scene.requestRender(),
|
||||
renderNow: () => scene.render(widget.clock.currentTime),
|
||||
renderOnce: () => nextPostRender(),
|
||||
renderOnce: (options) => nextPostRender(options),
|
||||
time: () => Cesium.JulianDate.toDate(widget.clock.currentTime).getTime(),
|
||||
};
|
||||
|
||||
|
||||
@@ -29,11 +29,19 @@ const SKY_DEFAULTS = Object.freeze({
|
||||
lighting: false,
|
||||
});
|
||||
|
||||
/** What this renderer can do; a caller may ask for fewer. */
|
||||
const CAPABILITIES = ['models'];
|
||||
|
||||
/**
|
||||
* @param {{container: HTMLElement, msaa?: number}} options
|
||||
* @param {{container: HTMLElement, msaa?: number, capabilities?: Iterable<string>}} options
|
||||
* `capabilities` limits the renderer to those it lists.
|
||||
* @returns {Promise<object>} A renderer with the members listed above.
|
||||
*/
|
||||
export async function createCesiumSceneRenderer({ container, msaa = 4 }) {
|
||||
export async function createCesiumSceneRenderer({
|
||||
container,
|
||||
msaa = 4,
|
||||
capabilities: only,
|
||||
}) {
|
||||
const credits = document.createElement('div');
|
||||
const widget = new Cesium.CesiumWidget(container, {
|
||||
baseLayer: false,
|
||||
@@ -60,7 +68,10 @@ export async function createCesiumSceneRenderer({ container, msaa = 4 }) {
|
||||
if (scene.moon) scene.moon.show = false;
|
||||
|
||||
const sky = { ...SKY_DEFAULTS };
|
||||
const capabilities = new Set(['models']);
|
||||
const allowed = only && new Set(only);
|
||||
const capabilities = new Set(
|
||||
CAPABILITIES.filter((c) => !allowed || allowed.has(c)),
|
||||
);
|
||||
|
||||
function applySky() {
|
||||
scene.globe.enableLighting = sky.lighting;
|
||||
@@ -134,7 +145,12 @@ export async function createCesiumSceneRenderer({ container, msaa = 4 }) {
|
||||
canvas: scene.canvas,
|
||||
frame,
|
||||
globe,
|
||||
draw: ports.draw,
|
||||
// A member of a capability left out is absent, not stubbed.
|
||||
draw: capabilities.has('models')
|
||||
? ports.draw
|
||||
: Object.fromEntries(
|
||||
Object.entries(ports.draw).filter(([kind]) => kind !== 'models'),
|
||||
),
|
||||
camera: ports.camera,
|
||||
query: ports.query,
|
||||
capture: ports.capture,
|
||||
|
||||
Reference in new issue
Block a user