mirror of
https://github.com/mauriceboe/TREK.git
synced 2026-11-12 23:31:02 +00:00
e28bd2fc99
MapLibre 6 and Mapbox GL 3 both throw out of their Map constructor without WebGL2. Every place that builds a GL map now asks one helper first, components/Map/glSupport.ts, and a browser without WebGL2 gets the Leaflet map with the user's tiles instead of a map that fails to start: - the trip map (MapViewAuto) and the journey maps (JourneyMapAuto) pick their renderer through useGlRenderer, which folds in the Mapbox token rule they each had; - the settings preview, desktop and phone, shows the Leaflet preview through useMapSettingsForm and says why under it; - the tour planner keeps the 2D map when 3D is asked for and says that 3D needs WebGL2; - the PDF route map and the Leaflet vector basemap already fell back and now report a GPU failure too. Behind the probe, buildGlMap wraps each constructor (MapViewGL, JourneyMapGL, the settings preview, the terrain map, the PDF map). A GPU that claims WebGL2 and still refuses the context is reported through utils/webgl, which turns the answer to no and tells every mounted map, so they switch to Leaflet without a reload; any other error is still thrown. Two new strings, map.webgl2.fallback and map.webgl2.terrain, in all 27 locales.
103 lines
3.8 KiB
TypeScript
103 lines
3.8 KiB
TypeScript
/**
|
|
* Whether this browser will hand a GL map a WebGL2 context. MapLibre 6 and
|
|
* Mapbox GL 3 both ask for webgl2 and nothing else.
|
|
*
|
|
* The default basemap has been a vector style since 4.1.0, and maplibre-gl builds
|
|
* its GL context inside the Map constructor: no context, no map, and the throw
|
|
* lands in a place no error boundary was ever meant to see (#2288). Asking a
|
|
* one-pixel probe first is cheaper and quieter than finding out from a stack
|
|
* trace, and it keeps the maplibre chunk, about a megabyte, off the wire for a
|
|
* browser that could never have used it.
|
|
*
|
|
* "Cannot" is a bigger group than broken hardware: hardware acceleration turned
|
|
* off in Chrome or Firefox, webgl.disabled in about:config, a driver on the
|
|
* browser's blocklist, a VM or remote desktop with no GL at all, and privacy
|
|
* builds that switch it off against fingerprinting.
|
|
*/
|
|
|
|
/**
|
|
* Probed once per page load, then remembered.
|
|
*
|
|
* A repeat probe costs a live WebGL context each time, and every browser caps how
|
|
* many it keeps: Chrome evicts the oldest at around sixteen. On a planner with a
|
|
* map and a settings preview open, a chatty probe is a map going blank.
|
|
*/
|
|
let probed: boolean | null = null
|
|
|
|
export function hasWebGL(): boolean {
|
|
if (probed === null) probed = probe()
|
|
return probed
|
|
}
|
|
|
|
const listeners = new Set<() => void>()
|
|
let failures = 0
|
|
|
|
/**
|
|
* A GL map that failed to build although the probe said yes: a GPU that offers
|
|
* WebGL2 and then cannot create the context, or a context budget that ran out.
|
|
* From here on the answer is no, and every map that asked is told, so it can
|
|
* redraw itself without WebGL instead of waiting for the next page load.
|
|
*/
|
|
export function reportWebGLFailure(): void {
|
|
if (probed === false) return
|
|
probed = false
|
|
failures++
|
|
for (const listener of listeners) listener()
|
|
}
|
|
|
|
/** For useSyncExternalStore: tells a map when reportWebGLFailure changed the answer. */
|
|
export function subscribeWebGL(listener: () => void): () => void {
|
|
listeners.add(listener)
|
|
return () => {
|
|
listeners.delete(listener)
|
|
}
|
|
}
|
|
|
|
/** For useSyncExternalStore: changes each time reportWebGLFailure turned the answer to no. */
|
|
export function webGLFailures(): number {
|
|
return failures
|
|
}
|
|
|
|
/**
|
|
* Test seam. The app never clears this, it only turns it to no through
|
|
* reportWebGLFailure: a browser does not gain WebGL2 mid-session.
|
|
*/
|
|
export function resetWebGLProbe(): void {
|
|
probed = null
|
|
}
|
|
|
|
function probe(): boolean {
|
|
// jsdom and anything SSR-ish have no canvas implementation, and "no WebGL" is
|
|
// the honest answer for them rather than a special case.
|
|
if (typeof document === 'undefined') return false
|
|
|
|
try {
|
|
const canvas = document.createElement('canvas')
|
|
// One pixel. The probe never draws, and a full backing store would allocate
|
|
// megabytes for an answer that is one bit wide.
|
|
canvas.width = 1
|
|
canvas.height = 1
|
|
|
|
// Exactly what maplibre-gl asks for (Map._setupPainter): WebGL2 and nothing
|
|
// else, since MapLibre 6 dropped WebGL1. A browser that only offers WebGL1
|
|
// passes a looser probe and then throws GPUInitializationError out of the Map
|
|
// constructor, which is the crash this probe exists to prevent.
|
|
const gl = canvas.getContext('webgl2')
|
|
if (!gl) {
|
|
console.warn('[basemap] this browser refuses a WebGL2 context, vector basemaps are unavailable')
|
|
return false
|
|
}
|
|
|
|
// Hand the context back now instead of waiting for the canvas to be
|
|
// collected. Contexts are a capped resource and the GC is under no obligation
|
|
// to hurry, so a probe that keeps one is a context the map behind it may not
|
|
// get.
|
|
gl.getExtension('WEBGL_lose_context')?.loseContext()
|
|
return true
|
|
} catch {
|
|
// A few locked-down builds throw out of getContext rather than returning
|
|
// null, and an answer is never worth an exception.
|
|
return false
|
|
}
|
|
}
|