mirror of
https://github.com/bilawalsidhu/gods-eye-view.git
synced 2026-11-14 23:32:20 +00:00
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.
127 lines
3.3 KiB
JavaScript
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');
|
|
});
|