Skip to content

Symbol SDF Performance Optimization: Render Halo and Glyph in a Single Pass (-40% Time Reduction)#7436

Open
xavierjs wants to merge 17 commits intomaplibre:mainfrom
xavierjs:xavierjs/drawSymbolSDFHaloWithoutOverdraw
Open

Symbol SDF Performance Optimization: Render Halo and Glyph in a Single Pass (-40% Time Reduction)#7436
xavierjs wants to merge 17 commits intomaplibre:mainfrom
xavierjs:xavierjs/drawSymbolSDFHaloWithoutOverdraw

Conversation

@xavierjs
Copy link
Copy Markdown
Contributor

@xavierjs xavierjs commented Apr 9, 2026

Hello,

Thank you for considering my PR.

Issue

Text is rendered as SDF symbols, with each glyph represented as a separate symbol. To enhance contrast, a white halo is often added around the text. When a halo is present, rendering occurs in two passes: the first pass draws the halo, and the second pass renders the glyphs themselves. Both passes use the same shader, symbol_SDF.glsl, with a uniform variable indicating whether the current pass is for the halo or the glyph.

This means the fragment shader runs for all pixels within the glyph, including those that are fully transparent. The vertex shader is also executed for each glyph (4 times if it is represented by a quad).

Proposed solution

Both the glyph and halo values are computed within the fragment shader, and the final composited result of the text and halo is rendered in a single pass. This approach reduces the number of shader executions by half.

Here is a screenshot portion of this example: http://localhost:9966/test/examples/filter-symbols-by-text-input.html
glyphHaloView

Now we enable the overdraw inspector by running map.showOverdrawInspector=truein the JS console. This animated GIF shows with and without the optimization. There is less overdraw for glyphs with a halo:
glyphHaloOverdraw

Benchmarks

There is no benchmark for rendering SDF symbols with a halo (LayerSymbol benchmark renders characters without a halo).
This PR introduces a new benchmark for that, LayerSymbolWithHalo. Here are the results:

Before:
Google Chrome, Macbook Pro M4 Max: 4.47ms
Google Chrome, Macbook Pro M4 Max with swiftshaders (hardware acceleration disabled): 335ms

After:
Google Chrome, Macbook Pro M4 Max: 3.13ms (30% time reduction)
Google Chrome, Macbook Pro M4 Max with swiftshaders (hardware acceleration disabled): 186ms (44% time reduction - it shoud theoretically tend to 50% with even crappier GPU)

Tests

npm run test-renderruns exactly as before. I just had to update the expected images for the halo tests.
Before, the expected images only had the halo rendered.
With this change, it is not possible to render only the halo. We need to render the glyph + the halo.

Misc

No related issues.
This PR does not include any visual changes and does not require additional tests.

Launch Checklist

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

@codecov
Copy link
Copy Markdown

codecov bot commented Apr 9, 2026

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 92.77%. Comparing base (7f61f6f) to head (a900489).
⚠️ Report is 9 commits behind head on main.

Additional details and impacted files
@@           Coverage Diff           @@
##             main    #7436   +/-   ##
=======================================
  Coverage   92.77%   92.77%           
=======================================
  Files         289      289           
  Lines       24017    24023    +6     
  Branches     5100     5103    +3     
=======================================
+ Hits        22282    22288    +6     
  Misses       1735     1735           

☔ 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.

Comment thread test/integration/render/tests/text-halo-width/default/expected.png
Comment thread test/integration/render/tests/text-halo-width/function/expected.png
@CommanderStorm CommanderStorm marked this pull request as draft April 12, 2026 16:51
@xavierjs
Copy link
Copy Markdown
Contributor Author

Now it renders the halo in 2 passes only if there is no overlap between glyph halos (it computes the overlap using text-letter-spacing and halo width).
The render tests are passing and the rendering is the same. If there is an overlap between 2 different text labels, and if there is no overlap between glyphs (so 1 pass halo rendering), the result will be more legible with this PR since the halo of the latest rendered text label will be rendered above the first rendered text label, so at least 1 text label will be legible (instead of 0).

@xavierjs xavierjs marked this pull request as ready for review April 14, 2026 13:37
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