Skip to content

fix(mapbox): MapboxOverlay: render deck layers in batches#9939

Merged
zbigg merged 16 commits intomasterfrom
zbigg/mapboxoverlay-group-rendering
Jan 16, 2026
Merged

fix(mapbox): MapboxOverlay: render deck layers in batches#9939
zbigg merged 16 commits intomasterfrom
zbigg/mapboxoverlay-group-rendering

Conversation

@zbigg
Copy link
Collaborator

@zbigg zbigg commented Jan 15, 2026

Related #8919

Background

Extensions like CollisionExtension/MaskExtension rely heavily on multi-pass rendering and (seem) to require that "all affected" layers using extension to be rendered in one deck._drawLayers call.

This PR experiments with assumptions that bugs related to extensions are largely caused by MapboxOverlay rendering each deck layer in complete separation from other deck layer even if they are in same "batch" (by slot or beforeId).

MapboxOverlay new rendering mode

  • _renderLayersInGroups: true - means that deck layers in same group are rendered in one "batch"

Change List

  • MapboxOverlay: render deck layer in batches grouped by beforeId/slot for extension support

@zbigg
Copy link
Collaborator Author

zbigg commented Jan 15, 2026

Hi,

I'm still working on small, minimal repro of actual bug but seems like it's same issue as #8919. Basically MapboxOverlay is rendering layers (including all pre/post-render effect system) one by one, whereas GoogleMapsOverlay does it in one shot and all extensions based on pre/post-effects are working as expected.

What we have found, if we have following layers:

  • mask layer
  • normal layer with MaskExtension to use mask

That if they are rendered in separate deck._drawLayers then mask isn't applied. Seems like same applies to layer with CollisionExtension - all of them need to be called in one deck._drawLayers run otherwise, only first works correctly.

Giving that MapboxOverlay is very fragile, i've hidden this "feature" behind property, so we don't change behavior for all users until solution is proved ok.

Note - i tried to achieve more-or-less same result that is "batching" renders by moving all layers under CompositeLayer, but seems like this is very tricky as

  • mask layers are identified internally my "root" layer.id
  • all code related to hover/click in app needed to be adapted

This made me believe that only proper solution is proper rendering management inside MapboxOverlay.

@zbigg zbigg changed the title Zbigg/mapboxoverlay group rendering fix(mapbox): MapboxOverlay: render deck layers in batches Jan 15, 2026
@coveralls
Copy link

coveralls commented Jan 15, 2026

Coverage Status

coverage: 91.114% (+0.01%) from 91.1%
when pulling 34947e7 on zbigg/mapboxoverlay-group-rendering
into 6e87cf8 on master.

Copy link
Collaborator

@chrisgervang chrisgervang left a comment

Choose a reason for hiding this comment

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

Thank you for this. Interesting finding - up until now I assumed there weren't any negative implications of drawing each layer in their own "batch", but it makes sense that these layers coupled by extensions should draw together.

For some background - this module basically creates a MapboxLayer for each deck layer so that they could be re-ordered with beforeId / slot if the user wanted to, but this PR suggests this isn't necessary and now I'm thinking batched drawing may be a better way to go as the default behavior since long standing features rely on it, and this solution preserves the re-ordering feature.

@Pessimistress are there any benefits to having 1:1 mapbox layer:deck layer?

If there aren't, I'm in favor of replacing resolveLayers and MapboxLayer's internals with a grouped implementation entirely.

I'd just ask that we add a remark in the docs about go to correctly group coupled layers, and explain why features like masks won't work across groups.

triggerRepaint(): void;
}

export type OverlayLayer = Layer<{
Copy link
Collaborator

Choose a reason for hiding this comment

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

Suggested change
export type OverlayLayer = Layer<{
export type MapLayer = Layer<{

Maybe?

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Well in this context "Map" is mapbox/libre Map and this type is specifically a deck Layer with specific props.
In this context i think it's more important to disntinguish

Full name would DeckLayerWithOverlayProps - which is too much.

I'll revert to Layer<LayerOverlayProps> which is long but also very good description of actual type.

@zbigg zbigg marked this pull request as ready for review January 16, 2026 08:45
@zbigg
Copy link
Collaborator Author

zbigg commented Jan 16, 2026

If there aren't, I'm in favor of replacing resolveLayers and MapboxLayer's internals with a grouped implementation entirely

If you're ok with this then great i can update PR. My context is that i don't understand if that API is good enough for all cases w.r.t extensions so i leaned towards "safe experiment". But after brief research i am still not sure if this is "robust" enough.

I see at least two issues:

1
it works well if all deck layers affected by extensions land in one group - it stops working if with more than one group

2
Mapbox/libre CustomLayerInterface specifies that all pre-rendering should be performed in prerender callback

If the layer needs to render to a texture, it should implement the prerender

I don't know what can possibly go wrong if we what we do - and we do that in render now, but i see signals that fully correct may be even more complex.

That means that full, robust solution would need to look like this:

  • prerender - layer one for all layer instances, running all "pre-render" passes - no API in deck (?)
  • groups... - actual screen passes
  • no idea how/if we do any post-processing and where it should render

I would still propose to make it experiment.

@zbigg zbigg requested a review from chrisgervang January 16, 2026 09:00
const layers = flatten(newLayers, Boolean) as OverlayLayer[];

function getGroupId(layer: OverlayLayer): string {
return `deck-layer-group:${layer.props.beforeId}:${layer.props.slot}`;
Copy link
Collaborator

Choose a reason for hiding this comment

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

Can slot and beforeId be used simultaneously? I understood it as you'd use one or the other, but I don't have experiencing using slot yet.

Copy link
Collaborator Author

@zbigg zbigg Jan 16, 2026

Choose a reason for hiding this comment

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

Can slot and beforeId be used simultaneously

I don't know. In that sense this is not different than current solution, also not sure how it will behave.

I'll change it to something clearly indicate which layer group comes from manual ordering (beforeId) and which is assigned to slot.


/* Mapbox custom layer methods */

onAdd(map: Map, gl: WebGL2RenderingContext): void {
Copy link
Collaborator

Choose a reason for hiding this comment

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

What about onRemove, etc?

Also, in reviewing the MapboxLayer I'm seeing some guards to prevent rendering prior to initialization and something about id needing to be immutable - were these intentionally omitted from the grouped version?

Copy link
Collaborator Author

@zbigg zbigg Jan 16, 2026

Choose a reason for hiding this comment

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

MapboxLayer registered itself in deck instance - we don't do that for MapboxLayerGroup, it's not needed.

(notice, we don't add it to "deck.userData")

Groups are just holders of beforeId/slot - they render all layers registered in deck that match this beforeId/slot - again no need to register anywhere/update/remove.

@zbigg zbigg force-pushed the zbigg/mapboxoverlay-group-rendering branch 2 times, most recently from 74fabb3 to b1a5ee3 Compare January 16, 2026 09:36
The constructor additionally accepts the following options:

- `interleaved` (boolean) - If `false`, a dedicated deck.gl canvas is added on top of the base map. If `true`, deck.gl layers are inserted into mapbox-gl's layer stack, and share the same `WebGL2RenderingContext` as the base map. Default is `false`. Note that interleaving with basemaps such as mapbox-gl-js v1 that only support WebGL 1 is not supported, see [compatibility](./overview#interleaved-renderer-compatibility).
- `_renderLayersInGroups` (boolean, _experimental_) In interleaved mode, render layers in batches to enable cross-layer extension handling. Default is `false`.
Copy link
Collaborator

Choose a reason for hiding this comment

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

I think having this be opt-in is the way to go, we can turn it on by default in the future, but I would only do that as part of a release

@zbigg zbigg force-pushed the zbigg/mapboxoverlay-group-rendering branch from d3eff73 to 5e6282e Compare January 16, 2026 11:08
@zbigg zbigg force-pushed the zbigg/mapboxoverlay-group-rendering branch from 5e6282e to cf731c1 Compare January 16, 2026 11:09
@zbigg
Copy link
Collaborator Author

zbigg commented Jan 16, 2026

@chrisgervang @felixpalmer I've applied suggestions and added unit-tests.


map.on('styledata', this._handleStyleChange);
resolveLayers(map, this._deck, [], this._props.layers);
this._resolveLayers([], this._props.layers);
Copy link
Collaborator

Choose a reason for hiding this comment

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

I see what you're doing, but honestly I would keep the first 2 parameters.

It's asking for trouble when later you have:

private _onRemoveInterleaved(map: Map): void {
    map.off('styledata', this._handleStyleChange);
    this._resolveLayers(this._props.layers, []);
    removeDeckInstance(map);
  }

what if in the future someone passes in a different map to _onRemoveInterleaved(map)?

@zbigg zbigg force-pushed the zbigg/mapboxoverlay-group-rendering branch from 6f62dd7 to 34947e7 Compare January 16, 2026 12:16
@zbigg zbigg merged commit 7f96305 into master Jan 16, 2026
5 checks passed
@zbigg zbigg deleted the zbigg/mapboxoverlay-group-rendering branch January 16, 2026 12:44
@charlieforward9
Copy link
Collaborator

I'm thinking batched drawing may be a better way to go as the default behavior since long standing features rely on it

@chrisgervang can you link those long standing features that rely on this for visibility?

@chrisgervang
Copy link
Collaborator

@charlieforward9 mask and collision extension are mentioned in the description. We don't have a list of extensions that use pre/post render effects but they can be found on the extensions module. Users can also implement custom extensions that need layers drawn together

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.

5 participants