diff --git a/CHANGELOG.md b/CHANGELOG.md index b933eab21d5..b43d798163d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -18,6 +18,7 @@ - Fix Terrain GPU resource leak: free FBO, textures, and meshes when terrain is disabled via `setTerrain(null)` ([#7288](https://github.com/maplibre/maplibre-gl-js/pull/7288)) (by [@johanrd](https://github.com/johanrd)) - Fix guard against partial layout in `PauseablePlacement` ([#7079](https://github.com/maplibre/maplibre-gl-js/pull/7079)) (by [@garethbowker](https://github.com/garethbowker)) - Fix missing tile encoding for MLT queryRenderedFeatures ([#7056](https://github.com/maplibre/maplibre-gl-js/pull/7056)) (by [@dannote](https://github.com/dannote) and [@ted-piotrowski](https://github.com/ted-piotrowski)) +- Fix unhandled framebuffer error when tab wakes from sleep: move framebuffer completeness check to after attachments are set, and add error handling to `redraw()` matching `triggerRepaint()` ([#7303](https://github.com/maplibre/maplibre-gl-js/pull/7303)) (by [@johanrd](https://github.com/johanrd)) - _...Add new stuff here..._ ## 5.20.2 diff --git a/src/geo/projection/globe_projection_error_measurement.ts b/src/geo/projection/globe_projection_error_measurement.ts index ad830310a7c..4a1d2430ec1 100644 --- a/src/geo/projection/globe_projection_error_measurement.ts +++ b/src/geo/projection/globe_projection_error_measurement.ts @@ -109,6 +109,7 @@ export class ProjectionErrorMeasurement { this._fbo = context.createFramebuffer(this._texWidth, this._texHeight, false, false); this._fbo.colorAttachment.set(texture); + this._fbo.checkFramebufferStatus(); if (isWebGL2(gl)) { this._pbo = gl.createBuffer(); diff --git a/src/gl/framebuffer.test.ts b/src/gl/framebuffer.test.ts new file mode 100644 index 00000000000..14b5da5be2a --- /dev/null +++ b/src/gl/framebuffer.test.ts @@ -0,0 +1,22 @@ +import {describe, test, expect, vi} from 'vitest'; +import {Context} from './context'; +import {Framebuffer} from './framebuffer'; + +describe('Framebuffer', () => { + test('constructor does not check framebuffer status before attachments are set', () => { + const gl = document.createElement('canvas').getContext('webgl'); + vi.spyOn(gl, 'checkFramebufferStatus').mockReturnValue(0); + const context = new Context(gl); + + expect(() => new Framebuffer(context, 256, 256, false, false)).not.toThrow(); + }); + + test('checkFramebufferStatus throws when framebuffer is incomplete', () => { + const gl = document.createElement('canvas').getContext('webgl'); + vi.spyOn(gl, 'checkFramebufferStatus').mockReturnValue(0); + const context = new Context(gl); + const fbo = new Framebuffer(context, 256, 256, false, false); + + expect(() => fbo.checkFramebufferStatus()).toThrow('Framebuffer is not complete'); + }); +}); diff --git a/src/gl/framebuffer.ts b/src/gl/framebuffer.ts index 35f52631b51..90e985555bf 100644 --- a/src/gl/framebuffer.ts +++ b/src/gl/framebuffer.ts @@ -28,6 +28,11 @@ export class Framebuffer { } else if (hasStencil) { throw new Error('Stencil cannot be set without depth'); } + } + + checkFramebufferStatus() { + const gl = this.context.gl; + this.context.bindFramebuffer.set(this.framebuffer); if (gl.checkFramebufferStatus(gl.FRAMEBUFFER) !== gl.FRAMEBUFFER_COMPLETE) { throw createFramebufferNotCompleteError(); } diff --git a/src/gl/render_pool.ts b/src/gl/render_pool.ts index 7a628269ba0..34d14af679a 100644 --- a/src/gl/render_pool.ts +++ b/src/gl/render_pool.ts @@ -47,6 +47,7 @@ export class RenderPool { } fbo.depthAttachment.set(this._context.createRenderbuffer(this._context.gl.DEPTH_STENCIL, this._tileSize, this._tileSize)); fbo.colorAttachment.set(texture.texture); + fbo.checkFramebufferStatus(); return {id, fbo, texture, stamp: -1, inUse: false}; } diff --git a/src/render/draw_heatmap.ts b/src/render/draw_heatmap.ts index 06c23a68152..8c5dcdda019 100644 --- a/src/render/draw_heatmap.ts +++ b/src/render/draw_heatmap.ts @@ -227,6 +227,7 @@ function createHeatmapFbo(context: Context, width: number, height: number): Fram const fbo = context.createFramebuffer(width, height, false, false); fbo.colorAttachment.set(texture); + fbo.checkFramebufferStatus(); return fbo; } diff --git a/src/render/draw_hillshade.ts b/src/render/draw_hillshade.ts index d1d78ddc9c7..6fd30d848f2 100644 --- a/src/render/draw_hillshade.ts +++ b/src/render/draw_hillshade.ts @@ -144,6 +144,7 @@ function prepareHillshade( fbo = tile.fbo = context.createFramebuffer(tileSize, tileSize, true, false); fbo.colorAttachment.set(renderTexture.texture); + fbo.checkFramebufferStatus(); } context.bindFramebuffer.set(fbo.framebuffer); diff --git a/src/render/terrain.ts b/src/render/terrain.ts index c1857563f6e..106ea3ee8b7 100644 --- a/src/render/terrain.ts +++ b/src/render/terrain.ts @@ -343,6 +343,7 @@ export class Terrain { this._fbo.depthAttachment.set(painter.context.createRenderbuffer(painter.context.gl.DEPTH_COMPONENT16, width, height)); } this._fbo.colorAttachment.set(texture === 'coords' ? this._fboCoordsTexture.texture : this._fboDepthTexture.texture); + this._fbo.checkFramebufferStatus(); return this._fbo; } diff --git a/src/ui/map.ts b/src/ui/map.ts index 04c2edb2016..bbc02ef4a5b 100644 --- a/src/ui/map.ts +++ b/src/ui/map.ts @@ -3726,7 +3726,13 @@ export class Map extends Camera { this._frameRequest.abort(); this._frameRequest = null; } - this._render(0); + try { + this._render(0); + } catch (error) { + if (!isAbortError(error) && !isFramebufferNotCompleteError(error)) { + throw error; + } + } } return this; } diff --git a/src/ui/map_tests/map_render.test.ts b/src/ui/map_tests/map_render.test.ts index d425b49eba7..bb6166f01fc 100644 --- a/src/ui/map_tests/map_render.test.ts +++ b/src/ui/map_tests/map_render.test.ts @@ -80,6 +80,17 @@ test('no render before style loaded', async () => { expect(loaded).toBeTruthy(); }); +test('redraw does not throw on framebuffer error', async () => { + const map = createMap({style: createStyle()}); + await map.once('idle'); + + vi.spyOn(map.painter, 'render').mockImplementationOnce(() => { + throw new Error('Framebuffer is not complete'); + }); + + expect(() => map.redraw()).not.toThrow(); +}); + test('redraw', async () => { const map = createMap();