mirror of
https://github.com/bilawalsidhu/gods-eye-view.git
synced 2026-11-12 06:12:18 +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.gl = null;
|
||||||
this.program = null;
|
this.program = null;
|
||||||
|
this.buffer = null;
|
||||||
this.locations = null;
|
this.locations = null;
|
||||||
this.frame = null;
|
this.frame = null;
|
||||||
this.refreshTimer = null;
|
this.refreshTimer = null;
|
||||||
@@ -275,8 +276,9 @@ class CockpitCloudEffectsController {
|
|||||||
}
|
}
|
||||||
|
|
||||||
initializeRenderer() {
|
initializeRenderer() {
|
||||||
|
let gl = null;
|
||||||
try {
|
try {
|
||||||
const gl = this.canvas.getContext('webgl', {
|
gl = this.canvas.getContext('webgl', {
|
||||||
alpha: true,
|
alpha: true,
|
||||||
antialias: false,
|
antialias: false,
|
||||||
depth: false,
|
depth: false,
|
||||||
@@ -318,6 +320,7 @@ class CockpitCloudEffectsController {
|
|||||||
|
|
||||||
this.gl = gl;
|
this.gl = gl;
|
||||||
this.program = program;
|
this.program = program;
|
||||||
|
this.buffer = buffer;
|
||||||
this.locations = {
|
this.locations = {
|
||||||
resolution: gl.getUniformLocation(program, 'uResolution'),
|
resolution: gl.getUniformLocation(program, 'uResolution'),
|
||||||
time: gl.getUniformLocation(program, 'uTime'),
|
time: gl.getUniformLocation(program, 'uTime'),
|
||||||
@@ -327,6 +330,7 @@ class CockpitCloudEffectsController {
|
|||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.warn('[Cockpit clouds] Renderer unavailable:', error);
|
console.warn('[Cockpit clouds] Renderer unavailable:', error);
|
||||||
this.canvas.dataset.status = 'unavailable';
|
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',
|
'gev:cockpit-weather-toggle',
|
||||||
this.onEnabledChange,
|
this.onEnabledChange,
|
||||||
);
|
);
|
||||||
if (this.program && this.gl) this.gl.deleteProgram(this.program);
|
this.releaseRenderer();
|
||||||
this.canvas.remove();
|
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) {
|
export function initCockpitCloudEffects(viewer, options) {
|
||||||
|
|||||||
@@ -1,19 +1,32 @@
|
|||||||
import test from 'node:test';
|
import test from 'node:test';
|
||||||
import assert from 'node:assert/strict';
|
import assert from 'node:assert/strict';
|
||||||
import {
|
import {
|
||||||
|
initCockpitCloudEffects,
|
||||||
cockpitCloudRenderSize,
|
cockpitCloudRenderSize,
|
||||||
cockpitWeatherRefreshDue,
|
cockpitWeatherRefreshDue,
|
||||||
cockpitWeatherEnabledFromStoredValue,
|
cockpitWeatherEnabledFromStoredValue,
|
||||||
} from './cockpitCloudEffects.js';
|
} from './cockpitCloudEffects.js';
|
||||||
|
|
||||||
test('cockpit cloud framebuffer stays low resolution on large displays', () => {
|
test('cockpit cloud framebuffer stays low resolution on large displays', () => {
|
||||||
assert.deepEqual(cockpitCloudRenderSize(2048, 1152), { width: 520, height: 293 });
|
assert.deepEqual(cockpitCloudRenderSize(2048, 1152), {
|
||||||
assert.deepEqual(cockpitCloudRenderSize(1280, 720), { width: 520, height: 293 });
|
width: 520,
|
||||||
|
height: 293,
|
||||||
|
});
|
||||||
|
assert.deepEqual(cockpitCloudRenderSize(1280, 720), {
|
||||||
|
width: 520,
|
||||||
|
height: 293,
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
test('cockpit cloud framebuffer never upscales or collapses to zero', () => {
|
test('cockpit cloud framebuffer never upscales or collapses to zero', () => {
|
||||||
assert.deepEqual(cockpitCloudRenderSize(640, 360), { width: 269, height: 151 });
|
assert.deepEqual(cockpitCloudRenderSize(640, 360), {
|
||||||
assert.deepEqual(cockpitCloudRenderSize(0, Number.NaN), { width: 1, height: 1 });
|
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', () => {
|
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', () => {
|
test('cockpit weather refreshes after time or meaningful movement', () => {
|
||||||
const anchor = { latitude: 30, longitude: -97 };
|
const anchor = { latitude: 30, longitude: -97 };
|
||||||
assert.equal(cockpitWeatherRefreshDue({
|
assert.equal(
|
||||||
nowMs: 1000,
|
cockpitWeatherRefreshDue({
|
||||||
fetchedAt: 500,
|
nowMs: 1000,
|
||||||
anchor,
|
fetchedAt: 500,
|
||||||
point: anchor,
|
anchor,
|
||||||
hasWeather: false,
|
point: anchor,
|
||||||
}), true);
|
hasWeather: false,
|
||||||
assert.equal(cockpitWeatherRefreshDue({
|
}),
|
||||||
nowMs: 60_000,
|
true,
|
||||||
fetchedAt: 0,
|
);
|
||||||
anchor,
|
assert.equal(
|
||||||
point: { latitude: 30.01, longitude: -97 },
|
cockpitWeatherRefreshDue({
|
||||||
hasWeather: true,
|
nowMs: 60_000,
|
||||||
}), false);
|
fetchedAt: 0,
|
||||||
assert.equal(cockpitWeatherRefreshDue({
|
anchor,
|
||||||
nowMs: 5 * 60_000,
|
point: { latitude: 30.01, longitude: -97 },
|
||||||
fetchedAt: 0,
|
hasWeather: true,
|
||||||
anchor,
|
}),
|
||||||
point: anchor,
|
false,
|
||||||
hasWeather: true,
|
);
|
||||||
}), true);
|
assert.equal(
|
||||||
assert.equal(cockpitWeatherRefreshDue({
|
cockpitWeatherRefreshDue({
|
||||||
nowMs: 60_000,
|
nowMs: 5 * 60_000,
|
||||||
fetchedAt: 0,
|
fetchedAt: 0,
|
||||||
anchor,
|
anchor,
|
||||||
point: { latitude: 30.3, longitude: -97 },
|
point: anchor,
|
||||||
hasWeather: true,
|
hasWeather: true,
|
||||||
}), 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