Skip to content
Closed
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
9 changes: 9 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,12 @@
## 3.21.1

### Bug fixes 🐞

- Fix map crashes on Android Pixel devices (Adreno GPUs) during zoom/pan interactions caused by symbol UBO dynamic indexing.
- Fix `readFloat`/`readUint` in the symbol vertex shader to use Adreno-safe explicit swizzle helpers instead of direct bracket indexing.
- Fix stale default in `getMaxFeatureCount` that could cause incorrect symbol batching limits.
- Guard against potential division by zero in the cutoff fade-range calculation on mobile viewports.

## 3.20.0-rc.1

### Features and improvements ✨
Expand Down
2 changes: 1 addition & 1 deletion src/data/bucket/symbol_properties_ubo.ts
Original file line number Diff line number Diff line change
Expand Up @@ -166,7 +166,7 @@ export class SymbolPropertiesUBO {
* Maximum number of features that fit in one UBO batch given a header.
* Returns Infinity when dataDrivenBlockSizeVec4 is 0 (all properties constant).
*/
static getMaxFeatureCount(header: SymbolPropertyHeader, propsDwords: number = 4096 - SymbolPropertiesUBO.HEADER_DWORDS): number {
static getMaxFeatureCount(header: SymbolPropertyHeader, propsDwords: number = 4096): number {
const dataDrivenBlockSizeDwords = header.dataDrivenBlockSizeVec4 * 4;
if (dataDrivenBlockSizeDwords === 0) return Infinity;
return Math.floor(propsDwords / dataDrivenBlockSizeDwords);
Expand Down
8 changes: 4 additions & 4 deletions src/gl/context.ts
Original file line number Diff line number Diff line change
Expand Up @@ -153,10 +153,10 @@ class Context {
// Force manual rendering for instanced draw calls having gl_InstanceID usage in the shader for PowerVR adapters
this.forceManualRenderingForInstanceIDShaders = (options && !!options.forceManualRenderingForInstanceIDShaders) || (this.renderer && this.renderer.indexOf("PowerVR") !== -1);

// Disable symbol UBO batching for PowerVR GPUs. PowerVR drivers do not correctly handle
// dynamic (non-uniform) indexing into UBO arrays, which is technically undefined behavior
// in GLSL ES 3.00. The fallback uses pragma-based paint properties.
this.disableSymbolUBO = (options && !!options.forceDisableSymbolUBO) || (this.renderer && this.renderer.indexOf("PowerVR") !== -1);
// Disable symbol UBO batching for PowerVR and Adreno GPUs. These drivers do not
// correctly handle dynamic (non-uniform) indexing into UBO arrays, which is technically
// undefined behavior in GLSL ES 3.00. The fallback uses pragma-based paint properties.
this.disableSymbolUBO = (options && !!options.forceDisableSymbolUBO) || (this.renderer && (this.renderer.indexOf("PowerVR") !== -1 || this.renderer.indexOf("Adreno") !== -1));

if (!this.options.extTextureFloatLinearForceOff) {
this.extTextureFloatLinear = gl.getExtension('OES_texture_float_linear');
Expand Down
2 changes: 1 addition & 1 deletion src/render/cutoff.ts
Original file line number Diff line number Diff line change
Expand Up @@ -76,7 +76,7 @@ export const getCutoffParams = (painter: Painter, cutoffFadeRange: number): Cuto
};
}

const zRange = tr._farZ - tr._nearZ;
const zRange = Math.max(tr._farZ - tr._nearZ, 1.0);
const fadeRangePixels = cutoffFadeRange * tr.height * FADE_RANGE_HEIGHT_SCALE;

// Half-rate exponential zoom scaling: grows with zoom but stays bounded
Expand Down
4 changes: 2 additions & 2 deletions src/shaders/symbol.vertex.glsl
Original file line number Diff line number Diff line change
Expand Up @@ -275,11 +275,11 @@ vec4 readVec4(uint baseOffsetVec4, uint propertyOffsetDwords) {
}

float readFloat(vec4 slot, uint propertyOffsetDwords) {
return slot[propertyOffsetDwords % DWORDS_PER_VEC4];
return vec4At(slot, propertyOffsetDwords % DWORDS_PER_VEC4);
}

uint readUint(uvec4 slot, uint offset) {
return slot[offset % DWORDS_PER_VEC4];
return uvec4At(slot, offset % DWORDS_PER_VEC4);
}

vec2 readVec2(vec4 slot, uint propertyOffsetDwords) {
Expand Down
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
{
"version": 8,
"metadata": {
"test": {
"height": 64,
"width": 64,
"forceDisableSymbolUBO": true
}
},
"center": [ 0, 0 ],
"zoom": 0,
"sources": {
"geojson": {
"type": "geojson",
"data": {
"type": "FeatureCollection",
"features": [
{
"type": "Feature",
"properties": { "x": 0 },
"geometry": {
"type": "Point",
"coordinates": [
0,
-10
]
}
},
{
"type": "Feature",
"properties": { "x": 1 },
"geometry": {
"type": "Point",
"coordinates": [
0,
10
]
}
}
]
}
}
},
"glyphs": "local://glyphs/{fontstack}/{range}.pbf",
"layers": [
{
"id": "text",
"type": "symbol",
"source": "geojson",
"layout": {
"text-allow-overlap": true,
"text-field": "Test",
"text-font": [
"Open Sans Semibold",
"Arial Unicode MS Bold"
]
},
"paint": {
"text-color": {
"property": "x",
"stops": [
[
0,
"red"
],
[
1,
"blue"
]
]
}
}
}
]
}
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
{
"version": 8,
"metadata": {
"test": {
"width": 64,
"height": 64,
"forceDisableSymbolUBO": true
}
},
"sources": {
"geojson": {
"type": "geojson",
"data": {
"type": "FeatureCollection",
"features": [
{
"type": "Feature",
"properties": { "x": 0 },
"geometry": {
"type": "Point",
"coordinates": [
0,
-10
]
}
}, {
"type": "Feature",
"properties": { "x": 1 },
"geometry": {
"type": "Point",
"coordinates": [
0,
10
]
}
}
]
}
}
},
"glyphs": "local://glyphs/{fontstack}/{range}.pbf",
"layers": [
{
"id": "symbol",
"type": "symbol",
"source": "geojson",
"layout": {
"text-field": "ABC",
"text-font": [
"Open Sans Semibold",
"Arial Unicode MS Bold"
]
},
"paint": {
"text-halo-width": 2,
"text-halo-color": {
"property": "x",
"stops": [
[
0,
"red"
],
[
1,
"blue"
]
]
}
}
}
]
}
1 change: 1 addition & 0 deletions test/integration/render-tests/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -162,6 +162,7 @@ export async function renderMap(style, options, currentTestName) {
extTextureFloatLinearForceOff: options.textureFloatLinear === undefined ? false : !options.textureFloatLinear,
// ordinary instancing is enabled by default, manual is disabled
forceManualRenderingForInstanceIDShaders: options.forceManualRenderingForInstanceIDShaders,
forceDisableSymbolUBO: options.forceDisableSymbolUBO,
},
worldview: options.worldview,
maxZoom: options.maxZoom,
Expand Down
6 changes: 3 additions & 3 deletions test/unit/data/symbol_property_binder_ubo.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -91,16 +91,16 @@ describe('SymbolPropertiesUBO', () => {

test('getMaxFeatureCount returns correct value', () => {
// dataDrivenBlockSizeVec4=1 → dataDrivenBlockSizeDwords=4
// propsDwords = 4096 - 12 (HEADER_DWORDS) = 4084
// maxFeatures = floor(4084 / 4) = 1021
// propsDwords = 4096
// maxFeatures = floor(4096 / 4) = 1024
const header: SymbolPropertyHeader = {
dataDrivenMask: 0b00000100,
zoomDependentMask: 0,
cameraMask: 0,
dataDrivenBlockSizeVec4: 1,
offsets: [0, 0, 0, 0, 0, 0, 0, 0, 0],
};
expect(SymbolPropertiesUBO.getMaxFeatureCount(header)).toEqual(1021);
expect(SymbolPropertiesUBO.getMaxFeatureCount(header)).toEqual(1024);
});

test('getMaxFeatureCount returns Infinity when all constant', () => {
Expand Down
93 changes: 93 additions & 0 deletions test/unit/gl/context_gpu_flags.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,93 @@
import {test, expect, describe} from '../../util/vitest';
import Context from '../../../src/gl/context';

describe('Context GPU flags', () => {
/**
* Returns a WebGL2 context whose WEBGL_debug_renderer_info extension
* reports the given renderer string. This lets us exercise the
* renderer-substring detection in the Context constructor without
* relying on the host GPU.
*/
function createGLWithRenderer(renderer: string): WebGL2RenderingContext {
const el = window.document.createElement('canvas');
const gl = el.getContext('webgl2');
if (!gl) throw new Error('WebGL2 context unavailable — cannot run GPU flag tests');

const UNMASKED_RENDERER = 0x9246; // WEBGL_debug_renderer_info constant
const fakeExt = {UNMASKED_RENDERER_WEBGL: UNMASKED_RENDERER, UNMASKED_VENDOR_WEBGL: 0x9245};

const origGetExtension = gl.getExtension.bind(gl);
const origGetParameter = gl.getParameter.bind(gl);

gl.getExtension = ((name: string) => {
if (name === 'WEBGL_debug_renderer_info') return fakeExt;
return origGetExtension(name);
}) as typeof gl.getExtension;

gl.getParameter = ((pname: number) => {
if (pname === UNMASKED_RENDERER) return renderer;
if (pname === fakeExt.UNMASKED_VENDOR_WEBGL) return 'Test Vendor';
return origGetParameter(pname);
}) as typeof gl.getParameter;

return gl;
}

/**
* Returns a WebGL2 context where WEBGL_debug_renderer_info is
* unavailable (getExtension returns null), so Context.renderer
* stays undefined.
*/
function createGLWithoutRendererInfo(): WebGL2RenderingContext {
const el = window.document.createElement('canvas');
const gl = el.getContext('webgl2');
if (!gl) throw new Error('WebGL2 context unavailable — cannot run GPU flag tests');

const origGetExtension = gl.getExtension.bind(gl);

gl.getExtension = ((name: string) => {
if (name === 'WEBGL_debug_renderer_info') return null;
return origGetExtension(name);
}) as typeof gl.getExtension;

return gl;
}

describe('disableSymbolUBO', () => {
test('enabled when forceDisableSymbolUBO option is set', () => {
const gl = createGLWithRenderer('Generic Desktop GPU');
const context = new Context(gl, {forceDisableSymbolUBO: true});
expect(context.disableSymbolUBO).toBe(true);
});

test('enabled when renderer contains Adreno', () => {
const gl = createGLWithRenderer('Adreno (TM) 730');
const context = new Context(gl);
expect(context.disableSymbolUBO).toBe(true);
});

test('enabled when renderer contains PowerVR', () => {
const gl = createGLWithRenderer('PowerVR Rogue GE8320');
const context = new Context(gl);
expect(context.disableSymbolUBO).toBe(true);
});

test('disabled for non-matching renderer', () => {
const gl = createGLWithRenderer('ANGLE (Apple, Apple M2 Max, OpenGL 4.1)');
const context = new Context(gl);
expect(context.disableSymbolUBO).toBe(false);
});

test('disabled when WEBGL_debug_renderer_info is unavailable', () => {
const gl = createGLWithoutRendererInfo();
const context = new Context(gl);
expect(context.disableSymbolUBO).toBeFalsy();
});

test('forceDisableSymbolUBO still works without renderer info', () => {
const gl = createGLWithoutRendererInfo();
const context = new Context(gl, {forceDisableSymbolUBO: true});
expect(context.disableSymbolUBO).toBe(true);
});
});
});
Loading
Loading