Skip to content

Fix unhandled framebuffer error when tab wakes from sleep#7303

Open
johanrd wants to merge 7 commits intomaplibre:mainfrom
johanrd:fix/framebuffer-error-on-tab-wake
Open

Fix unhandled framebuffer error when tab wakes from sleep#7303
johanrd wants to merge 7 commits intomaplibre:mainfrom
johanrd:fix/framebuffer-error-on-tab-wake

Conversation

@johanrd
Copy link
Copy Markdown
Contributor

@johanrd johanrd commented Mar 20, 2026

I saw this error in Sentry from a production app:

  • 14:21:04 — user navigates to a page with map, map loads with satellite tiles + heatmap
  • 14:46:38 — last user interaction
  • 07:07:49 — next morning, tile fetches resume (tab wakes up after ~16 hours)
  • 07:38:34Error: Framebuffer is not complete fires, unhandled

Two possible root causes:

  1. Framebuffer constructor checks the wrong FBO. gl.createFramebuffer() doesn't bind the new FBO, so checkFramebufferStatus(gl.FRAMEBUFFER) validates whatever was previously bound. After a long sleep the previously-bound FBO can be stale, causing a false-positive throw.

  2. redraw() has no error handling. triggerRepaint() already catches framebuffer errors, but redraw() — the resize observer path — does not. The error reaches the global handler.

Design decisions

Silent swallowing vs error event. This PR matches triggerRepaint()'s existing behavior: catch and discard. For transient errors (tab sleep) this is fine — the map shows the previous good frame and recovers on the next interaction. The downside is that consumers have no signal when a frame fails, so persistent errors would show stale content silently. An alternative would be to this.fire(new ErrorEvent(error)) before swallowing — happy to add this if preferred, and it could be applied to triggerRepaint() too.

Explicit checkFramebufferStatus() calls. The constructor check was moved to an explicit method that binds its own FBO before checking. Called at all 5 callsites after attachments are set. Automating this (e.g. inside ColorAttachment.set()) would be a refactor beyond scope.

Test plan

Test Before fix After fix
Framebuffer constructor with stale bound FBO Throws Does not throw
checkFramebufferStatus() on incomplete FBO Method does not exist Throws
redraw() with framebuffer error Unhandled error Caught silently

Checklist

  • Confirm your changes do not include backports from Mapbox projects (unless with compliant license)
  • Briefly describe the changes in this PR.
  • Link to related issues.
  • Include before/after visuals or gifs if this PR includes visual changes. — N/A, no visual changes
  • Write tests for all new functionality.
  • Document any changes to public APIs. — N/A, no public API changes
  • Post benchmark scores. — N/A
  • Add an entry to CHANGELOG.md under the ## main section.

Cowritten by claude

…dded a `checkFramebufferStatus()` method that callers use after attachments are set (at which point `ColorAttachment.set` has already bound the correct FBO). Added the call at all 5 existing callsites. Added try/catch to `redraw()` matching the existing pattern in `triggerRepaint()`.
@johanrd johanrd force-pushed the fix/framebuffer-error-on-tab-wake branch from c97e047 to 40f7ff2 Compare March 20, 2026 09:07
@johanrd johanrd marked this pull request as ready for review March 20, 2026 09:32
@johanrd johanrd marked this pull request as draft March 20, 2026 09:41
@johanrd johanrd force-pushed the fix/framebuffer-error-on-tab-wake branch from 3128c87 to c725d1d Compare March 20, 2026 10:15
@codecov
Copy link
Copy Markdown

codecov bot commented Mar 20, 2026

Codecov Report

❌ Patch coverage is 91.66667% with 1 line in your changes missing coverage. Please review.
✅ Project coverage is 92.71%. Comparing base (901b3af) to head (f8352bb).
⚠️ Report is 28 commits behind head on main.

Files with missing lines Patch % Lines
src/ui/map.ts 75.00% 1 Missing ⚠️
Additional details and impacted files
@@            Coverage Diff             @@
##             main    #7303      +/-   ##
==========================================
- Coverage   92.71%   92.71%   -0.01%     
==========================================
  Files         288      288              
  Lines       24042    24053      +11     
  Branches     5093     5094       +1     
==========================================
+ Hits        22291    22301      +10     
- Misses       1751     1752       +1     

☔ View full report in Codecov by Sentry.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

@johanrd johanrd marked this pull request as ready for review March 20, 2026 10:33
Comment thread src/gl/framebuffer.ts
}
}

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?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants