Scope render demand to each scene attachment

Give each scene its own render governor, owner holds and recent idle requests.
Make the Cesium render backend own its request mode directly, so the governor
drives engine.render on both engines without a dependency back from Cesium.
Route layer requests through their attached engine and capture the attachment's
governor for camera motions, orbit, annotations and shell controls. Release
retained-layer holds before clearing the outgoing engine, and destroy the
governor after scene consumers have detached.

Preserve Cesium's owner-keyed holds, pre-install buffering, last-release settling
frame and one-shot requests during continuous rendering. Navara keeps the same
render policy. There is no intended visible change on either engine. Simultaneous
scenes no longer share holds, and an old captured service cannot release a
replacement attachment's hold. Cover both rules on fake and headless Cesium
engines, including an old orbit ending after reattachment.

Viewer-reference lines fall from 910 to 891, Navara-kind checks from 2 to 1,
and engine-name lines from 33 to 32. All other coupling counts stay unchanged.

Checked in the browser, keyless: qa-perf passes on both engines with the
same idle, active and release counts as before (24 checks on Cesium, 22 on
Navara).
This commit is contained in:
German Khokhlov committed 2026-11-07 16:56:34 +04:00
1 parent 4252ed0798
commit 0459ee6d58
87 files changed
+720 -935

No files matched your search

+6 -3
View File
@@ -534,7 +534,7 @@ perform no network requests and discover no additional modules.
`maps/controller` coordinates scene changes and lifetimes. `maps/imagery`,
`maps/terrain` and `maps/3d` supply constructors; `maps/defaults` selects the
standard sources and their setup/fallback policy. The standalone facade wires
these to the application's render governor.
these to their engine's render requests.
A registry supplies `sources`, `defaultId`, `unknownId` and optional `recoveryId`.
Each source has a user-facing `descriptor`, availability/reason, and either an
@@ -638,8 +638,11 @@ refresh policy. `app/layerSession.js` defines the retained session fields and
handoff predicate. `standalone/sceneSwitcher.js` coordinates page handoff;
`ui/shellScene.js` owns scene-bound shell construction, while `applicationShell`
keeps page controls and policy. Request, host and shell queues are not yet one
queue. Overlay, render-governor, tracked-readout and pick services still include
module singletons; debug access still exposes the native viewer and tileset.
queue. Pick ownership, selection dispatch and render demand belong to
`scene.services`; render demand drives the engine contract, with no dependency
from the Cesium render backend to the governor. Overlay and tracked-readout
services still include module singletons; debug access still exposes the native
viewer and tileset.
## UI state owners
+7 -2
View File
@@ -277,8 +277,13 @@ one application, and only the globe-bound parts move to the next engine.
resolve these services with their attached engine; simultaneous engines do
not share owner predicates or click subscriptions. Detach releases both
services before the renderer is parked. Reattachment creates fresh services,
while retained layers register their current picks again. The overlay host,
render governor and tracked readout still have module-owned state.
while retained layers register their current picks again.
- **Scene render demand.** `scene.services.render` owns the attachment's holds
and recent idle requests. It drives `engine.render` on both engines;
Cesium implements its render mode directly. Retained layers identify their
attached engine, and scene callbacks capture their governor. Teardown clears
only that scene's holds; an old service cannot release a replacement's hold.
The overlay host and tracked readout still have module-owned state.
- **The session owns the state.** Catalog, layer models, sources, selection,
camera mode and URL state outlive a switch. A layer that is a session model
gets `attachRenderer(viewer, { engine })` / `detachRenderer()` instead of being rebuilt,
+18
View File
@@ -547,6 +547,24 @@ the same services on first use. The application destroys them on detach and
creates fresh services when reattaching a parked engine. Identical layer IDs in
simultaneous scenes cannot replace or unregister one another's picks.
## Render demand
`scene.services.render` owns continuous-render holds and the last 16 idle
request reasons for one attachment. `holdContinuousRender(id)` and
`releaseContinuousRender(id)` are idempotent per owner; the last release
requests one settling frame. `governorRequestRender(reason)` forwards a
one-shot request even while another owner holds continuous rendering.
The tools call `install()` after scene consumers have registered their holds.
The governor drives `engine.render` on every engine. Cesium implements
`setContinuous` with its scene's `requestRenderMode` and disables automatic
simulation-time renders with `maximumRenderTimeChange = Infinity`. Engine
internals request their own scene directly and never import the governor.
Layer service helpers take their attached engine as the final argument;
attachment-bound callbacks can capture `renderGovernorFor(engine)` directly.
Destroying the scene clears its holds and makes the captured service inert.
A parked engine gets a fresh service when reattached.
## Render effects
The optional `visualStyles`, `bloom` and `contactSonar` capabilities are
+4 -4
View File
@@ -3,14 +3,14 @@
"cesiumLayerRendererLines": 0,
"navaraLayerRendererLines": 189,
"cesiumImportFiles": 3,
"viewerLines": 910,
"navaraKindChecks": 2,
"viewerLines": 891,
"navaraKindChecks": 1,
"engineSniffCalls": 0,
"engineExtensionReads": 3,
"groundEffectPasses": 1,
"cesiumSdkLoads": 3,
"screenSpaceHandlerFiles": 4,
"engineNameLines": 33
"engineNameLines": 32
},
"measuredAt": "Enforce SDK ownership, relocate annotation and wind backends, and rehearse every shared display"
"measuredAt": "Scope render demand to scene attachments and make the Cesium render backend independent"
}
+5 -1
View File
@@ -191,7 +191,6 @@
"src/globe/cesium/starrySky.js",
"src/sky/stars.js",
"src/data/bundledJson.js",
"src/renderGovernor.js",
"src/globe/contract.js",
"src/maps/google3d.js"
],
@@ -980,6 +979,11 @@
"ui-shell": {
"exports": ["./ui/shell"],
"modules": [
"src/renderGovernor.js",
"src/data/sceneServices.js",
"src/data/pickRegistry.js",
"src/data/selectionDispatcher.js",
"src/data/trackingGesture.js",
"src/data/headingDegrees.js",
"src/data/distanceFormat.js",
"src/i18n/marker.js",
+1 -1
View File
@@ -1226,7 +1226,7 @@ async function main() {
await new Promise((resolve) => setTimeout(resolve, 2000));
remove();
return { frames, durationMs: performance.now() - started,
governor: getRenderGovernorDiagnostics() };
governor: getRenderGovernorDiagnostics(app.engine) };
});
const geomAfter30s = await serializeGeom(activeId);
evidence.geometry = {
+2 -2
View File
@@ -190,7 +190,7 @@ try {
// Use the governor's hold so an asynchronous idle transition cannot
// overwrite a raw scene flag during the sustained-render measurement.
const { holdContinuousRender } = await import('/src/renderGovernor.js');
holdContinuousRender('qa-military-names-fps');
holdContinuousRender('qa-military-names-fps', window.__godsEyeView.engine);
});
const t0 = await pageNow(page);
await sleep(3500);
@@ -198,7 +198,7 @@ try {
await page.evaluate(async () => {
const { releaseContinuousRender } =
await import('/src/renderGovernor.js');
releaseContinuousRender('qa-military-names-fps');
releaseContinuousRender('qa-military-names-fps', window.__godsEyeView.engine);
});
const metrics = frameStats(frames);
const seconds = frames.reduce((sum, frame) => sum + frame.dt, 0) / 1000;
-3
View File
@@ -5,7 +5,6 @@ import assert from 'node:assert/strict';
import { FEATURE_SOURCE_METHODS } from '../sources/featureSource.js';
import { createAnnotationResolver } from './resolver.js';
import { createAnnotationEngine } from './annotationEngine.js';
import { _resetRenderGovernorForTest } from '../renderGovernor.js';
import { cameraOnlyEngine } from '../testSupport/viewerEngine.mjs';
function viewerAt(lat, lon, height = 20_000) {
@@ -253,8 +252,6 @@ test('non-admin asks and entity facts keep the existing path', async () => {
test('the engine keeps every part on the mark, drawn at once or upgraded', async (t) => {
globalThis.requestAnimationFrame ??= () => 1;
globalThis.cancelAnimationFrame ??= () => {};
_resetRenderGovernorForTest();
t.after(() => _resetRenderGovernorForTest());
const polygons = [
[
[
+12 -12
View File
@@ -2,11 +2,7 @@ import { isUnavailableCapability } from '../sources/capability.js';
import { defaultGeospatial } from '../search/defaults.js';
import * as geo from '../geo/index.js';
import { greatCircleMeters } from '../geoDistance.js';
import {
holdContinuousRender,
releaseContinuousRender,
governorRequestRender,
} from '../renderGovernor.js';
import { renderGovernorFor } from '../renderGovernor.js';
import {
isRateLimitedOutcome,
resolveAnnotationTarget,
@@ -141,6 +137,7 @@ export function createAnnotationEngine({
outlineRetryDelaysMs = OUTLINE_RETRY_DELAYS_MS,
resolveTarget = resolveAnnotationTarget,
}) {
let renderGovernor = renderGovernorFor(globe);
/** @type {Map<string, object>} live annotations keyed by id */
const annotations = new Map();
let destroyed = false;
@@ -220,8 +217,8 @@ export function createAnnotationEngine({
// hidden from Draw's list lets the scene idle.
function syncAnnotationHold() {
const visible = [...annotations.values()].some((anno) => !anno.hidden);
if (visible) holdContinuousRender('annotations');
else releaseContinuousRender('annotations');
if (visible) renderGovernor?.holdContinuousRender('annotations');
else renderGovernor?.releaseContinuousRender('annotations');
}
/**
@@ -386,7 +383,7 @@ export function createAnnotationEngine({
syncAnnotationHold();
// The frame that takes a just-hidden mark off: with every mark hidden
// the hold is gone, and nothing else would draw it.
governorRequestRender('annotations');
renderGovernor?.governorRequestRender('annotations');
}
if (expired) notifyChange();
// Stop once the board is STABLE — persistent marks (persist=true is the default)
@@ -1616,7 +1613,7 @@ export function createAnnotationEngine({
// mark hidden before its first frame has no alpha change to wait for.
renderer.sync(annotations);
syncAnnotationHold();
governorRequestRender('annotations');
renderGovernor?.governorRequestRender('annotations');
ensureTicking();
notifyChange();
return true;
@@ -1817,6 +1814,8 @@ export function createAnnotationEngine({
dropQueuedOutlineUpgrades();
generation++;
const previous = renderer;
const previousGovernor = renderGovernor;
renderGovernor = null;
renderer = DETACHED_RENDERER;
globe = null;
for (const anno of annotations.values()) {
@@ -1830,7 +1829,7 @@ export function createAnnotationEngine({
try {
previous.destroy();
} finally {
releaseContinuousRender('annotations');
previousGovernor?.releaseContinuousRender('annotations');
}
},
/**
@@ -1841,6 +1840,7 @@ export function createAnnotationEngine({
if (destroyed) return;
engine.detachRenderer();
globe = nextGlobe;
renderGovernor = renderGovernorFor(globe);
renderer = nextRenderer ?? DETACHED_RENDERER;
for (const anno of annotations.values()) {
try {
@@ -1853,7 +1853,7 @@ export function createAnnotationEngine({
renderer.sync(annotations);
syncAnnotationHold();
ensureTicking();
governorRequestRender('annotations');
renderGovernor?.governorRequestRender('annotations');
},
/** Whether a globe draws the marks now. */
get attached() {
@@ -1869,7 +1869,7 @@ export function createAnnotationEngine({
tickHandle = null;
outlineListeners.clear();
changeListeners.clear();
releaseContinuousRender('annotations');
renderGovernor?.releaseContinuousRender('annotations');
renderer.destroy();
}
},
+49 -76
View File
@@ -1,3 +1,4 @@
import { createFakeEngine } from '../globe/testing/fakeEngine.js';
import { createStandalonePlaceSearch } from '../standalone/placeSearch.js';
// Voice-annotation resilience contract tests — pure logic, no network, no browser.
//
@@ -22,8 +23,7 @@ import {
import { resolveAnnotationTarget } from './annotationResolver.js';
import {
getRenderGovernorDiagnostics,
installRenderGovernor,
_resetRenderGovernorForTest,
renderGovernorFor,
} from '../renderGovernor.js';
// A resolver that replays a scripted sequence of outcomes (undefined = transient,
@@ -241,7 +241,7 @@ test('outline queue: an 8-spec batch keeps at most two FIFO upgrades in flight',
};
};
const engine = createAnnotationEngine({
globe: {},
globe: testGlobe,
renderer,
resolveTarget,
});
@@ -313,7 +313,7 @@ test('outline queue: clear drops queued-but-unstarted upgrades without a later f
}),
});
engine = createAnnotationEngine({
globe: {},
globe: testGlobe,
renderer,
resolveTarget,
});
@@ -379,7 +379,7 @@ test('retry: HTTP 429 Retry-After 5s gets one ladder-spaced retry; a second 429
});
const resolved = await resolveAnnotationTarget({
globe: {},
globe: testGlobe,
target: 'PT2 rate-limit fixture',
latitude: 12.345,
longitude: 67.89,
@@ -453,7 +453,7 @@ test('retry: a plain HTTP 500 still exhausts the existing 8s/25s transient ladde
});
const resolved = await resolveAnnotationTarget({
globe: {},
globe: testGlobe,
target: 'PT2 transient fixture',
latitude: -12.345,
longitude: -67.89,
@@ -711,11 +711,9 @@ const BUILDING_FP = {
test('fresh async building batch reserves slots together and releases failed/cleared installs', async (t) => {
installAnimationFrameStubs(t);
_resetRenderGovernorForTest();
t.after(() => _resetRenderGovernorForTest());
const h = controlledBuildingRenderer();
const engine = createAnnotationEngine({
globe: {},
globe: testGlobe,
renderer: h.renderer,
resolveTarget: async ({ target }) => ({
...BUILDING_FP,
@@ -744,7 +742,7 @@ test('fresh async building batch reserves slots together and releases failed/cle
assert.equal(engine.list()[0].pendingOutline, false);
engine.clear();
assert.equal(engine.count(), 0);
assert.ok(!getRenderGovernorDiagnostics().holds.includes('annotations'));
assert.ok(!getRenderGovernorDiagnostics(testGlobe).holds.includes('annotations'));
const cancelled = engine.annotate([{ type: 'area', target: 'third' }]);
await flushMicrotasks();
@@ -762,7 +760,7 @@ test('outline workers retain completion ownership and clear cancels resolved-but
const h = controlledBuildingRenderer();
const started = [];
const engine = createAnnotationEngine({
globe: {},
globe: testGlobe,
renderer: h.renderer,
resolveTarget: async ({ target }) => ({
...FIXED_POINT,
@@ -805,7 +803,7 @@ test('building recaption timeout retains old route and a newer replacement owns
installAnimationFrameStubs(t);
const h = controlledBuildingRenderer();
const engine = createAnnotationEngine({
globe: {},
globe: testGlobe,
renderer: h.renderer,
resolveTarget: async () => ({ ...BUILDING_FP }),
});
@@ -860,7 +858,7 @@ test('replacement cleanup failure still removes the old route and retains failed
installAnimationFrameStubs(t);
const h = controlledBuildingRenderer();
const engine = createAnnotationEngine({
globe: {}, renderer: h.renderer, resolveTarget: async () => ({ ...BUILDING_FP }),
globe: testGlobe, renderer: h.renderer, resolveTarget: async () => ({ ...BUILDING_FP }),
});
const initial = engine.annotate([{ type: 'area', target: 'Capitol', label: 'Old' }]);
await flushMicrotasks();
@@ -905,7 +903,7 @@ test('identical refresh revokes an older building replacement even if cancellati
installAnimationFrameStubs(t);
const h = controlledBuildingRenderer();
const engine = createAnnotationEngine({
globe: {}, renderer: h.renderer, resolveTarget: async () => ({ ...BUILDING_FP }),
globe: testGlobe, renderer: h.renderer, resolveTarget: async () => ({ ...BUILDING_FP }),
});
const initial = engine.annotate([{ type: 'area', target: 'Capitol', label: 'Old' }]);
await flushMicrotasks(); h.pending[0].finish(); await initial;
@@ -936,7 +934,7 @@ test('successful replacement survives failed old-route retirement and keeps clea
installAnimationFrameStubs(t);
const h = controlledBuildingRenderer();
const engine = createAnnotationEngine({
globe: {}, renderer: h.renderer, resolveTarget: async () => ({ ...BUILDING_FP }),
globe: testGlobe, renderer: h.renderer, resolveTarget: async () => ({ ...BUILDING_FP }),
});
const initial = engine.annotate([{ type: 'area', target: 'Capitol', label: 'Old' }]);
await flushMicrotasks(); h.pending[0].finish(); await initial;
@@ -975,7 +973,7 @@ test('pending fresh predecessor failure cannot erase a newer building intent', a
globalThis.requestAnimationFrame = (callback) => { frames.push(callback); return frames.length; };
const h = controlledBuildingRenderer();
const engine = createAnnotationEngine({
globe: {}, renderer: h.renderer, resolveTarget: async () => ({ ...BUILDING_FP }),
globe: testGlobe, renderer: h.renderer, resolveTarget: async () => ({ ...BUILDING_FP }),
});
const first = engine.annotate([{ type: 'area', target: 'Capitol', label: 'First' }]);
await flushMicrotasks();
@@ -1004,7 +1002,7 @@ test('duplicate pending building results reflect final failure rather than a dea
installAnimationFrameStubs(t);
const h = controlledBuildingRenderer();
const engine = createAnnotationEngine({
globe: {}, renderer: h.renderer, resolveTarget: async () => ({ ...BUILDING_FP }),
globe: testGlobe, renderer: h.renderer, resolveTarget: async () => ({ ...BUILDING_FP }),
});
const spec = { type: 'area', target: 'Capitol', label: 'Same' };
const batch = engine.annotate([spec, spec]);
@@ -1023,7 +1021,7 @@ test('separate duplicate callers observe the original building install outcome',
for (const outcome of ['installed', 'failed', 'timeout', 'clear']) {
const h = controlledBuildingRenderer();
const engine = createAnnotationEngine({
globe: {},
globe: testGlobe,
renderer: h.renderer,
resolveTarget: async () => ({ ...BUILDING_FP }),
});
@@ -1077,7 +1075,7 @@ test('a duplicate joins the physical deferred install but not another outline lo
const h = controlledBuildingRenderer();
let lookups = 0;
const engine = createAnnotationEngine({
globe: {},
globe: testGlobe,
renderer: h.renderer,
resolveTarget: async () => ({
...FIXED_POINT,
@@ -1114,12 +1112,10 @@ test('a duplicate joins the physical deferred install but not another outline lo
test('duplicate-replacement: a renderer throw leaves no phantom mark and no leaked hold', async (t) => {
installAnimationFrameStubs(t);
_resetRenderGovernorForTest();
t.after(() => _resetRenderGovernorForTest());
const { renderer, failAddOnce } = throwingRendererHarness();
const engine = createAnnotationEngine({
globe: {},
globe: testGlobe,
renderer,
resolveTarget: async () => ({ ...FIXED_POINT }),
});
@@ -1130,7 +1126,7 @@ test('duplicate-replacement: a renderer throw leaves no phantom mark and no leak
assert.equal(first.drawn, 1);
assert.equal(engine.list().length, 1);
assert.ok(
getRenderGovernorDiagnostics().holds.includes('annotations'),
getRenderGovernorDiagnostics(testGlobe).holds.includes('annotations'),
'a live mark must hold continuous render',
);
@@ -1161,19 +1157,17 @@ test('duplicate-replacement: a renderer throw leaves no phantom mark and no leak
'no phantom annotation survives the failed swap',
);
assert.ok(
!getRenderGovernorDiagnostics().holds.includes('annotations'),
!getRenderGovernorDiagnostics(testGlobe).holds.includes('annotations'),
'the annotations hold must be released — a leak defeats the idle governor permanently',
);
});
test('fresh path: a renderer throw is rolled back the same way', async (t) => {
installAnimationFrameStubs(t);
_resetRenderGovernorForTest();
t.after(() => _resetRenderGovernorForTest());
const { renderer, failAddOnce } = throwingRendererHarness();
const engine = createAnnotationEngine({
globe: {},
globe: testGlobe,
renderer,
resolveTarget: async () => ({ ...FIXED_POINT }),
});
@@ -1190,7 +1184,7 @@ test('fresh path: a renderer throw is rolled back the same way', async (t) => {
'no phantom annotation from a failed first add',
);
assert.ok(
!getRenderGovernorDiagnostics().holds.includes('annotations'),
!getRenderGovernorDiagnostics(testGlobe).holds.includes('annotations'),
'a never-rendered mark must not hold continuous render',
);
});
@@ -1232,12 +1226,10 @@ function partialStateRendererHarness() {
test('fresh path: a throw AFTER partial renderer state leaves nothing on the board', async (t) => {
installAnimationFrameStubs(t);
_resetRenderGovernorForTest();
t.after(() => _resetRenderGovernorForTest());
const { renderer, failAddOnce, live } = partialStateRendererHarness();
const engine = createAnnotationEngine({
globe: {},
globe: testGlobe,
renderer,
resolveTarget: async () => ({ ...FIXED_POINT }),
});
@@ -1254,7 +1246,7 @@ test('fresh path: a throw AFTER partial renderer state leaves nothing on the boa
'the rollback must release what the renderer had already created',
);
assert.ok(
!getRenderGovernorDiagnostics().holds.includes('annotations'),
!getRenderGovernorDiagnostics(testGlobe).holds.includes('annotations'),
'a never-rendered mark must not hold continuous render',
);
@@ -1273,12 +1265,10 @@ test('fresh path: a throw AFTER partial renderer state leaves nothing on the boa
test('duplicate-replacement: a throw AFTER partial renderer state leaves nothing on the board', async (t) => {
installAnimationFrameStubs(t);
_resetRenderGovernorForTest();
t.after(() => _resetRenderGovernorForTest());
const { renderer, failAddOnce, live } = partialStateRendererHarness();
const engine = createAnnotationEngine({
globe: {},
globe: testGlobe,
renderer,
resolveTarget: async () => ({ ...FIXED_POINT }),
});
@@ -1308,7 +1298,7 @@ test('duplicate-replacement: a throw AFTER partial renderer state leaves nothing
);
assert.equal(live.size, 0, 'and no half-swapped mark survives on the board');
assert.ok(
!getRenderGovernorDiagnostics().holds.includes('annotations'),
!getRenderGovernorDiagnostics(testGlobe).holds.includes('annotations'),
'the annotations hold must be released',
);
@@ -1335,7 +1325,7 @@ test('destroy releases the renderer and prevents subsequent annotation work', as
renderer.destroy = () => {
destroyed++;
};
const engine = createAnnotationEngine({ globe: {}, renderer });
const engine = createAnnotationEngine({ globe: testGlobe, renderer });
const drawn = await engine.annotate([
{ type: 'pin', longitude: -97.74, latitude: 30.27 },
]);
@@ -1350,7 +1340,7 @@ test('destroy releases the renderer and prevents subsequent annotation work', as
assert.equal(engine.count(), 0);
engine.destroy();
assert.equal(destroyed, 1);
assert.ok(!getRenderGovernorDiagnostics().holds.includes('annotations'));
assert.ok(!getRenderGovernorDiagnostics(testGlobe).holds.includes('annotations'));
});
test('a late annotation resolver cannot redraw after destruction', async (t) => {
@@ -1360,7 +1350,7 @@ test('a late annotation resolver cannot redraw after destruction', async (t) =>
let release;
let signal;
const engine = createAnnotationEngine({
globe: {},
globe: testGlobe,
renderer,
resolveTarget: (options) => {
signal = options.signal;
@@ -1419,13 +1409,11 @@ test('caller cancellation prevents a late annotation and camera mutation', async
test('only an explicit navigation request permits resolving distant annotation targets', async (t) => {
installAnimationFrameStubs(t);
_resetRenderGovernorForTest();
t.after(() => _resetRenderGovernorForTest());
const { renderer } = throwingRendererHarness();
renderer.destroy = () => {};
const received = [];
const engine = createAnnotationEngine({
globe: {},
globe: testGlobe,
renderer,
resolveTarget: async (options) => {
received.push(options);
@@ -1449,7 +1437,7 @@ test('recaption inherits outline credit from borrowed geometry rather than its n
const { renderer } = fakeRenderer();
let first = true;
const ring = [[-99,31],[-98.99,31],[-98.99,31.01],[-99,31]];
const engine = createAnnotationEngine({ globe: {}, renderer,
const engine = createAnnotationEngine({ globe: testGlobe, renderer,
resolveTarget: async () => {
if (first) { first = false; return { ...FIXED_POINT, ring, footprintKind: 'area', outlineCredit: 'tiles' }; }
return { ...FIXED_POINT, source: 'osm-local', resolveOutline: async () => null };
@@ -1467,8 +1455,6 @@ test('recaption inherits outline credit from borrowed geometry rather than its n
// frame and remove it, and hear about every change to the board.
test('hand-drawn marks: one mark can be hidden, renamed, recoloured, framed and removed', async (t) => {
installAnimationFrameStubs(t);
_resetRenderGovernorForTest();
t.after(() => _resetRenderGovernorForTest());
const { renderer, calls } = fakeRenderer();
const flights = [];
const engine = createAnnotationEngine({
@@ -1529,11 +1515,9 @@ test('hand-drawn marks: remove, hide, clear, expiry and framing have their effec
globalThis.cancelAnimationFrame = () => {};
let now = 1_000_000;
t.mock.method(performance, 'now', () => now);
_resetRenderGovernorForTest();
t.after(() => {
globalThis.requestAnimationFrame = originalRequestAnimationFrame;
globalThis.cancelAnimationFrame = originalCancelAnimationFrame;
_resetRenderGovernorForTest();
});
const runFrames = () => {
for (let i = 0; i < 5 && frames.length; i += 1) frames.shift()();
@@ -1636,10 +1620,8 @@ test('hand-drawn marks: remove, hide, clear, expiry and framing have their effec
test('hand-drawn marks: a voice mark or a hidden mark is not taken as their duplicate', async (t) => {
installAnimationFrameStubs(t);
_resetRenderGovernorForTest();
t.after(() => _resetRenderGovernorForTest());
const { renderer } = fakeRenderer();
const engine = createAnnotationEngine({ globe: {}, renderer });
const engine = createAnnotationEngine({ globe: testGlobe, renderer });
await engine.annotate(
[
{
@@ -1688,11 +1670,9 @@ test('hand-drawn marks: a voice mark or a hidden mark is not taken as their dupl
test('hand-drawn marks: only a manual mark can be restyled', async (t) => {
installAnimationFrameStubs(t);
_resetRenderGovernorForTest();
t.after(() => _resetRenderGovernorForTest());
const { renderer } = fakeRenderer();
const engine = createAnnotationEngine({
globe: {},
globe: testGlobe,
renderer,
resolveTarget: async () => ({ ...FIXED_POINT }),
});
@@ -1707,7 +1687,7 @@ test('a drawn line that rounds up to the next unit is labelled in it, not as 100
installAnimationFrameStubs(t);
const labels = [];
const renderer = { add(anno) { labels.push(anno.label); }, update() {}, remove() {}, sync() {}, destroy() {} };
const engine = createAnnotationEngine({ globe: {}, renderer });
const engine = createAnnotationEngine({ globe: testGlobe, renderer });
const metresPerDegree = (6371.0088 * 1000 * Math.PI) / 180;
for (const metres of [994, 996, 9940, 9960]) {
await engine.annotate([
@@ -1720,10 +1700,8 @@ test('a drawn line that rounds up to the next unit is labelled in it, not as 100
test('hand-drawn marks: two unnamed pins close together are two pins, whatever their colours', async (t) => {
installAnimationFrameStubs(t);
_resetRenderGovernorForTest();
t.after(() => _resetRenderGovernorForTest());
const { renderer } = fakeRenderer();
const engine = createAnnotationEngine({ globe: {}, renderer });
const engine = createAnnotationEngine({ globe: testGlobe, renderer });
const pin = (lon, color) => ({ type: 'pin', manual: true, latitude: 30.27, longitude: lon, label: '', color });
await engine.annotate([pin(-97.74, 'primary')], { persist: true });
await engine.annotate([pin(-97.7402, 'primary')], { persist: true });
@@ -1737,36 +1715,33 @@ test('hand-drawn marks: two unnamed pins close together are two pins, whatever t
test('hand-drawn marks: hiding a mark before its first frame takes it off at once', async (t) => {
installAnimationFrameStubs(t);
_resetRenderGovernorForTest();
t.after(() => _resetRenderGovernorForTest());
let syncs = 0;
const renderer = { add() {}, update() {}, remove() {}, sync() { syncs += 1; }, destroy() {} };
const engine = createAnnotationEngine({ globe: {}, renderer });
const engine = createAnnotationEngine({ globe: testGlobe, renderer });
await engine.annotate([{ type: 'pin', manual: true, latitude: 1, longitude: 1 }], { persist: true });
const [mark] = engine.list();
const before = syncs;
engine.setHidden(mark.id, true);
assert.ok(syncs > before, 'the renderer hears about it now');
assert.ok(
!getRenderGovernorDiagnostics().holds.includes('annotations'),
!getRenderGovernorDiagnostics(testGlobe).holds.includes('annotations'),
'and the render hold goes with the last visible mark',
);
});
test('hand-drawn marks: with every mark hidden the scene idles, and showing one wakes it', async (t) => {
installAnimationFrameStubs(t);
_resetRenderGovernorForTest();
t.after(() => _resetRenderGovernorForTest());
let renders = 0;
installRenderGovernor({ scene: { requestRenderMode: true, requestRender: () => (renders += 1) } });
testGlobe.render.request = () => (renders += 1);
renderGovernorFor(testGlobe).install();
const { renderer } = fakeRenderer();
const engine = createAnnotationEngine({ globe: {}, renderer });
const engine = createAnnotationEngine({ globe: testGlobe, renderer });
await engine.annotate([
{ type: 'pin', manual: true, latitude: 1, longitude: 1 },
{ type: 'pin', manual: true, latitude: 2, longitude: 2 },
], { persist: true });
const [first, second] = engine.list();
const held = () => getRenderGovernorDiagnostics().holds.includes('annotations');
const held = () => getRenderGovernorDiagnostics(testGlobe).holds.includes('annotations');
assert.ok(held(), 'visible marks keep the scene drawing');
engine.setHidden(first.id, true);
assert.ok(held(), 'one visible mark is enough');
@@ -1780,12 +1755,10 @@ test('hand-drawn marks: with every mark hidden the scene idles, and showing one
test('hand-drawn marks: a restyle the renderer cannot draw takes the mark off and says so', async (t) => {
installAnimationFrameStubs(t);
_resetRenderGovernorForTest();
t.after(() => _resetRenderGovernorForTest());
const { renderer, failAddOnce } = throwingRendererHarness();
let syncs = 0;
renderer.sync = () => (syncs += 1);
const engine = createAnnotationEngine({ globe: {}, renderer });
const engine = createAnnotationEngine({ globe: testGlobe, renderer });
await engine.annotate([{ type: 'pin', manual: true, latitude: 1, longitude: 1 }], { persist: true });
const [mark] = engine.list();
let changes = 0;
@@ -1799,8 +1772,6 @@ test('hand-drawn marks: a restyle the renderer cannot draw takes the mark off an
});
test('hand-drawn marks: a mark shown again right after it was drawn appears at once, not fading in', async (t) => {
_resetRenderGovernorForTest();
t.after(() => _resetRenderGovernorForTest());
const queued = [];
const raf = globalThis.requestAnimationFrame;
const caf = globalThis.cancelAnimationFrame;
@@ -1812,7 +1783,7 @@ test('hand-drawn marks: a mark shown again right after it was drawn appears at o
});
const tick = () => queued.splice(0).forEach((callback) => callback());
const { renderer } = fakeRenderer();
const engine = createAnnotationEngine({ globe: {}, renderer });
const engine = createAnnotationEngine({ globe: testGlobe, renderer });
await engine.annotate([{ type: 'pin', manual: true, latitude: 1, longitude: 1 }], { persist: true });
const [mark] = engine.list();
engine.setHidden(mark.id, true);
@@ -1825,8 +1796,6 @@ test('hand-drawn marks: a mark shown again right after it was drawn appears at o
test('removing one mark whose cleanup fails keeps it addressable for a retry', async (t) => {
installAnimationFrameStubs(t);
_resetRenderGovernorForTest();
t.after(() => _resetRenderGovernorForTest());
const { renderer, calls } = fakeRenderer();
let failRemove = true;
renderer.remove = () => {
@@ -1851,7 +1820,7 @@ test('a building install in the batch does not re-judge marks that never install
installAnimationFrameStubs(t);
const h = controlledBuildingRenderer();
const engine = createAnnotationEngine({
globe: {},
globe: testGlobe,
renderer: h.renderer,
resolveTarget: async ({ target }) =>
target === 'Tower' ? { ...BUILDING_FP, lon: -97.7 } : { ...FIXED_POINT },
@@ -1874,7 +1843,7 @@ test('plain recaption keeps the old mark addressable when its removal throws', a
installAnimationFrameStubs(t);
const { renderer } = fakeRenderer();
const engine = createAnnotationEngine({
globe: {}, renderer, resolveTarget: async () => ({ ...FIXED_POINT }),
globe: testGlobe, renderer, resolveTarget: async () => ({ ...FIXED_POINT }),
});
await engine.annotate([{ type: 'point', target: 'Capitol', label: 'Capitol' }]);
const old = engine.list()[0];
@@ -1899,3 +1868,7 @@ test('plain recaption keeps the old mark addressable when its removal throws', a
);
assert.equal(engine.count(), 0);
});
let testGlobe;
test.beforeEach(() => { testGlobe = createFakeEngine(); });
test.afterEach(() => { renderGovernorFor(testGlobe).destroy(); testGlobe.destroy(); });
-2
View File
@@ -6,7 +6,6 @@ import assert from 'node:assert/strict';
import { createApplicationTools } from './tools.js';
import { createSceneHost } from './sceneHost.js';
import { createFakeEngine } from '../globe/testing/fakeEngine.js';
import { _resetRenderGovernorForTest } from '../renderGovernor.js';
test('the debug handle leaves window between engines and comes back the same', async (t) => {
const saved = {
@@ -32,7 +31,6 @@ test('the debug handle leaves window between engines and comes back the same', a
removeEventListener() {},
};
t.after(() => {
_resetRenderGovernorForTest();
for (const [name, value] of Object.entries(saved)) {
if (value === undefined) delete globalThis[name];
else globalThis[name] = value;
+3 -1
View File
@@ -45,7 +45,9 @@ test('the scene engine defaults to the Cesium engine factory', () => {
test('the scene constructs the engine with the viewer and returns it beside the viewer', () => {
const scene = read('src/app/scene.js');
const viewerTeardown = scene.indexOf('uninstallRenderGovernor(viewer);');
const viewerTeardown = scene.indexOf(
'if (!viewer.isDestroyed()) viewer.destroy();',
);
const engineAt = scene.indexOf('createSceneEngine(viewer, {');
assert.ok(viewerTeardown >= 0 && engineAt >= 0);
// Cleanups run last-in first-out: the engine goes before the viewer.
+1 -1
View File
@@ -8,7 +8,7 @@ export class LayerPresentation {
manager,
{
weatherClock,
requestRender = governorRequestRender,
requestRender = (reason) => governorRequestRender(reason, manager.engine),
invalidateDetection = markDetectionSourcesChanged,
} = {},
) {
-2
View File
@@ -1,5 +1,4 @@
import { showOsmCredit, hideOsmCredit } from '../data/dataCredits.js';
import { governorRequestRender } from '../renderGovernor.js';
import {
clearSelectedEntityContextForLayer,
registerEntityContext,
@@ -24,7 +23,6 @@ export const localGeoJsonServices = Object.freeze({
removeEntityContextsForLayer,
selectEntityContext,
getSelectedEntityContext,
governorRequestRender,
showOsmCredit,
hideOsmCredit,
});
+16 -26
View File
@@ -1,13 +1,8 @@
import test from 'node:test';
import assert from 'node:assert/strict';
import { MapStackController } from '../mapStackController.js';
import {
installRenderGovernor,
uninstallRenderGovernor,
holdContinuousRender,
governorRequestRender,
getRenderGovernorDiagnostics,
} from '../renderGovernor.js';
import { createFakeEngine } from '../globe/testing/fakeEngine.js';
import { createSceneServices } from '../data/sceneServices.js';
test('destroy invalidates a pending imagery provider before it can touch the viewer', async () => {
let resolveProvider;
@@ -35,23 +30,18 @@ test('destroy invalidates a pending imagery provider before it can touch the vie
assert.equal(removed, 1);
});
test('uninstall only releases the current viewer and makes later render requests inactive', () => {
let renders = 0;
const viewer = {
scene: {
requestRender: () => {
renders++;
},
},
};
installRenderGovernor(viewer);
holdContinuousRender('fixture');
uninstallRenderGovernor({});
assert.equal(getRenderGovernorDiagnostics().installed, true);
uninstallRenderGovernor(viewer);
const before = renders;
governorRequestRender('late');
assert.equal(renders, before);
assert.equal(getRenderGovernorDiagnostics().installed, false);
assert.deepEqual(getRenderGovernorDiagnostics().holds, []);
test('scene teardown makes its later render requests inactive', () => {
const engine = createFakeEngine();
const services = createSceneServices(engine);
const governor = services.render;
governor.install();
governor.holdContinuousRender('fixture');
assert.equal(governor.getRenderGovernorDiagnostics().installed, true);
services.destroy();
engine.fake.resetCalls();
governor.governorRequestRender('late');
assert.equal(engine.fake.callsOf('render.request').length, 0);
assert.equal(governor.getRenderGovernorDiagnostics().installed, false);
assert.deepEqual(governor.getRenderGovernorDiagnostics().holds, []);
engine.destroy();
});
+1 -12
View File
@@ -7,11 +7,6 @@ import { t } from '../i18n/index.js';
import { DATA_CREDITS, registerDataCredits } from '../data/dataCredits.js';
import { createLayerAvailability } from './layers/renderers.js';
import {
governorRequestRender,
installEngineRenderGovernor,
uninstallRenderGovernor,
} from '../renderGovernor.js';
import { createNavaraAnnotationRenderer } from '../annotations/navaraAnnotationRenderer.js';
/** The Navara globe module, loaded on demand. */
@@ -93,12 +88,10 @@ export async function createNavaraScene({
});
defer(() => globe.destroy());
const { engine } = globe;
// The tools install the governor on this engine; it goes before the engine.
defer(() => uninstallRenderGovernor(engine));
signal.throwIfAborted();
loaderStatus.textContent = t('Initializing systems...');
const mapStackController = new MapController(null, {
requestRender: governorRequestRender,
requestRender: (reason) => engine.render.request(reason),
engine,
...mapOptions,
cesiumToken,
@@ -125,7 +118,6 @@ export async function createNavaraScene({
engine,
tileset: null,
suspend() {
uninstallRenderGovernor(engine);
engine.camera.cancelFlight();
engine.render.setContinuous(false);
globe.setRenderLoop(false);
@@ -156,9 +148,6 @@ export function navaraToolBackend({ engine }) {
// The session's tool on the engine's input and preview.
draw: () => ({ engine }),
recentImagery: () => ({ engine }),
// Holds drive engine.render.setContinuous; requests go to the engine. A
// parked engine uninstalls it (createNavaraScene suspend).
installGovernor: () => installEngineRenderGovernor(engine),
attach({ data, styleManager }, { signal, defer }) {
void styleManager.initialRestorePromise?.then(() => {
if (!signal.aborted) views.alignDaylightToRestoredView();
+12 -7
View File
@@ -3,10 +3,7 @@ import assert from 'node:assert/strict';
import { createApplicationScene } from './scene.js';
import { sceneServicesFor } from '../data/sceneServices.js';
import { createFakeEngine } from '../globe/testing/fakeEngine.js';
import {
installEngineRenderGovernor,
isRenderGovernorInstalled,
} from '../renderGovernor.js';
import { renderGovernorFor } from '../renderGovernor.js';
// The scene's Navara branch in Node: the globe loader is a seam, so the fake
// engine stands in for Navara and nothing Cesium is built.
@@ -165,12 +162,15 @@ test('the Navara scene tears down the controller, then the globe', async (t) =>
const run = await startScene();
// The tools install the governor on the engine (src/app/tools.js); the
// scene removes it before the engine goes.
installEngineRenderGovernor(run.engine);
renderGovernorFor(run.engine).install();
const view = globalThis.window;
await run.teardown();
assert.equal(run.globeDestroyed(), 1);
assert.equal(run.engine.isDestroyed(), true);
assert.equal(isRenderGovernorInstalled(run.engine), false);
assert.equal(
renderGovernorFor(run.engine).getRenderGovernorDiagnostics().installed,
false,
);
assert.equal(view.__GOOGLE_MAPS_API_KEY__, undefined);
});
@@ -262,16 +262,21 @@ test('parking or tearing down the Navara scene releases a running camera verb',
}
});
test('the application scene owns and releases its pick services', async () => {
test('the application scene owns and releases its pick and render services', async () => {
const run = await startScene({ googleApiKey: '' });
const { engine, services } = run.scene;
try {
assert.equal(sceneServicesFor(engine), services);
services.render.install();
services.render.holdContinuousRender('flights');
assert.equal(engine.render.isContinuous(), true);
services.selection.register('flights', { owns: (id) => id === 'contact' });
assert.equal(services.picking.findPickOwner('contact'), 'flights');
} finally {
await run.teardown();
}
assert.equal(services.render.getRenderGovernorDiagnostics().installed, false);
assert.deepEqual(services.render.getRenderGovernorDiagnostics().holds, []);
assert.equal(services.picking.findPickOwner('contact'), null);
services.picking.registerPickOwner('late', () => true);
assert.equal(services.picking.findPickOwner('contact'), null);
+1 -7
View File
@@ -11,10 +11,6 @@ import { registerDataCredits } from '../data/dataCredits.js';
import { configureCreditKeyboardAccess } from '../creditKeyboard.js';
import { MapStackController } from '../mapStackController.js';
import { initLogoGaze } from '../logoGaze.js';
import {
uninstallRenderGovernor,
governorRequestRender,
} from '../renderGovernor.js';
import { describeError } from './errors.js';
import { createSceneEngine } from './globeEngine.js';
import { onLocaleChange, t } from '../i18n/index.js';
@@ -157,7 +153,6 @@ async function createRenderer({
creditContainer,
});
defer(() => {
uninstallRenderGovernor(viewer);
if (!viewer.isDestroyed()) viewer.destroy();
});
// The globe engine over this viewer; Google's startup tileset is named
@@ -216,7 +211,7 @@ async function createRenderer({
loaderStatus.textContent = t('Initializing systems...');
const mapStackController = new MapController(viewer, {
requestRender: governorRequestRender,
requestRender: (reason) => engine.render.request(reason),
// Map sources reach the scene through the globe engine's maps.
engine,
...mapOptions,
@@ -247,7 +242,6 @@ async function createRenderer({
tileset,
mapStackController,
suspend() {
uninstallRenderGovernor(viewer);
engine.camera.cancelFlight();
releaseCameraVerbs(engine);
viewer.useDefaultRenderLoop = false;
+14 -16
View File
@@ -22,13 +22,7 @@ import {
import { KEYHOLE_OUTER_RADIUS } from '../celestialRing.js';
import { installGlobeInViewRecovery } from '../globe/cesium/viewer.js';
import { installLayerReleaseTrim } from '../sceneCommandRelease.js';
import {
installRenderGovernor,
getRenderGovernorDiagnostics,
governorRequestRender,
holdContinuousRender,
releaseContinuousRender,
} from '../renderGovernor.js';
import { renderGovernorFor } from '../renderGovernor.js';
/**
* Attach scene tools, rendering listeners and the application debug handle.
@@ -148,9 +142,11 @@ export async function createApplicationTools({
weatherEffects: controls.weatherEffects ?? null,
cockpitCloudEffects: null,
voiceCommands: null,
getRenderGovernorDiagnostics,
getRenderGovernorDiagnostics: () =>
renderGovernorFor(debug.engine)?.getRenderGovernorDiagnostics(),
surfaceServices: operations.surface,
requestRender: governorRequestRender,
requestRender: (reason) =>
renderGovernorFor(debug.engine)?.governorRequestRender(reason),
};
defer(() => {
if (window.__godsEyeView === debug) delete window.__godsEyeView;
@@ -266,6 +262,11 @@ const TOOL_BACKENDS = Object.freeze({
/** The Cesium tool backend: the shared tools and its own on the viewer. */
function cesiumToolBackend({ engine, tileset }) {
const viewer = cesiumViewerOf(engine);
const {
governorRequestRender,
holdContinuousRender,
releaseContinuousRender,
} = renderGovernorFor(engine);
return {
viewer,
startupChrome: {},
@@ -276,10 +277,6 @@ function cesiumToolBackend({ engine, tileset }) {
layer.attachTileset(tileset);
return { viewer };
},
// Idle render governor: flips the scene into requestRenderMode whenever
// nothing animates per frame. (perf wave 2) A parked viewer uninstalls
// it (src/app/scene.js suspend).
installGovernor: () => installRenderGovernor(viewer),
attach({ dataManager }, { defer }) {
// Cesium keeps a turned-off layer's draw commands (and so all its
// entities) alive in reused per-pass arrays; trim them after each
@@ -336,7 +333,7 @@ function cesiumToolBackend({ engine, tileset }) {
* engine kind's backend (TOOL_BACKENDS) says where the shared tools draw:
* `board()` and `draw()` give their attachScene arguments (null: not on
* this engine), `recentImagery(layer)` the panel's globe,
* `installGovernor()` the governor's; `attach(context, attachment)` adds
* `attach(context, attachment)` adds
* the engine's own tools, `suspend(hidden, controls)` its hidden-tab
* handling and `attachViews(context, attachment)` its embedded views. The
* shared tools keep their session state across engines. Returns the
@@ -358,6 +355,7 @@ async function attachEngineTools(
{ signal, defer },
) {
const { engine, mapStackController } = scene;
const governor = scene.services?.render ?? renderGovernorFor(engine);
const backend = (await TOOL_BACKENDS[engine.kind]())(scene);
// The page session's board, Draw and Import (src/app/sessionTools.js):
// an engine switch only takes their drawing off this globe.
@@ -412,7 +410,7 @@ async function attachEngineTools(
}
// Installed AFTER every module above has had its chance to register
// pre-install holds.
backend.installGovernor();
governor.install();
backend.attach?.({ data, styleManager, dataManager }, { signal, defer });
// Install the explicit scope mask used by the DISPLAY controls: a canvas
@@ -430,7 +428,7 @@ async function attachEngineTools(
backend.suspend?.(hidden, controls);
if (!hidden) {
data.presentation.flushVisible();
governorRequestRender('visibility-restore');
governor.governorRequestRender('visibility-restore');
}
};
document.addEventListener('visibilitychange', syncVisibilitySuspension);
+7 -6
View File
@@ -5,9 +5,8 @@ import { navaraModeNotice } from './navaraMode.js';
import { LAYER_MANIFEST, createLayerAvailability } from './layers/renderers.js';
import { createFakeEngine } from '../globe/testing/fakeEngine.js';
import {
_resetRenderGovernorForTest,
renderGovernorFor,
holdContinuousRender,
isRenderGovernorInstalled,
releaseContinuousRender,
} from '../renderGovernor.js';
@@ -47,7 +46,6 @@ test('without a viewer the tools run the governor on the engine, show the notice
},
};
t.after(() => {
_resetRenderGovernorForTest();
for (const [name, value] of Object.entries(saved)) {
if (value === undefined) delete globalThis[name];
else globalThis[name] = value;
@@ -146,10 +144,13 @@ test('without a viewer the tools run the governor on the engine, show the notice
'Data layers: Live Flights, Satellites',
);
// The governor drives the engine's continuous rendering.
assert.equal(isRenderGovernorInstalled(engine), true);
holdContinuousRender('qa');
assert.equal(
renderGovernorFor(engine).getRenderGovernorDiagnostics().installed,
true,
);
holdContinuousRender('qa', engine);
assert.equal(engine.render.isContinuous(), true);
releaseContinuousRender('qa');
releaseContinuousRender('qa', engine);
assert.equal(engine.render.isContinuous(), false);
const debug = window.__godsEyeView;
assert.equal(debug.viewer, null);
+42 -41
View File
@@ -30,8 +30,8 @@ const clone = (value) => Cesium.Cartesian3.clone(value);
const longitude = (camera) =>
Cesium.Cartographic.fromCartesian(camera.positionWC).longitude;
const wrappedAngle = Cesium.Math.negativePiToPi;
const hasRenderHold = () =>
getRenderGovernorDiagnostics().holds.includes('camera-verb');
const hasRenderHold = (engine) =>
getRenderGovernorDiagnostics(engine).holds.includes('camera-verb');
// The frame measure judges a pose; these tests read the Cesium camera's.
const globeFrameShare = (camera, options) =>
poseFrameShare(cesiumFramePose(camera), options);
@@ -160,7 +160,7 @@ function harness(
}
test('globe rotation yields to tracking acquired and cleared before the next tick', (t) => {
const { viewer, camera, tick } = harness(t);
const { viewer, camera, tick, engine } = harness(t);
assert.equal(
moveCamera({ motion: 'rotate_globe', mode: 'continuous' }).ok,
true,
@@ -176,7 +176,7 @@ test('globe rotation yields to tracking acquired and cleared before the next tic
viewer.trackedEntity = undefined;
tick();
samePose(camera, before);
assert.equal(hasRenderHold(), false);
assert.equal(hasRenderHold(engine), false);
});
test('tracking event keeps legacy yaw and an untracked globe rotation active', (t) => {
@@ -214,13 +214,14 @@ test('a replaced engine keeps no listener and its tracking stays out of the new
assert.equal(activeCameraMotionId(), id);
next.viewer.trackedEntity = { id: 'new-aircraft' };
assert.equal(getActiveCameraMotion(), null);
assert.equal(hasRenderHold(), false);
assert.equal(hasRenderHold(first.engine), false);
assert.equal(hasRenderHold(next.engine), false);
assert.equal(next.scene.preRender.numberOfListeners, 0);
});
for (const latitude of [0, 30, -60, 89.5]) {
test(`globe rotation preserves Earth framing and radius from latitude ${latitude} degrees`, (t) => {
const { camera, tick, finishOnce } = harness(t, { latitude });
const { camera, tick, finishOnce, engine } = harness(t, { latitude });
const before = projection(camera);
const radius = Cesium.Cartesian3.magnitude(camera.positionWC);
const direction = clone(camera.directionWC);
@@ -242,7 +243,7 @@ for (const latitude of [0, 30, -60, 89.5]) {
Cesium.Cartesian3.angleBetween(camera.directionWC, direction),
radians(15),
);
assert.equal(hasRenderHold(), false);
assert.equal(hasRenderHold(engine), false);
});
}
@@ -377,14 +378,14 @@ for (const [speed, degreesPerSecond] of [
['fast', 20],
]) {
test(`continuous globe rotation honors ${speed} speed and explicit stop`, (t) => {
const { camera, tick } = harness(t);
const { camera, tick, engine } = harness(t);
const result = moveCamera({
motion: 'rotate_globe',
mode: 'continuous',
speed,
});
assert.equal(result.ok, true);
assert.equal(hasRenderHold(), true);
assert.equal(hasRenderHold(engine), true);
tick(); // First tick initializes the motion's wall-clock anchor.
const before = clone(camera.directionWC);
tick(0.1);
@@ -395,7 +396,7 @@ for (const [speed, degreesPerSecond] of [
assert.equal(getActiveCameraMotion()?.kind, 'rotate_globe');
const stopped = moveCamera({ motion: 'stop' });
assert.equal(stopped.stopped, true);
assert.equal(hasRenderHold(), false);
assert.equal(hasRenderHold(engine), false);
const after = pose(camera);
tick();
samePose(camera, after);
@@ -405,21 +406,21 @@ for (const [speed, degreesPerSecond] of [
for (const event of ['pointerdown', 'touchstart', 'wheel']) {
test(`${event} immediately cancels globe rotation and releases the render hold`, (t) => {
const { camera, canvas, tick } = harness(t);
const { camera, canvas, tick, engine } = harness(t);
moveCamera({ motion: 'rotate_globe', mode: 'continuous' });
tick();
tick();
const before = pose(camera);
canvas.dispatchEvent(new Event(event));
assert.equal(getActiveCameraMotion(), null);
assert.equal(hasRenderHold(), false);
assert.equal(hasRenderHold(engine), false);
tick();
samePose(camera, before);
});
}
test('new navigation can cancel globe rotation before setting a new destination', (t) => {
const { camera, tick } = harness(t);
const { camera, tick, engine } = harness(t);
moveCamera({ motion: 'rotate_globe', mode: 'continuous' });
tick();
assert.equal(interruptCameraMotion('explicit-navigation').wasActive, true);
@@ -429,11 +430,11 @@ test('new navigation can cancel globe rotation before setting a new destination'
const destination = pose(camera);
tick();
samePose(camera, destination);
assert.equal(hasRenderHold(), false);
assert.equal(hasRenderHold(engine), false);
});
test('replacement motion rejects a stale globe owner cancellation', (t) => {
const { camera, tick } = harness(t);
const { camera, tick, engine } = harness(t);
moveCamera({ motion: 'rotate_globe', mode: 'continuous' });
const globeId = activeCameraMotionId();
moveCamera({ motion: 'rotate', direction: 'left', mode: 'continuous' });
@@ -446,11 +447,11 @@ test('replacement motion rejects a stale globe owner cancellation', (t) => {
tick();
near(Cesium.Cartesian3.distance(camera.positionWC, position), 0, 1e-6);
assert.equal(interruptCameraMotionIfActive(yawId).wasActive, true);
assert.equal(hasRenderHold(), false);
assert.equal(hasRenderHold(engine), false);
});
test('replacing the engine retires globe motion and its frame listener', (t) => {
const { viewer, scene, camera, tick } = harness(t);
const { viewer, scene, camera, tick, engine } = harness(t);
assert.equal(
moveCamera({ motion: 'rotate_globe', mode: 'continuous' }).ok,
true,
@@ -462,7 +463,7 @@ test('replacing the engine retires globe motion and its frame listener', (t) =>
t.after(() => nextEngine.destroy());
initCameraVerbs(nextEngine);
assert.equal(getActiveCameraMotion(), null);
assert.equal(hasRenderHold(), false);
assert.equal(hasRenderHold(engine), false);
assert.equal(scene.preRender.numberOfListeners, 0);
tick();
samePose(camera, before);
@@ -470,7 +471,7 @@ test('replacing the engine retires globe motion and its frame listener', (t) =>
for (const direction of ['up', 'down', 'diagonal']) {
test(`invalid globe direction ${direction} is refused before camera authority handoff`, (t) => {
const { camera } = harness(t);
const { camera, engine } = harness(t);
const before = pose(camera);
let handoffs = 0;
const result = moveCamera({ motion: 'rotate_globe', direction }, () => {
@@ -480,7 +481,7 @@ for (const direction of ['up', 'down', 'diagonal']) {
assert.match(result.error, /left or right/);
assert.equal(handoffs, 0);
assert.equal(getActiveCameraMotion(), null);
assert.equal(hasRenderHold(), false);
assert.equal(hasRenderHold(engine), false);
samePose(camera, before);
});
}
@@ -488,11 +489,11 @@ for (const direction of ['up', 'down', 'diagonal']) {
for (const mode of ['once', 'continuous']) {
for (const acquisition of ['tracking', 'reference frame']) {
test(`globe ${mode} yields to new ${acquisition} without moving the new owner`, (t) => {
const { viewer, camera, target, tick } = harness(t);
const { viewer, camera, target, tick, engine } = harness(t);
assert.equal(moveCamera({ motion: 'rotate_globe', mode }).ok, true);
tick();
tick();
assert.equal(hasRenderHold(), true);
assert.equal(hasRenderHold(engine), true);
const tracked = { id: 'new-tracked-aircraft' };
if (acquisition === 'tracking') viewer.trackedEntity = tracked;
else
@@ -504,7 +505,7 @@ for (const mode of ['once', 'continuous']) {
const transform = Cesium.Matrix4.clone(camera.transform);
tick();
assert.equal(getActiveCameraMotion(), null);
assert.equal(hasRenderHold(), false);
assert.equal(hasRenderHold(engine), false);
assert.equal(
viewer.trackedEntity,
acquisition === 'tracking' ? tracked : undefined,
@@ -518,7 +519,7 @@ for (const mode of ['once', 'continuous']) {
}
test('direct globe rotation refuses a tracked camera without disturbing its reference frame', (t) => {
const { viewer, camera, target } = harness(t);
const { viewer, camera, target, engine } = harness(t);
viewer.trackedEntity = { id: 'tracked-aircraft' };
camera.lookAt(target, new Cesium.HeadingPitchRange(0, radians(-40), 1_000));
const before = pose(camera);
@@ -529,7 +530,7 @@ test('direct globe rotation refuses a tracked camera without disturbing its refe
assert.equal(Cesium.Matrix4.equals(camera.transform, transform), true);
samePose(camera, before);
assert.equal(getActiveCameraMotion(), null);
assert.equal(hasRenderHold(), false);
assert.equal(hasRenderHold(engine), false);
});
test('the existing Globe View preset remains unchanged', () => {
@@ -553,7 +554,7 @@ for (const latitude of [0, 90, -90])
near(Cesium.Cartesian3.magnitude(camera.positionWC), radius, 1e-6);
});
test('polar preset admission keeps centre-distance although geodetic altitude varies', (t) => {
const { camera, tick } = harness(t, { latitude: 90 });
const { camera, tick, engine } = harness(t, { latitude: 90 });
const radius = Cesium.Cartesian3.magnitude(camera.positionWC);
assert.ok(Math.min(...arcHeights(camera, camera.upWC, Math.PI * 2)) > 0);
assert.equal(
@@ -569,7 +570,7 @@ test('polar preset admission keeps centre-distance although geodetic altitude va
assert.equal(getActiveCameraMotion().kind, 'rotate_globe');
near(Cesium.Cartesian3.magnitude(camera.positionWC), radius, 1e-6);
moveCamera({ motion: 'stop' });
assert.equal(hasRenderHold(), false);
assert.equal(hasRenderHold(engine), false);
});
test('close-up refusal preserves an inherited target frame', (t) => {
const { camera, target } = harness(t);
@@ -617,7 +618,7 @@ test('direct globe request refuses Cockpit before navigation mutation', (t) => {
);
assert.equal(calls, 0);
samePose(camera, before);
assert.equal(hasRenderHold(), false);
assert.equal(hasRenderHold(engine), false);
});
test('navigation handoff preserves the original world pose even when it levels a rolled camera', (t) => {
@@ -699,7 +700,7 @@ test('legacy orbit still revolves around the selected ground target', (t) => {
// Actual UI/tool authority routes, not a direct call to the interrupt helper.
test('new UI navigation intent stops globe motion before its destination and stale ticks', async (t) => {
const { NavigationController } = await import('./ui/navigationController.js');
const { viewer, camera, tick } = harness(t);
const { viewer, camera, tick, engine } = harness(t);
const navigation = new NavigationController({
viewer,
tracking: {
@@ -730,7 +731,7 @@ test('new UI navigation intent stops globe motion before its destination and sta
);
navigation._runExplicitNavigation('camera', () => {
assert.equal(getActiveCameraMotion(), null);
assert.equal(hasRenderHold(), false);
assert.equal(hasRenderHold(engine), false);
});
const after = pose(camera);
tick();
@@ -738,7 +739,7 @@ test('new UI navigation intent stops globe motion before its destination and sta
});
test('orbit-toggle controller stops globe motion before toggling', async (t) => {
const { LocationNavigation } = await import('./ui/locationNavigation.js');
const { target } = harness(t);
const { target, engine } = harness(t);
let toggled = false;
moveCamera({ motion: 'rotate_globe', mode: 'continuous' });
LocationNavigation.prototype._toggleOrbit.call({
@@ -748,7 +749,7 @@ test('orbit-toggle controller stops globe motion before toggling', async (t) =>
orbitController: {
toggle() {
assert.equal(getActiveCameraMotion(), null);
assert.equal(hasRenderHold(), false);
assert.equal(hasRenderHold(engine), false);
toggled = true;
return true;
},
@@ -842,13 +843,13 @@ for (const owner of ['tracking', 'cockpit', 'close-up'])
});
test('an external radius change retires globe rotation without another write', (t) => {
const { camera, tick } = harness(t);
const { camera, tick, engine } = harness(t);
moveCamera({ motion: 'rotate_globe', mode: 'continuous' });
camera.moveForward(1000);
const changed = pose(camera);
tick();
assert.equal(getActiveCameraMotion(), null);
assert.equal(hasRenderHold(), false);
assert.equal(hasRenderHold(engine), false);
samePose(camera, changed);
});
@@ -1083,7 +1084,7 @@ test('keyhole overlap after resize stops rotation before another pose write', (t
const before = pose(h.camera);
h.tick();
assert.equal(getActiveCameraMotion(), null);
assert.equal(hasRenderHold(), false);
assert.equal(hasRenderHold(h.engine), false);
samePose(h.camera, before);
});
@@ -1097,7 +1098,7 @@ test('a depth-near globe is unsupported without a reset offer', (t) => {
assert.equal(result.ok, false);
assert.equal(result.reason, 'unsupported-view');
samePose(h.camera, before);
assert.equal(hasRenderHold(), false);
assert.equal(hasRenderHold(h.engine), false);
});
test('fully depth-contained globe remains eligible at actual near/far limits', (t) => {
@@ -1125,7 +1126,7 @@ for (const [near, far] of [
});
test('bare globe rotation defaults right and continues beyond a nudge', (t) => {
const { camera, tick } = harness(t, { latitude: 0 });
const { camera, tick, engine } = harness(t, { latitude: 0 });
const before = projection(camera);
const result = moveCamera({ motion: 'rotate_globe' });
assert.equal(result.ok, true);
@@ -1137,7 +1138,7 @@ test('bare globe rotation defaults right and continues beyond a nudge', (t) => {
near(projection(camera).x, before.x);
near(projection(camera).y, before.y);
assert.equal(moveCamera({ motion: 'stop' }).stopped, true);
assert.equal(hasRenderHold(), false);
assert.equal(hasRenderHold(engine), false);
});
for (const [args, direction, mode, speed] of [
[{ mode: 'once' }, 'right', 'once', 'normal'],
@@ -1195,7 +1196,7 @@ test('canvas-only clipping after resize preserves a keyhole-fitting rotation', (
h.tick();
assert.notDeepEqual(pose(h.camera).position, before.position);
assert.equal(getActiveCameraMotion()?.kind, 'rotate_globe');
assert.equal(hasRenderHold(), true);
assert.equal(hasRenderHold(h.engine), true);
});
test('an invalid camera request leaves the active globe rotation untouched', (t) => {
@@ -1206,7 +1207,7 @@ test('an invalid camera request leaves the active globe rotation untouched', (t)
assert.equal(moveCamera({ motion: 'pan' }).ok, false);
assert.deepEqual(getActiveCameraMotion(), active);
samePose(h.camera, before);
assert.equal(hasRenderHold(), true);
assert.equal(hasRenderHold(h.engine), true);
});
test('close-up ultrawide refusal does not offer an unfit reset or move the camera', (t) => {
@@ -1223,7 +1224,7 @@ test('close-up ultrawide refusal does not offer an unfit reset or move the camer
assert.equal(result.reason, 'globe-reset-unavailable');
assert.equal(handoffs, 0);
samePose(h.camera, before);
assert.equal(hasRenderHold(), false);
assert.equal(hasRenderHold(h.engine), false);
});
/**
+9 -10
View File
@@ -34,10 +34,7 @@ import {
KEYHOLE_OUTER_RADIUS,
} from './celestialRing.js';
import { isScopeMaskEnabled } from './scopeMask.js';
import {
holdContinuousRender,
releaseContinuousRender,
} from './renderGovernor.js';
import { renderGovernorFor } from './renderGovernor.js';
let _isCockpitActive = null;
/** °/s by speed word — orbit; pan uses fractions of view height/s. */
@@ -1372,7 +1369,7 @@ export function suspendRouteFlight(id) {
const state = _active;
_active = null;
unbindEngineMotion();
releaseContinuousRender('camera-verb');
state.renderGovernor?.releaseContinuousRender('camera-verb');
return state;
}
@@ -1383,9 +1380,10 @@ export function suspendRouteFlight(id) {
export function resumeRouteFlight(state, engine) {
if (!state || !engine || _active) return false;
state.rig = engineRigFor(engine);
state.renderGovernor = renderGovernorFor(engine);
_active = state;
bindEngineMotion(state.rig);
holdContinuousRender('camera-verb');
_active?.renderGovernor?.holdContinuousRender('camera-verb');
return true;
}
/** Monotonic id for the motion currently running; 0 means "no motion". */
@@ -1447,6 +1445,7 @@ function countManualInput(engine) {
function setActiveMotion(state) {
_motionSeq += 1;
state.motionId = _motionSeq;
state.renderGovernor = renderGovernorFor(state.rig.engine);
_active = state;
bindEngineMotion(state.rig);
return state;
@@ -1694,7 +1693,7 @@ export function interruptCameraMotion(reason = 'interrupt') {
}
unbindEngineMotion();
_active = null;
releaseContinuousRender('camera-verb');
motion?.renderGovernor?.releaseContinuousRender('camera-verb');
return { wasActive, reason, leveled };
}
@@ -2604,7 +2603,7 @@ export function moveCamera(args = {}, runNavigation = null) {
mode,
pending: true,
});
holdContinuousRender('camera-verb');
_active?.renderGovernor?.holdContinuousRender('camera-verb');
return {
ok: true,
action: 'move_camera',
@@ -2623,7 +2622,7 @@ export function moveCamera(args = {}, runNavigation = null) {
setActiveMotion(state);
// Verb motion drives the camera per frame — hold until interrupted;
// interruptCameraMotion is the single release path. (perf wave 2)
holdContinuousRender('camera-verb');
_active?.renderGovernor?.holdContinuousRender('camera-verb');
return {
ok: true,
action: 'move_camera',
@@ -2860,7 +2859,7 @@ export function flyRoute(
});
flight.rig = rig;
setActiveMotion(flight);
holdContinuousRender('camera-verb');
_active?.renderGovernor?.holdContinuousRender('camera-verb');
return {
ok: true,
action: 'fly_route',
+9 -9
View File
@@ -27,8 +27,8 @@ import { getRenderGovernorDiagnostics } from './renderGovernor.js';
const WIDTH = 1280;
const HEIGHT = 800;
const radians = geo.math.toRadians;
const hasRenderHold = () =>
getRenderGovernorDiagnostics().holds.includes('camera-verb');
const hasRenderHold = (engine) =>
getRenderGovernorDiagnostics(engine).holds.includes('camera-verb');
/** Wall clock the verbs read (performance.now), moved only by the tests. */
function installClock(t) {
@@ -279,12 +279,12 @@ test('a once nudge spends its budget in 0.9 s and then lets go', (t) => {
initCameraVerbs(engine);
const before = camera();
assert.equal(moveCamera({ motion: 'tilt', direction: 'up' }).ok, true);
assert.equal(hasRenderHold(), true);
assert.equal(hasRenderHold(engine), true);
run(0.8);
assert.equal(getActiveCameraMotion()?.kind, 'tilt', 'still easing');
run(0.2);
assert.equal(getActiveCameraMotion(), null, 'finished after 0.9 s');
assert.equal(hasRenderHold(), false);
assert.equal(hasRenderHold(engine), false);
near(
geo.math.toDegrees(camera().pitch - before.pitch),
15,
@@ -337,7 +337,7 @@ test('manual input on the canvas stops the motion and releases the hold', (t) =>
run(0.3);
engine.host.canvas.dispatchEvent(new Event(event));
assert.equal(getActiveCameraMotion(), null, event);
assert.equal(hasRenderHold(), false, event);
assert.equal(hasRenderHold(engine), false, event);
assert.deepEqual(engine.camera.transform(), [...geo.matrix4.IDENTITY]);
engine.fake.resetCalls();
run(0.3);
@@ -367,7 +367,7 @@ test('a new verb supersedes the running one and stop ends it', (t) => {
motion: 'stop',
stopped: true,
});
assert.equal(hasRenderHold(), false);
assert.equal(hasRenderHold(engine), false);
});
test('one frame listener and three input listeners exist only while a motion runs', (t) => {
@@ -407,7 +407,7 @@ test('an engine switch releases the verb: the parked engine is never driven agai
// What the Navara scene's suspend does (src/app/scene.js).
releaseCameraVerbs(first.engine);
assert.equal(getActiveCameraMotion(), null);
assert.equal(hasRenderHold(), false);
assert.equal(hasRenderHold(first.engine), false);
first.engine.fake.resetCalls();
first.run(0.5);
assert.equal(first.engine.fake.callsOf('camera.setView').length, 0);
@@ -454,7 +454,7 @@ test('globe rotation yields when a follow takes the engine camera', (t) => {
viewFrom: geo.cartesian3.create(0, -2000, 1000),
});
assert.equal(getActiveCameraMotion(), null);
assert.equal(hasRenderHold(), false);
assert.equal(hasRenderHold(engine), false);
// And a request while following is refused before any handoff.
let handoffs = 0;
const refused = moveCamera({ motion: 'rotate_globe' }, () => {
@@ -514,7 +514,7 @@ test('fly_route flies the installed engine camera and lands level', (t) => {
run(result.durationS + 2);
assert.equal(getActiveCameraMotion(), null, 'landed');
near(geo.math.negativePiToPi(engine.camera.pose().roll), 0, 1e-5, 'roll');
assert.equal(hasRenderHold(), false);
assert.equal(hasRenderHold(engine), false);
});
test('a parked route flight resumes on another engine without jumping', (t) => {
+9 -4
View File
@@ -7,7 +7,7 @@ import {
} from './globe/cesium/celestial.js';
import { createCelestialEphemeris } from './celestialRing/ephemeris.js';
import { appClock } from './time/clock.js';
import { governorRequestRender } from './renderGovernor.js';
import { renderGovernorFor } from './renderGovernor.js';
/** Outer edge of the existing NVG/FLIR keyhole in normalized shader space. */
export const KEYHOLE_OUTER_RADIUS = 1.05;
@@ -413,6 +413,9 @@ export class CelestialRing {
this._engineView = viewer?.kind ? createCelestialView(viewer) : null;
this.viewer = this._engineView?.view ?? viewer;
this._projector = engine?.projector ?? null;
this._renderGovernor = renderGovernorFor(
engine ?? (viewer?.kind ? viewer : null),
);
this.enabled = !!enabled;
this.visible = false;
this._onAutoDisable =
@@ -460,7 +463,7 @@ export class CelestialRing {
// dirty flag up immediately. (review finding)
this._ephemerisDirty = true;
if (typeof document !== 'undefined' && document.hidden) return;
governorRequestRender('celestial-ephemeris');
this._renderGovernor?.governorRequestRender('celestial-ephemeris');
}, 60_000);
this.setEnabled(this.enabled);
}
@@ -526,7 +529,8 @@ export class CelestialRing {
this._moonCtx = ctx;
this._moonRenderKey = '';
} else return;
if (canvas && this.enabled) governorRequestRender('celestial-ring');
if (canvas && this.enabled)
this._renderGovernor?.governorRequestRender('celestial-ring');
}
/** Enable or disable the user preference for the effect. */
@@ -539,7 +543,8 @@ export class CelestialRing {
// clearing disable) must request its frame or the ring never draws at
// all. Camera motion covers every later repaint; the 60 s ephemeris
// timer requests its own. (perf wave 2 fix — owner playtest finding)
if (this.enabled !== wasEnabled) governorRequestRender('celestial-ring');
if (this.enabled !== wasEnabled)
this._renderGovernor?.governorRequestRender('celestial-ring');
if (!this.enabled) {
this.visible = false;
this._clear();
+28 -24
View File
@@ -1136,7 +1136,7 @@ export function createBhoteKoshiEventLayer({
if (!_enabled || generation !== _comparisonSurfaceGeneration) return;
syncFloodSurfaceMode();
updateProgress(_progress);
renderHost.request('bhote-koshi-comparison-ready');
renderHost.request('bhote-koshi-comparison-ready', _engine);
}
function attachDataManager(dataManager) {
@@ -1421,7 +1421,8 @@ export function createBhoteKoshiEventLayer({
}
_sceneMediaPlaybackBeatId = null;
if (pauseMedia) _embeddedMedia?.pause?.();
if (hadPlayback) renderHost.request('bhote-koshi-scene-media-hold');
if (hadPlayback)
renderHost.request('bhote-koshi-scene-media-hold', _engine);
}
function completeSceneMediaPlayback() {
@@ -1439,7 +1440,7 @@ export function createBhoteKoshiEventLayer({
if (_sceneMediaPlaybackBeatId !== beatId) return;
_sceneMediaPlaybackTimer = null;
_sceneMediaPlaybackBeatId = null;
renderHost.request('bhote-koshi-scene-media-exit');
renderHost.request('bhote-koshi-scene-media-exit', _engine);
}, exitDurationSec * 1000) ?? null;
}
@@ -1762,7 +1763,8 @@ export function createBhoteKoshiEventLayer({
if (_sceneRevealAnimationFrame != null)
cancelAnimationFrame(_sceneRevealAnimationFrame);
_sceneRevealAnimationFrame = null;
if (_sceneRevealHoldActive) renderHost.release(SCENE_REVEAL_RENDER_HOLD);
if (_sceneRevealHoldActive)
renderHost.release(SCENE_REVEAL_RENDER_HOLD, _engine);
_sceneRevealHoldActive = false;
}
@@ -1773,7 +1775,8 @@ export function createBhoteKoshiEventLayer({
if (_scenePathDeadlineTimer != null)
globalThis.window?.clearTimeout?.(_scenePathDeadlineTimer);
_scenePathDeadlineTimer = null;
if (_scenePathHoldActive) renderHost.release(SCENE_PATH_RENDER_HOLD);
if (_scenePathHoldActive)
renderHost.release(SCENE_PATH_RENDER_HOLD, _engine);
_scenePathHoldActive = false;
}
@@ -1787,8 +1790,9 @@ export function createBhoteKoshiEventLayer({
_scenePathRevealProgress = targetProgress;
updateFloodTrace(_scenePathRevealProgress);
updateSurgeFront(_scenePathRevealProgress);
renderHost.request(requestReason);
if (_scenePathHoldActive) renderHost.release(SCENE_PATH_RENDER_HOLD);
renderHost.request(requestReason, _engine);
if (_scenePathHoldActive)
renderHost.release(SCENE_PATH_RENDER_HOLD, _engine);
_scenePathHoldActive = false;
}
@@ -1959,7 +1963,7 @@ export function createBhoteKoshiEventLayer({
lastTickAt = tickAt;
return activeElapsedMs;
};
renderHost.hold(SCENE_PATH_RENDER_HOLD);
renderHost.hold(SCENE_PATH_RENDER_HOLD, _engine);
_scenePathHoldActive = true;
// RAF normally paints every intermediate frame. This independent deadline
// is registered before Cesium starts the flight, so a throttled or briefly
@@ -2008,7 +2012,7 @@ export function createBhoteKoshiEventLayer({
startProgress + (targetProgress - startProgress) * eased;
updateFloodTrace(_scenePathRevealProgress);
updateSurgeFront(_scenePathRevealProgress);
renderHost.request('bhote-koshi-scene-path-travel');
renderHost.request('bhote-koshi-scene-path-travel', _engine);
if (linear < 1) {
_scenePathAnimationFrame = requestAnimationFrame(step);
} else {
@@ -2066,7 +2070,7 @@ export function createBhoteKoshiEventLayer({
if (sequenceWindow) updateProgress(sequenceWindow.startProgress);
let lastTickAt = performance.now();
let activeElapsedMs = 0;
renderHost.hold(SCENE_REVEAL_RENDER_HOLD);
renderHost.hold(SCENE_REVEAL_RENDER_HOLD, _engine);
_sceneRevealHoldActive = true;
const step = (now) => {
if (
@@ -2097,12 +2101,12 @@ export function createBhoteKoshiEventLayer({
updateFloodTrace(floodProgress);
updateSurgeFront(floodProgress);
}
renderHost.request('bhote-koshi-scene-beat-reveal');
renderHost.request('bhote-koshi-scene-beat-reveal', _engine);
if (linear < 1) {
_sceneRevealAnimationFrame = requestAnimationFrame(step);
} else {
_sceneRevealAnimationFrame = null;
renderHost.release(SCENE_REVEAL_RENDER_HOLD);
renderHost.release(SCENE_REVEAL_RENDER_HOLD, _engine);
_sceneRevealHoldActive = false;
}
};
@@ -2147,7 +2151,7 @@ export function createBhoteKoshiEventLayer({
),
})
) {
renderHost.request('bhote-koshi-evidence-video-frame');
renderHost.request('bhote-koshi-evidence-video-frame', _engine);
}
_evidenceVideoFrameHandle = video.requestVideoFrameCallback(drawFrame);
};
@@ -2176,7 +2180,7 @@ export function createBhoteKoshiEventLayer({
previousIndex >= 0 &&
redrawEvidenceSlot(previousIndex)
) {
renderHost.request('bhote-koshi-evidence-poster-restore');
renderHost.request('bhote-koshi-evidence-poster-restore', _engine);
}
}
@@ -2204,7 +2208,7 @@ export function createBhoteKoshiEventLayer({
)
return;
_evidenceVideoReady = true;
renderHost.request('bhote-koshi-evidence-video-ready');
renderHost.request('bhote-koshi-evidence-video-ready', _engine);
syncEvidenceMedia();
};
video.addEventListener?.('loadeddata', markReady, { once: true });
@@ -2215,7 +2219,7 @@ export function createBhoteKoshiEventLayer({
video !== _evidenceVideo
)
return;
renderHost.request('bhote-koshi-evidence-video-seek');
renderHost.request('bhote-koshi-evidence-video-seek', _engine);
});
video.src = eventAsset(videoPath);
video.load?.();
@@ -2363,7 +2367,7 @@ export function createBhoteKoshiEventLayer({
mediaProgress: presentation.localProgress,
})
) {
renderHost.request('bhote-koshi-evidence-video-frame');
renderHost.request('bhote-koshi-evidence-video-frame', _engine);
}
}
@@ -2509,7 +2513,7 @@ export function createBhoteKoshiEventLayer({
slot.poster = poster;
poster = null;
if (!redrawEvidenceSlot(index)) return;
renderHost.request('bhote-koshi-evidence-poster');
renderHost.request('bhote-koshi-evidence-poster', _engine);
} catch (error) {
closeFrame(poster);
if (
@@ -2785,7 +2789,7 @@ export function createBhoteKoshiEventLayer({
if (releaseCamera)
cameraForScene()?.lookAtTransform?.(Cesium.Matrix4.IDENTITY);
syncCinematicButton();
renderHost.request('bhote-koshi-cinematic-stop');
renderHost.request('bhote-koshi-cinematic-stop', _engine);
}
function startCinematic({ play = true } = {}) {
@@ -3446,7 +3450,7 @@ export function createBhoteKoshiEventLayer({
applyCinematicCamera();
syncEvidenceMedia();
syncPanel();
renderHost.request('bhote-koshi-progress');
renderHost.request('bhote-koshi-progress', _engine);
}
function setSplit(split) {
@@ -3454,7 +3458,7 @@ export function createBhoteKoshiEventLayer({
_splitControl?.setValue(_split);
_native?.setSplit(_split);
syncPanel();
renderHost.request('bhote-koshi-split');
renderHost.request('bhote-koshi-split', _engine);
}
function stopPlayback() {
@@ -3462,7 +3466,7 @@ export function createBhoteKoshiEventLayer({
_playing = false;
if (_animationFrame != null) cancelAnimationFrame(_animationFrame);
_animationFrame = null;
renderHost.release(FLOOD_RENDER_HOLD);
renderHost.release(FLOOD_RENDER_HOLD, _engine);
syncEvidenceMedia();
syncPanel();
}
@@ -3486,7 +3490,7 @@ export function createBhoteKoshiEventLayer({
_playing = true;
_playStartProgress = _progress;
_playStartedAt = performance.now();
renderHost.hold(FLOOD_RENDER_HOLD);
renderHost.hold(FLOOD_RENDER_HOLD, _engine);
syncEvidenceMedia();
syncPanel();
_animationFrame = requestAnimationFrame(animationTick);
@@ -3832,7 +3836,7 @@ export function createBhoteKoshiEventLayer({
try {
await comparisonLease?.release();
} finally {
renderHost.request('bhote-koshi-disable');
renderHost.request('bhote-koshi-disable', _engine);
}
return true;
}
+1 -4
View File
@@ -291,10 +291,7 @@ export function createBhoteKoshiLocatorLayer({
let _native = null;
let _pausedApproach = null;
const cameraForScene = () => _engine?.camera ?? _viewer?.camera;
const requestRender = (reason) =>
_engine?.kind === 'navara'
? _engine.render.request(reason)
: requestSceneRender(reason);
const requestRender = (reason) => requestSceneRender(reason, _engine);
let _dataSource = null;
let _enabled = false;
let _boundaryReady = false;
+4 -4
View File
@@ -400,7 +400,7 @@ function _scheduleCyberSonarRender(nowMs) {
_cyberSonarRenderTimer = globalThis.setTimeout(() => {
_cyberSonarRenderTimer = null;
if (isCyberSonarActive()) {
governorRequestRender('cyber-sonar-cadence');
governorRequestRender('cyber-sonar-cadence', _engine);
}
}, delay);
}
@@ -655,7 +655,7 @@ function _syncSurfaceVisibility() {
// owes itself follow-up frames only while a bounded animation is in flight
// (see `detectionRenderDemand.js` for why a hold was wrong here, and why
// removing it is safe).
governorRequestRender('detection-visibility');
governorRequestRender('detection-visibility', _engine);
if (!_hostSurface) return;
_hostSurface.style.display = _mode === MODE_OFF ? 'none' : 'block';
_hostSurface.style.opacity = _suspended ? '0' : '1';
@@ -750,7 +750,7 @@ function _shouldPaintDetectionLane(frame) {
if (_lastDiagnostics)
_lastDiagnostics.throttleSkipCount = _throttleSkipCount;
if (decision.requestFollowUp)
governorRequestRender('detection-paint-skipped');
governorRequestRender('detection-paint-skipped', _engine);
return false;
}
return true;
@@ -815,7 +815,7 @@ function _paintDetectionLane(frame) {
solvePending: result.solvePending === true,
})
) {
governorRequestRender('detection-animation');
governorRequestRender('detection-animation', _engine);
}
}
+5 -5
View File
@@ -275,9 +275,9 @@ test('detection holds nothing, and asks for its own frames instead', async () =>
assert.doesNotMatch(source, /releaseContinuousRender/,
'and with no hold there is nothing to release');
assert.match(source, /governorRequestRender\('detection-visibility'\)/,
assert.match(source, /governorRequestRender\('detection-visibility', _engine\)/,
'mode/suspend transitions request their own repaint');
assert.match(source, /if \(\s*detectionNeedsFollowUpFrame\(\{[\s\S]*?\}\)\s*\) \{\s*\n\s*governorRequestRender\('detection-animation'\);/,
assert.match(source, /if \(\s*detectionNeedsFollowUpFrame\(\{[\s\S]*?\}\)\s*\) \{\s*\n\s*governorRequestRender\('detection-animation', _engine\);/,
'the follow-up frame is gated on the policy, never unconditional');
// The demand call must read the FRAME's timestamp, not a fresh sample.
@@ -339,7 +339,7 @@ test('detection holds nothing, and asks for its own frames instead', async () =>
'the valve asks the shared policy instead of inlining its own threshold');
assert.doesNotMatch(shouldPaint, /_lastPaintMs > 22/,
'the old inline threshold must be gone, not shadowing the policy');
assert.match(shouldPaint, /if \(decision\.requestFollowUp\)\s*governorRequestRender\('detection-paint-skipped'\)/,
assert.match(shouldPaint, /if \(decision\.requestFollowUp\)\s*governorRequestRender\('detection-paint-skipped', _engine\)/,
'skipping a paint must hand the request forward, not swallow it');
// The scanline must not go back to the frame counter: that is what made a
@@ -478,13 +478,13 @@ test('aircraft brackets stay prompt because the aircraft layers hold the render
assert.ok(enable, `${file}: enable() is still identifiable`);
assert.match(
enable,
/holdContinuousRender\('(flights|military)'\)/,
/holdContinuousRender\(\s*'(flights|military)',\s*flightState\._renderer\?\.engine,?\s*\)/,
`${file}: enabling the layer must hold continuous render — detection no longer ` +
'holds one, so this is what keeps its AIR brackets repainting on a parked scene',
);
assert.match(
source,
/releaseContinuousRender\('(flights|military)'\)/,
/releaseContinuousRender\(\s*'(flights|military)',\s*flightState\._renderer\?\.engine,?\s*\)/,
`${file}: and the hold must be released, or the governor can never idle`,
);
}
+8 -10
View File
@@ -17,8 +17,7 @@ import {
import { DataLayerManager } from './manager.js';
import {
getRenderGovernorDiagnostics,
installRenderGovernor,
_resetRenderGovernorForTest,
renderGovernorFor,
} from '../renderGovernor.js';
import { loadLayerRenderers } from '../app/layers/renderers.js';
@@ -214,6 +213,7 @@ test('a quake poll still reaches the screen with the render loop idle', async ()
const originalFetch = globalThis.fetch;
const renderRequests = [];
const ground = createFakeGroundHarness();
ground.engine.render.request = () => renderRequests.push(Date.now());
const viewer = {
scene: { requestRenderMode: false, requestRender: () => renderRequests.push(Date.now()) },
};
@@ -231,8 +231,7 @@ test('a quake poll still reaches the screen with the render loop idle', async ()
}),
});
_resetRenderGovernorForTest();
installRenderGovernor(viewer);
renderGovernorFor(ground.engine).install();
// Installing the governor enters idle mode, which itself paints one settling
// frame. Drop that from the baseline or the enable assertion below can pass on
// the install alone, without the manager ever requesting anything.
@@ -244,9 +243,9 @@ test('a quake poll still reaches the screen with the render loop idle', async ()
await manager.setEnabled('earthquakes', true, { origin: 'test' });
// The whole point of the change: enabling quakes must not pin the loop on.
assert.equal(getRenderGovernorDiagnostics().mode, 'idle', 'quakes must not force continuous render');
assert.deepEqual(getRenderGovernorDiagnostics().holds, []);
assert.equal(viewer.scene.requestRenderMode, true, 'the governor really is in idle mode');
assert.equal(getRenderGovernorDiagnostics(ground.engine).mode, 'idle', 'quakes must not force continuous render');
assert.deepEqual(getRenderGovernorDiagnostics(ground.engine).holds, []);
assert.equal(ground.engine.render.isContinuous(), false, 'the governor really is in idle mode');
// ...and the poll that populated the discs must still have asked for a frame,
// because in idle mode nothing repaints on its own.
@@ -255,7 +254,7 @@ test('a quake poll still reaches the screen with the render loop idle', async ()
// Named, not merely counted: the frame has to come from the manager's
// visibility request, not from some incidental repaint.
assert.ok(
getRenderGovernorDiagnostics().recentRequests.some(({ reason }) => reason === 'layer-visibility'),
getRenderGovernorDiagnostics(ground.engine).recentRequests.some(({ reason }) => reason === 'layer-visibility'),
'the enable frame must be the manager\'s layer-visibility request',
);
@@ -267,7 +266,7 @@ test('a quake poll still reaches the screen with the render loop idle', async ()
renderRequests.length > beforeRefresh,
'each refresh tick must request its own render while the loop is idle',
);
const reasons = getRenderGovernorDiagnostics().recentRequests.map(({ reason }) => reason);
const reasons = getRenderGovernorDiagnostics(ground.engine).recentRequests.map(({ reason }) => reason);
assert.ok(
reasons.includes('layer-tick:earthquakes'),
`the tick request must be attributed to the layer, got ${JSON.stringify(reasons)}`,
@@ -275,7 +274,6 @@ test('a quake poll still reaches the screen with the render loop idle', async ()
} finally {
globalThis.fetch = originalFetch;
await manager.setEnabled('earthquakes', false, { origin: 'test' }).catch(() => {});
_resetRenderGovernorForTest();
ground.destroy();
}
});
+23 -13
View File
@@ -32,9 +32,8 @@ import aisLiveVesselsLayer, {
} from './aisLiveVessels.js';
import militaryInstallationsLayer from './militaryInstallations.js';
import {
_resetRenderGovernorForTest,
getRenderGovernorDiagnostics,
installRenderGovernor,
renderGovernorFor,
} from '../renderGovernor.js';
import {
AWARENESS_RADIUS_M,
@@ -54,7 +53,10 @@ import { registerLayerRenderers } from '../app/layers/renderers.js';
// the layer through the renderer registry, under an engine kind of its own.
const VIEWER_ENGINE = 'awareness-viewer-test';
registerLayerRenderers(VIEWER_ENGINE, {
'military-awareness': ({ viewer }) => ({
'military-awareness': (engine) => {
const { viewer } = engine;
return {
engine,
container: viewer.container,
canvas: viewer.canvas,
get camera() {
@@ -70,9 +72,21 @@ registerLayerRenderers(VIEWER_ENGINE, {
viewer.camera.flyToBoundingSphere({ center, radius }, { duration }),
createRim: () => ({ move() {}, setAlpha() {}, destroy() {} }),
createPulses: () => null,
}),
};
},
});
const viewerEngine = (viewer) => ({ kind: VIEWER_ENGINE, viewer });
const viewerEngines = new WeakMap();
const viewerEngine = (viewer) => {
if (!viewerEngines.has(viewer)) viewerEngines.set(viewer, {
kind: VIEWER_ENGINE, viewer,
render: {
request: () => viewer.scene.requestRender?.(),
setContinuous: (enabled) => { viewer.scene.requestRenderMode = !enabled; },
isContinuous: () => viewer.scene.requestRenderMode === false,
},
});
return viewerEngines.get(viewer);
};
const militaryAwarenessSource = readLayerSource(
new URL('./militaryAwareness.js', import.meta.url),
@@ -3922,13 +3936,12 @@ test('parked Contacts takes no continuous-render hold and requests at most the p
{ icao24: 'neighbour-flight', position: neighbour, distanceM: 4000 },
],
});
_resetRenderGovernorForTest();
const runtime = installAwarenessRuntime();
const holds = () => getRenderGovernorDiagnostics().holds;
const holds = () => getRenderGovernorDiagnostics(viewerEngine(runtime.viewer)).holds;
try {
Date.now = () => clockMs;
installRenderGovernor(runtime.viewer);
renderGovernorFor(viewerEngine(runtime.viewer)).install();
runtime.viewer.camera = {
positionWC: new Cesium.Cartesian3(1_000_000, 200_000, 300_000),
heading: 0,
@@ -4003,7 +4016,6 @@ test('parked Contacts takes no continuous-render hold and requests at most the p
Date.now = realNow;
runtime.restore();
restoreCollections();
_resetRenderGovernorForTest();
}
});
@@ -4058,13 +4070,12 @@ test('a moving view over failed feeds takes no continuous-render hold', () => {
// Every dependency reports unavailable, so every cohort summarizes to count
// null — the exact state the old Boolean(state.results) gate still held for.
const restoreCollections = stubAwarenessCollections({});
_resetRenderGovernorForTest();
const runtime = installAwarenessRuntime({ isEnabled: () => false });
const holds = () => getRenderGovernorDiagnostics().holds;
const holds = () => getRenderGovernorDiagnostics(viewerEngine(runtime.viewer)).holds;
try {
Date.now = () => clockMs;
installRenderGovernor(runtime.viewer);
renderGovernorFor(viewerEngine(runtime.viewer)).install();
runtime.viewer.camera = {
positionWC: new Cesium.Cartesian3(1_000_000, 200_000, 300_000),
heading: 0,
@@ -4096,7 +4107,6 @@ test('a moving view over failed feeds takes no continuous-render hold', () => {
Date.now = realNow;
runtime.restore();
restoreCollections();
_resetRenderGovernorForTest();
}
});
+4 -15
View File
@@ -19,9 +19,8 @@ import {
} from './groundFloor.js';
import { _resetFireAnchorsForTest } from './fireAnchors.js';
import {
_resetRenderGovernorForTest,
getRenderGovernorDiagnostics,
installRenderGovernor,
renderGovernorFor,
} from '../renderGovernor.js';
import * as Cesium from 'cesium';
import { getContextStore, registerEntityContext, selectEntityContext, getSelectedEntityContext, clearSelectedEntityContextForLayer } from './contextStore.js';
@@ -283,7 +282,6 @@ async function runInstallationLoad({
const originalFetch = globalThis.fetch;
const requests = [];
const contextEvents = [];
_resetRenderGovernorForTest();
globalThis.document = { addEventListener() {}, removeEventListener() {} };
globalThis.window = {
dispatchEvent(event) {
@@ -312,17 +310,8 @@ async function runInstallationLoad({
return { ok: true, status: 200, json: async () => payload };
};
const engine = engineOver(VIEWPORT);
// Enough of a viewer for the real render governor, so one-shot render
// requests are observable.
const viewer = {
scene: {
requestRenderMode: false,
maximumRenderTimeChange: 0,
requestRender() {},
},
};
militaryInstallationsLayer.init(null, { engine });
installRenderGovernor(viewer);
renderGovernorFor(engine).install();
militaryInstallationsLayer.enable();
await militaryInstallationsLayer.update();
@@ -338,11 +327,11 @@ async function runInstallationLoad({
engine.fake.input('click', { position: { x: 0, y: 0 } });
engine.fake.unscript('pick');
},
renderRequests: () => getRenderGovernorDiagnostics().recentRequests.map((item) => item.reason),
renderRequests: () => getRenderGovernorDiagnostics(engine).recentRequests.map((item) => item.reason),
restore() {
militaryInstallationsLayer.destroy();
renderGovernorFor(engine).destroy();
engine.destroy();
_resetRenderGovernorForTest();
globalThis.fetch = originalFetch;
if (originalDocument === undefined) delete globalThis.document;
else globalThis.document = originalDocument;
+4
View File
@@ -1,3 +1,4 @@
import { createRenderGovernor } from '../renderGovernor.js';
import { createPickRegistry } from './pickRegistry.js';
import { createSelectionDispatcher } from './selectionDispatcher.js';
@@ -8,17 +9,20 @@ const scenes = new WeakMap();
/** Create the services for one attachment, including reuse of a parked engine. */
export function createSceneServices(engine, options = {}) {
scenes.get(engine)?.destroy();
const render = createRenderGovernor(engine);
const picking = createPickRegistry();
const selection = createSelectionDispatcher(engine, { ...options, picking });
let destroyed = false;
const services = {
picking,
selection,
render,
destroy() {
if (destroyed) return;
destroyed = true;
selection.destroy();
picking.destroy();
render.destroy();
},
};
scenes.set(engine, services);
+1 -3
View File
@@ -45,9 +45,7 @@ import { createCesiumSurface } from './surface.js';
* imagery.js) are test seams, `createStage` and `createSonar` too
* (render.js).
*
* `render.setContinuous` takes a hold on the app's singleton renderGovernor
* (src/renderGovernor.js), which governs the one viewer it was installed on;
* so one Cesium engine per page, on that viewer.
* `render` owns this scene's requests and continuous-render mode.
* @returns {import('../contract.js').GlobeEngine}
*/
export function createCesiumEngine(viewer, options = {}) {
+1 -9
View File
@@ -2,11 +2,6 @@ import test from 'node:test';
import assert from 'node:assert/strict';
import * as Cesium from 'cesium';
import { assertGlobeEngine, engineCapabilities } from '../contract.js';
import {
_resetRenderGovernorForTest,
getRenderGovernorDiagnostics,
installRenderGovernor,
} from '../../renderGovernor.js';
import { createContractSuite } from '../testing/contractSuite.mjs';
import { createCesiumEngine } from './createCesiumEngine.js';
import { createCameraViewer } from './testing/cameraViewer.mjs';
@@ -15,8 +10,6 @@ import {
createFakeViewer,
} from './testing/fakeViewer.mjs';
test.afterEach(() => _resetRenderGovernorForTest());
function snapshot(viewer) {
const { scene } = viewer;
return {
@@ -85,7 +78,6 @@ test('the render option reaches render.request', () => {
test('destroy releases holds, listeners, handlers and engine-owned map state', async () => {
const viewer = createFakeViewer();
installRenderGovernor(viewer);
const handlers = createFakeEventHandlers();
const engine = createCesiumEngine(viewer, {
createEventHandler: handlers.create,
@@ -104,7 +96,7 @@ test('destroy releases holds, listeners, handlers and engine-owned map state', a
assert.equal(viewer.scene.camera.changed.numberOfListeners, 0);
assert.equal(viewer.scene.postRender.numberOfListeners, 0);
assert.equal(handlers.handlers[0].destroyed, true);
assert.deepEqual(getRenderGovernorDiagnostics().holds, []);
assert.equal(viewer.scene.requestRenderMode, true);
assert.deepEqual(viewer.staticCredits, []);
assert.deepEqual(viewer.imageryList, []);
});
+6 -23
View File
@@ -1,15 +1,5 @@
// GlobeRender on the app's render governor, which owns Cesium's
// requestRenderMode and maximumRenderTimeChange; the engine only takes a
// hold under its own owner id. The full-frame effects (visual styles,
// bloom, sharpening) are the scene's post stages (postStages.js), the
// contact sonar the scene's native draw commands (sonar.js).
// Cesium owns its render mode and scene effects. Scene services drive this API.
import {
governorRequestRender,
holdContinuousRender,
isRenderGovernorInstalled,
releaseContinuousRender,
} from '../../renderGovernor.js';
import { CONTACT_SONAR_INITIAL, mergeContactSonar } from '../contract.js';
import { createCesiumPostStages } from './postStages.js';
import { createCesiumSonar } from './sonar.js';
@@ -21,8 +11,7 @@ export const RENDER_OWNER = 'globe-engine';
* @param {{ requestRender?: (reason: string) => void,
* createStage?: (options: object) => object,
* createSonar?: typeof createCesiumSonar }} [options] `requestRender`
* defaults to the render governor when it governs this viewer, else the
* scene itself (before the governor is installed, or on another viewer);
* defaults to the scene itself;
* `createStage` (postStages.js) and `createSonar` are test seams.
* @returns {{ render: import('../contract.js').GlobeRender,
* sonarDiagnostics: () => object, destroy: () => void }}
@@ -30,15 +19,12 @@ export const RENDER_OWNER = 'globe-engine';
export function createCesiumRender(
viewer,
{
requestRender = (reason) => {
if (isRenderGovernorInstalled(viewer)) governorRequestRender(reason);
else viewer.scene.requestRender?.();
},
requestRender = () => viewer.scene.requestRender?.(),
createStage,
createSonar = createCesiumSonar,
} = {},
) {
let held = false;
viewer.scene.maximumRenderTimeChange = Infinity;
const effects = createCesiumPostStages(viewer, {
requestRender,
...(createStage ? { createStage } : {}),
@@ -69,11 +55,8 @@ export function createCesiumRender(
requestRender(reason);
},
setContinuous(enabled) {
held = Boolean(enabled);
if (held) holdContinuousRender(RENDER_OWNER);
else releaseContinuousRender(RENDER_OWNER);
viewer.scene.requestRenderMode = !enabled;
},
// The scene's real mode: another owner's hold keeps it continuous.
isContinuous: () => viewer.scene.requestRenderMode === false,
...effects.render,
setContactSonar(options) {
@@ -93,7 +76,7 @@ export function createCesiumRender(
render,
sonarDiagnostics: () => sonar.diagnostics(),
destroy() {
if (held) render.setContinuous(false);
render.setContinuous(false);
removeFrameWatch?.();
sonar.destroy();
effects.destroy();
+24 -80
View File
@@ -1,17 +1,8 @@
import test from 'node:test';
import assert from 'node:assert/strict';
import {
_resetRenderGovernorForTest,
getRenderGovernorDiagnostics,
holdContinuousRender,
installRenderGovernor,
} from '../../renderGovernor.js';
import { createCesiumRender, RENDER_OWNER } from './render.js';
import { createFakeViewer } from './testing/fakeViewer.mjs';
test.beforeEach(() => _resetRenderGovernorForTest());
test.after(() => _resetRenderGovernorForTest());
test('request goes through the injected requestRender with a reason', () => {
const viewer = createFakeViewer();
const reasons = [];
@@ -19,84 +10,37 @@ test('request goes through the injected requestRender with a reason', () => {
requestRender: (reason) => reasons.push(reason),
});
render.request();
assert.deepEqual(reasons, [RENDER_OWNER]);
render.request('world-overlay');
assert.deepEqual(reasons, [RENDER_OWNER, 'world-overlay']);
assert.equal(viewer.callsOf('scene.requestRender').length, 0);
});
test('request defaults to the render governor', () => {
const viewer = createFakeViewer();
installRenderGovernor(viewer);
const { render } = createCesiumRender(viewer);
render.request();
assert.equal(viewer.callsOf('scene.requestRender').length, 1);
assert.deepEqual(
getRenderGovernorDiagnostics().recentRequests.map(({ reason }) => reason),
[RENDER_OWNER],
);
});
test('creation leaves the scene and the governor alone', () => {
test('creation disables time-driven renders and preserves the current mode', () => {
const viewer = createFakeViewer();
viewer.scene.maximumRenderTimeChange = 0.5;
viewer.scene.requestRenderMode = false;
createCesiumRender(viewer);
assert.equal(viewer.scene.requestRenderMode, true);
assert.equal(viewer.scene.maximumRenderTimeChange, 0.5);
assert.equal(getRenderGovernorDiagnostics().installed, false);
assert.deepEqual(getRenderGovernorDiagnostics().holds, []);
});
test('setContinuous holds and releases the governor under its own owner', () => {
const viewer = createFakeViewer();
installRenderGovernor(viewer);
const { render } = createCesiumRender(viewer);
assert.equal(render.isContinuous(), false);
render.setContinuous(true);
assert.deepEqual(getRenderGovernorDiagnostics().holds, [RENDER_OWNER]);
assert.equal(viewer.scene.requestRenderMode, false);
assert.equal(render.isContinuous(), true);
render.setContinuous(false);
assert.deepEqual(getRenderGovernorDiagnostics().holds, []);
assert.equal(render.isContinuous(), false);
assert.equal(viewer.scene.maximumRenderTimeChange, Infinity);
});
test("isContinuous reports the scene's mode, including other owners' holds", () => {
const viewer = createFakeViewer();
installRenderGovernor(viewer);
const { render } = createCesiumRender(viewer);
holdContinuousRender('flights');
render.setContinuous(false);
assert.equal(render.isContinuous(), true);
});
test('destroy releases the engine hold', () => {
const viewer = createFakeViewer();
installRenderGovernor(viewer);
const { render, destroy } = createCesiumRender(viewer);
render.setContinuous(true);
destroy();
assert.deepEqual(getRenderGovernorDiagnostics().holds, []);
});
test('request passes a caller reason through to the governor', () => {
const viewer = createFakeViewer();
installRenderGovernor(viewer);
const { render } = createCesiumRender(viewer);
render.request('world-overlay');
assert.deepEqual(
getRenderGovernorDiagnostics().recentRequests.map(({ reason }) => reason),
['world-overlay'],
);
});
test('without a governor on its viewer, request renders the scene directly', () => {
const viewer = createFakeViewer();
const { render } = createCesiumRender(viewer);
render.request();
assert.equal(viewer.callsOf('scene.requestRender').length, 1);
const governed = createFakeViewer();
installRenderGovernor(governed);
render.request();
assert.equal(viewer.callsOf('scene.requestRender').length, 2);
assert.equal(governed.callsOf('scene.requestRender').length, 0);
assert.deepEqual(getRenderGovernorDiagnostics().recentRequests, []);
test('render mode, requests and destruction affect only the owning scene', () => {
const first = createFakeViewer();
const second = createFakeViewer();
const a = createCesiumRender(first);
const b = createCesiumRender(second);
a.render.setContinuous(true);
b.render.setContinuous(true);
assert.equal(a.render.isContinuous(), true);
assert.equal(b.render.isContinuous(), true);
a.render.setContinuous(false);
assert.equal(a.render.isContinuous(), false);
assert.equal(b.render.isContinuous(), true);
a.render.request('world-overlay');
assert.equal(first.callsOf('scene.requestRender').length, 1);
assert.equal(second.callsOf('scene.requestRender').length, 0);
a.destroy();
assert.equal(b.render.isContinuous(), true);
b.destroy();
assert.equal(b.render.isContinuous(), false);
});
+1 -4
View File
@@ -1,6 +1,5 @@
import * as Cesium from 'cesium';
import { loadBundledJson } from '../../data/bundledJson.js';
import { governorRequestRender } from '../../renderGovernor.js';
import { buildStarVertices, STAR_VERTEX_FLOATS } from '../../sky/stars.js';
/**
@@ -143,9 +142,7 @@ export class StarrySky {
* @param {() => void} [options.requestRender] renders one frame when an
* asset or the precise rotation arrives; the idle governor by default.
*/
constructor({
requestRender = () => governorRequestRender('sky-assets'),
} = {}) {
constructor({ requestRender = () => {} } = {}) {
/** Matches `SkyBox#show`. */
this.show = true;
this.milkyWayIntensity = MILKY_WAY_INTENSITY;
+5 -3
View File
@@ -387,7 +387,7 @@ export const loadCesiumPhotorealTileset = (keys) =>
loadPhotorealisticTileset(Cesium, keys);
/** Cesium.Viewer options of the standard globe viewer. */
export function applicationViewerOptions(creditContainer) {
export function applicationViewerOptions(creditContainer, requestRender) {
return {
timeline: false,
animation: false,
@@ -403,7 +403,7 @@ export function applicationViewerOptions(creditContainer) {
baseLayer: false,
// Replaces the blocky Tycho-2 default. An option, not `skyBox: false`,
// which would also stop CesiumWidget from creating the Sun and Moon.
skyBox: new StarrySky(),
skyBox: new StarrySky({ requestRender }),
creditContainer,
msaaSamples: 4,
contextOptions: { webgl: { preserveDrawingBuffer: true } },
@@ -416,7 +416,9 @@ export function createApplicationViewer({ container, creditContainer }) {
throw new TypeError('Viewer and credit containers are required');
const viewer = new Cesium.Viewer(
container,
applicationViewerOptions(creditContainer),
applicationViewerOptions(creditContainer, () =>
viewer.scene.requestRender(),
),
);
try {
viewer.targetFrameRate = 60;
+1 -2
View File
@@ -1,5 +1,4 @@
import * as Cesium from 'cesium';
import { governorRequestRender } from '../../renderGovernor.js';
/** Whole-Earth input limit; explicit camera destinations can go farther. */
export const MAXIMUM_ZOOM_DISTANCE_M = 40_000_000;
@@ -84,7 +83,7 @@ function readInput(aggregator, bindings) {
export function installZoomOutGuard(
viewer,
{
requestRender = governorRequestRender,
requestRender = () => viewer.scene.requestRender(),
subscribeCameraHandoff,
createAggregator = (canvas) => new Cesium.CameraEventAggregator(canvas),
} = {},
+4 -1
View File
@@ -83,7 +83,10 @@ export function createAircraftSceneBinding({
const renderer = flightState._renderer;
if (!renderer) return;
removeSceneListeners();
services.render.releaseContinuousRender(id);
services.render.releaseContinuousRender(
id,
flightState._renderer?.engine,
);
flightState._trackedCameraFrameStop?.();
flightState._trackedCameraFrameStop = null;
if (flightState._trackedEntity) {
+2 -2
View File
@@ -75,7 +75,7 @@ export function createAlprCamerasLayer({
if (state.status === status && state.error === error) return;
state.status = status;
state.error = error;
governorRequestRender('alpr-status');
governorRequestRender('alpr-status', sceneEngine);
}
function scheduleUnavailableRetry() {
@@ -260,7 +260,7 @@ export function createAlprCamerasLayer({
state.pendingQueryBox = null;
state.loading = false;
state.retrying = false;
governorRequestRender('alpr-status');
governorRequestRender('alpr-status', sceneEngine);
}
}
}
+9 -9
View File
@@ -296,7 +296,7 @@ export function createAlprDisplay(engine, { state, services, source }) {
}
resolveFloors();
flushSprites();
governorRequestRender('alpr-surface');
governorRequestRender('alpr-surface', engine);
}
/**
@@ -352,7 +352,7 @@ export function createAlprDisplay(engine, { state, services, source }) {
}
if (!moved) return;
flushSprites();
governorRequestRender('alpr-floor');
governorRequestRender('alpr-floor', engine);
});
floorRequest = request;
}
@@ -399,7 +399,7 @@ export function createAlprDisplay(engine, { state, services, source }) {
creditTimer = null;
removeCredit?.();
removeCredit = null;
governorRequestRender('alpr-credit');
governorRequestRender('alpr-credit', engine);
}
/**
@@ -419,7 +419,7 @@ export function createAlprDisplay(engine, { state, services, source }) {
creditPresented = true;
removeCredit = engine.credits.addStatic(creditMarkup, { onScreen: true });
creditTimer = setTimeout(hideOnMapCredit, CREDIT_DISPLAY_MS);
governorRequestRender('alpr-credit');
governorRequestRender('alpr-credit', engine);
}
function renderRecords() {
@@ -442,7 +442,7 @@ export function createAlprDisplay(engine, { state, services, source }) {
dropSelectedSprite();
clearSelectedEntityContextForLayer(LAYER_ID);
}
governorRequestRender('alpr-render');
governorRequestRender('alpr-render', engine);
const ids = new Set(shown.map((record) => record.id));
let changed = false;
for (const id of [...markers.keys()])
@@ -553,7 +553,7 @@ export function createAlprDisplay(engine, { state, services, source }) {
updateSelectedAnchor();
overlay.sync(visibleRecords);
flushSprites();
governorRequestRender('alpr-selection');
governorRequestRender('alpr-selection', engine);
return true;
}
@@ -566,7 +566,7 @@ export function createAlprDisplay(engine, { state, services, source }) {
overlay.sync(visibleRecords);
flushSprites();
clearSelectedEntityContextForLayer(LAYER_ID);
governorRequestRender('alpr-selection');
governorRequestRender('alpr-selection', engine);
}
/**
@@ -600,7 +600,7 @@ export function createAlprDisplay(engine, { state, services, source }) {
geo.cartesian3.distance(next, marker.displayPosition) > 0.1
) {
marker.displayPosition = next;
governorRequestRender('alpr-anchor');
governorRequestRender('alpr-anchor', engine);
}
}
@@ -637,7 +637,7 @@ export function createAlprDisplay(engine, { state, services, source }) {
range: 800,
},
);
governorRequestRender('alpr-focus');
governorRequestRender('alpr-focus', engine);
return true;
}
+5 -4
View File
@@ -111,8 +111,9 @@ export function createLifecycle({
function syncAwarenessRenderHold() {
if (layerState.enabled && parts.model.awarenessNeedsContinuousRender())
holdContinuousRender('military-awareness');
else releaseContinuousRender('military-awareness');
holdContinuousRender('military-awareness', layerState.display?.engine);
else
releaseContinuousRender('military-awareness', layerState.display?.engine);
}
function attachRuntimeListeners() {
@@ -218,7 +219,7 @@ export function createLifecycle({
layerState.cameraMoving = false;
// No frames will arrive to run the per-frame release once the listener is
// gone, so drop the hold here.
releaseContinuousRender('military-awareness');
releaseContinuousRender('military-awareness', layerState.display?.engine);
parts.rendering.cancelDirectionOverlayUpdate();
layerState.runtimeListenersAttached = false;
}
@@ -320,7 +321,7 @@ export function createLifecycle({
disable() {
layerState.enabled = false;
releaseContinuousRender('military-awareness');
releaseContinuousRender('military-awareness', layerState.display?.engine);
detachRuntimeListeners();
parts.panel.stopAwarenessPageRotation();
layerState.cohortPages.clear();
+1
View File
@@ -46,6 +46,7 @@ export function createAwarenessDisplay(engine) {
});
const style = AWARENESS_RANGE_RING_STYLE;
return {
engine,
container: engine.host.container,
canvas: engine.host.canvas,
// Camera-shaped: positionWC, heading, rightWC, upWC, refreshed per frame.
+4 -4
View File
@@ -457,7 +457,7 @@ export function createRendering({
visual.rim.destroy();
visual.disc?.destroy();
// Idle mode renders only on request; a removed ring must not linger.
governorRequestRender('awareness-visual');
governorRequestRender('awareness-visual', layerState.display?.engine);
}
layerState.visual = null;
}
@@ -529,7 +529,7 @@ export function createRendering({
if (performance.now() < wake.untilMs && !belowRim())
timer = globalThis.setTimeout(() => {
timer = null;
governorRequestRender('awareness-pulse');
governorRequestRender('awareness-pulse', layerState.display?.engine);
}, CYBER_SONAR_RENDER_INTERVAL_MS);
});
setAwarenessEchoField({
@@ -612,7 +612,7 @@ export function createRendering({
stopPulses,
removeMotionListener: watchReducedMotion(),
};
governorRequestRender('awareness-visual');
governorRequestRender('awareness-visual', layerState.display?.engine);
}
function renderVisual(subject) {
@@ -642,7 +642,7 @@ export function createRendering({
if (performance.now() < visual.wake?.untilMs)
visual.disc?.move(groundCenter);
// Content actually changed — buy exactly one frame instead of holding.
governorRequestRender('awareness-visual');
governorRequestRender('awareness-visual', layerState.display?.engine);
}
return;
}
+2 -2
View File
@@ -37,7 +37,7 @@ export function createStationPoints({ state, services, parts }, points) {
function ensureCityPoints(cityId, stationMap) {
// Deferred debounce/fetch completions change points after the camera
// settled: each commit needs one frame in idle mode.
services.render.governorRequestRender('bikeshare-points');
services.render.governorRequestRender('bikeshare-points', state._engine);
const runtime = ensureCityRuntime(cityId);
for (const station of stationMap.values()) {
@@ -120,7 +120,7 @@ export function createStationPoints({ state, services, parts }, points) {
* @param {Map<string, Object>} statusMap - Parsed station status map.
*/
function applyStatusToPoints(cityId, statusMap) {
services.render.governorRequestRender('bikeshare-status');
services.render.governorRequestRender('bikeshare-status', state._engine);
const runtime = state._cityRuntime.get(cityId);
if (!runtime) return;
+1 -1
View File
@@ -66,7 +66,7 @@ export function createViewport({ state: layerState, services, parts, camera }) {
*/
function deactivateCity(cityId) {
governorRequestRender('bikeshare-deactivate');
governorRequestRender('bikeshare-deactivate', layerState._engine);
parts.ingestion.abortInFlight(layerState._inFlightInfo, cityId);
parts.ingestion.abortInFlight(layerState._inFlightStatus, cityId);
parts.rendering?.removeCityPoints(cityId);
+2 -2
View File
@@ -153,9 +153,9 @@ export function createControls({ state: layerState, services, parts, source }) {
layerState._gizmo?.setEnabled(true);
// ADJUST mode: gizmo drags mutate entity geometry from pointer events,
// which don't trigger renders in requestRenderMode. (perf wave 2)
holdContinuousRender('cctv-adjust');
holdContinuousRender('cctv-adjust', parts.renderer?.engine);
} else {
releaseContinuousRender('cctv-adjust');
releaseContinuousRender('cctv-adjust', parts.renderer?.engine);
layerState._gizmo?.setEnabled(false);
}
}
+7 -4
View File
@@ -453,7 +453,10 @@ export function createLifecycle({
if (layerState._calibrationMode) {
parts.calibration.ensureGizmo();
layerState._gizmo?.setEnabled(true);
services.render.holdContinuousRender('cctv-adjust');
services.render.holdContinuousRender(
'cctv-adjust',
parts.renderer?.engine,
);
}
settleView();
},
@@ -481,7 +484,7 @@ export function createLifecycle({
null;
layerState._gizmo?.destroy();
layerState._gizmo = null;
releaseContinuousRender('cctv-adjust');
releaseContinuousRender('cctv-adjust', parts.renderer?.engine);
layerState._removeClick?.();
layerState._removeClick = null;
parts.projection.stopProjectionLoop();
@@ -580,7 +583,7 @@ export function createLifecycle({
unregisterPickOwner('cctv', parts.renderer?.engine);
// ADJUST mode does not survive a layer toggle — predictable re-entry.
layerState._calibrationMode = false;
releaseContinuousRender('cctv-adjust');
releaseContinuousRender('cctv-adjust', parts.renderer?.engine);
layerState._gizmo?.setEnabled(false);
layerState._removeFocusAppearListener?.();
layerState._removeFocusAppearListener = null;
@@ -631,7 +634,7 @@ export function createLifecycle({
layerState._gizmo = null;
}
layerState._calibrationMode = false;
releaseContinuousRender('cctv-adjust');
releaseContinuousRender('cctv-adjust', parts.renderer?.engine);
layerState._removeClick?.();
layerState._removeClick = null;
parts.projection.stopProjectionLoop();
+8 -5
View File
@@ -389,7 +389,7 @@ export function createProjection({
layerState._projectionRaf = 0;
layerState._projectionTimer = 0;
if (!parts.renderer?.ready || !parts.model.projectionLoopIsNeeded()) {
releaseContinuousRender('cctv-projection');
releaseContinuousRender('cctv-projection', parts.renderer?.engine);
return;
}
@@ -401,7 +401,10 @@ export function createProjection({
if (active.projection && !active.projection.video) {
parts.frames.drawProjectionFrame(active);
if (parts.frames.refreshProjectionTextures(active))
governorRequestRender('cctv-projection-image');
governorRequestRender(
'cctv-projection-image',
parts.renderer?.engine,
);
}
}
@@ -412,12 +415,12 @@ export function createProjection({
layerState._activeFocusStyleCount,
);
if (continuous) {
holdContinuousRender('cctv-projection');
holdContinuousRender('cctv-projection', parts.renderer?.engine);
layerState._projectionRaf = requestAnimationFrame(tick);
} else {
// Stills retain the existing fetch/texture cadence without rendering
// unchanged pixels at display refresh rate.
releaseContinuousRender('cctv-projection');
releaseContinuousRender('cctv-projection', parts.renderer?.engine);
layerState._projectionTimer = setTimeout(
tick,
PROJECTION_TEXTURE_SWAP_MS,
@@ -438,7 +441,7 @@ export function createProjection({
cancelAnimationFrame(layerState._projectionRaf);
layerState._projectionRaf = 0;
}
releaseContinuousRender('cctv-projection');
releaseContinuousRender('cctv-projection', parts.renderer?.engine);
}
/**
+10 -9
View File
@@ -999,8 +999,9 @@ export function createDirectionsLayer({
const shouldHold = _enabled && _routeDrawn;
if (shouldHold === _renderHeld) return;
_renderHeld = shouldHold;
if (shouldHold) services.render.holdContinuousRender?.('directions');
else services.render.releaseContinuousRender?.('directions');
if (shouldHold)
services.render.holdContinuousRender?.('directions', _engine);
else services.render.releaseContinuousRender?.('directions', _engine);
}
function clearRouteGraphics() {
@@ -1053,7 +1054,7 @@ export function createDirectionsLayer({
pushSteps();
// A card already open on a step that has just been anchored moves with it.
if (_selectedStep !== null) refreshStepCard(_selectedStep);
services.render.governorRequestRender('directions-anchor');
services.render.governorRequestRender('directions-anchor', _engine);
if (!unresolved) {
_anchorAttempts = 0;
_anchorStartedAt = 0;
@@ -1089,7 +1090,7 @@ export function createDirectionsLayer({
_anchorAttempts = 0;
_anchorStartedAt = Date.now();
anchorStepPoints();
services.render.governorRequestRender('directions-route');
services.render.governorRequestRender('directions-route', _engine);
}
async function requestRoute() {
@@ -1149,7 +1150,7 @@ export function createDirectionsLayer({
clearTimeout(timer);
if (_routeAbort === controller) _routeAbort = null;
if (seq === _routeSeq) notifyRow();
services.render.governorRequestRender('directions-route');
services.render.governorRequestRender('directions-route', _engine);
}
}
@@ -1214,7 +1215,7 @@ export function createDirectionsLayer({
placeMarker(which, point);
if (_a && _b) void requestRoute();
else notifyRow();
services.render.governorRequestRender('directions-place');
services.render.governorRequestRender('directions-place', _engine);
return true;
}
@@ -1277,7 +1278,7 @@ export function createDirectionsLayer({
placeMarker('a', null);
placeMarker('b', null);
clearRouteGraphics();
services.render.governorRequestRender('directions-clear');
services.render.governorRequestRender('directions-clear', _engine);
}
/**
@@ -1434,7 +1435,7 @@ export function createDirectionsLayer({
}
_selectedStep = index;
_display?.selectStep(index);
services.render.governorRequestRender('directions-select');
services.render.governorRequestRender('directions-select', _engine);
return true;
}
@@ -1601,7 +1602,7 @@ export function createDirectionsLayer({
}
if (patch.fly) flyCurrentRoute();
notifyRow();
services.render.governorRequestRender('directions-params');
services.render.governorRequestRender('directions-params', _engine);
return true;
},
@@ -193,7 +193,7 @@ for (const makeHarness of GROUND_HARNESSES) {
true,
);
assert.ok(
getRenderGovernorDiagnostics().holds.includes('directions'),
getRenderGovernorDiagnostics(f.engine).holds.includes('directions'),
'a drawn route holds continuous render',
);
if (f.cesium) {
@@ -215,7 +215,7 @@ for (const makeHarness of GROUND_HARNESSES) {
f.layer.setParams({ clear: true });
assert.equal(f.set(IDS.route), undefined);
assert.ok(
!getRenderGovernorDiagnostics().holds.includes('directions'),
!getRenderGovernorDiagnostics(f.engine).holds.includes('directions'),
'a cleared route releases it',
);
});
+4 -1
View File
@@ -150,7 +150,10 @@ export function createViewport({
settleTimer = setTimeout(() => {
settleTimer = null;
if (layerState._enabled && !layerState._destroyed)
governorRequestRender('firms-lod-settle');
governorRequestRender(
'firms-lod-settle',
layerState._renderer?.engine,
);
}, LOD_CHECK_MS + 40);
});
}
+4 -3
View File
@@ -115,7 +115,8 @@ export function createLifecycle({
const renderer = flightState._renderer;
flightState.enabled = true;
renderer?.fleet.setVisible(true);
if (renderer) holdContinuousRender('flights'); // per-frame animator
if (renderer)
holdContinuousRender('flights', flightState._renderer?.engine); // per-frame animator
renderer?.models.setVisible(true);
parts.tracking._setCockpitContactMode(
globalThis.document?.body.classList.contains('cockpit-mode') === true,
@@ -181,7 +182,7 @@ export function createLifecycle({
parts.controller._abortActiveUpdates();
parts.tracking._cancelPendingTrackingRestore();
flightState._renderer?.fleet.setVisible(false);
releaseContinuousRender('flights');
releaseContinuousRender('flights', flightState._renderer?.engine);
parts.rendering._releaseModels();
flightState._renderer?.models.setVisible(false);
parts.tracking._clearTracking();
@@ -201,7 +202,7 @@ export function createLifecycle({
flightState._enrichActive = 0;
flightState._enrichLastDispatchMs = 0;
parts.controller._abortActiveUpdates();
releaseContinuousRender('flights'); // direct-destroy path (perf wave 2 fix)
releaseContinuousRender('flights', flightState._renderer?.engine); // direct-destroy path (perf wave 2 fix)
parts.tracking._clearTracking();
parts.tracking._destroyTrail();
parts.tracking._cancelPendingTrackingRestore();
+1 -1
View File
@@ -69,7 +69,7 @@ export function createIngestion({
if (layerState.status === status && layerState.error === error) return;
layerState.status = status;
layerState.error = error;
governorRequestRender('installations-status');
governorRequestRender('installations-status', layerState.engine);
}
async function loadInstallations() {
+2 -2
View File
@@ -327,7 +327,7 @@ export function createInstallationsDisplay(
})),
);
fill.commit();
governorRequestRender('installation-selection-fill');
governorRequestRender('installation-selection-fill', engine);
}
function clearRendered() {
@@ -433,7 +433,7 @@ export function createInstallationsDisplay(
}
// Post-moveEnd debounced fetches commit after the camera settles; the
// rebuilt sets need one frame in idle mode.
governorRequestRender('installations-render');
governorRequestRender('installations-render', engine);
syncOsmCredit(state.records);
syncPlacesCredit(state.records);
const records = renderableRecords();
@@ -101,7 +101,7 @@ export function createNamedMarkers(
function cameraChanged() {
refresh();
services.render.governorRequestRender('installation-names');
services.render.governorRequestRender('installation-names', engine);
}
// Small pose changes raise no camera event: test the discs before the
@@ -232,7 +232,7 @@ export function createNamedMarkers(
points.commit();
if (state.enabled) publishLabels();
refresh();
services.render.governorRequestRender('installation-names');
services.render.governorRequestRender('installation-names', engine);
}
return {
+6 -5
View File
@@ -70,7 +70,7 @@ export function createLifecycle({
);
parts.launchPad.initLaunchPadZonePrimitive();
if (layerState._enabled) {
holdContinuousRender('rocket-launches');
holdContinuousRender('rocket-launches', layerState._engine);
getRenderer().setVisible(true);
layerState._orbitMatches = 0;
for (const launch of layerState._launches)
@@ -144,16 +144,17 @@ export function createLifecycle({
layerState._satelliteTelemetry.clear();
layerState._replayTracks.clear();
parts.rendering.removeMissionElements();
if (layerState._enabled)
releaseContinuousRender('rocket-launches', layerState._engine);
layerState._renderer.destroy();
layerState._renderer = layerState._engine = null;
if (layerState._enabled) releaseContinuousRender('rocket-launches');
},
async enable() {
layerState._sourceController.abort();
layerState._sourceController = new AbortController();
layerState._enabled = true;
holdContinuousRender('rocket-launches'); // per-frame animator (perf wave 2)
holdContinuousRender('rocket-launches', layerState._engine); // per-frame animator (perf wave 2)
layerState._lifecycleToken++;
layerState._postTleRetryCount = 0;
try {
@@ -218,7 +219,7 @@ export function createLifecycle({
entryView,
layerState._entryLandedView,
);
releaseContinuousRender('rocket-launches');
releaseContinuousRender('rocket-launches', layerState._engine);
layerState._lifecycleToken++;
layerState._updateDirty = false;
parts.ingestion.clearPostTleRetry();
@@ -244,7 +245,7 @@ export function createLifecycle({
layerState._resumeRenderer?.();
layerState._waitForRenderer = layerState._resumeRenderer = null;
layerState._sourceController.abort();
releaseContinuousRender('rocket-launches'); // direct-destroy path (perf wave 2 fix)
releaseContinuousRender('rocket-launches', layerState._engine); // direct-destroy path (perf wave 2 fix)
layerState._enabled = false;
layerState._lifecycleToken++;
layerState._updateDirty = false;
+9 -6
View File
@@ -136,7 +136,7 @@ export function createLocalAdsbLayer({
onChange: () => {
for (const marker of markers.values()) refreshClass(marker);
refreshSelectedCard();
governorRequestRender('local-adsb-enrichment');
governorRequestRender('local-adsb-enrichment', renderer?.engine);
},
});
@@ -441,8 +441,8 @@ export function createLocalAdsbLayer({
const want = Boolean(renderer) && enabled && markers.size > 0;
if (want === renderHeld) return;
renderHeld = want;
if (want) holdContinuousRender(RENDER_HOLD_ID);
else releaseContinuousRender(RENDER_HOLD_ID);
if (want) holdContinuousRender(RENDER_HOLD_ID, renderer?.engine);
else releaseContinuousRender(RENDER_HOLD_ID, renderer?.engine);
}
function requestEnrichment() {
@@ -488,7 +488,8 @@ export function createLocalAdsbLayer({
}
requestEnrichment();
updateRenderHold();
if (live.size || removed) governorRequestRender('local-adsb-update');
if (live.size || removed)
governorRequestRender('local-adsb-update', renderer?.engine);
// Detection re-solves labels only when the set of contacts changes, not
// on every position report.
if (added || removed) markSourcesChanged('local-adsb-update');
@@ -521,7 +522,7 @@ export function createLocalAdsbLayer({
marker.entity.gevLabelModel = cardModel(marker, now());
selectEntityContext(marker.entity);
if (!reselect) startTrail(marker);
governorRequestRender('local-adsb-selection');
governorRequestRender('local-adsb-selection', renderer?.engine);
return true;
}
@@ -560,7 +561,7 @@ export function createLocalAdsbLayer({
onEmptyClick() {
if (!enabled || !selectedId) return;
clearSelection();
governorRequestRender('local-adsb-selection');
governorRequestRender('local-adsb-selection', renderer?.engine);
},
});
}
@@ -667,6 +668,8 @@ export function createLocalAdsbLayer({
releaseTrail();
models?.destroy();
models = null;
releaseContinuousRender(RENDER_HOLD_ID, renderer.engine);
renderHeld = false;
renderer.detach();
renderer.destroy();
renderer = null;
+4 -3
View File
@@ -98,7 +98,8 @@ export function createLifecycle({
const renderer = flightState._renderer;
flightState.enabled = true;
renderer?.fleet.setVisible(true);
if (renderer) holdContinuousRender('military'); // per-frame animator
if (renderer)
holdContinuousRender('military', flightState._renderer?.engine); // per-frame animator
renderer?.models.setVisible(true);
parts.tracking._setCockpitContactMode(
globalThis.document?.body.classList.contains('cockpit-mode') === true,
@@ -161,7 +162,7 @@ export function createLifecycle({
parts.controller._abortActiveUpdates();
parts.tracking._cancelPendingTrackingRestore();
flightState._renderer?.fleet.setVisible(false);
releaseContinuousRender('military');
releaseContinuousRender('military', flightState._renderer?.engine);
parts.rendering._releaseModels();
flightState._renderer?.models.setVisible(false);
parts.tracking._clearTracking();
@@ -181,7 +182,7 @@ export function createLifecycle({
setMilitaryLayerActive(false);
flightState.lifetime.abort();
parts.controller._abortActiveUpdates();
releaseContinuousRender('military'); // direct-destroy path (perf wave 2 fix)
releaseContinuousRender('military', flightState._renderer?.engine); // direct-destroy path (perf wave 2 fix)
parts.tracking._clearTracking();
parts.tracking._destroyTrail();
parts.tracking._cancelPendingTrackingRestore();
+2 -1
View File
@@ -227,7 +227,8 @@ export function createRendering({
scheduleRadioOverlayPublish();
// The horizon timer can commit AFTER the camera settles and the governor
// parks the scene — a changed show flag needs one frame. (perf wave 2 fix)
if (visibilityChanged) governorRequestRender('radio-horizon');
if (visibilityChanged)
governorRequestRender('radio-horizon', parts.renderer?.engine);
}
function reconcileStations(stations) {
+5 -4
View File
@@ -126,13 +126,14 @@ export function createLifecycle({
}
layerState._detectionObjects.clear();
renderer.removePoints(layerState._pointCollection);
if (layerState._enabled)
releaseContinuousRender('satellites', layerState._engine);
renderer.destroy();
layerState._engine =
layerState._renderer =
layerState._pointCollection =
null;
layerState._overlayHost.clearSource(ISS_OVERLAY_SOURCE_ID);
if (layerState._enabled) releaseContinuousRender('satellites');
},
attachRenderer(
@@ -187,7 +188,7 @@ export function createLifecycle({
enable() {
layerState._enabled = true;
holdContinuousRender('satellites'); // per-frame animator (perf wave 2)
holdContinuousRender('satellites', layerState._engine); // per-frame animator (perf wave 2)
if (layerState._pointCollection)
layerState._pointCollection.show =
parts.controls.satelliteVisualsVisible(
@@ -225,7 +226,7 @@ export function createLifecycle({
parts.catalog._abortActiveUpdates();
parts.tracking._cancelPendingTrackingRestore();
layerState._enabled = false;
releaseContinuousRender('satellites');
releaseContinuousRender('satellites', layerState._engine);
if (layerState._pointCollection) layerState._pointCollection.show = false;
for (const path of layerState._orbitPaths.values())
path.primitive.show = false;
@@ -252,7 +253,7 @@ export function createLifecycle({
layerState._resumeRenderer?.();
layerState._waitForRenderer = layerState._resumeRenderer = null;
parts.catalog._abortActiveUpdates();
releaseContinuousRender('satellites'); // direct-destroy path (perf wave 2 fix)
releaseContinuousRender('satellites', layerState._engine); // direct-destroy path (perf wave 2 fix)
layerState._enabled = false;
parts.tracking._clearTracking();
parts.tracking._cancelPendingTrackingRestore();
+10 -1
View File
@@ -95,7 +95,16 @@ export function createStreetLevelLayer({
function providerContext(entry) {
return Object.freeze({
services: state.services,
services: {
...state.services,
render: {
governorRequestRender: (reason) =>
state.services.render?.governorRequestRender?.(
reason,
state.map?.engine,
),
},
},
/** Resolved against now, so "since N days" keeps up with the clock. */
getFilter: () => resolveFilter(state.filter),
/** The layer and this provider's switch are both on. */
+1 -1
View File
@@ -126,7 +126,7 @@ export function createLineCoverage({
: Infinity;
function requestRender() {
render?.governorRequestRender?.(renderKey);
render?.governorRequestRender?.(renderKey, state.map?.engine);
}
function tileKey({ x, y, z }) {
+1 -1
View File
@@ -27,7 +27,7 @@ export function createMarker({ state }) {
const { render } = state.services;
function requestRender() {
render?.governorRequestRender?.('street-level-marker');
render?.governorRequestRender?.('street-level-marker', state.map?.engine);
}
function ensure(view) {
+1 -1
View File
@@ -67,7 +67,7 @@ export function createSequenceCones({
const { render } = state.services;
function requestRender() {
render?.governorRequestRender?.(renderKey);
render?.governorRequestRender?.(renderKey, state.map?.engine);
}
function notify() {
+3 -2
View File
@@ -29,9 +29,10 @@ export function createAnimation({
layerState._renderHeld = needed;
if (needed) {
layerState._lastAnimTime = 0;
services.render.holdContinuousRender('traffic');
services.render.holdContinuousRender('traffic', layerState._engine);
layerState._engine?.render.request('traffic');
} else services.render.releaseContinuousRender('traffic');
} else
services.render.releaseContinuousRender('traffic', layerState._engine);
}
/** Writes the dots to the engine's display (once a frame, and on a clear). */
+1 -1
View File
@@ -199,7 +199,7 @@ export function createLifecycle({
// The dots keep their look (`dot.point`) for the next engine.
layerState._display?.destroy();
if (layerState._renderHeld)
services.render.releaseContinuousRender('traffic');
services.render.releaseContinuousRender('traffic', layerState._engine);
layerState._renderHeld = false;
layerState._engine = layerState._display = null;
layerState._waitForScene ??= new Promise((resolve) => {
+1 -1
View File
@@ -196,7 +196,7 @@ export function createHeight({ state, services, parts }) {
// adopted by the next bounded reread.
for (const entry of entries) parts.trails.prepareEntry(entry);
parts.rendering.requestVisibility();
governorRequestRender('transit-floor');
governorRequestRender('transit-floor', parts.renderer?.engine);
if (state._floorAttempts++ < FLOOR_REREAD_ATTEMPTS)
state._floorTimer = setTimeout(rereadFloors, FLOOR_REREAD_MS);
}
+3 -3
View File
@@ -147,7 +147,7 @@ export function createIngestion({ state, services, parts, source }) {
// Nothing else would notice: the per-frame pass returns early on an empty
// fleet, so the hold would sit there held by a fleet that no longer exists.
parts.rendering.syncRenderHold();
governorRequestRender('transit-evict');
governorRequestRender('transit-evict', parts.renderer?.engine);
state._dataManager?.refreshLayerStats?.();
}
return removed;
@@ -190,7 +190,7 @@ export function createIngestion({ state, services, parts, source }) {
removeFeedVehicles(feed.id);
status.count = 0;
parts.rendering.syncRenderHold();
governorRequestRender('transit-stale-refused');
governorRequestRender('transit-stale-refused', parts.renderer?.engine);
state._dataManager?.refreshLayerStats?.();
return;
}
@@ -414,7 +414,7 @@ export function createIngestion({ state, services, parts, source }) {
state._detectRevision++;
parts.rendering.requestVisibility();
parts.rendering.syncRenderHold();
governorRequestRender('transit-poll');
governorRequestRender('transit-poll', parts.renderer?.engine);
state._dataManager?.refreshLayerStats?.();
}
+3 -2
View File
@@ -246,7 +246,8 @@ export function createLifecycle({
state._detectBuiltAt = -Infinity;
state._visible.clear();
state._moving.clear();
if (state._renderHeld) releaseContinuousRender('transit');
if (state._renderHeld)
releaseContinuousRender('transit', parts.renderer?.engine);
state._renderHeld = false;
parts.renderer.destroy();
parts.renderer =
@@ -309,7 +310,7 @@ export function createLifecycle({
state._rotationPose = null;
state._rotationDirty = false;
if (state._renderHeld) {
releaseContinuousRender('transit');
releaseContinuousRender('transit', parts.renderer?.engine);
state._renderHeld = false;
}
},
+13 -15
View File
@@ -37,13 +37,11 @@ import {
releaseCameraSensitivity,
} from '../../data/cameraSensitivity.js';
import {
_resetRenderGovernorForTest,
getRenderGovernorDiagnostics,
governorRequestRender,
holdContinuousRender,
installRenderGovernor,
renderGovernorFor,
releaseContinuousRender,
uninstallRenderGovernor,
} from '../../renderGovernor.js';
const BOSTON = { lat: 42.3601, lon: -71.0589 };
@@ -283,7 +281,7 @@ function harness(
},
});
installRenderGovernor(viewer);
renderGovernorFor(engine).install();
layer.init(viewer, { engine });
t.after(() => {
@@ -293,8 +291,8 @@ function harness(
/* a test may already have destroyed it */
}
engine.destroy();
uninstallRenderGovernor(viewer);
_resetRenderGovernorForTest();
renderGovernorFor(engine).destroy();
globalThis.fetch = originalFetch;
if (originalDocument === undefined) delete globalThis.document;
else globalThis.document = originalDocument;
@@ -361,7 +359,7 @@ function harness(
frame();
}
},
holds: () => getRenderGovernorDiagnostics().holds,
holds: () => getRenderGovernorDiagnostics(engine).holds,
ground,
meshCalls,
/**
@@ -839,7 +837,7 @@ test('the render hold follows real motion, and is released when everything settl
);
app.frame();
assert.equal(app.holds().includes('transit'), false);
assert.equal(app.viewer.scene.requestRenderMode, true);
assert.equal(!app.engine.render.isContinuous(), true);
// A second fix IS motion to come: the vehicle is armed and the loop held.
app.advance(TRANSIT_POLL_MS);
@@ -861,7 +859,7 @@ test('the render hold follows real motion, and is released when everything settl
app.holds().includes('transit'),
'a moving vehicle keeps the loop awake',
);
assert.equal(app.viewer.scene.requestRenderMode, false);
assert.equal(!app.engine.render.isContinuous(), false);
// Past the second fix it holds and the loop is released again.
app.run(TRANSIT_POLL_MS);
@@ -870,7 +868,7 @@ test('the render hold follows real motion, and is released when everything settl
false,
'a settled fleet does not hold the GPU',
);
assert.equal(app.viewer.scene.requestRenderMode, true);
assert.equal(!app.engine.render.isContinuous(), true);
});
test('disable hands back the camera sensitivity it borrowed', async (t) => {
const app = harness(t);
@@ -1112,7 +1110,7 @@ test('a parked fleet that keeps reporting the same position stops asking for fra
);
assert.equal(app.state()._moving.size, 0);
}
assert.equal(app.viewer.scene.requestRenderMode, true);
assert.equal(!app.engine.render.isContinuous(), true);
assert.equal(app.vehicles().length, 2, 'and the fleet is still on screen');
// One vehicle finally moves: that IS work, and the hold comes back.
@@ -1198,7 +1196,7 @@ test('the last vehicle leaving mid-segment takes the render hold with it', async
false,
'no hold may outlive the fleet that justified it',
);
assert.equal(app.viewer.scene.requestRenderMode, true);
assert.equal(!app.engine.render.isContinuous(), true);
assert.ok(Date.now() - fetchedAt > FEED_EVICT_AFTER_MS);
});
test('only the vehicles near the view are animated frame by frame', async (t) => {
@@ -3110,7 +3108,7 @@ for (const bounds of ['null', 'stale']) {
parts.rendering.refreshVisibility();
assert.equal(state._visible.size, loaded.count);
assert.equal(state._moving.size, loaded.count);
assert.equal(app.viewer.scene.requestRenderMode, false);
assert.equal(!app.engine.render.isContinuous(), false);
const entry = app.vehicles()[0],
before = Cesium.Cartesian3.clone(entry.marker.position);
app.run(1000, 16);
@@ -3182,7 +3180,7 @@ for (const count of [800, 3000]) {
before = Cesium.Cartesian3.clone(entry.marker.position);
app.run(1000, 16);
assert.ok(Cesium.Cartesian3.distance(before, entry.marker.position) > 0.9);
assert.equal(app.viewer.scene.requestRenderMode, false);
assert.equal(!app.engine.render.isContinuous(), false);
});
}
@@ -3235,7 +3233,7 @@ test('intercepted scripted reports resolve their explicit floor and wake without
assert.equal(entry.surfaceReady, true);
assert.equal(entry.marker.show, true);
assert.ok(app.state()._moving.has(entry));
assert.equal(app.viewer.scene.requestRenderMode, false);
assert.equal(!app.engine.render.isContinuous(), false);
const before = Cesium.Cartesian3.clone(entry.marker.position);
app.run(1000, 16);
assert.ok(Cesium.Cartesian3.distance(before, entry.marker.position) > 1);
+12 -8
View File
@@ -168,7 +168,10 @@ export function createRendering({ state, services, parts }) {
placeSample(entry);
schedulePlayback(entry);
syncRenderHold();
governorRequestRender('transit-motion-boundary');
governorRequestRender(
'transit-motion-boundary',
parts.renderer?.engine,
);
},
Math.max(1, sample.nextWakeMonoMs - performance.now()),
);
@@ -280,7 +283,7 @@ export function createRendering({ state, services, parts }) {
state._stylePreset = next;
for (const entry of state._vehicles.values()) applySpriteStyle(entry);
parts.trails.style();
governorRequestRender('transit-style');
governorRequestRender('transit-style', parts.renderer?.engine);
}
/**
@@ -358,12 +361,12 @@ export function createRendering({ state, services, parts }) {
// frames on — it is not moving.
const working = state._enabled && state._moving.size > 0;
if (working && !state._renderHeld) {
holdContinuousRender('transit');
holdContinuousRender('transit', parts.renderer?.engine);
state._renderHeld = true;
} else if (!working && state._renderHeld) {
releaseContinuousRender('transit');
releaseContinuousRender('transit', parts.renderer?.engine);
state._renderHeld = false;
governorRequestRender('transit-settled');
governorRequestRender('transit-settled', parts.renderer?.engine);
}
}
@@ -421,7 +424,8 @@ export function createRendering({ state, services, parts }) {
)
: 0;
}
if (state._heightDirty.size) governorRequestRender('transit-height-batch');
if (state._heightDirty.size)
governorRequestRender('transit-height-batch', parts.renderer?.engine);
syncRenderHold();
}
@@ -508,7 +512,7 @@ export function createRendering({ state, services, parts }) {
state._shownCount = state._visible.size;
if (changed) state._detectRevision++;
syncRenderHold();
governorRequestRender('transit-visibility');
governorRequestRender('transit-visibility', parts.renderer?.engine);
return state._shownCount;
}
function resumePlayback() {
@@ -529,7 +533,7 @@ export function createRendering({ state, services, parts }) {
parts.selection.refreshSelectedCard(true);
state._detectRevision++;
syncRenderHold();
governorRequestRender('transit-re-entry');
governorRequestRender('transit-re-entry', parts.renderer?.engine);
}
// Text and membership refreshes are bounded timer work, never paint work.
function maintainPresentation() {
+1 -1
View File
@@ -106,7 +106,7 @@ export function createSelection({ state, services, parts }) {
parts.rendering.paintSelected(entry, true);
parts.trails.select(entry);
refreshSelectedCard(true);
governorRequestRender('transit-select');
governorRequestRender('transit-select', parts.renderer?.engine);
}
function onKeyDown(event) {
+12 -3
View File
@@ -189,7 +189,10 @@ export function createTrails({ state, services, parts, source }) {
});
entry.trailVertices = vertices;
entry.trailSegments = all;
services.render.governorRequestRender('transit-trail-history');
services.render.governorRequestRender(
'transit-trail-history',
parts.renderer?.engine,
);
}
}
return changed;
@@ -300,7 +303,10 @@ export function createTrails({ state, services, parts, source }) {
parts.height.anchorFloors();
prepareEntry(entry);
update();
services.render.governorRequestRender('transit-trail-backfill');
services.render.governorRequestRender(
'transit-trail-backfill',
parts.renderer?.engine,
);
})
.catch(() => {
// Session history remains usable if the optional retained read fails.
@@ -313,7 +319,10 @@ export function createTrails({ state, services, parts, source }) {
if (!state._enabled || selected !== entry) return;
parts.rendering.sampleIdle(entry);
update();
services.render.governorRequestRender('transit-trail-age');
services.render.governorRequestRender(
'transit-trail-age',
parts.renderer?.engine,
);
}, 1000);
}
function detachRenderer() {
+1 -1
View File
@@ -113,7 +113,7 @@ export function createViewport({ state, services, parts }) {
}
parts.ingestion.sweepAgedVehicles(Date.now());
parts.rendering.syncRenderHold();
governorRequestRender('transit-proximity');
governorRequestRender('transit-proximity', parts.renderer?.engine);
}
function onCameraChanged() {
+3 -3
View File
@@ -189,7 +189,7 @@ export function createLifecycle({
state.preRenderRemover?.();
state.preRenderRemover = null;
components.rendering.cancelTrailingVisibilityPass();
releaseContinuousRender('ais-vessels');
releaseContinuousRender('ais-vessels', state.renderer?.engine);
unregisterPickOwner('ais-live-vessels', state.renderer?.engine);
components.tracking.detachSelectedVesselTrail();
for (const record of state.records.all)
@@ -242,7 +242,7 @@ export function createLifecycle({
removeViewListener();
state.feed.enabled = false;
invalidateAisSession();
releaseContinuousRender('ais-vessels');
releaseContinuousRender('ais-vessels', state.renderer?.engine);
components.rendering.cancelTrailingVisibilityPass();
unregisterPickOwner('ais-live-vessels', state.renderer?.engine);
components.rendering.setVisible(false);
@@ -261,7 +261,7 @@ export function createLifecycle({
destroy() {
removeViewListener();
invalidateAisSession();
releaseContinuousRender('ais-vessels'); // direct-destroy path (perf wave 2 fix)
releaseContinuousRender('ais-vessels', state.renderer?.engine); // direct-destroy path (perf wave 2 fix)
components.rendering.cancelTrailingVisibilityPass();
if (state.feed.abort) state.feed.abort.abort();
unregisterPickOwner('ais-live-vessels', state.renderer?.engine);
+7 -5
View File
@@ -215,7 +215,8 @@ export function createRendering({
);
state.trailingVisibilityTimer = vesselState._aisRuntime.setTimeout(() => {
state.trailingVisibilityTimer = null;
if (state.feed.enabled) governorRequestRender('ais-vessels-visibility');
if (state.feed.enabled)
governorRequestRender('ais-vessels-visibility', state.renderer?.engine);
}, waitMs);
}
@@ -232,7 +233,8 @@ export function createRendering({
function updateVisibility(force = false) {
if (!state.feed.enabled || (!state.renderer && !state.viewer)) return;
if (force) governorRequestRender('ais-vessels-update');
if (force)
governorRequestRender('ais-vessels-update', state.renderer?.engine);
const now = focusNowMs(performance.now());
const focusTarget = getFocusTarget();
const regularPass =
@@ -261,7 +263,7 @@ export function createRendering({
}
if (focusPass) {
state.activeFocusCount = 0;
releaseContinuousRender('ais-vessels');
releaseContinuousRender('ais-vessels', state.renderer?.engine);
}
return;
}
@@ -314,8 +316,8 @@ export function createRendering({
});
state.activeFocusCount = result.activeCount;
if (result.activeCount > 0 || result.transitioning)
holdContinuousRender('ais-vessels');
else releaseContinuousRender('ais-vessels');
holdContinuousRender('ais-vessels', state.renderer?.engine);
else releaseContinuousRender('ais-vessels', state.renderer?.engine);
}
}
+4 -1
View File
@@ -104,7 +104,10 @@ export function createRendering({ state, services, parts }) {
return;
}
commit();
services.render?.governorRequestRender?.(`wildlife-sightings-${reason}`);
services.render?.governorRequestRender?.(
`wildlife-sightings-${reason}`,
state.engine,
);
};
function beginBatch() {
batchDepth += 1;
+3 -1
View File
@@ -22,7 +22,9 @@ export class MapStackController extends MapSourceController {
? options.initialStack || 'photoreal'
: registry.defaultId,
...options,
requestRender: governorRequestRender,
requestRender:
options.requestRender ??
((reason) => governorRequestRender(reason, options.engine)),
});
this.googleTileset = options.googleTileset || null;
this.cesiumToken = String(options.cesiumToken || '').trim();
+5 -7
View File
@@ -1,8 +1,5 @@
import * as geo from './geo/index.js';
import {
holdContinuousRender,
releaseContinuousRender,
} from './renderGovernor.js';
import { renderGovernorFor } from './renderGovernor.js';
/**
* OrbitController — smooth orbit around a target point.
@@ -43,7 +40,8 @@ export class OrbitController {
this.active = true;
// Orbit mutates the camera from preRender — without a hold the loop
// starves after its first idle frame. (perf wave 2)
holdContinuousRender('camera-orbit');
this._renderGovernor = renderGovernorFor(this.engine);
this._renderGovernor?.holdContinuousRender('camera-orbit');
// Start from the camera's current heading for seamless transition
this.angle = camera.pose().heading;
@@ -71,7 +69,7 @@ export class OrbitController {
*/
stop() {
this.active = false;
releaseContinuousRender('camera-orbit');
this._renderGovernor?.releaseContinuousRender('camera-orbit');
if (this._removeListener) {
this._removeListener();
this._removeListener = null;
@@ -87,7 +85,7 @@ export class OrbitController {
attachEngine(engine) {
if (this.active || this._removeListener) {
this.active = false;
releaseContinuousRender('camera-orbit');
this._renderGovernor?.releaseContinuousRender('camera-orbit');
this._removeListener?.();
this._removeListener = null;
}
+7 -8
View File
@@ -3,12 +3,7 @@ import assert from 'node:assert/strict';
import * as geo from './geo/index.js';
import { createFakeEngine } from './globe/testing/fakeEngine.js';
import { OrbitController } from './orbit.js';
import {
_resetRenderGovernorForTest,
getRenderGovernorDiagnostics,
} from './renderGovernor.js';
test.afterEach(() => _resetRenderGovernorForTest());
import { getRenderGovernorDiagnostics } from './renderGovernor.js';
test('orbit locks onto the target every frame from the current heading, and stop releases it', (t) => {
t.mock.timers.enable({ apis: ['Date'], now: 1000 });
@@ -25,7 +20,9 @@ test('orbit locks onto the target every frame from the current heading, and stop
orbit.toggle(target, { radius: 800, pitch: -20, speed: 10 }),
true,
);
assert.ok(getRenderGovernorDiagnostics().holds.includes('camera-orbit'));
assert.ok(
getRenderGovernorDiagnostics(engine).holds.includes('camera-orbit'),
);
t.mock.timers.tick(500);
engine.fake.frame();
const [first] = engine.fake.callsOf('camera.lookAt');
@@ -37,7 +34,9 @@ test('orbit locks onto the target every frame from the current heading, and stop
assert.equal(first.args[1].range, 800);
assert.equal(orbit.toggle(), false);
assert.equal(engine.fake.callsOf('camera.release').length, 1);
assert.ok(!getRenderGovernorDiagnostics().holds.includes('camera-orbit'));
assert.ok(
!getRenderGovernorDiagnostics(engine).holds.includes('camera-orbit'),
);
engine.fake.frame();
assert.equal(
engine.fake.callsOf('camera.lookAt').length,
+74 -186
View File
@@ -1,197 +1,85 @@
/**
* Idle render governor — the wave-2 flagship of the 2026-08-05 perf
* investigation.
*
* The problem: Cesium's default render loop repaints every vsync forever, so
* the app burned ~60% GPU + ~54% of a core with ZERO layers enabled and a
* parked camera. The fix: flip the scene into Cesium's `requestRenderMode`
* whenever nothing animates per frame, and return to the continuous loop the
* moment something does.
*
* Architecture — a binary mode driven by ref-counted holds:
*
* - **Continuous mode** (`requestRenderMode = false`, today's behavior)
* while ANY hold is registered. Every per-frame animator — fleet
* interpolation, traffic sim, satellite motion, tracked-entity follow,
* style crossfades, CCTV projection — registers a hold for exactly the
* lifetime of its scene-loop listener or animation. While one is active,
* behavior is byte-identical to pre-governor main: the locked
* interpolation/tracking invariants are preserved by construction.
* - **Idle mode** (`requestRenderMode = true`) when zero holds. Cesium
* auto-renders on camera input and tile loads; every other scene mutation
* must call `governorRequestRender()` for its one frame. Discrete
* mutators (layer poll ticks, slider writes, annotation changes) route
* through that.
*
* Holds are identity-keyed (a Set of owner ids), NOT a counter — a module
* that double-holds or double-releases cannot corrupt the mode. Owners are
* short stable strings ('flights', 'traffic', 'style-anim', …) so the
* diagnostics read like a story.
*
* The governor is O(1) passive: no per-frame work of its own, ever.
*
* Globe engine: the governor is the Cesium engine's render backend.
* `engine.render.request()/setContinuous()` (src/globe/cesium/render.js)
* delegate here, so the governor never uses `engine.render` (that would be a
* cycle) and keeps the viewer for what the contract leaves to the engine:
* `scene.requestRenderMode`, `scene.maximumRenderTimeChange` and
* `scene.requestRender()`. It needs no frame events (`engine.frames`).
*
* Any other globe engine (Navara) is the governor's backend instead: install
* it on the engine (installEngineRenderGovernor) and the owner-counted holds
* drive `engine.render.setContinuous`, requests go to `engine.render.request`.
*/
import { sceneServicesFor } from './data/sceneServices.js';
let _viewer = null;
/** The non-Cesium engine the governor drives, or null (Cesium: `_viewer`). */
let _engine = null;
let _installed = false;
const _holds = new Set();
/** Debug trail of the most recent one-shot render requests (idle mode only). */
const _recentRequests = [];
const RECENT_REQUEST_CAP = 16;
function applyMode() {
if (!_installed) return;
const continuous = _holds.size > 0;
if (_engine) {
if (_engine.render.isContinuous() === continuous) return;
_engine.render.setContinuous(continuous);
if (!continuous) _engine.render.request('governor-idle');
return;
/** Owner-keyed render demand for one scene attachment, on any globe engine. */
export function createRenderGovernor(engine) {
const holds = new Set();
const recentRequests = [];
let installed = false;
let destroyed = false;
function applyMode() {
if (!installed || destroyed) return;
const continuous = holds.size > 0;
if (engine.render.isContinuous() === continuous) return;
engine.render.setContinuous(continuous);
if (!continuous) engine.render.request('governor-idle');
}
if (!_viewer?.scene) return;
const scene = _viewer.scene;
if (scene.requestRenderMode === !continuous) return;
scene.requestRenderMode = !continuous;
if (!continuous) {
// Entering idle: render one settling frame so anything the last
// continuous frame mutated is on screen before the loop stops.
scene.requestRender?.();
}
}
/**
* Install the governor on a Cesium viewer, as the Cesium engine's render
* backend. Idempotent. Before install, hold/release still record into the
* holds set (and apply at install time); requests are safe no-ops — so
* modules can call all three unconditionally in tests without a viewer.
* @param {Cesium.Viewer} viewer
* @returns {void}
*/
export function installRenderGovernor(viewer) {
if (!viewer?.scene)
throw new TypeError('installRenderGovernor requires a Cesium viewer');
_engine = null;
_viewer = viewer;
_installed = true;
// Never let Cesium re-render on simulation-time deltas behind our back —
// idle means idle. All re-renders are camera/tiles (Cesium-native) or
// explicit requests.
viewer.scene.maximumRenderTimeChange = Infinity;
applyMode();
}
/**
* Install the governor on a globe engine that renders by itself (Navara):
* the holds drive `engine.render.setContinuous`, requests go to
* `engine.render.request`. Idempotent, like installRenderGovernor.
* @param {import('./globe/contract.js').GlobeEngine} engine
* @returns {void}
*/
export function installEngineRenderGovernor(engine) {
if (typeof engine?.render?.setContinuous !== 'function')
throw new TypeError('installEngineRenderGovernor requires a globe engine');
_engine = engine;
_viewer = engine;
_installed = true;
applyMode();
}
/**
* Register a continuous-render hold. Idempotent per owner.
* Call where the owner's per-frame work BEGINS (scene listener installed,
* animation starts, tracking begins).
* @param {string} ownerId Short stable id, e.g. 'flights', 'traffic'.
* @returns {void}
*/
export function holdContinuousRender(ownerId) {
if (!ownerId) return;
_holds.add(ownerId);
applyMode();
}
/**
* Release a hold. Safe when never held.
* Call where the owner's per-frame work ENDS (listener removed, animation
* settled, tracking stopped, layer disabled).
* @param {string} ownerId
* @returns {void}
*/
export function releaseContinuousRender(ownerId) {
if (!ownerId) return;
_holds.delete(ownerId);
applyMode();
}
/**
* One-shot render request for a discrete scene mutation (layer tick, slider
* write, annotation change). Always forwards to scene.requestRender() — in
* continuous mode that is a harmless flag set (and forwarding closes the
* request-then-last-release race); only idle-mode requests are recorded in
* diagnostics. Cheap enough to call unconditionally after any mutation.
* @param {string} [reason] For diagnostics only.
* @returns {void}
*/
export function governorRequestRender(reason = 'unspecified') {
if (!_installed || (!_engine && !_viewer?.scene)) return;
if (_holds.size === 0) {
_recentRequests.push({ reason, at: Date.now() });
if (_recentRequests.length > RECENT_REQUEST_CAP) _recentRequests.shift();
}
if (_engine) _engine.render.request(reason);
else _viewer.scene.requestRender?.();
}
/**
* @returns {{installed: boolean, mode: 'continuous'|'idle', holds: string[],
* recentRequests: Array<{reason: string, at: number}>}}
*/
export function getRenderGovernorDiagnostics() {
return {
installed: _installed,
mode: _holds.size > 0 ? 'continuous' : 'idle',
holds: [..._holds].sort(),
recentRequests: [..._recentRequests],
install() {
if (destroyed) return;
installed = true;
applyMode();
},
holdContinuousRender(ownerId) {
if (destroyed || !ownerId) return;
holds.add(ownerId);
applyMode();
},
releaseContinuousRender(ownerId) {
if (destroyed || !ownerId) return;
holds.delete(ownerId);
applyMode();
},
governorRequestRender(reason = 'unspecified') {
if (!installed || destroyed) return;
if (holds.size === 0) {
recentRequests.push({ reason, at: Date.now() });
if (recentRequests.length > 16) recentRequests.shift();
}
engine.render.request(reason);
},
getRenderGovernorDiagnostics() {
return {
installed,
mode: holds.size > 0 ? 'continuous' : 'idle',
holds: [...holds].sort(),
recentRequests: [...recentRequests],
};
},
destroy() {
if (destroyed) return;
holds.clear();
applyMode();
installed = false;
destroyed = true;
recentRequests.length = 0;
},
};
}
/**
* Whether the governor is installed on this viewer (its render requests then
* go through `governorRequestRender`).
* @param {object} viewer
* @returns {boolean}
*/
export function isRenderGovernorInstalled(viewer) {
return _installed && viewer != null && _viewer === viewer;
/** Portable callers resolve the same governor exposed as scene.services.render. */
export function renderGovernorFor(engine) {
return sceneServicesFor(engine)?.render ?? null;
}
/** Release the installed viewer after its animation owners have stopped. */
export function uninstallRenderGovernor(viewer) {
if (_viewer !== viewer) return;
_viewer = null;
_engine = null;
_installed = false;
_holds.clear();
_recentRequests.length = 0;
export function holdContinuousRender(ownerId, engine) {
renderGovernorFor(engine)?.holdContinuousRender(ownerId);
}
/** Test seam: reset module state between unit tests. */
export function _resetRenderGovernorForTest() {
_viewer = null;
_engine = null;
_installed = false;
_holds.clear();
_recentRequests.length = 0;
export function releaseContinuousRender(ownerId, engine) {
renderGovernorFor(engine)?.releaseContinuousRender(ownerId);
}
export function governorRequestRender(reason, engine) {
renderGovernorFor(engine)?.governorRequestRender(reason);
}
export function getRenderGovernorDiagnostics(engine) {
return (
renderGovernorFor(engine)?.getRenderGovernorDiagnostics() ?? {
installed: false,
mode: 'idle',
holds: [],
recentRequests: [],
}
);
}
+104 -152
View File
@@ -1,156 +1,108 @@
import { test, beforeEach } from 'node:test';
import test from 'node:test';
import assert from 'node:assert/strict';
import {
installRenderGovernor,
installEngineRenderGovernor,
holdContinuousRender,
releaseContinuousRender,
governorRequestRender,
getRenderGovernorDiagnostics,
_resetRenderGovernorForTest,
isRenderGovernorInstalled,
uninstallRenderGovernor,
} from './renderGovernor.js';
import { OrbitController } from './orbit.js';
import { GROUND_HARNESSES } from './testSupport/groundDisplayEngines.mjs';
import { createSceneServices, sceneServicesFor } from './data/sceneServices.js';
import { renderGovernorFor, governorRequestRender } from './renderGovernor.js';
function makeViewer() {
const calls = { requestRender: 0 };
const scene = {
requestRenderMode: false,
maximumRenderTimeChange: 0,
requestRender() { calls.requestRender += 1; },
};
return { viewer: { scene }, scene, calls };
for (const makeHarness of GROUND_HARNESSES) {
test(`${makeHarness.name}: owner holds, installation and idle requests`, (t) => {
const harness = makeHarness();
const { engine } = harness;
const services = createSceneServices(engine);
const governor = services.render;
t.after(() => { services.destroy(); harness.destroy(); });
const requests = [];
const request = engine.render.request;
engine.render.request = (reason) => { requests.push(reason); request(reason); };
assert.equal(renderGovernorFor(engine), governor);
governor.holdContinuousRender('flights');
governor.governorRequestRender('before-install');
assert.equal(requests.length, 0);
governor.install();
assert.equal(engine.render.isContinuous(), true);
governor.holdContinuousRender('flights');
governor.holdContinuousRender('satellites');
governor.governorRequestRender('while-held');
assert.equal(requests.at(-1), 'while-held', 'requests forward even while held');
assert.deepEqual(governor.getRenderGovernorDiagnostics().recentRequests, []);
governor.releaseContinuousRender('flights');
assert.equal(engine.render.isContinuous(), true);
assert.deepEqual(governor.getRenderGovernorDiagnostics().holds, ['satellites']);
governor.releaseContinuousRender('satellites');
assert.equal(engine.render.isContinuous(), false);
assert.equal(requests.at(-1), 'governor-idle', 'last release paints a settling frame');
const settled = requests.length;
governor.releaseContinuousRender('satellites');
governor.releaseContinuousRender('absent');
assert.equal(requests.length, settled, 'absent releases do not wake an idle engine');
for (let i = 0; i < 20; i++) governorRequestRender(`tick-${i}`, engine);
const diagnostics = governor.getRenderGovernorDiagnostics();
assert.equal(diagnostics.recentRequests.length, 16);
assert.equal(diagnostics.recentRequests[0].reason, 'tick-4');
diagnostics.holds.push('external');
assert.deepEqual(governor.getRenderGovernorDiagnostics().holds, []);
if (harness.viewer) assert.equal(harness.viewer.scene.maximumRenderTimeChange, Infinity);
});
test(`${makeHarness.name}: identical owners and teardown stay in their scene`, (t) => {
const first = makeHarness();
const second = makeHarness();
const a = createSceneServices(first.engine);
const b = createSceneServices(second.engine);
t.after(() => { sceneServicesFor(first.engine).destroy(); b.destroy(); first.destroy(); second.destroy(); });
a.render.install();
b.render.install();
a.render.holdContinuousRender('flights');
b.render.holdContinuousRender('flights');
a.render.releaseContinuousRender('flights');
assert.equal(first.engine.render.isContinuous(), false);
assert.equal(second.engine.render.isContinuous(), true);
a.destroy();
const replacement = createSceneServices(first.engine);
replacement.render.install();
replacement.render.holdContinuousRender('flights');
const oldRelease = a.render.releaseContinuousRender;
oldRelease('flights');
a.render.install();
a.render.holdContinuousRender('late');
a.render.governorRequestRender('late');
a.destroy();
assert.deepEqual(a.render.getRenderGovernorDiagnostics(), {
installed: false, mode: 'idle', holds: [], recentRequests: [],
});
assert.equal(first.engine.render.isContinuous(), true);
assert.equal(second.engine.render.isContinuous(), true);
b.destroy();
assert.equal(second.engine.render.isContinuous(), false);
assert.equal(first.engine.render.isContinuous(), true);
replacement.destroy();
assert.equal(first.engine.render.isContinuous(), false);
assert.equal(sceneServicesFor(first.engine), replacement, 'late lookups cannot revive a detached scene');
});
}
beforeEach(() => _resetRenderGovernorForTest());
test('install with zero holds enters idle mode and pins maximumRenderTimeChange', () => {
const { viewer, scene } = makeViewer();
installRenderGovernor(viewer);
assert.equal(scene.requestRenderMode, true);
assert.equal(scene.maximumRenderTimeChange, Infinity);
assert.equal(getRenderGovernorDiagnostics().mode, 'idle');
});
test('a hold flips to continuous; releasing the last hold returns to idle with a settling frame', () => {
const { viewer, scene, calls } = makeViewer();
installRenderGovernor(viewer);
const settleBaseline = calls.requestRender;
holdContinuousRender('flights');
assert.equal(scene.requestRenderMode, false);
assert.equal(getRenderGovernorDiagnostics().mode, 'continuous');
releaseContinuousRender('flights');
assert.equal(scene.requestRenderMode, true);
// Entering idle renders one settling frame.
assert.equal(calls.requestRender, settleBaseline + 1);
});
test('holds are identity-keyed: double-hold cannot leak, double-release cannot corrupt', () => {
const { viewer, scene } = makeViewer();
installRenderGovernor(viewer);
holdContinuousRender('traffic');
holdContinuousRender('traffic');
releaseContinuousRender('traffic');
assert.equal(scene.requestRenderMode, true, 'single release clears an idempotent double-hold');
releaseContinuousRender('traffic');
releaseContinuousRender('never-held');
assert.equal(scene.requestRenderMode, true);
});
test('mode stays continuous until the LAST holder releases', () => {
const { viewer, scene } = makeViewer();
installRenderGovernor(viewer);
holdContinuousRender('flights');
holdContinuousRender('satellites');
releaseContinuousRender('flights');
assert.equal(scene.requestRenderMode, false);
assert.deepEqual(getRenderGovernorDiagnostics().holds, ['satellites']);
releaseContinuousRender('satellites');
assert.equal(scene.requestRenderMode, true);
});
test('governorRequestRender forwards to the scene and records reasons only in idle mode', () => {
const { viewer, calls } = makeViewer();
installRenderGovernor(viewer);
const baseline = calls.requestRender;
governorRequestRender('layer-tick:earthquakes');
assert.equal(calls.requestRender, baseline + 1);
assert.equal(getRenderGovernorDiagnostics().recentRequests.at(-1).reason, 'layer-tick:earthquakes');
holdContinuousRender('flights');
const idleRequests = getRenderGovernorDiagnostics().recentRequests.length;
governorRequestRender('slider');
assert.equal(
getRenderGovernorDiagnostics().recentRequests.length,
idleRequests,
'continuous-mode requests are not recorded as idle diagnostics',
);
});
test('hold/release/request are safe no-ops before install (test environments without a viewer)', () => {
holdContinuousRender('flights');
releaseContinuousRender('flights');
governorRequestRender('noop');
assert.equal(getRenderGovernorDiagnostics().installed, false);
});
test('holds registered before install apply at install time', () => {
holdContinuousRender('flights');
const { viewer, scene } = makeViewer();
installRenderGovernor(viewer);
assert.equal(scene.requestRenderMode, false, 'pre-install hold keeps continuous mode');
releaseContinuousRender('flights');
assert.equal(scene.requestRenderMode, true);
});
test('isRenderGovernorInstalled names the one governed viewer', async () => {
const { isRenderGovernorInstalled, uninstallRenderGovernor } = await import('./renderGovernor.js');
const { viewer } = makeViewer();
const other = makeViewer().viewer;
assert.equal(isRenderGovernorInstalled(viewer), false);
installRenderGovernor(viewer);
assert.equal(isRenderGovernorInstalled(viewer), true);
assert.equal(isRenderGovernorInstalled(other), false);
uninstallRenderGovernor(viewer);
assert.equal(isRenderGovernorInstalled(viewer), false);
});
test('on a non-Cesium globe engine the governor drives engine.render with owner-counted holds', async () => {
const { createFakeEngine } = await import('./globe/testing/fakeEngine.js');
_resetRenderGovernorForTest();
const engine = createFakeEngine({ kind: 'navara' });
holdContinuousRender('before-install');
installEngineRenderGovernor(engine);
assert.equal(engine.render.isContinuous(), true, 'a hold taken before install applies');
releaseContinuousRender('before-install');
assert.equal(engine.render.isContinuous(), false);
holdContinuousRender('a');
holdContinuousRender('b');
assert.equal(engine.render.isContinuous(), true);
releaseContinuousRender('a');
assert.equal(engine.render.isContinuous(), true, 'b still holds');
releaseContinuousRender('b');
assert.equal(engine.render.isContinuous(), false);
engine.fake.resetCalls();
governorRequestRender('slider');
assert.deepEqual(engine.fake.callsOf('render.request').map((c) => c.args), [['slider']]);
assert.equal(isRenderGovernorInstalled(engine), true);
uninstallRenderGovernor(engine);
_resetRenderGovernorForTest();
});
test('releasing an absent Navara hold does not wake an already idle engine', async () => {
const { createFakeEngine } = await import('./globe/testing/fakeEngine.js');
_resetRenderGovernorForTest();
const engine = createFakeEngine({ kind: 'navara' });
installEngineRenderGovernor(engine);
engine.fake.resetCalls();
for (let i = 0; i < 22; i++) releaseContinuousRender('cctv-projection');
assert.equal(engine.fake.callsOf('render.request').length, 0);
holdContinuousRender('video');
releaseContinuousRender('video');
assert.equal(engine.fake.callsOf('render.request').length, 1);
releaseContinuousRender('video');
assert.equal(engine.fake.callsOf('render.request').length, 1);
uninstallRenderGovernor(engine);
_resetRenderGovernorForTest();
});
for (const makeHarness of GROUND_HARNESSES) {
test(`${makeHarness.name}: an old orbit release cannot idle a replacement attachment`, (t) => {
const harness = makeHarness();
const { engine } = harness;
const first = createSceneServices(engine);
first.render.install();
const oldOrbit = new OrbitController(engine);
const newOrbit = new OrbitController(engine);
t.after(() => {
oldOrbit.stop(); newOrbit.stop(); sceneServicesFor(engine).destroy(); harness.destroy();
});
oldOrbit.start({ x: 6378137, y: 0, z: 0 });
assert.equal(engine.render.isContinuous(), true);
first.destroy();
const next = createSceneServices(engine);
next.render.install();
newOrbit.start({ x: 6378137, y: 0, z: 0 });
oldOrbit.stop();
assert.deepEqual(next.render.getRenderGovernorDiagnostics().holds, ['camera-orbit']);
assert.equal(engine.render.isContinuous(), true);
newOrbit.stop();
assert.equal(engine.render.isContinuous(), false);
});
}
-8
View File
@@ -46,11 +46,6 @@ import {
setDetectionTuning,
} from '../data/detection.js';
import { isTr3b, toggleTr3b } from '../data/tr3bRegistry.js';
import {
holdContinuousRender,
releaseContinuousRender,
governorRequestRender,
} from '../renderGovernor.js';
import {
setScopeMaskEnabled,
isScopeMaskEnabled,
@@ -105,9 +100,6 @@ export class StyleManager extends ApplicationShell {
setDetectionTuning,
isTr3b,
toggleTr3b,
holdContinuousRender,
releaseContinuousRender,
governorRequestRender,
setScopeMaskEnabled,
isScopeMaskEnabled,
setScopeMaskFeather,
+1 -1
View File
@@ -694,7 +694,7 @@ test('the Display selector, voice schema and final stylesheet expose Cyber', ()
assert.match(detection, /sonarSampler\.label\(sonarFactor\)/);
assert.match(detection, /createCyberSonarSampler\(width, height, now\)/);
assert.match(cyberStyles, /data-cyber-sonar='off'/);
assert.match(detection, /governorRequestRender\('cyber-sonar-cadence'\)/);
assert.match(detection, /governorRequestRender\('cyber-sonar-cadence', _engine\)/);
assert.match(
detection,
/const animatingCount = countAnimatingRenderEntries\(renderEntries\);/,
+9 -2
View File
@@ -1,3 +1,4 @@
import { renderGovernorFor } from '../renderGovernor.js';
import { retainsLayerSession } from '../app/layerSession.js';
import { resolveImageryHost } from '../layers/weather/imageryHost.js';
import { LayerBindings } from './layerBindings.js';
@@ -28,7 +29,10 @@ export class ShellScene extends ShellFacade {
/** Share restoration, visual settings and layer bindings of this globe. */
_buildSceneControls() {
const { viewer, engine, mapStackController } = this._scene;
const services = this.services;
const services = {
...this.services,
...renderGovernorFor(this._scene.engine),
};
const layerSession = this._layerSession;
this._shareRestoration = new ShareRestoration({
engine,
@@ -141,7 +145,10 @@ export class ShellScene extends ShellFacade {
/** Cockpit and the celestial ring of this globe. */
_buildSceneCockpit() {
const { viewer, engine } = this._scene;
const services = this.services;
const services = {
...this.services,
...renderGovernorFor(this._scene.engine),
};
const { CelestialRing } = services;
this._cockpitCoordinator = new CockpitCoordinator({
viewer,