Files
gods-eye-view/src/globe/navara/frameDescriptor.test.mjs
T
German Khokhlov ebc717df8b Dispose a Navara layer effect's pass when the layer detaches so engine switches stop retaining its material and textures
Layers re-create their ground effects on every attach. Navara's removePass leaves the EffectPass undisposed, so each switch kept its EffectMaterial in three's program and material caches, together with the uniforms' DataTextures and their Float32Array data (earthquakes: about 47 KB of buffers and one WebGL program per switch). engine.LayerEffectDesc now disposes the raw pass on delete, as the app's own effects do.
2026-11-04 00:07:15 +04:00

127 lines
3.3 KiB
JavaScript

import assert from 'node:assert/strict';
import test from 'node:test';
import {
layerEffectDesc,
withFrameUpdates,
withPassDisposal,
} 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);
});
test('a deleted effect disposes the pass the composer no longer holds', () => {
const events = [];
const pass = { dispose: () => events.push('dispose') };
class Base {
_instance = { rawPass: pass };
get raw() {
return this._instance;
}
onDestroy() {
events.push('remove');
this._instance = undefined;
}
}
const effect = new (withPassDisposal(Base))();
effect.onDestroy();
assert.deepEqual(events, ['remove', 'dispose']);
// A pass that was never created: nothing to dispose.
const empty = new (withPassDisposal(
class {
onDestroy() {}
},
))();
assert.doesNotThrow(() => empty.onDestroy());
// The pass is disposed even when Navara's removal throws.
class Failing extends Base {
onDestroy() {
throw Error('removal failed');
}
}
events.length = 0;
assert.throws(() => new (withPassDisposal(Failing))().onDestroy(), /removal/);
assert.deepEqual(events, ['dispose']);
});
test('a layer effect re-created on every attach leaves no pass, material or frame callback behind', () => {
const frames = new Set();
const view = {
on: (name, cb) => frames.add(cb),
off: (name, cb) => frames.delete(cb),
};
const live = new Set();
class EffectDesc {
view = view;
onCreate() {
const pass = {
material: {},
dispose: () => live.delete(pass),
};
live.add(pass);
this._instance = { rawPass: pass };
}
get raw() {
return this._instance;
}
onDestroy() {
// Navara's removePass: out of the composer, not disposed.
this._instance = undefined;
}
}
// As the earthquakes ground effect: disposes its own effect, then super.
class LayerEffect extends layerEffectDesc(EffectDesc) {
updateFrame() {}
onDestroy() {
this.effectDisposed = true;
super.onDestroy();
}
}
for (let attach = 0; attach < 50; attach++) {
const effect = new LayerEffect();
effect.onCreate();
effect.onDestroy();
assert.equal(effect.effectDisposed, true);
}
assert.equal(live.size, 0, 'every pass of a deleted effect is disposed');
assert.equal(frames.size, 0, 'no frame callback outlives its effect');
});