mirror of
https://github.com/bilawalsidhu/gods-eye-view.git
synced 2026-11-07 20:04:00 +00:00
Release the cockpit cloud WebGL context when its controller is destroyed
This commit is contained in:
1 parent
12cd73bed9
commit
64759c552e
2 files changed
+199
-34
No files matched your search
@@ -199,6 +199,7 @@ class CockpitCloudEffectsController {
|
||||
|
||||
this.gl = null;
|
||||
this.program = null;
|
||||
this.buffer = null;
|
||||
this.locations = null;
|
||||
this.frame = null;
|
||||
this.refreshTimer = null;
|
||||
@@ -275,8 +276,9 @@ class CockpitCloudEffectsController {
|
||||
}
|
||||
|
||||
initializeRenderer() {
|
||||
let gl = null;
|
||||
try {
|
||||
const gl = this.canvas.getContext('webgl', {
|
||||
gl = this.canvas.getContext('webgl', {
|
||||
alpha: true,
|
||||
antialias: false,
|
||||
depth: false,
|
||||
@@ -318,6 +320,7 @@ class CockpitCloudEffectsController {
|
||||
|
||||
this.gl = gl;
|
||||
this.program = program;
|
||||
this.buffer = buffer;
|
||||
this.locations = {
|
||||
resolution: gl.getUniformLocation(program, 'uResolution'),
|
||||
time: gl.getUniformLocation(program, 'uTime'),
|
||||
@@ -327,6 +330,7 @@ class CockpitCloudEffectsController {
|
||||
} catch (error) {
|
||||
console.warn('[Cockpit clouds] Renderer unavailable:', error);
|
||||
this.canvas.dataset.status = 'unavailable';
|
||||
if (gl && !this.gl) gl.getExtension('WEBGL_lose_context')?.loseContext();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -605,9 +609,26 @@ class CockpitCloudEffectsController {
|
||||
'gev:cockpit-weather-toggle',
|
||||
this.onEnabledChange,
|
||||
);
|
||||
if (this.program && this.gl) this.gl.deleteProgram(this.program);
|
||||
this.releaseRenderer();
|
||||
this.canvas.remove();
|
||||
}
|
||||
|
||||
/**
|
||||
* Free the GPU objects and the canvas's WebGL context. The application is
|
||||
* rebuilt on every globe switch, and browsers cap live contexts, so a
|
||||
* destroyed controller must not leave its context for garbage collection.
|
||||
*/
|
||||
releaseRenderer() {
|
||||
const gl = this.gl;
|
||||
this.gl = null;
|
||||
this.locations = null;
|
||||
if (!gl) return;
|
||||
if (this.buffer) gl.deleteBuffer(this.buffer);
|
||||
if (this.program) gl.deleteProgram(this.program);
|
||||
this.buffer = null;
|
||||
this.program = null;
|
||||
gl.getExtension('WEBGL_lose_context')?.loseContext();
|
||||
}
|
||||
}
|
||||
|
||||
export function initCockpitCloudEffects(viewer, options) {
|
||||
|
||||
@@ -1,19 +1,32 @@
|
||||
import test from 'node:test';
|
||||
import assert from 'node:assert/strict';
|
||||
import {
|
||||
initCockpitCloudEffects,
|
||||
cockpitCloudRenderSize,
|
||||
cockpitWeatherRefreshDue,
|
||||
cockpitWeatherEnabledFromStoredValue,
|
||||
} from './cockpitCloudEffects.js';
|
||||
|
||||
test('cockpit cloud framebuffer stays low resolution on large displays', () => {
|
||||
assert.deepEqual(cockpitCloudRenderSize(2048, 1152), { width: 520, height: 293 });
|
||||
assert.deepEqual(cockpitCloudRenderSize(1280, 720), { width: 520, height: 293 });
|
||||
assert.deepEqual(cockpitCloudRenderSize(2048, 1152), {
|
||||
width: 520,
|
||||
height: 293,
|
||||
});
|
||||
assert.deepEqual(cockpitCloudRenderSize(1280, 720), {
|
||||
width: 520,
|
||||
height: 293,
|
||||
});
|
||||
});
|
||||
|
||||
test('cockpit cloud framebuffer never upscales or collapses to zero', () => {
|
||||
assert.deepEqual(cockpitCloudRenderSize(640, 360), { width: 269, height: 151 });
|
||||
assert.deepEqual(cockpitCloudRenderSize(0, Number.NaN), { width: 1, height: 1 });
|
||||
assert.deepEqual(cockpitCloudRenderSize(640, 360), {
|
||||
width: 269,
|
||||
height: 151,
|
||||
});
|
||||
assert.deepEqual(cockpitCloudRenderSize(0, Number.NaN), {
|
||||
width: 1,
|
||||
height: 1,
|
||||
});
|
||||
});
|
||||
|
||||
test('cockpit weather defaults off and enables only from an explicit saved opt-in', () => {
|
||||
@@ -25,32 +38,163 @@ test('cockpit weather defaults off and enables only from an explicit saved opt-i
|
||||
|
||||
test('cockpit weather refreshes after time or meaningful movement', () => {
|
||||
const anchor = { latitude: 30, longitude: -97 };
|
||||
assert.equal(cockpitWeatherRefreshDue({
|
||||
nowMs: 1000,
|
||||
fetchedAt: 500,
|
||||
anchor,
|
||||
point: anchor,
|
||||
hasWeather: false,
|
||||
}), true);
|
||||
assert.equal(cockpitWeatherRefreshDue({
|
||||
nowMs: 60_000,
|
||||
fetchedAt: 0,
|
||||
anchor,
|
||||
point: { latitude: 30.01, longitude: -97 },
|
||||
hasWeather: true,
|
||||
}), false);
|
||||
assert.equal(cockpitWeatherRefreshDue({
|
||||
nowMs: 5 * 60_000,
|
||||
fetchedAt: 0,
|
||||
anchor,
|
||||
point: anchor,
|
||||
hasWeather: true,
|
||||
}), true);
|
||||
assert.equal(cockpitWeatherRefreshDue({
|
||||
nowMs: 60_000,
|
||||
fetchedAt: 0,
|
||||
anchor,
|
||||
point: { latitude: 30.3, longitude: -97 },
|
||||
hasWeather: true,
|
||||
}), true);
|
||||
assert.equal(
|
||||
cockpitWeatherRefreshDue({
|
||||
nowMs: 1000,
|
||||
fetchedAt: 500,
|
||||
anchor,
|
||||
point: anchor,
|
||||
hasWeather: false,
|
||||
}),
|
||||
true,
|
||||
);
|
||||
assert.equal(
|
||||
cockpitWeatherRefreshDue({
|
||||
nowMs: 60_000,
|
||||
fetchedAt: 0,
|
||||
anchor,
|
||||
point: { latitude: 30.01, longitude: -97 },
|
||||
hasWeather: true,
|
||||
}),
|
||||
false,
|
||||
);
|
||||
assert.equal(
|
||||
cockpitWeatherRefreshDue({
|
||||
nowMs: 5 * 60_000,
|
||||
fetchedAt: 0,
|
||||
anchor,
|
||||
point: anchor,
|
||||
hasWeather: true,
|
||||
}),
|
||||
true,
|
||||
);
|
||||
assert.equal(
|
||||
cockpitWeatherRefreshDue({
|
||||
nowMs: 60_000,
|
||||
fetchedAt: 0,
|
||||
anchor,
|
||||
point: { latitude: 30.3, longitude: -97 },
|
||||
hasWeather: true,
|
||||
}),
|
||||
true,
|
||||
);
|
||||
});
|
||||
|
||||
// A WebGL context that records what is allocated, freed and lost.
|
||||
function fakeWebGl() {
|
||||
const calls = [];
|
||||
const gl = {
|
||||
VERTEX_SHADER: 1,
|
||||
FRAGMENT_SHADER: 2,
|
||||
ARRAY_BUFFER: 3,
|
||||
STATIC_DRAW: 4,
|
||||
FLOAT: 5,
|
||||
COLOR_BUFFER_BIT: 6,
|
||||
LINK_STATUS: 7,
|
||||
COMPILE_STATUS: 8,
|
||||
calls,
|
||||
createShader: () => ({}),
|
||||
shaderSource() {},
|
||||
compileShader() {},
|
||||
getShaderParameter: () => true,
|
||||
getShaderInfoLog: () => '',
|
||||
deleteShader() {},
|
||||
createProgram: () => ({ program: true }),
|
||||
attachShader() {},
|
||||
linkProgram() {},
|
||||
getProgramParameter: () => true,
|
||||
getProgramInfoLog: () => '',
|
||||
deleteProgram: (program) => calls.push(['deleteProgram', program]),
|
||||
createBuffer: () => ({ buffer: true }),
|
||||
bindBuffer() {},
|
||||
bufferData() {},
|
||||
deleteBuffer: (buffer) => calls.push(['deleteBuffer', buffer]),
|
||||
getAttribLocation: () => 0,
|
||||
enableVertexAttribArray() {},
|
||||
vertexAttribPointer() {},
|
||||
getUniformLocation: () => ({}),
|
||||
viewport() {},
|
||||
clearColor() {},
|
||||
clear() {},
|
||||
getExtension: (name) =>
|
||||
name === 'WEBGL_lose_context'
|
||||
? { loseContext: () => calls.push(['loseContext']) }
|
||||
: null,
|
||||
};
|
||||
return gl;
|
||||
}
|
||||
|
||||
test('a destroyed cockpit cloud controller frees its GPU objects and WebGL context', () => {
|
||||
const saved = Object.fromEntries(
|
||||
[
|
||||
'document',
|
||||
'window',
|
||||
'localStorage',
|
||||
'CustomEvent',
|
||||
'requestAnimationFrame',
|
||||
'cancelAnimationFrame',
|
||||
].map((name) => [name, globalThis[name]]),
|
||||
);
|
||||
const gl = fakeWebGl();
|
||||
let contexts = 0;
|
||||
const canvas = {
|
||||
dataset: {},
|
||||
classList: { add() {}, remove() {} },
|
||||
setAttribute() {},
|
||||
remove() {
|
||||
canvas.removed = true;
|
||||
},
|
||||
getContext() {
|
||||
contexts += 1;
|
||||
return gl;
|
||||
},
|
||||
};
|
||||
globalThis.document = {
|
||||
createElement: () => canvas,
|
||||
body: { appendChild() {}, classList: { contains: () => true } },
|
||||
};
|
||||
globalThis.window = {
|
||||
innerWidth: 1280,
|
||||
innerHeight: 720,
|
||||
matchMedia: () => ({ matches: false }),
|
||||
addEventListener() {},
|
||||
removeEventListener() {},
|
||||
dispatchEvent() {},
|
||||
setTimeout: () => 1,
|
||||
clearTimeout() {},
|
||||
};
|
||||
globalThis.localStorage = { getItem: () => '1', setItem() {} };
|
||||
globalThis.CustomEvent = class {
|
||||
constructor(type, init) {
|
||||
this.type = type;
|
||||
this.detail = init?.detail;
|
||||
}
|
||||
};
|
||||
globalThis.requestAnimationFrame = () => 7;
|
||||
globalThis.cancelAnimationFrame = () => {};
|
||||
try {
|
||||
const clouds = initCockpitCloudEffects(null, { weatherService: {} });
|
||||
clouds.start();
|
||||
clouds.stop();
|
||||
clouds.start();
|
||||
assert.equal(
|
||||
contexts,
|
||||
1,
|
||||
'one context per controller, reused across starts',
|
||||
);
|
||||
const { program, buffer } = clouds;
|
||||
clouds.destroy();
|
||||
assert.deepEqual(gl.calls, [
|
||||
['deleteBuffer', buffer],
|
||||
['deleteProgram', program],
|
||||
['loseContext'],
|
||||
]);
|
||||
assert.equal(clouds.gl, null);
|
||||
assert.equal(canvas.removed, true);
|
||||
} finally {
|
||||
for (const [name, value] of Object.entries(saved)) {
|
||||
if (value === undefined) delete globalThis[name];
|
||||
else globalThis[name] = value;
|
||||
}
|
||||
}
|
||||
});
|
||||
Reference in new issue
Block a user