-
Notifications
You must be signed in to change notification settings - Fork 2.2k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Data-driven styling support for *-pattern properties #6289
Conversation
2cfe261
to
049229b
Compare
this is ready for an initial review, with the caveat that there are two open questions:
luckily, cross-faded properties are already interpolated along zoom-levels and thus require the same data to be passed for |
return z > p.zoomHistory.lastIntegerZoom ? | ||
{ fromScale: 2, toScale: 1, t: fraction + (1 - fraction) * t } : | ||
{ fromScale: 0.5, toScale: 1, t: 1 - (1 - t) * fraction }; | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
When I implement fill-pattern
and fill-extrusion-pattern
dds in a follow-up PR, I will probably move this function to the parent StyleLayer
class.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
👍 along with the CrossfadeParameters
type
src/style/cross_faded.js
Outdated
@@ -3,6 +3,9 @@ | |||
export type CrossFaded<T> = { | |||
from: T, | |||
to: T, | |||
min: T, | |||
mid: T, | |||
max: T, |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
when I implement the remaining *-pattern
properties, I will be able to remove the from
and to
components from this type in favor of the StyleLayer
getter for cross fade parameters.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It looks like it should be possible now for CrossFaded<T>
to be pared down to {min: T, mid: T, max: T}
. Maybe adding from: 'min' | 'max'
to CrossfadeParameters
so that in places where we're doing pattern.from
, we can do pattern[crossfade.from]
instead?
Yep, this is something that's true of all data-driven paint properties. We handle it in |
🤦♀️ well, that's embarrassing. I think I confused myself about this step where we determine which glyphs to add to the tile's https://github.com/mapbox/mapbox-gl-js/pull/6289/files#diff-b1205e64508e847d415d42243c10212bR158 it shouldn't be an issue to loop through each layer with data-driven |
I think we should aim to handle non-data-driven |
049229b
to
d42aee4
Compare
d42aee4
to
3336ad6
Compare
Getting some diffs after moving uniform bindings to the they're tiny and I haven't looked too much into them – if they're acceptable diffs I will update the tests, and if not I will spend more time trying to fix. I also have a version of this PR that reduces the # attributes to 1, but requires dynamic checking and possibly binding of the vertex buffer on each frame/tile (there is a separate vertex buffer per tile depending on if the user is zooming in or zooming out for crossfading to work correctly) – should I pursue that route or stick with the current approach? |
3336ad6
to
6a07db0
Compare
Updating the tests for those diffs sounds good. Or, if you want to, minimize the tests so that they don't show those minor differences (use inline GeoJSON with simple geometries, 64x64 size, etc.). Reducing the number of attributes to one sounds great! It should be fairly cheap to switch vertex buffers on the fly, and it'll get cheaper once we have state tracking that allows us to rebind only the attributes that have changed. |
Thanks @anandthakker for helping me better understand floating point precision 🔢 This is ready for review, with the caveat that I still need to fix this PR for non-integer zoom stops because that is currently broken for both data-driven styling and camera functions. DIdn't notice this was broken until late this afternoon, so I will aim to address that tomorrow. |
src/source/tile.js
Outdated
@@ -224,9 +225,8 @@ class Tile { | |||
|
|||
const gl = context.gl; | |||
|
|||
if (this.iconAtlasImage) { | |||
this.iconAtlasTexture = new Texture(context, this.iconAtlasImage, gl.RGBA); | |||
this.iconAtlasImage = null; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Looks like we were previously clearing this.iconAtlasImage
to avoid creating the Texture
every time. Should we do something analogous?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
yes – I had meant to come back to this. setting iconAtlas.image
to null was upsetting flow but I will revisit! 👍
src/data/program_configuration.js
Outdated
} else { | ||
self.binders[property] = new CompositeExpressionBinder(value.value, name, type, useIntegerZoom, zoom); | ||
keys.push(`/z_${name}`); | ||
const structArrayLayout = layoutType(property, type, 'composite'); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Nit: since structArrayLayout
is actually a class, capitalize the var
* @private | ||
*/ | ||
|
||
export class CrossFadedDataDrivenProperty<T> extends DataDrivenProperty<?CrossFaded<T>> { |
This comment was marked as resolved.
This comment was marked as resolved.
Sorry, something went wrong.
This comment was marked as resolved.
This comment was marked as resolved.
Sorry, something went wrong.
src/style/properties.js
Outdated
{ from: min, to: mid, fromScale: 2, toScale: 1, t: fraction + (1 - fraction) * t } : | ||
{ from: max, to: mid, fromScale: 0.5, toScale: 1, t: 1 - (1 - t) * fraction }; | ||
{ from: min, to: mid, min: min, mid: mid, max: max, fromScale: 2, toScale: 1, t: fraction + (1 - fraction) * t } : | ||
{ from: max, to: mid, min: min, mid: mid, max: max, fromScale: 0.5, toScale: 1, t: 1 - (1 - t) * fraction }; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Nit: this can just be min, mid, max
instead of min: min, mid: mid, max: max
.
return z > p.zoomHistory.lastIntegerZoom ? | ||
{ fromScale: 2, toScale: 1, t: fraction + (1 - fraction) * t } : | ||
{ fromScale: 0.5, toScale: 1, t: 1 - (1 - t) * fraction }; | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
👍 along with the CrossfadeParameters
type
src/data/bucket/line_attributes.js
Outdated
]); | ||
|
||
export default lineLayoutAttributes; | ||
export const {members, size, alignment} = lineLayoutAttributes; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Since this module now exports more than one set of attributes, let's drop the default export in favor of two named exports (See #6336)
src/data/bucket/line_bucket.js
Outdated
if (programConfiguration.binders && programConfiguration.binders['line-pattern'] && programConfiguration.binders['line-pattern'].isDataDriven()) { | ||
this.dataDrivenPatternLayers.push(i); | ||
} | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Rather than storing this on this
, could we compute it locally in populate()
? (Also: why is this an array of indices into this.layers
rather than an array of layers themselves?)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
One more question: why use binder.isDataDriven()
rather than checking layer.paint.get('line-pattern')
?
src/data/bucket/line_bucket.js
Outdated
} | ||
} | ||
|
||
// used if line-pattern is data-driven |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Isn't addFeatures()
also used if line-pattern is constant?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
yep, sorry – an artifact from an earlier iteration 🙈
src/data/program_configuration.js
Outdated
|
||
} | ||
|
||
class PatternConstantBinder<T> extends ConstantBinder<T> { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'm still inclined to say PatternConstantBinder
should be a top-level class rather than a subclass of ConstantBinder
(and likewise for PatternCompositeExpressionBinder
).
ca66d46
to
40c68f5
Compare
src/render/draw_line.js
Outdated
const programId = | ||
layer.paint.get('line-dasharray') ? 'lineSDF' : | ||
layer.paint.get('line-pattern') ? 'linePattern' : 'line'; | ||
linePattern && linePattern.constantOr((1: any)) ? 'linePattern' : 'line'; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
open to better ways of doing this check – basically if line-pattern
is undefined, constantOr(1)
will return undefined, but linePattern
itself is no longer falsy if line-pattern
is undefined because it is now a PossiblyEvaluatedPropertyValue
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'm okay with this as-is, since it's analogous to how we do other such checks.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Is there a way to eliminate the any
cast?
40c68f5
to
43adf3d
Compare
@charandas sorry for the delay. we have to wait to merge until the implementation in mapbox-gl-native is complete because the two libraries share shaders. Its hopefully almost done 🙏⏳mapbox/mapbox-gl-native#12284 thanks for the bug report – working on a fix for that now! |
2f8d0b4
to
fb233d7
Compare
@mollymerp looks like a pretty huge regression for Layout — is that spurious or reproducible with repeated runs? |
@mourner 😞 yep its reproducable and kind of expected because there is an additional worker-main thread round trip for *-pattern layers to fetch the icons needed to layout. I'm trying something quickly to avoid this if the pattern property is not used, and hopefully that will reduce the regression. |
src/style/style_layer.js
Outdated
@@ -162,6 +186,7 @@ class StyleLayer extends Evented { | |||
} | |||
|
|||
recalculate(parameters: EvaluationParameters) { | |||
this._parameters = parameters; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Can we avoid adding this state?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
we can get rid of it if we pass the evaluation parameters from Map#_render
to painter#render
and through to the draw calls. Is that preferable?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I took a deeper look and understand things a lot better now: CrossFadedDataDrivenProperty
wants access to the same state -- fromScale
, toScale
, and t
-- that CrossFadedProperty
returns via CrossFaded
. So here's what you could do support them both in a common way:
- Rename
this._parameters
tothis._crossfadeParameters
and change the type toCrossfadeParameters
. - Move
getCrossfadeParameters()
toEvaluationParameters
, so the above line in the PR becomesthis._crossfadeParameters = parameters.getCrossfadeParameters()
. (This isn't strictly necessary, butgetCrossfadeParameters()
seems like it should live onEvaluationParameters
per Feature Envy.) - Remove
fromScale
,toScale
, andt
fromCrossFaded<T>
and the duplicate calculation of these values fromCrossFadedProperty<T>::_calculate
. Instead, have layers that use eitherCrossFadedProperty
orCrossFadedDataDrivenProperty
obtain the calculated values from the same source:this._crossfadeParameters
.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Seems like CrossFaded<T>
should become {min: T, mid: T, max: T}
. This would allow it to be used in more places, and maybe DRY up some of the duplication in the bucket classes.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@jfirebaugh {min, mid, max}
are only needed for data-driven property evaluation, and to
, from
is still useful for constant and camera expressions, and since the expression evaluation in the binders returns T
, not CrossFaded<T>
I've kept the to
, from
values for now.
I DRYd up the bucket code in f1a06a3 and took your suggestions for reworking getCrossfadeParameters
in 9f6bac9. Thank you!!
src/data/bucket/fill_bucket.js
Outdated
fillFeature.id = feature.id; | ||
} | ||
|
||
this.features.push(fillFeature); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Is there a reason that calculating the needed pattern images and iterating over features need to happen at the same time? If not, let's split that into separate steps, so that buckets don't need to create a temporary array of BucketFeature
s.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
this follows the pattern that is also used in symbol layout. we could split it into separate steps, but this way avoids repeating the work of parsing features out of the vector layers here:
mapbox-gl-js/src/source/worker_tile.js
Lines 77 to 94 in 2d0c4bc
for (const sourceLayerId in layerFamilies) { | |
const sourceLayer = data.layers[sourceLayerId]; | |
if (!sourceLayer) { | |
continue; | |
} | |
if (sourceLayer.version === 1) { | |
warnOnce(`Vector tile source "${this.source}" layer "${sourceLayerId}" ` + | |
`does not use vector tile spec v2 and therefore may have some rendering errors.`); | |
} | |
const sourceLayerIndex = sourceLayerCoder.encode(sourceLayerId); | |
const features = []; | |
for (let index = 0; index < sourceLayer.length; index++) { | |
const feature = sourceLayer.feature(index); | |
features.push({ feature, index, sourceLayerIndex }); | |
} | |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I was going to say, since we're restricting *-pattern
properties to values where the possible outputs are contained as literals within the expression, we can take advantage of that to calculate the needed pattern images without reference to the actual features. But it looks like you've lifted that restriction in the latest revision. Do you remember the original rationale for it? On the one hand, it's nice that it would allow this optimization, but on the other, I can definitely envision someone wanting to use e.g. "fill-pattern": ["get", "pattern-name"]
, since that's been so useful for icon-image
.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think the original reason for the restriction was to avoid the per-feature evaluation before the layout step, but yes it has been lifted and I agree, the ["get", "pattern-name"] use-case is probably going to be one users will appreciate and use.
src/style/properties.js
Outdated
*/ | ||
|
||
export class CrossFadedDataDrivenProperty<T> extends DataDrivenProperty<?CrossFaded<T>> { | ||
possibleOutputs: Array<T>; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Let's figure out how to avoid adding this state. These classes are already tough to understand and reason about even though their methods avoid stateful side-effects. I fear that adding statefulness to them will add another level of complexity.
layout benchmark looking much better after 3cd1985 |
59d62d7
to
ad748a0
Compare
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Great work here @mollymerp! You took an on an ambitious feature without an obvious solution, drove it to completion without compromising on performance or code quality, and in the process deepened your knowledge of key parts of the codebase and C++ template programming. 👏
I agree, the ["get", "pattern-name"] use-case is probably going to be one users will appreciate and use.
👍 Is a test for this case included?
src/style/style_layer.js
Outdated
@@ -25,6 +25,12 @@ import type { | |||
FilterSpecification | |||
} from '../style-spec/types'; | |||
|
|||
export type CrossfadeParameters = { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This is duplicated in evaluation_parameters.js -- let's import it from there.
37d0687
to
8b723b4
Compare
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I just noted a couple things that jumped out at me but I don't think any of them are clear or important enough to block this pr. Don't let them stop you from merging unless you feel like they are things that should be done before merging.
setConstantPatternPositions(posTo: ImagePosition, posFrom: ImagePosition) { | ||
this.patternPositions.patternTo = posTo.tlbr; | ||
this.patternPositions.patternFrom = posFrom.tlbr; | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'm not sure if this is something we need to address in this pr, but:
I think it would be good to get rid of this state and indirect way of setting this values. It would be good if they could be calculated from the values passed to program.draw(...)
. Maybe by passing patternPositions
or imageAtlas
through? Another way to think about this could be to evaluate a PossiblyEvaluatedPropertyValue<T>
for a specific tile (turn (icon_name, atlas) -> coords).
addFeature(patternFeature, geometry, index, {}); | ||
} | ||
|
||
options.featureIndex.insert(feature, geometry, index, sourceLayerIndex, bucketIndex); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This method handles a couple non-pattern related things, like loading the geometry and indexing features. Is there anything here that can be separated and shared with buckets that don't have patterns (like circles?). It's very possible that doing this wouldn't make sense. Either way I don't see this as something that needs to block this pr.
…ll-extrusion-pattern properties allow multiple attributes per style-spec property add CrossFadedDataDrivenProp for line-pattern DDS" convert line_pattern shaders to use pragmas create layouts for data-driven line-pattern vertex buffers add source function support for line-pattern to line bucket population and draw code use min, mid, max images for cross-fading data-driven patterns also use tile's IconAtlas for data constant line-pattern extend Binders to support line-pattern properties add initial render test nit fix ensure all possible icons for line-pattern camera funcs are added to the icon atlas make arguments needed for ddpattern required set binder type on property make pattern attributes independent of line layer implement data-driven styling for fill-pattern add dds render test for fill-pattern eliminate black flash on setPaintProperty with a pattern value extend integer-only evaluation to CrossFadedDataDrivenProps address review comments remove getPossibleOutputs and fix rendering extend feature state updating to CrossFadedCompositeBinder use getPossibleOutputs instead of iterating over all features add 1px padding wrap to sprites separate icon and pattern sprites in ImageAtlas to fix wrapping in -pattern properties rename imageAtlas --> iconAtlas now that it holds both icons and pattern images update to use new style-spec expression schema implement fill-extrusion-pattern dds address review comments simplify imageAtlas check remove redundant CrossFaded properties backport #6665 remove unpack function for pattern attrs backport #6745 and fix rebase flubs update with uniform binding state management expose possibleOutputs() at the StylePropertyExpression level add some query tests rebase fix Don't wait for pattern images to layout layers no -pattern property set bonus: remove limitation on non-deterministic expression outputs for pattern properties and reliance on `possibleOutputs()` state remove getPossibleOutputs from CrossFadedDataDrivenProperty refactor CrossFaded and CrossfadeParameters DRY bucket code with util function refactor pattern bucket functions
8b723b4
to
53b2a01
Compare
Needed
Otherwise, this import wasn't working for me. Just FYI @mollymerp |
Thanks for the heads up @charandas -- should be addressed by #7205. |
Thanks @jfirebaugh @mollymerp. Did something significant change as to the interface for this using I was able to get I am doing something as simple as:
I can also confirm that the feature-state is loaded with the |
yep @charandas – thanks for the flag. this was a bug I introduced as part of a performance related refactor. working on a fix now. |
todos:
CrossFadedDataDrivenProperty
or potentially refactorCrossFaded
outline-pattern
propertiesLaunch Checklist