Unsubscribe removed Navara effects and models from warm view frames

This commit is contained in:
German Khokhlov committed 2026-11-03 05:16:20 +04:00
1 parent 14f7b32537
commit 05b646daa3
16 files changed
+90 -19

No files matched your search

+1
View File
@@ -3616,6 +3616,7 @@
"src/geo/developerError.js",
"src/time/clock.js",
"src/globe/navara/overlay.js",
"src/globe/navara/frameDescriptor.js",
"src/globe/cesium/clock.js",
"src/globe/pickDelivery.js",
"src/globe/navara/clock.js",
+2 -2
View File
@@ -273,7 +273,7 @@ export async function createNavaraScene({
alignDaylightToRestoredView: globe.alignDaylightToRestoredView,
MeshDesc: globe.MeshDesc,
NavaraColor: globe.NavaraColor,
EffectDesc: globe.EffectDesc,
EffectDesc: globe.LayerEffectDesc,
NavaraEffect: globe.NavaraEffect,
DrapedMesh: globe.DrapedMesh,
setupMaterialForDrape: globe.setupMaterialForDrape,
@@ -441,7 +441,7 @@ export function createViewerlessTools({
if (window.__godsEyeView === debug) delete window.__godsEyeView;
});
// Voice on the engine (phase 3, Task 9): the W1 tools drive the engine;
// the viewer-only ones (director, annotations…) refuse.
// the Director uses the shared session; viewer-only tools declare availability.
const voiceCommands = initGevVoiceCommands({
...voice,
annotationResolver: operations.annotationResolver,
+18
View File
@@ -0,0 +1,18 @@
// Navara auto-binds descriptor.update on creation but does not unregister
// that bound function when its handle is deleted. GEV's synchronous effects
// and meshes subscribe with their own callback so a parked view can be reused.
export function withFrameUpdates(Base) {
return class extends Base {
#frame = (time) => this.updateFrame(time);
onCreate() {
super.onCreate();
this.view.on('preRender', this.#frame);
}
onDestroy() {
this.view.off('preRender', this.#frame);
super.onDestroy();
}
};
}
+43
View File
@@ -0,0 +1,43 @@
import assert from 'node:assert/strict';
import test from 'node:test';
import { withFrameUpdates } from './frameDescriptor.js';
test('deleting a descriptor unsubscribes only that renderer from the warm view', () => {
const frames = new Set();
const view = {
on(name, cb) {
assert.equal(name, 'preRender');
frames.add(cb);
},
off(name, cb) {
assert.equal(name, 'preRender');
frames.delete(cb);
},
};
class Base {
view = view;
onCreate() {}
onDestroy() {
this.destroyed = true;
}
}
class Layer extends withFrameUpdates(Base) {
ticks = [];
updateFrame(time) {
this.ticks.push(time);
}
}
const a = new Layer(),
b = new Layer();
a.onCreate();
b.onCreate();
assert.equal(typeof a.update, 'undefined', 'no duplicate SDK subscription');
for (const cb of frames) cb(1);
a.onDestroy();
for (const cb of frames) cb(2);
assert.deepEqual(a.ticks, [1]);
assert.deepEqual(b.ticks, [1, 2]);
assert.equal(a.destroyed, true);
b.onDestroy();
assert.equal(frames.size, 0);
});
+2
View File
@@ -2,6 +2,7 @@
// the product way (viewOptions.js) and the GlobeEngine on it. Load it with a
// dynamic import() so Navara stays out of the Cesium-mode bundle.
import { withFrameUpdates } from './frameDescriptor.js';
import ThreeView, {
Color as NavaraColor,
MeshDesc,
@@ -209,6 +210,7 @@ export async function createNavaraGlobe({
NavaraColor,
MeshDesc,
EffectDesc,
LayerEffectDesc: withFrameUpdates(EffectDesc),
NavaraEffect,
DrapedMesh,
setupMaterialForDrape,
+3 -2
View File
@@ -9,6 +9,7 @@
// asks for one frame (view.forceUpdate); per-frame work runs only on frames
// the view draws anyway (S5), so an idle view stays idle.
import { withFrameUpdates } from '../frameDescriptor.js';
import {
MeshDesc,
PickableInstancedMeshWrapper,
@@ -25,7 +26,7 @@ import { createNavaraOverlay } from '../overlay.js';
const DESC_NAME = 'gevMoverSet';
const registered = new WeakSet();
class GevMoverSetDesc extends MeshDesc {
class GevMoverSetDesc extends withFrameUpdates(MeshDesc) {
constructor(view, ctx, config) {
super(view, ctx, config);
this.gev = config[DESC_NAME];
@@ -59,7 +60,7 @@ class GevMoverSetDesc extends MeshDesc {
return group;
}
update(time) {
updateFrame(time) {
updateMover(this.gev.readFrameCamera, this.ctx, this.gev.mover, time);
}
+7 -3
View File
@@ -27,15 +27,19 @@ test('disposing a sprite atlas drops decoded images and detaches pending load ca
const previous = globalThis.Image;
let image;
globalThis.Image = class {
constructor() { image = this; }
constructor() {
image = this;
}
};
t.after(() => {
if (previous) globalThis.Image = previous;
else delete globalThis.Image;
});
const options = normalizeMoverSetOptions({
...spriteOptions, sprite: {
sizePx: 24, atlas: { url: '/bracket.svg', cells: { a: [0, 0, 1, 1] } },
...spriteOptions,
sprite: {
sizePx: 24,
atlas: { url: '/bracket.svg', cells: { a: [0, 0, 1, 1] } },
},
});
for (const loaded of [false, true]) {
+3 -2
View File
@@ -12,6 +12,7 @@
// FIRMS opts into a separate native-stage overlay before sharpening/AA.
// Only that stage owns an encoded-RGBA8 blend target; W1 stays in place.
import { withFrameUpdates } from './frameDescriptor.js';
import { EffectDesc, Pass as NavaraPass } from '@navaramap/three';
import { GevOverlayPass } from './overlayPass.js';
import { RGBADepthPacking, Scene } from 'three';
@@ -20,7 +21,7 @@ export const OVERLAY_EFFECT_KEY = 'gevOverlay';
const PRE_SHARPEN_KEY = 'gevPreSharpenOverlay';
const registeredViews = new WeakMap();
class GevOverlayEffectDesc extends EffectDesc {
class GevOverlayEffectDesc extends withFrameUpdates(EffectDesc) {
static key = OVERLAY_EFFECT_KEY;
// Navara takes the first existing anchor, so the latest pass comes first.
static insertAfter = ['fxaa', 'smaa', 'gevSharpen', 'toneMapping'];
@@ -56,7 +57,7 @@ class GevOverlayEffectDesc extends EffectDesc {
// Each drawn frame: the scene depth the overlays test against, then each
// overlay's own per-frame work (pixel footprint, origin, viewport).
update(time) {
updateFrame(time) {
const mrt = this.find('mrt');
this.overlay.uniforms.tGevSceneDepth.value =
mrt?.depthBufferPacking === RGBADepthPacking ? mrt.depthBuffer : null;
+3 -2
View File
@@ -20,6 +20,7 @@
// has no public call to merge effects. A disabled pass is skipped: off costs
// nothing.
import { withFrameUpdates } from './frameDescriptor.js';
import { Effect as NavaraEffect, EffectDesc } from '@navaramap/three';
import { Effect, EffectAttribute } from 'postprocessing';
import { RGBADepthPacking, Uniform } from 'three';
@@ -86,7 +87,7 @@ class UnsharpMaskEffect extends Effect {
}
/** config.gevSharpen: { amount } (Cesium's `amount` uniform). */
export class SharpenEffectDesc extends EffectDesc {
export class SharpenEffectDesc extends withFrameUpdates(EffectDesc) {
static key = SHARPEN_EFFECT_KEY;
static insertAfter = ['toneMapping'];
static insertBefore = ['smaa', 'fxaa', 'gevOverlay', 'final'];
@@ -105,7 +106,7 @@ export class SharpenEffectDesc extends EffectDesc {
// Each frame: the scene depth for the sky mask, when Navara packs it as
// RGBA (0.2.1 does); otherwise the whole frame is sharpened.
update() {
updateFrame() {
if (!this.effect) return;
const mrt = this.find('mrt');
const depth =
+1 -1
View File
@@ -230,7 +230,7 @@ function groundEffectClass(EffectDesc, NavaraEffect) {
});
return new NavaraEffect(this.view.camera.raw, this.effect);
}
update() {
updateFrame() {
const state = this.owner.state,
camera = this.view.camera.raw,
mrt = this.find('mrt');
+1 -1
View File
@@ -40,7 +40,7 @@ function createDrapeEffectClass(EffectDesc, NavaraEffect) {
return new NavaraEffect(this.view.camera.raw, this.effect);
}
update() {
updateFrame() {
const mrt = this.find('mrt');
this.owner.globeNormal.value = mrt?.globeNormalBuffer ?? null;
this.owner.globeDepth.value = mrt?.globeDepthBuffer ?? null;
@@ -108,7 +108,7 @@ export function createImageryGround(
});
return new NavaraEffect(this.view.camera.raw, this.effect);
}
update() {
updateFrame() {
if (!this.effect) return;
const camera = this.view.camera.raw,
u = this.effect.uniforms,
@@ -244,7 +244,7 @@ function groundEffectClass(EffectDesc, NavaraEffect) {
});
return new NavaraEffect(this.view.camera.raw, this.effect);
}
update() {
updateFrame() {
const state = this.owner.state,
camera = this.view.camera.raw;
if (!this.effect || !state || !camera) return;
+1 -1
View File
@@ -232,7 +232,7 @@ function descriptor(EffectDesc, NavaraEffect) {
});
return new NavaraEffect(this.view.camera.raw, this.effect);
}
update() {
updateFrame() {
if (!this.effect) return;
const { state, displayT, active, color, pixelRatio } = this.owner;
const camera = this.view.camera.raw;
+1 -1
View File
@@ -118,7 +118,7 @@ function weatherPass(engine, { view, EffectDesc, NavaraEffect }) {
});
return new NavaraEffect(this.view.camera.raw, this.effect);
}
update() {
updateFrame() {
if (!this.effect) return;
const camera = this.view.camera.raw,
u = this.effect.uniforms;
+2 -2
View File
@@ -1,5 +1,5 @@
// Only the renderer and its map survive a switch. The outgoing UI, data feeds,
// tools and their subscriptions are still disposed with their application.
// This cache owns the parked renderer and its map. The layer session owns
// feeds, selection, media and the Director across both renderer attachments.
export function createRendererCache() {
const idle = new Map();
let retainKind;