diff --git a/src/css/maplibre-gl.css b/src/css/maplibre-gl.css index 0554dead745..53c399d5d67 100644 --- a/src/css/maplibre-gl.css +++ b/src/css/maplibre-gl.css @@ -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; + } +} diff --git a/src/geo/projection/globe_projection_error_measurement.ts b/src/geo/projection/globe_projection_error_measurement.ts index 65a15394a7c..ade9df55785 100644 --- a/src/geo/projection/globe_projection_error_measurement.ts +++ b/src/geo/projection/globe_projection_error_measurement.ts @@ -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'; /** @@ -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() { @@ -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); diff --git a/src/render/painter.test.ts b/src/render/painter.test.ts index b511ebe3d83..4e38bac3fb5 100644 --- a/src/render/painter.test.ts +++ b/src/render/painter.test.ts @@ -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; @@ -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); @@ -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); } diff --git a/src/render/painter.ts b/src/render/painter.ts index b50b1bc5980..d02ee538818 100644 --- a/src/render/painter.ts +++ b/src/render/painter.ts @@ -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; diff --git a/src/render/terrain.test.ts b/src/render/terrain.test.ts index 15a06ebf7dc..d470e5c7c61 100644 --- a/src/render/terrain.test.ts +++ b/src/render/terrain.test.ts @@ -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; diff --git a/src/shaders/shaders.ts b/src/shaders/shaders.ts index 571716a4b5a..359e4f0891b 100644 --- a/src/shaders/shaders.ts +++ b/src/shaders/shaders.ts @@ -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('); -} diff --git a/src/style/style_layer/custom_style_layer.ts b/src/style/style_layer/custom_style_layer.ts index 9ab1e3f593d..08c0d00c7b9 100644 --- a/src/style/style_layer/custom_style_layer.ts +++ b/src/style/style_layer/custom_style_layer.ts @@ -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 @@ -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() { @@ -171,7 +171,7 @@ export type CustomRenderMethod = (gl: WebGLRenderingContext|WebGL2RenderingConte * gl, * modelViewProjectionMatrix: matrix * }: { - * gl: WebGLRenderingContext | WebGL2RenderingContext; + * gl: WebGL2RenderingContext; * modelViewProjectionMatrix: Float32Array; * }) { * gl.useProgram(this.program); @@ -228,7 +228,7 @@ 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. @@ -236,7 +236,7 @@ export interface CustomLayerInterface { * @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) { diff --git a/src/ui/default_locale.ts b/src/ui/default_locale.ts index 867e5ca03cb..0bff4677771 100644 --- a/src/ui/default_locale.ts +++ b/src/ui/default_locale.ts @@ -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', }; diff --git a/src/ui/map.ts b/src/ui/map.ts index ff1afe2c055..ebb7ed969bd 100644 --- a/src/ui/map.ts +++ b/src/ui/map.ts @@ -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. @@ -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; /** @@ -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)); @@ -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; @@ -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(); diff --git a/src/ui/map_tests/map_canvas.test.ts b/src/ui/map_tests/map_canvas.test.ts index 7b7e3b5ba32..3d9e4dc5b5f 100644 --- a/src/ui/map_tests/map_canvas.test.ts +++ b/src/ui/map_tests/map_canvas.test.ts @@ -11,8 +11,8 @@ describe('Max Canvas Size option', () => { const container = window.document.createElement('div'); Object.defineProperty(container, 'clientWidth', {value: 2048}); Object.defineProperty(container, 'clientHeight', {value: 2048}); - vi.spyOn(WebGLRenderingContext.prototype, 'drawingBufferWidth', 'get').mockReturnValue(8192); - vi.spyOn(WebGLRenderingContext.prototype, 'drawingBufferHeight', 'get').mockReturnValue(8192); + vi.spyOn(WebGL2RenderingContext.prototype, 'drawingBufferWidth', 'get').mockReturnValue(8192); + vi.spyOn(WebGL2RenderingContext.prototype, 'drawingBufferHeight', 'get').mockReturnValue(8192); const map = createMap({container, maxCanvasSize: [8192, 8192], pixelRatio: 5}); map.resize(); expect(map.getCanvas().width).toBe(8192); @@ -23,8 +23,8 @@ describe('Max Canvas Size option', () => { const container = window.document.createElement('div'); Object.defineProperty(container, 'clientWidth', {value: 1024}); Object.defineProperty(container, 'clientHeight', {value: 2048}); - vi.spyOn(WebGLRenderingContext.prototype, 'drawingBufferWidth', 'get').mockReturnValue(8192); - vi.spyOn(WebGLRenderingContext.prototype, 'drawingBufferHeight', 'get').mockReturnValue(4096); + vi.spyOn(WebGL2RenderingContext.prototype, 'drawingBufferWidth', 'get').mockReturnValue(8192); + vi.spyOn(WebGL2RenderingContext.prototype, 'drawingBufferHeight', 'get').mockReturnValue(4096); const map = createMap({container, maxCanvasSize: [8192, 4096], pixelRatio: 3}); map.resize(); expect(map.getCanvas().width).toBe(2048); @@ -35,8 +35,8 @@ describe('Max Canvas Size option', () => { const container = window.document.createElement('div'); Object.defineProperty(container, 'clientWidth', {value: 12834}); Object.defineProperty(container, 'clientHeight', {value: 9000}); - vi.spyOn(WebGLRenderingContext.prototype, 'drawingBufferWidth', 'get').mockReturnValue(4096); - vi.spyOn(WebGLRenderingContext.prototype, 'drawingBufferHeight', 'get').mockReturnValue(8192); + vi.spyOn(WebGL2RenderingContext.prototype, 'drawingBufferWidth', 'get').mockReturnValue(4096); + vi.spyOn(WebGL2RenderingContext.prototype, 'drawingBufferHeight', 'get').mockReturnValue(8192); const map = createMap({container, maxCanvasSize: [4096, 8192], pixelRatio: 1}); map.resize(); expect(map.getCanvas().width).toBe(4096); @@ -47,8 +47,8 @@ describe('Max Canvas Size option', () => { const container = window.document.createElement('div'); Object.defineProperty(container, 'clientWidth', {value: 2048}); Object.defineProperty(container, 'clientHeight', {value: 2048}); - vi.spyOn(WebGLRenderingContext.prototype, 'drawingBufferWidth', 'get').mockReturnValue(3072); - vi.spyOn(WebGLRenderingContext.prototype, 'drawingBufferHeight', 'get').mockReturnValue(3072); + vi.spyOn(WebGL2RenderingContext.prototype, 'drawingBufferWidth', 'get').mockReturnValue(3072); + vi.spyOn(WebGL2RenderingContext.prototype, 'drawingBufferHeight', 'get').mockReturnValue(3072); const map = createMap({container, maxCanvasSize: [3072, 3072], pixelRatio: 1.25}); map.resize(); expect(map.getCanvas().width).toBe(2560); diff --git a/src/ui/map_tests/map_webgl.test.ts b/src/ui/map_tests/map_webgl.test.ts index 9e92266c98f..939e614cb88 100644 --- a/src/ui/map_tests/map_webgl.test.ts +++ b/src/ui/map_tests/map_webgl.test.ts @@ -1,6 +1,5 @@ import {beforeEach, afterEach, test, expect, vi} from 'vitest'; import {createMap, beforeMapTest} from '../../util/test/util'; -import {ensureError} from '../../util/util'; let originalGetContext: typeof HTMLCanvasElement.prototype.getContext; beforeEach(() => { @@ -73,67 +72,38 @@ test('does not fire "webglcontextrestored" after remove has been called', async expect(spy).not.toHaveBeenCalled(); }); -test('WebGL error while creating map', () => { +test('WebGL2 error fires ErrorEvent and shows overlay', () => { HTMLCanvasElement.prototype.getContext = function (type: string) { - if (type === 'webgl2' || type === 'webgl') { + if (type === 'webgl2') { const errorEvent = new Event('webglcontextcreationerror'); (errorEvent as any).statusMessage = 'mocked webglcontextcreationerror message'; (this as HTMLCanvasElement).dispatchEvent(errorEvent); return null; } }; - try { - createMap(); - } catch (e) { - const errorMessageObject = JSON.parse(ensureError(e).message); - - // this message is from map code - expect(errorMessageObject.message).toBe('Failed to initialize WebGL'); - - // this is from test mock - expect(errorMessageObject.statusMessage).toBe('mocked webglcontextcreationerror message'); - } + const container = window.document.createElement('div'); + const errorSpy = vi.fn(); + const map = createMap({container}); + map.on('error', errorSpy); + + // The error overlay should be present + const overlay = container.querySelector('.maplibregl-webgl-error'); + expect(overlay).toBeTruthy(); + // Should contain a link to the WebGL help page + const link = overlay.querySelector('a'); + expect(link).toBeTruthy(); + expect(link.href).toContain('wiki.openstreetmap.org'); }); -test('Check Map is being created with desired WebGL version', () => { - HTMLCanvasElement.prototype.getContext = function (type: string) { - const errorEvent = new Event('webglcontextcreationerror'); - (errorEvent as any).statusMessage = `${type} is not supported`; - (this as HTMLCanvasElement).dispatchEvent(errorEvent); +test('Error overlay appears when getContext webgl2 returns null', () => { + HTMLCanvasElement.prototype.getContext = function (_type: string) { return null; }; - - try { - createMap({canvasContextAttributes: {contextType: 'webgl2'}}); - } catch (e) { - const errorMessageObject = JSON.parse(ensureError(e).message); - expect(errorMessageObject.statusMessage).toBe('webgl2 is not supported'); - } - - try { - createMap({canvasContextAttributes: {contextType: 'webgl'}}); - } catch (e) { - const errorMessageObject = JSON.parse(ensureError(e).message); - expect(errorMessageObject.statusMessage).toBe('webgl is not supported'); - } - -}); - -test('Check Map falls back to WebGL if WebGL 2 is not supported', () => { - const mockGetContext = vi.fn().mockImplementation((type: string) => { - if (type === 'webgl2') {return null;} - return originalGetContext.apply(this, [type]); - }); - HTMLCanvasElement.prototype.getContext = mockGetContext; - - try { - createMap(); - } catch(_) { // eslint-disable-line @typescript-eslint/no-unused-vars - } - expect(mockGetContext).toHaveBeenCalledTimes(2); - expect(mockGetContext.mock.calls[0][0]).toBe('webgl2'); - expect(mockGetContext.mock.calls[1][0]).toBe('webgl'); - + const container = window.document.createElement('div'); + createMap({container}); + const overlay = container.querySelector('.maplibregl-webgl-error'); + expect(overlay).toBeTruthy(); + expect(overlay.textContent).toContain('WebGL'); }); test('Hit WebGL max drawing buffer limit', () => { diff --git a/src/util/test/null_gl.ts b/src/util/test/null_gl.ts new file mode 100644 index 00000000000..9292b5df6cf --- /dev/null +++ b/src/util/test/null_gl.ts @@ -0,0 +1,263 @@ +import {vi} from 'vitest'; + +/** + * WebGL2-specific constants + */ +const webgl2Enums = { + PIXEL_PACK_BUFFER: 0x88EB, + PIXEL_UNPACK_BUFFER: 0x88EC, + PIXEL_PACK_BUFFER_BINDING: 0x88ED, + PIXEL_UNPACK_BUFFER_BINDING: 0x88EF, + STREAM_READ: 0x88E1, + STREAM_COPY: 0x88E2, + STATIC_READ: 0x88E5, + STATIC_COPY: 0x88E6, + DYNAMIC_READ: 0x88E9, + DYNAMIC_COPY: 0x88EA, + HALF_FLOAT: 0x140B, + RGB16F: 0x881B, + RGBA16F: 0x881A, + RGB32F: 0x8815, + RGBA32F: 0x8814, + R16F: 0x822D, + RG16F: 0x822F, + SYNC_GPU_COMMANDS_COMPLETE: 0x9117, + ALREADY_SIGNALED: 0x911A, + TIMEOUT_EXPIRED: 0x911B, + CONDITION_SATISFIED: 0x911C, + WAIT_FAILED: 0x911D, + SYNC_FLUSH_COMMANDS_BIT: 0x00000001, + READ_BUFFER: 0x0C02, +}; + +/** + * WebGL1 constants + */ +const webgl1Enums = { + VERSION: 0x1F02, + MAX_TEXTURE_SIZE: 0x0D33, + COLOR_ATTACHMENT0: 0x8CE0, + FRAMEBUFFER: 0x8D40, + RENDERBUFFER: 0x8D41, + FRAMEBUFFER_COMPLETE: 0x8CD5, + DEPTH_BUFFER_BIT: 0x00000100, + STENCIL_BUFFER_BIT: 0x00000400, + COLOR_BUFFER_BIT: 0x00004000, + POINTS: 0, + LINES: 1, + LINE_LOOP: 2, + LINE_STRIP: 3, + TRIANGLES: 4, + TRIANGLE_STRIP: 5, + TRIANGLE_FAN: 6, + ZERO: 0, + ONE: 1, + SRC_COLOR: 0x0300, + ONE_MINUS_SRC_COLOR: 0x0301, + SRC_ALPHA: 0x0302, + ONE_MINUS_SRC_ALPHA: 0x0303, + DST_ALPHA: 0x0304, + ONE_MINUS_DST_ALPHA: 0x0305, + DST_COLOR: 0x0306, + ONE_MINUS_DST_COLOR: 0x0307, + CONSTANT_COLOR: 0x8001, + FUNC_ADD: 0x8006, + BLEND: 0x0BE2, + DEPTH_TEST: 0x0B71, + STENCIL_TEST: 0x0B90, + CULL_FACE: 0x0B44, + SCISSOR_TEST: 0x0C11, + FRONT: 0x0404, + BACK: 0x0405, + CW: 0x0900, + CCW: 0x0901, + NEVER: 0x0200, + LESS: 0x0201, + EQUAL: 0x0202, + LEQUAL: 0x0203, + GREATER: 0x0204, + NOTEQUAL: 0x0205, + GEQUAL: 0x0206, + ALWAYS: 0x0207, + KEEP: 0x1E00, + REPLACE: 0x1E01, + INCR: 0x1E02, + DECR: 0x1E03, + BYTE: 0x1400, + UNSIGNED_BYTE: 0x1401, + SHORT: 0x1402, + UNSIGNED_SHORT: 0x1403, + INT: 0x1404, + UNSIGNED_INT: 0x1405, + FLOAT: 0x1406, + ALPHA: 0x1906, + RGB: 0x1907, + RGBA: 0x1908, + DEPTH_COMPONENT: 0x1902, + DEPTH_COMPONENT16: 0x81A5, + DEPTH_STENCIL: 0x84F9, + DEPTH_STENCIL_ATTACHMENT: 0x821A, + DEPTH_ATTACHMENT: 0x8D00, + TEXTURE_2D: 0x0DE1, + TEXTURE_MAG_FILTER: 0x2800, + TEXTURE_MIN_FILTER: 0x2801, + TEXTURE_WRAP_S: 0x2802, + TEXTURE_WRAP_T: 0x2803, + LINEAR: 0x2601, + NEAREST: 0x2600, + LINEAR_MIPMAP_NEAREST: 0x2701, + NEAREST_MIPMAP_LINEAR: 0x2702, + REPEAT: 0x2901, + CLAMP_TO_EDGE: 0x812F, + TEXTURE0: 0x84C0, + TEXTURE1: 0x84C1, + TEXTURE2: 0x84C2, + TEXTURE3: 0x84C3, + TEXTURE4: 0x84C4, + ARRAY_BUFFER: 0x8892, + ELEMENT_ARRAY_BUFFER: 0x8893, + STATIC_DRAW: 0x88E4, + DYNAMIC_DRAW: 0x88E8, + VERTEX_SHADER: 0x8B31, + FRAGMENT_SHADER: 0x8B30, + COMPILE_STATUS: 0x8B81, + LINK_STATUS: 0x8B82, + UNPACK_ALIGNMENT: 0x0CF5, + UNPACK_FLIP_Y_WEBGL: 0x9240, + UNPACK_PREMULTIPLY_ALPHA_WEBGL: 0x9241, + MAX_TEXTURE_IMAGE_UNITS: 0x8872, +}; + +const allEnums = {...webgl1Enums, ...webgl2Enums}; + +/** All WebGL1 method names that return a generic stub value */ +const webgl1Methods = [ + 'activeTexture', 'attachShader', 'bindAttribLocation', 'bindBuffer', + 'bindFramebuffer', 'bindRenderbuffer', 'bindTexture', 'blendColor', + 'blendEquation', 'blendEquationSeparate', 'blendFunc', 'blendFuncSeparate', + 'bufferData', 'bufferSubData', 'clear', 'clearColor', 'clearDepth', + 'clearStencil', 'colorMask', 'compileShader', 'copyTexImage2D', + 'copyTexSubImage2D', 'cullFace', 'deleteBuffer', 'deleteFramebuffer', + 'deleteProgram', 'deleteRenderbuffer', 'deleteShader', 'deleteTexture', + 'depthFunc', 'depthMask', 'depthRange', 'detachShader', 'disable', + 'disableVertexAttribArray', 'drawArrays', 'drawElements', 'enable', + 'enableVertexAttribArray', 'finish', 'flush', 'framebufferRenderbuffer', + 'framebufferTexture2D', 'frontFace', 'generateMipmap', 'getActiveAttrib', + 'getActiveUniform', 'getError', + 'getRenderbufferParameter', 'getShaderPrecisionFormat', + 'getShaderSource', 'getTexParameter', 'getUniform', 'getUniformLocation', + 'getVertexAttrib', 'getVertexAttribOffset', 'hint', 'isBuffer', + 'isEnabled', 'isFramebuffer', 'isProgram', 'isRenderbuffer', 'isShader', + 'isTexture', 'lineWidth', 'linkProgram', 'pixelStorei', 'polygonOffset', + 'readPixels', 'renderbufferStorage', 'sampleCoverage', 'scissor', + 'shaderSource', 'stencilFunc', 'stencilFuncSeparate', 'stencilMask', + 'stencilMaskSeparate', 'stencilOp', 'stencilOpSeparate', 'texParameterf', + 'texParameteri', 'texImage2D', 'texSubImage2D', 'uniform1f', 'uniform1fv', + 'uniform1i', 'uniform1iv', 'uniform2f', 'uniform2fv', 'uniform2i', + 'uniform2iv', 'uniform3f', 'uniform3fv', 'uniform3i', 'uniform3iv', + 'uniform4f', 'uniform4fv', 'uniform4i', 'uniform4iv', 'uniformMatrix2fv', + 'uniformMatrix3fv', 'uniformMatrix4fv', 'useProgram', 'validateProgram', + 'vertexAttrib1f', 'vertexAttrib1fv', 'vertexAttrib2f', 'vertexAttrib2fv', + 'vertexAttrib3f', 'vertexAttrib3fv', 'vertexAttrib4f', 'vertexAttrib4fv', + 'vertexAttribPointer', 'viewport', +]; + +/** WebGL2-specific method names (no-ops) */ +const webgl2Methods = [ + 'bindVertexArray', 'deleteVertexArray', 'readBuffer', + 'getBufferSubData', 'bindBufferBase', 'bindBufferRange', + 'beginQuery', 'endQuery', 'getQuery', 'getQueryParameter', + 'drawBuffers', 'clearBufferfv', 'clearBufferiv', 'clearBufferuiv', + 'clearBufferfi', 'vertexAttribIPointer', 'vertexAttribDivisor', + 'drawArraysInstanced', 'drawElementsInstanced', + 'texStorage2D', 'texStorage3D', 'texImage3D', 'texSubImage3D', + 'renderbufferStorageMultisample', 'blitFramebuffer', + 'invalidateFramebuffer', 'invalidateSubFramebuffer', + 'uniform1ui', 'uniform2ui', 'uniform3ui', 'uniform4ui', + 'uniformMatrix2x3fv', 'uniformMatrix3x2fv', 'uniformMatrix2x4fv', + 'uniformMatrix4x2fv', 'uniformMatrix3x4fv', 'uniformMatrix4x3fv', +]; + +let _idCounter = 1; + +/** + * A null/stub WebGL2RenderingContext following the luma.gl NullDevice pattern. + * All methods are vi.fn() stubs, all enum constants are correct values. + * Methods that create resources return opaque stub objects. + */ +export class NullWebGL2RenderingContext { + readonly canvas: HTMLCanvasElement; + private _contextAttributes: WebGLContextAttributes | null; + + constructor(canvas: HTMLCanvasElement, contextAttributes?: WebGLContextAttributes) { + this.canvas = canvas; + this._contextAttributes = contextAttributes ?? null; + + // No-op methods must be per-instance so vi.fn() call tracking is isolated between tests. + for (const method of [...webgl1Methods, ...webgl2Methods]) { + (this as any)[method] = vi.fn(); + } + } + + // Defined as configurable getters after the class so vi.spyOn can override them. + declare drawingBufferWidth: number; + declare drawingBufferHeight: number; + + // --- Methods that need non-trivial return values --- + getParameter = vi.fn((pname: number) => { + if (pname === allEnums.VERSION) return 'WebGL 2.0'; + if (pname === allEnums.MAX_TEXTURE_SIZE) return 4096; + if (pname === allEnums.MAX_TEXTURE_IMAGE_UNITS) return 16; + return 0; + }); + getExtension = vi.fn((_name: string): any => { + // Return an object for extensions maplibre probes + if (_name === 'EXT_texture_filter_anisotropic') return {MAX_TEXTURE_MAX_ANISOTROPY_EXT: 0x84FF}; + if (_name === 'EXT_color_buffer_half_float') return {RGBA16F_EXT: 0x881A, RGB16F_EXT: 0x881B}; + if (_name === 'EXT_color_buffer_float') return {}; + return null; + }); + getContextAttributes = vi.fn(() => this._contextAttributes); + getSupportedExtensions = vi.fn((): string[] => []); + isContextLost = vi.fn((): boolean => false); + getShaderParameter = vi.fn((): any => true); + getProgramParameter = vi.fn((): any => true); + getAttribLocation = vi.fn((): number => 0); + getShaderInfoLog = vi.fn((): string => ''); + getProgramInfoLog = vi.fn((): string => ''); + createBuffer = vi.fn((): WebGLBuffer => ({__id: _idCounter++} as any)); + createTexture = vi.fn((): WebGLTexture => ({__id: _idCounter++} as any)); + createFramebuffer = vi.fn((): WebGLFramebuffer => ({__id: _idCounter++} as any)); + createRenderbuffer = vi.fn((): WebGLRenderbuffer => ({__id: _idCounter++} as any)); + createProgram = vi.fn((): WebGLProgram => ({__id: _idCounter++} as any)); + createShader = vi.fn((): WebGLShader => ({__id: _idCounter++} as any)); + createVertexArray = vi.fn((): WebGLVertexArrayObject => ({__id: _idCounter++} as any)); + createQuery = vi.fn((): WebGLQuery => ({__id: _idCounter++} as any)); + fenceSync = vi.fn((): WebGLSync => ({__id: _idCounter++} as any)); + clientWaitSync = vi.fn((): number => allEnums.CONDITION_SATISFIED); + deleteSync = vi.fn(); + checkFramebufferStatus = vi.fn((): number => allEnums.FRAMEBUFFER_COMPLETE); +} + +// Enum constants are immutable — assign once on the prototype instead of per-instance. +for (const [k, v] of Object.entries(allEnums)) { + (NullWebGL2RenderingContext.prototype as any)[k] = v; +} + +// Configurable getters so vi.spyOn(..., 'get') works in tests. +Object.defineProperty(NullWebGL2RenderingContext.prototype, 'drawingBufferWidth', { + get() { return this.canvas?.width ?? 300; }, + configurable: true, +}); +Object.defineProperty(NullWebGL2RenderingContext.prototype, 'drawingBufferHeight', { + get() { return this.canvas?.height ?? 150; }, + configurable: true, +}); + +/** + * Create a NullWebGL2RenderingContext suitable for use with maplibre's Context class. + */ +export function createNullGL(): WebGL2RenderingContext { + const c = document.createElement('canvas'); + return new NullWebGL2RenderingContext(c) as unknown as WebGL2RenderingContext; +} diff --git a/src/util/test/util.ts b/src/util/test/util.ts index 7f60cf3e101..6574da4ed4c 100644 --- a/src/util/test/util.ts +++ b/src/util/test/util.ts @@ -108,19 +108,6 @@ export function beforeMapTest() { setPerformance(); setMatchMedia(); setResizeObserver(); - // remove the following when the following is merged and released: https://github.com/Adamfsk/jest-webgl-canvas-mock/pull/5 - (WebGLRenderingContext.prototype as any).bindVertexArray = WebGLRenderingContext.prototype.getExtension('OES_vertex_array_object').bindVertexArrayOES; - (WebGLRenderingContext.prototype as any).createVertexArray = WebGLRenderingContext.prototype.getExtension('OES_vertex_array_object').createVertexArrayOES; - if (!WebGLRenderingContext.prototype.drawingBufferHeight && !WebGLRenderingContext.prototype.drawingBufferWidth) { - Object.defineProperty(WebGLRenderingContext.prototype, 'drawingBufferWidth', { - get: vi.fn(), - configurable: true, - }); - Object.defineProperty(WebGLRenderingContext.prototype, 'drawingBufferHeight', { - get: vi.fn(), - configurable: true, - }); - } } export function getWrapDispatcher() { diff --git a/src/webgl/context.ts b/src/webgl/context.ts index abcd7b88898..ceecac7e8db 100644 --- a/src/webgl/context.ts +++ b/src/webgl/context.ts @@ -15,7 +15,6 @@ import type { StructArrayMember } from '../util/struct_array'; import type {Color} from '@maplibre/maplibre-gl-style-spec'; -import {isWebGL2} from './webgl2'; type ClearArgs = { color?: Color; @@ -28,7 +27,7 @@ type ClearArgs = { * A webgl wrapper class to allow injection, mocking and abstraction */ export class Context { - gl: WebGLRenderingContext | WebGL2RenderingContext; + gl: WebGL2RenderingContext; currentNumAttributes: number; maxTextureSize: number; @@ -67,11 +66,8 @@ export class Context { extTextureFilterAnisotropic: EXT_texture_filter_anisotropic | null; extTextureFilterAnisotropicMax?: GLfloat; - HALF_FLOAT?: GLenum; - RGBA16F?: GLenum; - RGB16F?: GLenum; - constructor(gl: WebGLRenderingContext | WebGL2RenderingContext) { + constructor(gl: WebGL2RenderingContext) { this.gl = gl; this.clearColor = new ClearColor(this); this.clearDepth = new ClearDepth(this); @@ -113,18 +109,8 @@ export class Context { this.maxTextureSize = gl.getParameter(gl.MAX_TEXTURE_SIZE); - if (isWebGL2(gl)) { - this.HALF_FLOAT = gl.HALF_FLOAT; - const extColorBufferHalfFloat = gl.getExtension('EXT_color_buffer_half_float'); - this.RGBA16F = gl.RGBA16F ?? extColorBufferHalfFloat?.RGBA16F_EXT; - this.RGB16F = gl.RGB16F ?? extColorBufferHalfFloat?.RGB16F_EXT; - gl.getExtension('EXT_color_buffer_float'); - } else { - gl.getExtension('EXT_color_buffer_half_float'); - gl.getExtension('OES_texture_half_float_linear'); - const extTextureHalfFloat = gl.getExtension('OES_texture_half_float'); - this.HALF_FLOAT = extTextureHalfFloat?.HALF_FLOAT_OES; - } + gl.getExtension('EXT_color_buffer_half_float'); + gl.getExtension('EXT_color_buffer_float'); } setDefault() { @@ -297,17 +283,11 @@ export class Context { } createVertexArray(): WebGLVertexArrayObject | undefined { - if (isWebGL2(this.gl)) - return this.gl.createVertexArray(); - return this.gl.getExtension('OES_vertex_array_object')?.createVertexArrayOES(); + return this.gl.createVertexArray(); } deleteVertexArray(x: WebGLVertexArrayObject | undefined) { - if (isWebGL2(this.gl)) { - this.gl.deleteVertexArray(x); - return; - } - this.gl.getExtension('OES_vertex_array_object')?.deleteVertexArrayOES(x); + this.gl.deleteVertexArray(x); } unbindVAO() { diff --git a/src/webgl/draw/draw_heatmap.ts b/src/webgl/draw/draw_heatmap.ts index 9a1f9f9d798..be7a9ccf71c 100644 --- a/src/webgl/draw/draw_heatmap.ts +++ b/src/webgl/draw/draw_heatmap.ts @@ -217,12 +217,7 @@ function createHeatmapFbo(context: Context, width: number, height: number): Fram gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.LINEAR); gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.LINEAR); - // Use the higher precision half-float texture where available (producing much smoother looking heatmaps); - // Otherwise, fall back to a low precision texture - const numType = context.HALF_FLOAT ?? gl.UNSIGNED_BYTE; - const internalFormat = context.RGBA16F ?? gl.RGBA; - - gl.texImage2D(gl.TEXTURE_2D, 0, internalFormat, width, height, 0, gl.RGBA, numType, null); + gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA16F, width, height, 0, gl.RGBA, gl.HALF_FLOAT, null); const fbo = context.createFramebuffer(width, height, false, false); fbo.colorAttachment.set(texture); diff --git a/src/webgl/program.ts b/src/webgl/program.ts index 9ecbded9f82..6c2fd53ee86 100644 --- a/src/webgl/program.ts +++ b/src/webgl/program.ts @@ -1,8 +1,7 @@ -import {type PreparedShader, shaders, transpileVertexShaderToWebGL1, transpileFragmentShaderToWebGL1} from '../shaders/shaders'; +import {type PreparedShader, shaders} from '../shaders/shaders'; import {type ProgramConfiguration} from '../data/program_configuration'; import {VertexArrayObject} from './vertex_array_object'; import {type Context} from './context'; -import {isWebGL2} from './webgl2'; import type {SegmentVector} from '../data/segment'; import type {VertexBuffer} from './vertex_buffer'; @@ -74,9 +73,7 @@ export class Program { } const defines = configuration ? configuration.defines() : []; - if (isWebGL2(gl)) { - defines.unshift('#version 300 es'); - } + defines.unshift('#version 300 es'); if (showOverdrawInspector) { defines.push('#define OVERDRAW_INSPECTOR;'); } @@ -90,13 +87,8 @@ export class Program { defines.push(...extraDefines); } - let fragmentSource = defines.concat(shaders.prelude.fragmentSource, projectionPrelude.fragmentSource, source.fragmentSource).join('\n'); - let vertexSource = defines.concat(shaders.prelude.vertexSource, projectionPrelude.vertexSource, source.vertexSource).join('\n'); - - if (!isWebGL2(gl)) { - fragmentSource = transpileFragmentShaderToWebGL1(fragmentSource); - vertexSource = transpileVertexShaderToWebGL1(vertexSource); - } + const fragmentSource = defines.concat(shaders.prelude.fragmentSource, projectionPrelude.fragmentSource, source.fragmentSource).join('\n'); + const vertexSource = defines.concat(shaders.prelude.vertexSource, projectionPrelude.vertexSource, source.vertexSource).join('\n'); const fragmentShader = gl.createShader(gl.FRAGMENT_SHADER); if (gl.isContextLost()) { diff --git a/src/webgl/render_pool.test.ts b/src/webgl/render_pool.test.ts index 0c1c4ea64b5..bbd4b49b76b 100644 --- a/src/webgl/render_pool.test.ts +++ b/src/webgl/render_pool.test.ts @@ -1,13 +1,13 @@ -import {describe, test, expect, vi} from 'vitest'; +import {describe, test, expect} from 'vitest'; import {Context} from './context'; import {RenderPool} from './render_pool'; +import {createNullGL} from '../util/test/null_gl'; describe('render pool', () => { const POOL_SIZE = 3; function createAndFillPool(): RenderPool { - const gl = document.createElement('canvas').getContext('webgl'); - vi.spyOn(gl, 'checkFramebufferStatus').mockReturnValue(gl.FRAMEBUFFER_COMPLETE); + const gl = createNullGL(); const pool = new RenderPool(new Context(gl), POOL_SIZE, 512); for (let i = 0; i < POOL_SIZE; i++) { pool.useObject(pool.getOrCreateFreeObject()); @@ -16,7 +16,7 @@ describe('render pool', () => { } test('create pool should not be full', () => { - const gl = document.createElement('canvas').getContext('webgl'); + const gl = createNullGL(); const pool = new RenderPool(new Context(gl), POOL_SIZE, 512); expect(pool.isFull()).toBeFalsy(); }); diff --git a/src/webgl/render_to_texture.test.ts b/src/webgl/render_to_texture.test.ts index 6253ab2f6b6..0f77184d2f9 100644 --- a/src/webgl/render_to_texture.test.ts +++ b/src/webgl/render_to_texture.test.ts @@ -17,10 +17,10 @@ import {type RasterStyleLayer} from '../style/style_layer/raster_style_layer'; import {type HillshadeStyleLayer} from '../style/style_layer/hillshade_style_layer'; import {type BackgroundStyleLayer} from '../style/style_layer/background_style_layer'; import {DepthMode} from '../webgl/depth_mode'; +import {createNullGL} from '../util/test/null_gl'; describe('render to texture', () => { - const gl = document.createElement('canvas').getContext('webgl'); - vi.spyOn(gl, 'checkFramebufferStatus').mockReturnValue(gl.FRAMEBUFFER_COMPLETE); + const gl = createNullGL(); const backgroundLayer = { id: 'maine-background', type: 'background', diff --git a/src/webgl/state.test.ts b/src/webgl/state.test.ts index cab77c14207..ac27032a450 100644 --- a/src/webgl/state.test.ts +++ b/src/webgl/state.test.ts @@ -3,13 +3,11 @@ import {type IValue, ClearColor, ClearDepth, ClearStencil, ColorMask, DepthMask, import {Context} from './context'; import {Color} from '@maplibre/maplibre-gl-style-spec'; import {deepEqual} from '../util/util'; +import {createNullGL} from '../util/test/null_gl'; describe('Value classes', () => { - const gl = document.createElement('canvas').getContext('webgl') as WebGL2RenderingContext; - // Remove when https://github.com/Adamfsk/jest-webgl-canvas-mock/pull/5 is merged - gl.createVertexArray = gl.getExtension('OES_vertex_array_object')?.createVertexArrayOES; - gl.bindVertexArray = gl.getExtension('OES_vertex_array_object')?.bindVertexArrayOES; + const gl = createNullGL(); const context = new Context(gl); const valueTest = (Constructor: new (...args:any[]) => IValue, diff --git a/src/webgl/texture.test.ts b/src/webgl/texture.test.ts index b2fb76059ad..af24f7f0273 100644 --- a/src/webgl/texture.test.ts +++ b/src/webgl/texture.test.ts @@ -2,6 +2,7 @@ import {describe, expect, test} from 'vitest'; import {Context} from './context'; import {Texture} from './texture'; import {premultiplyAlpha, RGBAImage} from '../util/image'; +import {createNullGL} from '../util/test/null_gl'; describe('Texture', () => { describe('glPixelStore state is reset after texture creation', () => { @@ -11,8 +12,7 @@ describe('Texture', () => { }, new Uint8Array([1, 2, 3, 4, 5, 6, 7, 8])); function getContext(): Context { - const gl = document.createElement('canvas').getContext('webgl') as WebGL2RenderingContext; - return new Context(gl); + return new Context(createNullGL()); } function checkPixelStoreState(context: Context): void { @@ -39,7 +39,7 @@ describe('Texture', () => { }); test('bind restores handle after corruption (#2811)', () => { - const gl = document.createElement('canvas').getContext('webgl') as WebGL2RenderingContext; + const gl = createNullGL(); const context = new Context(gl); const image = new RGBAImage({width: 2, height: 1}, new Uint8Array([1, 2, 3, 4, 5, 6, 7, 8])); const texture = new Texture(context, image, gl.RGBA); diff --git a/src/webgl/texture.ts b/src/webgl/texture.ts index 796210ea97e..75050c167dc 100644 --- a/src/webgl/texture.ts +++ b/src/webgl/texture.ts @@ -96,21 +96,21 @@ export class Texture { context.pixelStoreUnpackPremultiplyAlpha.setDefault(); } - private _uploadDomImage(image: TexImageSource, gl: WebGLRenderingContext | WebGL2RenderingContext) { + private _uploadDomImage(image: TexImageSource, gl: WebGL2RenderingContext) { gl.texImage2D(gl.TEXTURE_2D, 0, this.format, this.format, gl.UNSIGNED_BYTE, image); } - private _uploadRawData(image: DataTextureImage, wantPremultiply: boolean, width: number, height: number, gl: WebGLRenderingContext | WebGL2RenderingContext) { + private _uploadRawData(image: DataTextureImage, wantPremultiply: boolean, width: number, height: number, gl: WebGL2RenderingContext) { let {data} = image; if (wantPremultiply && data) data = premultiplyAlpha(data); gl.texImage2D(gl.TEXTURE_2D, 0, this.format, width, height, 0, this.format, gl.UNSIGNED_BYTE, data); } - private _updateDomImage(image: TexImageSource, x: number, y: number, gl: WebGLRenderingContext | WebGL2RenderingContext) { + private _updateDomImage(image: TexImageSource, x: number, y: number, gl: WebGL2RenderingContext) { gl.texSubImage2D(gl.TEXTURE_2D, 0, x, y, gl.RGBA, gl.UNSIGNED_BYTE, image); } - private _updateRawData(image: DataTextureImage, wantPremultiply: boolean, x: number, y: number, width: number, height: number, gl: WebGLRenderingContext | WebGL2RenderingContext) { + private _updateRawData(image: DataTextureImage, wantPremultiply: boolean, x: number, y: number, width: number, height: number, gl: WebGL2RenderingContext) { let {data} = image; if (wantPremultiply && data) data = premultiplyAlpha(data); gl.texSubImage2D(gl.TEXTURE_2D, 0, x, y, width, height, gl.RGBA, gl.UNSIGNED_BYTE, data); diff --git a/src/webgl/uniform_binding.ts b/src/webgl/uniform_binding.ts index 14397d16663..8202229000d 100644 --- a/src/webgl/uniform_binding.ts +++ b/src/webgl/uniform_binding.ts @@ -15,7 +15,7 @@ export type UniformLocations = {[_: string]: WebGLUniformLocation}; * A base uniform abstract class */ abstract class Uniform { - gl: WebGLRenderingContext|WebGL2RenderingContext; + gl: WebGL2RenderingContext; location: WebGLUniformLocation; current: T; diff --git a/src/webgl/value.ts b/src/webgl/value.ts index be49bb3a89c..caacaa32acb 100644 --- a/src/webgl/value.ts +++ b/src/webgl/value.ts @@ -1,5 +1,4 @@ import {Color} from '@maplibre/maplibre-gl-style-spec'; -import {isWebGL2} from './webgl2'; import type {Context} from './context'; import type { @@ -27,7 +26,7 @@ export interface IValue { } class BaseValue implements IValue { - gl: WebGLRenderingContext|WebGL2RenderingContext; + gl: WebGL2RenderingContext; current: T; default: T; dirty: boolean; @@ -426,11 +425,7 @@ export class BindVertexArray extends BaseValue { if (v === this.current && !this.dirty) return; const gl = this.gl; - if (isWebGL2(gl)) { - gl.bindVertexArray(v); - } else { - gl.getExtension('OES_vertex_array_object')?.bindVertexArrayOES(v); - } + gl.bindVertexArray(v); this.current = v; this.dirty = false; diff --git a/src/webgl/vertex_buffer.test.ts b/src/webgl/vertex_buffer.test.ts index 479c3963054..aeb60e5b481 100644 --- a/src/webgl/vertex_buffer.test.ts +++ b/src/webgl/vertex_buffer.test.ts @@ -3,12 +3,13 @@ import {VertexBuffer} from './vertex_buffer'; import {StructArrayLayout3i6} from '../data/array_types.g'; import {Context} from './context'; import {type StructArrayMember} from '../util/struct_array'; +import {createNullGL} from '../util/test/null_gl'; describe('VertexBuffer', () => { - let gl: WebGLRenderingContext; + let gl: WebGL2RenderingContext; beforeEach(() => { - gl = document.createElement('canvas').getContext('webgl'); + gl = createNullGL(); }); class TestArray extends StructArrayLayout3i6 {} diff --git a/src/webgl/vertex_buffer.ts b/src/webgl/vertex_buffer.ts index 12c5096ec22..c534aabacfd 100644 --- a/src/webgl/vertex_buffer.ts +++ b/src/webgl/vertex_buffer.ts @@ -64,7 +64,7 @@ export class VertexBuffer { gl.bufferSubData(gl.ARRAY_BUFFER, 0, array.arrayBuffer); } - enableAttributes(gl: WebGLRenderingContext|WebGL2RenderingContext, program: Program) { + enableAttributes(gl: WebGL2RenderingContext, program: Program) { for (const member of this.attributes) { const attribIndex: number | void = program.attributes[member.name]; if (attribIndex !== undefined) { @@ -79,7 +79,7 @@ export class VertexBuffer { * @param program - The active WebGL program * @param vertexOffset - Index of the starting vertex of the segment */ - setVertexAttribPointers(gl: WebGLRenderingContext|WebGL2RenderingContext, program: Program, vertexOffset?: number | null) { + setVertexAttribPointers(gl: WebGL2RenderingContext, program: Program, vertexOffset?: number | null) { for (const member of this.attributes) { const attribIndex: number | void = program.attributes[member.name]; diff --git a/src/webgl/webgl2.ts b/src/webgl/webgl2.ts deleted file mode 100644 index 2b8f18a82e0..00000000000 --- a/src/webgl/webgl2.ts +++ /dev/null @@ -1,12 +0,0 @@ -const cache = new WeakMap(); -export function isWebGL2( - gl: WebGLRenderingContext | WebGL2RenderingContext -): gl is WebGL2RenderingContext { - if (cache.has(gl)) { - return cache.get(gl); - } else { - const value = gl.getParameter(gl.VERSION)?.startsWith('WebGL 2.0'); - cache.set(gl, value); - return value; - } -} diff --git a/test/bench/benchmarks/customlayer.ts b/test/bench/benchmarks/customlayer.ts index 741edca9f59..042213dc5e3 100644 --- a/test/bench/benchmarks/customlayer.ts +++ b/test/bench/benchmarks/customlayer.ts @@ -89,7 +89,7 @@ class Tent3D implements CustomLayerInterface { gl.bufferData(gl.ELEMENT_ARRAY_BUFFER, indexArray, gl.STATIC_DRAW); } - render(gl: WebGLRenderingContext | WebGL2RenderingContext, options: CustomRenderMethodInput) { + render(gl: WebGL2RenderingContext, options: CustomRenderMethodInput) { gl.useProgram(this.program); gl.bindBuffer(gl.ARRAY_BUFFER, this.vertexBuffer); gl.bindBuffer(gl.ELEMENT_ARRAY_BUFFER, this.indexBuffer); diff --git a/test/build/shaders.test.ts b/test/build/shaders.test.ts index a6403f3a387..5f52be58e79 100644 --- a/test/build/shaders.test.ts +++ b/test/build/shaders.test.ts @@ -1,39 +1,8 @@ -import {transpileVertexShaderToWebGL1, transpileFragmentShaderToWebGL1} from '../../src/shaders/shaders'; import {describe, test, expect} from 'vitest'; import {globSync} from 'glob'; import fs from 'fs'; describe('Shaders', () => { - test('webgl2 to webgl1 transpiled shaders should be identical', () => { - const vertexSourceWebGL2 = ` - in vec3 aPos; - uniform mat4 u_matrix; - void main() { - gl_Position = u_matrix * vec4(aPos, 1.0); - gl_PointSize = 20.0; - }`; - const fragmentSourceWebGL2 = ` - out highp vec4 fragColor; - void main() { - fragColor = vec4(1.0, 0.0, 0.0, 1.0); - }`; - const vertexSourceWebGL1 = ` - attribute vec3 aPos; - uniform mat4 u_matrix; - void main() { - gl_Position = u_matrix * vec4(aPos, 1.0); - gl_PointSize = 20.0; - }`; - const fragmentSourceWebGL1 = ` - void main() { - gl_FragColor = vec4(1.0, 0.0, 0.0, 1.0); - }`; - const vertexSourceTranspiled = transpileVertexShaderToWebGL1(vertexSourceWebGL2); - const fragmentSourceTranspiled = transpileFragmentShaderToWebGL1(fragmentSourceWebGL2); - expect(vertexSourceTranspiled.trim()).equals(vertexSourceWebGL1.trim()); - expect(fragmentSourceTranspiled.trim()).equals(fragmentSourceWebGL1.trim()); - }); - // reference: https://webgl2fundamentals.org/webgl/lessons/webgl1-to-webgl2.html test('built-in shaders should be written in WebGL2', () => { const shaderFiles = globSync('../../src/shaders/glsl/*.glsl'); diff --git a/test/build/sourcemaps.test.ts b/test/build/sourcemaps.test.ts index 507d41fd1cb..9a615e50223 100644 --- a/test/build/sourcemaps.test.ts +++ b/test/build/sourcemaps.test.ts @@ -80,6 +80,6 @@ describe('main sourcemap', () => { const s1 = setMinus(actualEntriesInSourcemapJSON, expectedEntriesInSourcemapJSON); expect(s1.length).toBeLessThan(5); const s2 = setMinus(expectedEntriesInSourcemapJSON, actualEntriesInSourcemapJSON); - expect(s2.length).toBeLessThan(17); + expect(s2.length).toBeLessThan(18); }); }); diff --git a/test/examples/check-if-webgl-is-supported.html b/test/examples/check-if-webgl-is-supported.html index d845695915f..d276d009e0d 100644 --- a/test/examples/check-if-webgl-is-supported.html +++ b/test/examples/check-if-webgl-is-supported.html @@ -16,28 +16,28 @@