Skip to content
This repository has been archived by the owner on Aug 8, 2023. It is now read-only.

Tighten size check #8562

Merged
merged 6 commits into from
Apr 10, 2017
Merged

Tighten size check #8562

merged 6 commits into from
Apr 10, 2017

Conversation

brunoabinader
Copy link
Member

@brunoabinader brunoabinader commented Mar 29, 2017

Some platforms provide their own size type defaulting to -1 in both width and height e.g. Qt's QSize. We should special case this and make this part of the assertion check.

@brunoabinader brunoabinader added Core The cross-platform C++ core, aka mbgl Qt Mapbox Maps SDK for Qt, aka Qt Location Mapbox GL labels Mar 29, 2017
@brunoabinader brunoabinader self-assigned this Mar 29, 2017
@mention-bot
Copy link

@brunoabinader, thanks for your PR! By analyzing this pull request, we identified @tmpsantos, @1ec5 and @jfirebaugh to be potential reviewers.

Copy link
Member

@kkaefer kkaefer left a comment

Choose a reason for hiding this comment

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

I disagree with this change. util::Size is used internally in Mapbox GL, and converting platform-specific size types to util::Size is the platform binding's responsibility: The Qt wrapper should convert QSize to util::Size and back.

@brunoabinader
Copy link
Member Author

@kkaefer I've added a sanitizer for the Qt wrapper when converting from QSize to mbgl::Size. I've also modified the approach to not touch mbgl::Size check, but instead failing in Transform::resize for zero-ed sizes and asserting a valid size in mbgl::util::tileCover.

@@ -13,7 +13,7 @@ namespace mbgl {
class Projection {
public:
// Map pixel width at given scale.
static double worldSize(double scale) {
static constexpr double worldSize(double scale) {
Copy link
Contributor

Choose a reason for hiding this comment

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

What's the rationale for this change?

Copy link
Member Author

@brunoabinader brunoabinader Mar 30, 2017

Choose a reason for hiding this comment

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

Nothing special besides the fact util::tileSize is already known in compile time - as requested below I'll subtract this cosmetic change from this PR 👍

Actually this is needed if we want to keep TransformState ctors constexpr - Projection::worldSize is used to initialize Bc and Cc here.

Copy link
Contributor

Choose a reason for hiding this comment

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

Is there a need for TransformState to be constexpr? Based on the history with GCC 4.9 it sounded like something we should avoid using unless it's truly necessary.

Copy link
Member Author

Choose a reason for hiding this comment

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

@jfirebaugh my understanding is that using constexpr allows clearer understanding of what we want to do, and what the compiler is possible of calculating in compile time - I personally find class/struct ctors using constexpr to be a more elegant pattern too.

There are specific cases where constexpr usage is bogus on GCC 4.9 - though this is not the case for simple cases like the ones above i.e. we still use plain constexpr in lots of places in our https://github.com/mapbox/mapbox-gl-native/tree/qt-staging branch.

As example, there are cases where constexpr works fine in GCC 4.9 - like this example, and cases where it doesn't.

Probably the best way to make sure we're compliant with GCC 4.9 is to have a GCC 4.9-based build bot running as part of our CI, but I guess that's out of scope for this PR.

Copy link
Contributor

Choose a reason for hiding this comment

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

using constexpr allows clearer understanding of what we want to do

How so? We don't inherently intend that these calculations or constructors happen at compile time, and the fact that they (currently) can is not an essential feature of the code.

I don't think we should go around adding constexpr everywhere it's possible to do so. If we're talking about introducing coding guidelines for C++ features, then I would say my guideline for constexpr is "use only in cases where compile-time evaluation is an intended or essential feature of the code, or for general-purpose library functions which may need to be used in constexpr contexts."

@@ -46,6 +46,10 @@ Transform::Transform(MapObserver& observer_,
#pragma mark - Map View

bool Transform::resize(const Size size) {
if (!size) {
return false;
Copy link
Contributor

Choose a reason for hiding this comment

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

If (0, 0) is an invalid size, shouldn't this throw rather than silently doing nothing?

Copy link
Member Author

Choose a reason for hiding this comment

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

I supposed that was the meaning of the return bool - should we make it void instead and throw on such cases?

Copy link
Contributor

Choose a reason for hiding this comment

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

I think at one point the return value was used to check if the size was changing, but it looks like this may be used only in tests. If so, yes, void and throw on invalid input.

Copy link
Member Author

Choose a reason for hiding this comment

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

We can use try/catch in the tests 👍

TransformState(ConstrainMode = ConstrainMode::HeightOnly, ViewportMode = ViewportMode::Default);
constexpr TransformState() = default;

constexpr TransformState(ConstrainMode constrain_, ViewportMode viewport_)
Copy link
Contributor

Choose a reason for hiding this comment

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

This removes the ability to construct with a single argument. (Could you please limit your changes to fixing the main issue, and propose cosmetic changes in a separate PR?)

Copy link
Member Author

Choose a reason for hiding this comment

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

This removes the ability to construct with a single argument.

Do we have a need for this? If so, we could add an extra ctor that receives a single ConstrainMode.

Could you please limit your changes to fixing the main issue, and propose cosmetic changes in a separate PR?

Fair enough - though I've seen this as a common behavior in some of our PRs.

double min_scale = std::pow(2, 0);
double max_scale = std::pow(2, 20);
double min_scale = 1.0; // 2^0
double max_scale = 1048576.0; // 2^20
Copy link
Contributor

Choose a reason for hiding this comment

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

What's the rationale for this change?

Copy link
Member Author

Choose a reason for hiding this comment

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

These values are known and std::pow is not constexpr, causing the TransformState ctor to not be constexpr-able too.

@@ -74,13 +77,17 @@ class TransformState {
double zoomScale(double zoom) const;
double scaleZoom(double scale) const;

constexpr explicit operator bool() const {
Copy link
Contributor

Choose a reason for hiding this comment

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

Let's not use operator bool() for validity checks. If we need a validity check, name it bool valid() const. But first, try to eliminate the possibility of an invalid state.

Copy link
Member Author

Choose a reason for hiding this comment

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

Let's not use operator bool() for validity checks.

We've discussed this before, yet we still use explicit operator bool in other parts of the code e.g. Size::operator bool.

I don't have a strong opining about replacing it with something like valid(), however I'd prefer to have a separate PR that replaces every explicit operator bool usage for validation in our code 👍

Copy link
Member Author

Choose a reason for hiding this comment

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

👉 #8601.

{
mapObj = std::make_unique<mbgl::Map>(
*this, sanitizedSize(size),
Copy link
Member

Choose a reason for hiding this comment

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

why is size invalid here and needs to be sanitized in the first place?

Copy link
Member Author

Choose a reason for hiding this comment

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

Because QSize default constructor initializes witdh and height with -1. This causes an overflow when converting from int to uint32_t- using std::numeric_limits<uint32_t>::max() for width and height has proven to cause undefined behavior in places like tileCover.

Qt sometimes initializes a dummy map plugin as means of discovering which map plugins it has available - when it does, it uses a default QSize for initializing the item size.

@@ -46,6 +46,10 @@ Transform::Transform(MapObserver& observer_,
#pragma mark - Map View

bool Transform::resize(const Size size) {
if (!size) {
return false;
}
Copy link
Member

Choose a reason for hiding this comment

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

Why do we need to return early here?

Copy link
Member Author

Choose a reason for hiding this comment

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

Because if we allow Transform to have a zero-ed size, it can result in division-by-zero issues e.g. here.

@brunoabinader
Copy link
Member Author

brunoabinader commented Apr 3, 2017

@jfirebaugh I've moved the constexpr changes to a specific patch removed the constexpr-related changes and made Transform::resize throw in case it receives an invalid size.

Copy link
Contributor

@ivovandongen ivovandongen left a comment

Choose a reason for hiding this comment

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

Android changes look 👍

Copy link
Contributor

@tmpsantos tmpsantos left a comment

Choose a reason for hiding this comment

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

Qt 👍

mbgl::Size sanitizedSize(const QSize& size) {
return mbgl::Size {
size.width() <= 0 ? 0 : static_cast<uint32_t>(size.width()),
size.height() <= 0 ? 0 : static_cast<uint32_t>(size.height())
Copy link
Contributor

Choose a reason for hiding this comment

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

You could use std::max here, but I'm OK with ternary.

Copy link
Member Author

Choose a reason for hiding this comment

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

Good point, I'll make the change 👍

@brunoabinader
Copy link
Member Author

@kkaefer @jfirebaugh can you please re-review?

@jfirebaugh
Copy link
Contributor

Can you replace valid with empty or isEmpty (and flip the meaning)? There's nothing inherently invalid about an empty size.

Copy link
Contributor

@jfirebaugh jfirebaugh left a comment

Choose a reason for hiding this comment

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

👍 on operator boolisEmpty changes. IIRC @1ec5 had some concerns about the 64⨯64 minimum size, so let's me sure he weighs in.

@1ec5
Copy link
Contributor

1ec5 commented Apr 6, 2017

On iOS and macOS, it’s possible to create a view with zero size then give it size at some point before the view appears on screen, such as after the view is added to the view hierarchy. This pattern is especially common in iOS applications that build the interface programmatically using manual layout such as -[UIView sizeToFit], which causes the view to resize to fit its subviews.

As reported in #1572, the current implementation on iOS and macOS accommodates this pattern only partially. On master, the following code:

MGLMapView *mapView = [[MGLMapView alloc] initWithFrame:CGRectZero];
mapView.frame = CGRectMake(0, 0, 300, 300);
[self.view addSubview:mapView];

results in the following console spew:

2017-04-06 13:32:11.888 Mapbox GL[72997:2170914] Failed to bind EAGLDrawable: <CAEAGLLayer: 0x60800042e7e0> to GL_RENDERBUFFER 1
2017-04-06 13:32:11.889 Mapbox GL[72997:2170914] Failed to make complete framebuffer object 8cd6

but otherwise everything is perfectly fine.

However, swap two lines so that the view is added to the view hierarchy before it gets some size, and the application crashes with an OpenGL error:

2017-04-06 13:32:56.752 Mapbox GL[73050:2172273] Failed to bind EAGLDrawable: <CAEAGLLayer: 0x600000234e00> to GL_RENDERBUFFER 1
2017-04-06 13:32:56.753 Mapbox GL[73050:2172273] Failed to make complete framebuffer object 8cd6
2017-04-06 13:32:56.756 Mapbox GL[73050:2172273] Failed to bind EAGLDrawable: <CAEAGLLayer: 0x600000234e00> to GL_RENDERBUFFER 1
2017-04-06 13:32:56.756 Mapbox GL[73050:2172273] Failed to make complete framebuffer object 8cd6
libc++abi.dylib: terminating with uncaught exception of type mbgl::gl::Error: glGetIntegerv(GL_VIEWPORT, viewport): Error GL_INVALID_ENUM at /Users/mxn/hub/mapbox-gl-native/src/mbgl/gl/value.cpp:264

As I understand it, this PR would ensure that the crash occurs earlier with a more informative error message. Unfortunately, it also causes the first case (setting the size right after initialization) to crash in the same way:

2017-04-06 13:40:43.244 Mapbox GL[73386:2182614] Failed to bind EAGLDrawable: <CAEAGLLayer: 0x60000043bbc0> to GL_RENDERBUFFER 1
2017-04-06 13:40:43.244 Mapbox GL[73386:2182614] Failed to make complete framebuffer object 8cd6
libc++abi.dylib: terminating with uncaught exception of type std::runtime_error: failed to resize: size is empty

I think this would be a surprising change for developers who build their interface programmatically. Is there any way to modify mbgl::Map so that it only trips this new size assertion when the map is visible?

If not, I suppose we could implement #1572 (comment) by making 64×64 points the minimum size in -[MGLMapView size], which is used when creating mbgl::Map and when the view itself is resized:

diff --git a/platform/ios/src/MGLMapView.mm b/platform/ios/src/MGLMapView.mm
index d80e741c0..67b5ac09f 100644
--- a/platform/ios/src/MGLMapView.mm
+++ b/platform/ios/src/MGLMapView.mm
@@ -576,8 +576,12 @@ public:
 
 - (mbgl::Size)size
 {
-    return { static_cast<uint32_t>(self.bounds.size.width),
-             static_cast<uint32_t>(self.bounds.size.height) };
+    CGSize size = CGSizeMake(MAX(self.bounds.size.width, 64),
+                             MAX(self.bounds.size.height, 64));
+    return {
+        static_cast<uint32_t>(size.width),
+        static_cast<uint32_t>(size.height),
+    };
 }
 
 - (mbgl::Size)framebufferSize
diff --git a/platform/macos/src/MGLMapView.mm b/platform/macos/src/MGLMapView.mm
index e19755044..bccc7cff9 100644
--- a/platform/macos/src/MGLMapView.mm
+++ b/platform/macos/src/MGLMapView.mm
@@ -307,8 +307,12 @@ public:
 }
 
 - (mbgl::Size)size {
-    return { static_cast<uint32_t>(self.bounds.size.width),
-             static_cast<uint32_t>(self.bounds.size.height) };
+    CGSize size = CGSizeMake(MAX(self.bounds.size.width, 64),
+                             MAX(self.bounds.size.height, 64));
+    return {
+        static_cast<uint32_t>(size.width),
+        static_cast<uint32_t>(size.height),
+    };
 }
 
 - (mbgl::Size)framebufferSize {

Methods like -[MGLMapView convertCoordinate:toPointToView:] will return incorrect values until the map is resized beyond 64×64 points, but that’s still better than crashing. Besides -size, would -framebufferSize also need to be limited like this?

/cc @boundsj

@1ec5
Copy link
Contributor

1ec5 commented Apr 6, 2017

Note that, even with the diff proposed in #8562 (comment), the console spew about failing to bind the drawable persists but appears to be harmless:

2017-04-06 15:13:01.489 Mapbox GL[75226:2237986] Failed to bind EAGLDrawable: <CAEAGLLayer: 0x608000225780> to GL_RENDERBUFFER 1
2017-04-06 15:13:01.490 Mapbox GL[75226:2237986] Failed to make complete framebuffer object 8cd6

@brunoabinader
Copy link
Member Author

On iOS and macOS, it’s possible to create a view with zero size then give it size at some point before the view appears on screen

We are not going to modify this pattern - as you mentioned the size can be silently updated to 64x64 inside size getter (or initWithFrame).

The suggested patch for iOS is no different from the approach took by MGLStyleTests and MGLSourceQueryTests.

Is there any way to modify mbgl::Map so that it only trips this new size assertion when the map is visible?

Initializing a map with a valid (by valid I mean at least the minimum texture size as specified by the OpenGL ES 2.0 which is 64x64) size is currently mandatory in our code - because we are immediately supposed to be able to call render(). One alternative would be to have a check in core to ignore render() calls until we have a valid texture size set.

Complexity aside, I believe the less painful change would be to just set the size to 64x64 silently when passing it to the Map object - this way developers won't notice the change and we can be safeguarded. This approach is i.e. the proposed one for Qt and Android already.

@kkaefer kkaefer dismissed their stale review April 7, 2017 10:10

outdated review

@1ec5
Copy link
Contributor

1ec5 commented Apr 8, 2017

Alright, let’s go with silently upping the size to 64×64 points. Does -framebufferSize need to have a minimum of 64×64 points as well?

@brunoabinader
Copy link
Member Author

Does -framebufferSize need to have a minimum of 64×64 points as well?

The only restriction I'm aware of for framebuffer sizes is that their width and height values must be a power of 2 for OpenGL versions prior to 2.0.

@brunoabinader brunoabinader merged commit a9e09c7 into master Apr 10, 2017
@brunoabinader brunoabinader deleted the tighten-size-check branch April 10, 2017 15:01
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Labels
Core The cross-platform C++ core, aka mbgl Qt Mapbox Maps SDK for Qt, aka Qt Location Mapbox GL
Projects
None yet
Development

Successfully merging this pull request may close these issues.

7 participants