Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
36 changes: 36 additions & 0 deletions src/css/maplibre-gl.css
Original file line number Diff line number Diff line change
Expand Up @@ -905,3 +905,39 @@ a.maplibregl-ctrl-logo.maplibregl-compact {
left: 0 !important;
z-index: 99999;
}

.maplibregl-webgl-error {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
display: flex;
align-items: center;
justify-content: center;
background: #f0f0f0;
color: #333;
font: 14px/1.5 -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;
text-align: center;
padding: 20px;
box-sizing: border-box;
}

/* stylelint-disable-next-line no-descending-specificity */
.maplibregl-webgl-error a {
color: #0078a8;
}

.maplibregl-webgl-error .maplibregl-webgl-error-short {
display: none;
}

@media (max-width: 480px) {
.maplibregl-webgl-error .maplibregl-webgl-error-full {
display: none;
}

.maplibregl-webgl-error .maplibregl-webgl-error-short {
display: block;
}
}
78 changes: 32 additions & 46 deletions src/geo/projection/globe_projection_error_measurement.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,6 @@ import {SegmentVector} from '../../data/segment';
import {PosArray, TriangleIndexArray} from '../../data/array_types.g';
import posAttributes from '../../data/pos_attributes';
import {type Framebuffer} from '../../webgl/framebuffer';
import {isWebGL2} from '../../webgl/webgl2';
import {type ProjectionGPUContext} from './projection';

/**
Expand Down Expand Up @@ -110,12 +109,10 @@ export class ProjectionErrorMeasurement {
this._fbo = context.createFramebuffer(this._texWidth, this._texHeight, false, false);
this._fbo.colorAttachment.set(texture);

if (isWebGL2(gl)) {
this._pbo = gl.createBuffer();
gl.bindBuffer(gl.PIXEL_PACK_BUFFER, this._pbo);
gl.bufferData(gl.PIXEL_PACK_BUFFER, 4, gl.STREAM_READ);
gl.bindBuffer(gl.PIXEL_PACK_BUFFER, null);
}
this._pbo = gl.createBuffer();
gl.bindBuffer(gl.PIXEL_PACK_BUFFER, this._pbo);
gl.bufferData(gl.PIXEL_PACK_BUFFER, 4, gl.STREAM_READ);
gl.bindBuffer(gl.PIXEL_PACK_BUFFER, null);
}

public destroy() {
Expand Down Expand Up @@ -175,55 +172,44 @@ export class ProjectionErrorMeasurement {
'$clipping', this._fullscreenTriangle.vertexBuffer, this._fullscreenTriangle.indexBuffer,
this._fullscreenTriangle.segments);

if (this._pbo && isWebGL2(gl)) {
// Read back into PBO
gl.bindBuffer(gl.PIXEL_PACK_BUFFER, this._pbo);
gl.readBuffer(gl.COLOR_ATTACHMENT0);
gl.readPixels(0, 0, this._texWidth, this._texHeight, this._texFormat, this._texType, 0);
gl.bindBuffer(gl.PIXEL_PACK_BUFFER, null);
const sync = gl.fenceSync(gl.SYNC_GPU_COMMANDS_COMPLETE, 0);
gl.flush();

this._readbackQueue = {
frameNumberIssued: this._updateCount,
sync,
};
} else {
// Read it back later.
this._readbackQueue = {
frameNumberIssued: this._updateCount,
sync: null,
};
}
// Read back into PBO
gl.bindBuffer(gl.PIXEL_PACK_BUFFER, this._pbo);
gl.readBuffer(gl.COLOR_ATTACHMENT0);
gl.readPixels(0, 0, this._texWidth, this._texHeight, this._texFormat, this._texType, 0);
gl.bindBuffer(gl.PIXEL_PACK_BUFFER, null);
const sync = gl.fenceSync(gl.SYNC_GPU_COMMANDS_COMPLETE, 0);
gl.flush();

this._readbackQueue = {
frameNumberIssued: this._updateCount,
sync,
};
}

private _tryReadback(): void {
const gl = this._cachedRenderContext.context.gl;

if (this._pbo && this._readbackQueue && isWebGL2(gl)) {
// WebGL 2 path
const waitResult = gl.clientWaitSync(this._readbackQueue.sync, 0, 0);
if (!this._readbackQueue) {
return;
}

if (waitResult === gl.WAIT_FAILED) {
warnOnce('WebGL2 clientWaitSync failed.');
this._readbackQueue = null;
this._lastReadbackFrame = this._updateCount;
return;
}
const waitResult = gl.clientWaitSync(this._readbackQueue.sync, 0, 0);

if (waitResult === gl.TIMEOUT_EXPIRED) {
return; // Wait one more frame
}
if (waitResult === gl.WAIT_FAILED) {
warnOnce('WebGL2 clientWaitSync failed.');
this._readbackQueue = null;
this._lastReadbackFrame = this._updateCount;
return;
}

gl.bindBuffer(gl.PIXEL_PACK_BUFFER, this._pbo);
gl.getBufferSubData(gl.PIXEL_PACK_BUFFER, 0, this._resultBuffer, 0, 4);
gl.bindBuffer(gl.PIXEL_PACK_BUFFER, null);
} else {
// WebGL1 compatible
this._bindFramebuffer();
gl.readPixels(0, 0, this._texWidth, this._texHeight, this._texFormat, this._texType, this._resultBuffer);
if (waitResult === gl.TIMEOUT_EXPIRED) {
return; // Wait one more frame
}

gl.bindBuffer(gl.PIXEL_PACK_BUFFER, this._pbo);
gl.getBufferSubData(gl.PIXEL_PACK_BUFFER, 0, this._resultBuffer, 0, 4);
gl.bindBuffer(gl.PIXEL_PACK_BUFFER, null);

// If we made it here, _resultBuffer contains the new measurement
this._readbackQueue = null;
this._measuredError = ProjectionErrorMeasurement._parseRGBA8float(this._resultBuffer);
Expand Down
5 changes: 3 additions & 2 deletions src/render/painter.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import {MercatorTransform} from '../geo/projection/mercator_transform';
import {Style} from '../style/style';
import {StubMap} from '../util/test/util';
import {Texture} from '../webgl/texture';
import {createNullGL} from '../util/test/null_gl';

describe('render', () => {
let painter: Painter;
Expand All @@ -21,7 +22,7 @@ describe('render', () => {
};

beforeEach(() => {
const gl = document.createElement('canvas').getContext('webgl');
const gl = createNullGL();
const transform = new MercatorTransform({minZoom: 0, maxZoom: 22, minPitch: 0, maxPitch: 60, renderWorldCopies: true});
transform.resize(512, 512);
painter = new Painter(gl, transform);
Expand Down Expand Up @@ -49,7 +50,7 @@ describe('render', () => {

describe('tile texture pool', () => {
function createPainterWithPool() {
const gl = document.createElement('canvas').getContext('webgl');
const gl = createNullGL();
const transform = new MercatorTransform({minZoom: 0, maxZoom: 22, minPitch: 0, maxPitch: 60, renderWorldCopies: true});
return new Painter(gl, transform);
}
Expand Down
2 changes: 1 addition & 1 deletion src/render/painter.ts
Original file line number Diff line number Diff line change
Expand Up @@ -125,7 +125,7 @@ export class Painter {
// every time the camera-matrix changes the terrain-facilitators will be redrawn.
terrainFacilitator: {depthDirty: boolean; coordsDirty: boolean; matrix: mat4; renderTime: number};

constructor(gl: WebGLRenderingContext | WebGL2RenderingContext, transform: IReadonlyTransform) {
constructor(gl: WebGL2RenderingContext, transform: IReadonlyTransform) {
this.drawFunctions = webglDrawFunctions;
this.context = new Context(gl);
this.transform = transform;
Expand Down
6 changes: 3 additions & 3 deletions src/render/terrain.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,13 +14,13 @@ import type {TileManager} from '../tile/tile_manager';
import type {TerrainSpecification} from '@maplibre/maplibre-gl-style-spec';
import type {DEMData} from '../data/dem_data';
import type {Painter} from './painter';
import {createNullGL} from '../util/test/null_gl';

describe('Terrain', () => {
let gl: WebGLRenderingContext;
let gl: WebGL2RenderingContext;

beforeEach(() => {
gl = document.createElement('canvas').getContext('webgl');
vi.spyOn(gl, 'checkFramebufferStatus').mockReturnValue(gl.FRAMEBUFFER_COMPLETE);
gl = createNullGL();
vi.spyOn(gl, 'readPixels').mockImplementation((_1, _2, _3, _4, _5, _6, rgba) => {
rgba[0] = 0;
rgba[1] = 0;
Expand Down
16 changes: 0 additions & 16 deletions src/shaders/shaders.ts
Original file line number Diff line number Diff line change
Expand Up @@ -220,19 +220,3 @@ uniform ${precision} ${type} u_${name};
return {fragmentSource, vertexSource, staticAttributes: vertexAttributes, staticUniforms: shaderUniforms};
}

/** Transpile WebGL2 vertex shader source to WebGL1 */
export function transpileVertexShaderToWebGL1(source: string): string {
return source
.replace(/\bin\s/g, 'attribute ')
.replace(/\bout\s/g, 'varying ')
.replace(/texture\(/g, 'texture2D(');
}

/** Transpile WebGL2 fragment shader source to WebGL1 */
export function transpileFragmentShaderToWebGL1(source: string): string {
return source
.replace(/\bin\s/g, 'varying ')
.replace('out highp vec4 fragColor;', '')
.replace(/fragColor/g, 'gl_FragColor')
.replace(/texture\(/g, 'texture2D(');
}
10 changes: 5 additions & 5 deletions src/style/style_layer/custom_style_layer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -111,7 +111,7 @@ export type CustomRenderMethodInput = {
* @param gl - The map's gl context.
* @param options - Argument object with render inputs like camera properties.
*/
export type CustomRenderMethod = (gl: WebGLRenderingContext|WebGL2RenderingContext, options: CustomRenderMethodInput) => void;
export type CustomRenderMethod = (gl: WebGL2RenderingContext, options: CustomRenderMethodInput) => void;

/**
* Interface for custom style layers. This is a specification for
Expand Down Expand Up @@ -141,7 +141,7 @@ export type CustomRenderMethod = (gl: WebGLRenderingContext|WebGL2RenderingConte
* this.renderingMode = '2d';
* }
*
* onAdd(map: maplibregl.Map, gl: WebGLRenderingContext | WebGL2RenderingContext) {
* onAdd(map: maplibregl.Map, gl: WebGL2RenderingContext) {
* const vertexSource = `
* uniform mat4 u_matrix;
* void main() {
Expand Down Expand Up @@ -171,7 +171,7 @@ export type CustomRenderMethod = (gl: WebGLRenderingContext|WebGL2RenderingConte
* gl,
* modelViewProjectionMatrix: matrix
* }: {
* gl: WebGLRenderingContext | WebGL2RenderingContext;
* gl: WebGL2RenderingContext;
* modelViewProjectionMatrix: Float32Array;
* }) {
* gl.useProgram(this.program);
Expand Down Expand Up @@ -228,15 +228,15 @@ export interface CustomLayerInterface {
* @param map - The Map this custom layer was just added to.
* @param gl - The gl context for the map.
*/
onAdd?(map: Map, gl: WebGLRenderingContext | WebGL2RenderingContext): void;
onAdd?(map: Map, gl: WebGL2RenderingContext): void;
/**
* Optional method called when the layer has been removed from the Map with {@link Map.removeLayer}. This
* gives the layer a chance to clean up gl resources and event listeners.
*
* @param map - The Map this custom layer was just added to.
* @param gl - The gl context for the map.
*/
onRemove?(map: Map, gl: WebGLRenderingContext | WebGL2RenderingContext): void;
onRemove?(map: Map, gl: WebGL2RenderingContext): void;
}

export function validateCustomStyleLayer(layerObject: CustomLayerInterface) {
Expand Down
4 changes: 4 additions & 0 deletions src/ui/default_locale.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,4 +24,8 @@ export const defaultLocale = {
'CooperativeGesturesHandler.WindowsHelpText': 'Use Ctrl + scroll to zoom the map',
'CooperativeGesturesHandler.MacHelpText': 'Use ⌘ + scroll to zoom the map',
'CooperativeGesturesHandler.MobileHelpText': 'Use two fingers to move the map',
'Map.WebGL2NotSupported.Full': 'We are sorry, but it seems that your browser does not support WebGL2, a technology for rendering 3D graphics on the web.',
'Map.WebGL2NotSupported.Short': 'WebGL2 is required to display this map.',
'Map.WebGL2NotSupported.LearnMore': 'Read more',
'Map.WebGL2NotSupported.LearnMoreUrl': 'https://wiki.openstreetmap.org/wiki/This_map_requires_WebGL',
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This page only speak of "WebGL" being required - not a single reference to webgl2. I don't know if we best make a separate page for webgl2, or if a note on the existing page about maplibre v6+.

Copy link
Copy Markdown
Member Author

@CommanderStorm CommanderStorm Apr 13, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Once we upgrade OSM I will have to upgrade that wiki anyhow. So should be the require a different docs.

Alternatively, we can just have a dup of that, but it would likely end up being a dup..

};
64 changes: 47 additions & 17 deletions src/ui/map.ts
Original file line number Diff line number Diff line change
Expand Up @@ -70,8 +70,10 @@ import type {ICameraHelper} from '../geo/projection/camera_helper';

const version = packageJSON.version;

export type WebGLSupportedVersions = 'webgl2' | 'webgl' | undefined;
export type WebGLContextAttributesWithType = WebGLContextAttributes & {contextType?: WebGLSupportedVersions};
export type ContextType = 'webgl2';
/** @deprecated Use {@link ContextType} instead. */
export type WebGLSupportedVersions = ContextType | undefined;
export type WebGLContextAttributesWithType = WebGLContextAttributes & {contextType?: ContextType};

/**
* The {@link Map} options object.
Expand Down Expand Up @@ -129,8 +131,8 @@ export type MapOptions = {
/**
* Set of WebGLContextAttributes that are applied to the WebGL context of the map.
* See https://developer.mozilla.org/en-US/docs/Web/API/HTMLCanvasElement/getContext for more details.
* `contextType` can be set to `webgl2` or `webgl` to force a WebGL version. Not setting it, Maplibre will do it's best to get a suitable context.
* @defaultValue antialias: false, powerPreference: 'high-performance', preserveDrawingBuffer: false, failIfMajorPerformanceCaveat: false, desynchronized: false, contextType: 'webgl2withfallback'
* `contextType` is restricted to `'webgl2'`. This option is kept as a forward-looking API for future WebGPU support.
* @defaultValue antialias: false, powerPreference: 'high-performance', preserveDrawingBuffer: false, failIfMajorPerformanceCaveat: false, desynchronized: false, contextType: 'webgl2'
*/
canvasContextAttributes?: WebGLContextAttributesWithType;
/**
Expand Down Expand Up @@ -775,6 +777,7 @@ export class Map extends Camera {

this._setupContainer();
this._setupPainter();
if (!this.painter) return;

this.on('move', () => this._update(false));
this.on('moveend', () => this._update(false));
Expand Down Expand Up @@ -3465,26 +3468,52 @@ export class Map extends Camera {
}
}, {once: true});

let gl: WebGL2RenderingContext | WebGLRenderingContext | null = null;
if (this._canvasContextAttributes.contextType) {
gl = this._canvas.getContext(this._canvasContextAttributes.contextType, attributes) as WebGL2RenderingContext | WebGLRenderingContext;
} else {
gl = this._canvas.getContext('webgl2', attributes) || this._canvas.getContext('webgl', attributes);
}
const gl: WebGL2RenderingContext | null = this._canvas.getContext('webgl2', attributes);

if (!gl) {
const msg = 'Failed to initialize WebGL';
if (webglcontextcreationerrorDetailObject) {
webglcontextcreationerrorDetailObject.message = msg;
throw new Error(JSON.stringify(webglcontextcreationerrorDetailObject));
} else {
throw new Error(msg);
}
this._showWebGL2Error(webglcontextcreationerrorDetailObject);
return;
}

this.painter = new Painter(gl, this.transform);
}

_showWebGL2Error(webglcontextcreationerrorDetailObject: any) {
const fullText = this._getUIString('Map.WebGL2NotSupported.Full');
const shortText = this._getUIString('Map.WebGL2NotSupported.Short');
const learnMore = this._getUIString('Map.WebGL2NotSupported.LearnMore');
const webglLink = this._getUIString('Map.WebGL2NotSupported.LearnMoreUrl');

const errorDiv = DOM.create('div', 'maplibregl-webgl-error', this._container);

const fullMsg = DOM.create('p', 'maplibregl-webgl-error-full', errorDiv);
fullMsg.textContent = `${fullText} ${shortText} `;
const fullLink = DOM.create('a', undefined, fullMsg);
fullLink.href = webglLink;
fullLink.target = '_blank';
fullLink.rel = 'noopener noreferrer';
fullLink.textContent = learnMore;

const shortMsg = DOM.create('p', 'maplibregl-webgl-error-short', errorDiv);
shortMsg.textContent = `${shortText} `;
const shortLink = DOM.create('a', undefined, shortMsg);
shortLink.href = webglLink;
shortLink.target = '_blank';
shortLink.rel = 'noopener noreferrer';
shortLink.textContent = learnMore;

const msg = 'Failed to initialize WebGL';
if (webglcontextcreationerrorDetailObject) {
webglcontextcreationerrorDetailObject.message = msg;
}

this.fire(new ErrorEvent(new Error(
webglcontextcreationerrorDetailObject
? JSON.stringify(webglcontextcreationerrorDetailObject)
: msg
)));
}

override migrateProjection(newTransform: ITransform, newCameraHelper: ICameraHelper) {
super.migrateProjection(newTransform, newCameraHelper);
this.painter.transform = newTransform;
Expand Down Expand Up @@ -3539,6 +3568,7 @@ export class Map extends Camera {
this._lostContextStyle = {style: null, images: null};

this._setupPainter();
if (!this.painter) return;
this.resize();
this._update();
this._resizeInternal();
Expand Down
Loading
Loading