Skip to content
Open
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
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
1 change: 1 addition & 0 deletions src/geo/projection/globe_projection_error_measurement.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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();
Expand Down
22 changes: 22 additions & 0 deletions src/gl/framebuffer.test.ts
Original file line number Diff line number Diff line change
@@ -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');
});
});
5 changes: 5 additions & 0 deletions src/gl/framebuffer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,11 @@ export class Framebuffer {
} else if (hasStencil) {
throw new Error('Stencil cannot be set without depth');
}
}

checkFramebufferStatus() {
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

This isn't really checking status, does it?

Copy link
Copy Markdown
Contributor Author

@johanrd johanrd Apr 11, 2026

Choose a reason for hiding this comment

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

@HarelM Good catch — I think this name is papering over a deeper question about whether the check should exist at all. Some history:

It started as a debug-only assert:

assert(gl.checkFramebufferStatus(gl.FRAMEBUFFER) === gl.FRAMEBUFFER_COMPLETE);
as a sanity check for "did I forget an attachment?" in the debug build.

#1485 turned it into a production throw, and you flagged exactly this risk on that PR:

The only main "issue" I see work this approach is that there is a functionally change in this commit: before this code simply didn't exist in production so if there was an assert that might be triggered but didn't cause any actual error the user wouldn't know as opposed to now where there might be "throws" that cause actual problems.

That seems to reflect what's showing up in Sentry now.

Then #5266 tightened the

}).catch(() => {}); // ignore abort error
but specifically carved out framebuffer-not-complete.

Zooming out

Given all that, I'd suggest deleting ensureFramebufferComplete() and all 5 call sites instead of renaming it. After that, isFramebufferNotCompleteError and the redraw() try/catch guard an error that can no longer be thrown from our code, and can go too.

The alternative is developing an active recovery path (force context loss/restore on failure), but honestly WebGL may handle a transient framebuffer blip fine on its own if we just get out of the way. Happy to restructure the PR either direction.

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Given the history you dug, I trust you understand this better than me, so if you think a direction with worth pursuing, feel free to do so.
I would like to get @birkskyum and @ToHold 's input here since @birkskyum recently fixed a long lasting bug with framebuffer misalignment and @ToHold did a lot of work around recovery from context loss.

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

CC: @mvanhorn who's also added some PRs around context restored.

Is there a sudden interest in making this work well which wasn't a big motivation before?

const gl = this.context.gl;
this.context.bindFramebuffer.set(this.framebuffer);
if (gl.checkFramebufferStatus(gl.FRAMEBUFFER) !== gl.FRAMEBUFFER_COMPLETE) {
throw createFramebufferNotCompleteError();
}
Expand Down
1 change: 1 addition & 0 deletions src/gl/render_pool.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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};
}

Expand Down
1 change: 1 addition & 0 deletions src/render/draw_heatmap.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}
Expand Down
1 change: 1 addition & 0 deletions src/render/draw_hillshade.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down
1 change: 1 addition & 0 deletions src/render/terrain.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}

Expand Down
8 changes: 7 additions & 1 deletion src/ui/map.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}
Expand Down
11 changes: 11 additions & 0 deletions src/ui/map_tests/map_render.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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();

Expand Down
Loading