mirror of
https://github.com/bilawalsidhu/gods-eye-view.git
synced 2026-11-04 10:24:02 +00:00
Lose the throwaway WebGL2 probe context right after the link probe instead of leaving it for GC; browsers (Safari especially) cap live contexts. Note that affected devices also lose globe-basemap fog, credit the changelog entry and move it to the top. Co-Authored-By: Claude Opus 5.5 (1M context) <[email protected]>
190 lines
7.1 KiB
JavaScript
190 lines
7.1 KiB
JavaScript
/**
|
|
* Workaround for Apple's Metal/ANGLE shader translator rejecting Cesium's
|
|
* per-vertex model atmosphere.
|
|
*
|
|
* Cesium's `AtmosphereStageVS` calls a function whose `out` parameters are
|
|
* bound DIRECTLY to varyings:
|
|
*
|
|
* czm_computeGroundAtmosphereScattering(
|
|
* v_positionWC, lightDirection,
|
|
* v_atmosphereRayleighColor, v_atmosphereMieColor, v_atmosphereOpacity);
|
|
*
|
|
* ANGLE's Metal backend wraps each `out` argument in an `ANGLE_out(...)`
|
|
* helper that takes a `thread T &`. Vertex-output struct members live in
|
|
* `__metal_generic`, which cannot bind to `thread`, so the program fails to
|
|
* LINK (not compile) and Cesium tears the render loop down:
|
|
*
|
|
* RuntimeError: Program failed to link. ... MSL compilation error:
|
|
* reference to type 'thread float3' could not bind to an lvalue of type
|
|
* '__metal_generic float3'
|
|
*
|
|
* This is the only shader in Cesium that binds varyings to `out` params --
|
|
* SkyAtmosphere and the ground-atmosphere fragment path both route through
|
|
* locals first, so they are unaffected and stay on.
|
|
*
|
|
* Cesium attaches that stage in `ModelSceneGraph.configurePipeline` under a
|
|
* single condition:
|
|
*
|
|
* const fogRenderable = frameState.fog.enabled && frameState.fog.renderable;
|
|
* if (fogRenderable) modelPipelineStages.push(AtmospherePipelineStage);
|
|
*
|
|
* so clearing `scene.fog.renderable` keeps the broken stage out of the
|
|
* pipeline entirely -- the program is never built, so it cannot fail to link.
|
|
*
|
|
* We clear `renderable` rather than `enabled` deliberately:
|
|
* - `Fog.update` still computes fog density when only `renderable` is off,
|
|
* preserving the distance-based screen-space-error scaling that governs
|
|
* 3D Tiles refinement. Clearing `enabled` returns early and forfeits it.
|
|
* - `visualSettings.js` saves and restores `scene.fog.enabled` around IR
|
|
* boost. Parking our fix on `enabled` would let that restore resurrect
|
|
* the broken shader mid-session; `renderable` is orthogonal to it.
|
|
*
|
|
* @module app/atmosphereCompat
|
|
*/
|
|
|
|
/** Minimal vertex shader reproducing the exact out-param-to-varying bind. */
|
|
const PROBE_VERTEX_SHADER = `#version 300 es
|
|
precision highp float;
|
|
in vec3 a_position;
|
|
out vec3 v_probe;
|
|
void writeOut(out vec3 value) { value = vec3(1.0); }
|
|
void main() {
|
|
writeOut(v_probe);
|
|
gl_Position = vec4(a_position, 1.0);
|
|
}`;
|
|
|
|
const PROBE_FRAGMENT_SHADER = `#version 300 es
|
|
precision highp float;
|
|
in vec3 v_probe;
|
|
out vec4 fragColor;
|
|
void main() { fragColor = vec4(v_probe, 1.0); }`;
|
|
|
|
/**
|
|
* True when the user agent is iOS or iPadOS.
|
|
*
|
|
* iPadOS 13+ defaults to desktop-mode browsing and reports `MacIntel` with no
|
|
* iPad token, so it is identified by touch points instead -- real Macs report
|
|
* `maxTouchPoints` of 0, including Apple Silicon ones that also claim
|
|
* `MacIntel`. Brave/Chrome/Firefox on iOS are all WebKit underneath and share
|
|
* the same Metal backend, so matching the platform rather than the browser is
|
|
* what we want.
|
|
*/
|
|
export function isAppleMobilePlatform(navigatorLike = globalThis.navigator) {
|
|
if (!navigatorLike) return false;
|
|
const ua = String(navigatorLike.userAgent || '');
|
|
if (/\b(iPad|iPhone|iPod)\b/.test(ua)) return true;
|
|
// Desktop-mode iPadOS presents as a Mac. What separates it from a real Mac
|
|
// is touch: Macs report 0 touch points, including Apple Silicon ones that
|
|
// also claim `MacIntel`. `navigator.platform` is deprecated and may be
|
|
// empty, so the UA's Macintosh token is accepted as an equal signal rather
|
|
// than relying on platform alone.
|
|
const platform = String(navigatorLike.platform || '');
|
|
const macLike =
|
|
platform === 'MacIntel' ||
|
|
platform === 'MacARM' ||
|
|
/\bMacintosh\b/.test(ua);
|
|
const touchPoints = Number(navigatorLike.maxTouchPoints) || 0;
|
|
return macLike && touchPoints > 1;
|
|
}
|
|
|
|
/**
|
|
* Link the probe program and report whether the driver rejects the pattern.
|
|
*
|
|
* Returns `false` when WebGL2 is unavailable so that a missing context is
|
|
* never mistaken for a broken driver -- Cesium has its own handling for that,
|
|
* and the caller still falls back to platform detection.
|
|
*/
|
|
export function probeOutParamVaryingLinkFailure(gl) {
|
|
if (!gl) return false;
|
|
const vs = gl.createShader(gl.VERTEX_SHADER);
|
|
const fs = gl.createShader(gl.FRAGMENT_SHADER);
|
|
const program = gl.createProgram();
|
|
if (!vs || !fs || !program) return false;
|
|
try {
|
|
gl.shaderSource(vs, PROBE_VERTEX_SHADER);
|
|
gl.compileShader(vs);
|
|
gl.shaderSource(fs, PROBE_FRAGMENT_SHADER);
|
|
gl.compileShader(fs);
|
|
// A compile failure here is not the bug we are probing for: the driver
|
|
// rejects this pattern at LINK time, after both stages compile cleanly.
|
|
if (
|
|
!gl.getShaderParameter(vs, gl.COMPILE_STATUS) ||
|
|
!gl.getShaderParameter(fs, gl.COMPILE_STATUS)
|
|
)
|
|
return false;
|
|
gl.attachShader(program, vs);
|
|
gl.attachShader(program, fs);
|
|
gl.linkProgram(program);
|
|
return !gl.getProgramParameter(program, gl.LINK_STATUS);
|
|
} catch {
|
|
return false;
|
|
} finally {
|
|
gl.deleteShader(vs);
|
|
gl.deleteShader(fs);
|
|
gl.deleteProgram(program);
|
|
}
|
|
}
|
|
|
|
/** Build a throwaway WebGL2 context for the probe, or null if unavailable. */
|
|
function defaultProbeContext() {
|
|
try {
|
|
const canvas =
|
|
typeof OffscreenCanvas === 'function'
|
|
? new OffscreenCanvas(1, 1)
|
|
: globalThis.document?.createElement('canvas');
|
|
return canvas?.getContext('webgl2') ?? null;
|
|
} catch {
|
|
return null;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Decide whether the per-vertex model atmosphere must be kept out of the
|
|
* pipeline on this device.
|
|
*
|
|
* The probe is authoritative when it fires, so a future driver fix silently
|
|
* restores the effect without a code change. Platform detection is the
|
|
* backstop for the case where no context can be created -- on Apple mobile a
|
|
* link failure is fatal and unrecoverable, so we would rather lose fog than
|
|
* gamble the whole render loop on the probe having run.
|
|
*/
|
|
export function shouldDisableModelAtmosphere({
|
|
navigatorLike = globalThis.navigator,
|
|
createProbeContext = defaultProbeContext,
|
|
} = {}) {
|
|
const gl = createProbeContext();
|
|
if (gl) {
|
|
let linkFails;
|
|
try {
|
|
linkFails = probeOutParamVaryingLinkFailure(gl);
|
|
} finally {
|
|
// Browsers cap live WebGL contexts (Safari especially); release the
|
|
// throwaway probe context now rather than waiting for GC.
|
|
try {
|
|
gl.getExtension?.('WEBGL_lose_context')?.loseContext();
|
|
} catch {
|
|
// A context that cannot be released is left to GC.
|
|
}
|
|
}
|
|
if (linkFails) return true;
|
|
// Probe ran and the driver accepted the pattern. Apple mobile is still
|
|
// quarantined: the probe program is far simpler than Cesium's, and a
|
|
// false negative there costs the entire scene.
|
|
return isAppleMobilePlatform(navigatorLike);
|
|
}
|
|
return isAppleMobilePlatform(navigatorLike);
|
|
}
|
|
|
|
/**
|
|
* Keep Cesium's broken model-atmosphere stage out of the pipeline.
|
|
*
|
|
* Returns true when the workaround was applied. Safe to call on any scene;
|
|
* scenes without a fog object are left untouched.
|
|
*/
|
|
export function applyModelAtmosphereWorkaround(scene, options = {}) {
|
|
if (!scene?.fog) return false;
|
|
if (!shouldDisableModelAtmosphere(options)) return false;
|
|
scene.fog.renderable = false;
|
|
return true;
|
|
}
|