Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Dual-Stack WebGL Runtime with WebGL2 to WebGL1 Fallback #5198

Merged
merged 14 commits into from
Dec 16, 2024
3 changes: 3 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,11 +1,14 @@
## main

### ✨ Features and improvements

- Add support for projection type expression as part of a refactoring of the transfrom and projection classes ([#5139](https://github.com/maplibre/maplibre-gl-js/pull/5139))
- ⚠️ Support setting WebGL context options on map creation ([#5196](https://github.com/maplibre/maplibre-gl-js/pull/5196)). Previously supported WebGL context options like `antialias`, `preserveDrawingBuffer` and `failIfMajorPerformanceCaveat` must now be defined inside the `canvasContextAttributes` object on `MapOptions`.
- Dual-Stack WebGL Runtime with WebGL2 to WebGL1 Fallback ([#5198](https://github.com/maplibre/maplibre-gl-js/pull/5198))
- _...Add new stuff here..._

### 🐞 Bug fixes

- Fix globe custom layers being supplied incorrect matrices after projection transition to mercator ([#5150](https://github.com/maplibre/maplibre-gl-js/pull/5150))
- Fix custom 3D models disappearing during projection transition ([#5150](https://github.com/maplibre/maplibre-gl-js/pull/5150))
- Fix regression in NavigationControl compass on Firefox and Safari browsers ([#5205](https://github.com/maplibre/maplibre-gl-js/pull/5205))
Expand Down
56 changes: 12 additions & 44 deletions build/generate-shaders.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,51 +12,9 @@ console.log('Generating shaders');
* It will also create a simple package.json file to allow importing this package in webpack
*/

const vertex = globSync('./src/shaders/*.vertex.glsl');
for (const file of vertex) {
const code = fs.readFileSync(file, 'utf8');
const content = glslToTs(code, 'vertex');
const fileName = path.join('.', 'src', 'shaders', `${file.split(path.sep).splice(-1)}.g.ts`);
fs.writeFileSync(fileName, content);
}

console.log(`Finished converting ${vertex.length} vertex shaders`);

const fragment = globSync('./src/shaders/*.fragment.glsl');
for (const file of fragment) {
const code = fs.readFileSync(file, 'utf8');
const content = glslToTs(code, 'fragment');
const fileName = path.join('.', 'src', 'shaders', `${file.split(path.sep).splice(-1)}.g.ts`);
fs.writeFileSync(fileName, content);
}

console.log(`Finished converting ${fragment.length} fragment shaders`);

function glslToTs(code: string, type: 'fragment'|'vertex'): string {
code = code
.trim(); // strip whitespace at the start/end

// WebGL1 Compat -- Start

if (type === 'fragment') {
code = code
.replace(/\bin\s/g, 'varying ') // For fragment shaders, replace "in " with "varying "
.replace('out highp vec4 fragColor;', '');
}

if (type === 'vertex') {
code = code
.replace(/\bin\s/g, 'attribute ') // For vertex shaders, replace "in " with "attribute "
.replace(/\bout\s/g, 'varying '); // For vertex shaders, replace "out " with "varying "
}

code = code
.replace(/fragColor/g, 'gl_FragColor')
.replace(/texture\(/g, 'texture2D(');

// WebGL1 Compat -- End
Comment on lines -39 to -57
Copy link
Contributor Author

Choose a reason for hiding this comment

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

this behavior is moved entirely into transpileToWebGL1() function in shaders.ts to be executed at runtime (also see program.ts changes).


function glslToTs(code: string): string {
code = code
.trim() // strip whitespace at the start/end
.replace(/\s*\/\/[^\n]*\n/g, '\n') // strip double-slash comments
.replace(/\n+/g, '\n') // collapse multi line breaks
.replace(/\n\s+/g, '\n') // strip indentation
Expand All @@ -66,3 +24,13 @@ function glslToTs(code: string, type: 'fragment'|'vertex'): string {
return `// This file is generated. Edit build/generate-shaders.ts, then run \`npm run codegen\`.
export default ${JSON.stringify(code).replaceAll('"', '\'')};\n`;
}

const shaderFiles = globSync('./src/shaders/*.glsl');
for (const file of shaderFiles) {
const glslFile = fs.readFileSync(file, 'utf8');
const tsSource = glslToTs(glslFile);
const fileName = path.join('.', 'src', 'shaders', `${file.split(path.sep).splice(-1)}.g.ts`);
fs.writeFileSync(fileName, tsSource);
}

console.log(`Finished converting ${shaderFiles.length} shaders`);
15 changes: 12 additions & 3 deletions src/render/program.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
import {type PreparedShader, shaders} from '../shaders/shaders';
import {type PreparedShader, shaders, transpileVertexShaderToWebGL1, transpileFragmentShaderToWebGL1} from '../shaders/shaders';
import {type ProgramConfiguration} from '../data/program_configuration';
import {VertexArrayObject} from './vertex_array_object';
import {type Context} from '../gl/context';
import {isWebGL2} from '../gl/webgl2';

import type {SegmentVector} from '../data/segment';
import type {VertexBuffer} from '../gl/vertex_buffer';
Expand Down Expand Up @@ -72,6 +73,9 @@
}

const defines = configuration ? configuration.defines() : [];
if (isWebGL2(gl)) {
defines.unshift('#version 300 es');
}

Check warning on line 78 in src/render/program.ts

View check run for this annotation

Codecov / codecov/patch

src/render/program.ts#L77-L78

Added lines #L77 - L78 were not covered by tests
Comment on lines +76 to +78
Copy link
Contributor Author

Choose a reason for hiding this comment

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

this is a WebGL2 requirement in every shader source code, if we're on WebGL2.

if (showOverdrawInspector) {
defines.push('#define OVERDRAW_INSPECTOR;');
}
Expand All @@ -82,8 +86,13 @@
defines.push(projectionDefine);
}

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');
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 fragmentShader = gl.createShader(gl.FRAGMENT_SHADER);
if (gl.isContextLost()) {
Expand Down
107 changes: 61 additions & 46 deletions src/shaders/shaders.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@

// Disable Flow annotations here because Flow doesn't support importing GLSL files

import preludeFrag from './_prelude.fragment.glsl.g';
Expand Down Expand Up @@ -77,52 +76,51 @@ export type PreparedShader = {
};

export const shaders = {
prelude: compile(preludeFrag, preludeVert),
projectionMercator: compile('', projectionMercatorVert),
projectionGlobe: compile('', projectionGlobeVert),
background: compile(backgroundFrag, backgroundVert),
backgroundPattern: compile(backgroundPatternFrag, backgroundPatternVert),
circle: compile(circleFrag, circleVert),
clippingMask: compile(clippingMaskFrag, clippingMaskVert),
heatmap: compile(heatmapFrag, heatmapVert),
heatmapTexture: compile(heatmapTextureFrag, heatmapTextureVert),
collisionBox: compile(collisionBoxFrag, collisionBoxVert),
collisionCircle: compile(collisionCircleFrag, collisionCircleVert),
debug: compile(debugFrag, debugVert),
depth: compile(clippingMaskFrag, depthVert),
fill: compile(fillFrag, fillVert),
fillOutline: compile(fillOutlineFrag, fillOutlineVert),
fillOutlinePattern: compile(fillOutlinePatternFrag, fillOutlinePatternVert),
fillPattern: compile(fillPatternFrag, fillPatternVert),
fillExtrusion: compile(fillExtrusionFrag, fillExtrusionVert),
fillExtrusionPattern: compile(fillExtrusionPatternFrag, fillExtrusionPatternVert),
hillshadePrepare: compile(hillshadePrepareFrag, hillshadePrepareVert),
hillshade: compile(hillshadeFrag, hillshadeVert),
line: compile(lineFrag, lineVert),
lineGradient: compile(lineGradientFrag, lineGradientVert),
linePattern: compile(linePatternFrag, linePatternVert),
lineSDF: compile(lineSDFFrag, lineSDFVert),
raster: compile(rasterFrag, rasterVert),
symbolIcon: compile(symbolIconFrag, symbolIconVert),
symbolSDF: compile(symbolSDFFrag, symbolSDFVert),
symbolTextAndIcon: compile(symbolTextAndIconFrag, symbolTextAndIconVert),
terrain: compile(terrainFrag, terrainVert),
terrainDepth: compile(terrainDepthFrag, terrainVertDepth),
terrainCoords: compile(terrainCoordsFrag, terrainVertCoords),
projectionErrorMeasurement: compile(projectionErrorMeasurementFrag, projectionErrorMeasurementVert),
atmosphere: compile(atmosphereFrag, atmosphereVert),
sky: compile(skyFrag, skyVert),
prelude: prepare(preludeFrag, preludeVert),
projectionMercator: prepare('', projectionMercatorVert),
projectionGlobe: prepare('', projectionGlobeVert),
background: prepare(backgroundFrag, backgroundVert),
backgroundPattern: prepare(backgroundPatternFrag, backgroundPatternVert),
circle: prepare(circleFrag, circleVert),
clippingMask: prepare(clippingMaskFrag, clippingMaskVert),
heatmap: prepare(heatmapFrag, heatmapVert),
heatmapTexture: prepare(heatmapTextureFrag, heatmapTextureVert),
collisionBox: prepare(collisionBoxFrag, collisionBoxVert),
collisionCircle: prepare(collisionCircleFrag, collisionCircleVert),
debug: prepare(debugFrag, debugVert),
depth: prepare(clippingMaskFrag, depthVert),
fill: prepare(fillFrag, fillVert),
fillOutline: prepare(fillOutlineFrag, fillOutlineVert),
fillOutlinePattern: prepare(fillOutlinePatternFrag, fillOutlinePatternVert),
fillPattern: prepare(fillPatternFrag, fillPatternVert),
fillExtrusion: prepare(fillExtrusionFrag, fillExtrusionVert),
fillExtrusionPattern: prepare(fillExtrusionPatternFrag, fillExtrusionPatternVert),
hillshadePrepare: prepare(hillshadePrepareFrag, hillshadePrepareVert),
hillshade: prepare(hillshadeFrag, hillshadeVert),
line: prepare(lineFrag, lineVert),
lineGradient: prepare(lineGradientFrag, lineGradientVert),
linePattern: prepare(linePatternFrag, linePatternVert),
lineSDF: prepare(lineSDFFrag, lineSDFVert),
raster: prepare(rasterFrag, rasterVert),
symbolIcon: prepare(symbolIconFrag, symbolIconVert),
symbolSDF: prepare(symbolSDFFrag, symbolSDFVert),
symbolTextAndIcon: prepare(symbolTextAndIconFrag, symbolTextAndIconVert),
terrain: prepare(terrainFrag, terrainVert),
terrainDepth: prepare(terrainDepthFrag, terrainVertDepth),
terrainCoords: prepare(terrainCoordsFrag, terrainVertCoords),
projectionErrorMeasurement: prepare(projectionErrorMeasurementFrag, projectionErrorMeasurementVert),
atmosphere: prepare(atmosphereFrag, atmosphereVert),
sky: prepare(skyFrag, skyVert),
};

// Expand #pragmas to #ifdefs.

function compile(fragmentSource: string, vertexSource: string): PreparedShader {
/** Expand #pragmas to #ifdefs, extract attributes and uniforms */
function prepare(fragmentSource: string, vertexSource: string): PreparedShader {
const re = /#pragma mapbox: ([\w]+) ([\w]+) ([\w]+) ([\w]+)/g;
Copy link
Contributor Author

Choose a reason for hiding this comment

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

a small rename and proper jsdoc comment on the function.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

a small rename and proper jsdoc comment on the function.


const staticAttributes = vertexSource.match(/attribute ([\w]+) ([\w]+)/g);
const vertexAttributes = vertexSource.match(/in ([\w]+) ([\w]+)/g);
const fragmentUniforms = fragmentSource.match(/uniform ([\w]+) ([\w]+)([\s]*)([\w]*)/g);
const vertexUniforms = vertexSource.match(/uniform ([\w]+) ([\w]+)([\s]*)([\w]*)/g);
const staticUniforms = vertexUniforms ? vertexUniforms.concat(fragmentUniforms) : fragmentUniforms;
const shaderUniforms = vertexUniforms ? vertexUniforms.concat(fragmentUniforms) : fragmentUniforms;

const fragmentPragmas = {};

Expand All @@ -131,7 +129,7 @@ function compile(fragmentSource: string, vertexSource: string): PreparedShader {
if (operation === 'define') {
return `
#ifndef HAS_UNIFORM_u_${name}
varying ${precision} ${type} ${name};
in ${precision} ${type} ${name};
#else
Comment on lines -134 to +132
Copy link
Contributor Author

Choose a reason for hiding this comment

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

now that we assume original shaders to be in WebGL2, we should update our attribute and uniform extraction/scanning code.

uniform ${precision} ${type} u_${name};
#endif
Expand All @@ -154,8 +152,8 @@ uniform ${precision} ${type} u_${name};
return `
#ifndef HAS_UNIFORM_u_${name}
uniform lowp float u_${name}_t;
attribute ${precision} ${attrType} a_${name};
varying ${precision} ${type} ${name};
in ${precision} ${attrType} a_${name};
out ${precision} ${type} ${name};
#else
Comment on lines -157 to +156
Copy link
Contributor Author

Choose a reason for hiding this comment

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

^ same as above

uniform ${precision} ${type} u_${name};
#endif
Expand Down Expand Up @@ -185,7 +183,7 @@ uniform ${precision} ${type} u_${name};
return `
#ifndef HAS_UNIFORM_u_${name}
uniform lowp float u_${name}_t;
attribute ${precision} ${attrType} a_${name};
in ${precision} ${attrType} a_${name};
#else
Comment on lines -188 to +186
Copy link
Contributor Author

Choose a reason for hiding this comment

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

^ same as above

uniform ${precision} ${type} u_${name};
#endif
Expand Down Expand Up @@ -213,5 +211,22 @@ uniform ${precision} ${type} u_${name};
}
});

return {fragmentSource, vertexSource, staticAttributes, staticUniforms};
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(');
}
48 changes: 48 additions & 0 deletions test/build/shaders.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
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');
for (const shaderFile of shaderFiles) {
const shaderSource = fs.readFileSync(shaderFile, 'utf8');
expect(shaderSource.includes('attribute')).toBe(false);
expect(shaderSource.includes('varying')).toBe(false);
expect(shaderSource.includes('gl_FragColor')).toBe(false);
expect(shaderSource.includes('texture2D')).toBe(false);
}
});
});
Loading