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

C++ bindings for OpenGL #6468

Merged
merged 2 commits into from
Oct 30, 2016
Merged

C++ bindings for OpenGL #6468

merged 2 commits into from
Oct 30, 2016

Conversation

jfirebaugh
Copy link
Contributor

@jfirebaugh jfirebaugh commented Sep 27, 2016

This PR introduces mid-level bindings for OpenGL, specifically a procedural API that wraps a single primitive draw operation, with all state affecting the result provided via method parameters rather than the OpenGL default of implicit state and bindings. The primary design reference for this API was glium, with secondary inspiration from regl.

This parameters for a draw operation are the following:

  • Program: A compiled vertex and fragment shader.
  • Parameters: General OpenGL state such as depth test, stencil test, and blend mode. The variety of OpenGL methods for configuring the depth test, stencil test, and blend mode have been rationalized with an algebraic data type for each of Depth, Stencil, and Blend.
  • AttributeSources: A collection of sources of vertex attribute data. A source can be a vertex buffer or a slice of one (glVertexAttribPointer), or a constant scalar or vector value (glVertexAttrib*). (It remains to be seen if we use the latter for DDS or go the route of GL JS and generate shader source code that uses attributes or uniforms as appropriate.) A buffer or buffer slice comes with a specification of its layout.
  • Uniforms: A collection of uniform locations and values. Values are typed, and valid types include scalars and vectors as well as texture objects. For the latter, the implementation handles activating an unused texture unit.
  • Primitives: The specification of what geometry to draw. This concept unifies unindexed (glDrawArrays) and indexed (glDrawElements) drawing. Input can be array buffer or element buffer objects or slices thereof. Buffer types are templated on their geometry, e.g. ElementBuffer<Triangles, uint16_t>.

Type safety is generally provided at the level of individual parameters, and notably not at the level of matching types between shader source and uniform or attribute types, or between attribute types and C++ object layout. Some additional safety can be obtained by writing higher-level bindings atop this API, e.g. along the lines of the API for uniforms proposed by @kkaefer here.

Likewise, we can layer a declarative, object-based API more like regl atop this one, as suggested in this gl-js issue. Doing so may simplify the communication pathway between workers and the renderer, and provide a clearer path to use VAOs transparently than with a purely procedural approach.

TODOs

The remaining items are numerous; I'll edit to flesh out this section.

  • Probably the first item is to land the unification of gl::ObjectStore and gl::Config (Merge gl::ObjectStore and gl::Config #6470). The prepare and draw functions I've sketched here should be methods on this object, and refer to its internal state to avoid no-op OpenGL operations as gl::Config currently does.

cc @kkaefer @lucaswoj

@mention-bot
Copy link

@jfirebaugh, thanks for your PR! By analyzing this pull request, we identified @ansis to be potential reviewers.

@@ -76,7 +76,7 @@ struct DepthMask {
}
};

struct ColorMask {
struct ColorMaskValue {
Copy link
Contributor Author

Choose a reason for hiding this comment

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

Renames here to avoid conflicts with new type names. I envision that all the structs in gl_values.hpp will eventually go away.

Copy link
Contributor

Choose a reason for hiding this comment

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

The goal of these values, and their surrounding Value objects is to prevent duplicate state from being set. It seems like this is no longer the case with this code? Will it come back as work progresses?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Yep, I was just waiting for #6470 to land.

@@ -76,7 +76,7 @@ struct DepthMask {
}
};

struct ColorMask {
struct ColorMaskValue {
Copy link
Contributor

Choose a reason for hiding this comment

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

The goal of these values, and their surrounding Value objects is to prevent duplicate state from being set. It seems like this is no longer the case with this code? Will it come back as work progresses?

gl::Uniforms {
gl::UniformMatrix<4> { program.u_matrix, matrix }
},
tileStencilBuffer);
Copy link
Contributor

Choose a reason for hiding this comment

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

How does this draw call know that it should draw triangles?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I was thinking that vertex buffer types would be tagged with a primitive type, but in some cases we want to reuse vertexes in multiple primitive types. So I think this API will probably need to be revised to accept vertex sources and index source in separate parameters, with a marker value for the latter to indicate unindexed drawing (e.g. Unindexed<Triangles>()). This is what the glium API does.

Copy link
Contributor

Choose a reason for hiding this comment

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

Yeah, in some cases we're using the same buffer for different draw operations.

gl::Stencil::Replace
},
gl::Blend::disabled(),
gl::ColorMask::disabled()
Copy link
Contributor

Choose a reason for hiding this comment

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

Do all of these have to be specified? Can we use some defaults here?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Yes, we could allow defaults. However, given that part of the motivation for moving to this API is to eliminate errors caused by forgetting to specify the desired state, I suggest that we start by making all parameters required. We can define additional helpers in the vein of gl::*::disabled() to package up common values. For example I anticipate we'll want gl::Blend::alphaTransparency() and functions for the most common depth and stencil settings.

gl::AttributeSources {},
gl::Uniforms {
gl::UniformMatrix<4> { program.u_matrix, matrix }
},
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 way to ensure that all uniforms in a program can be set? In the past, we've seen issues with not all uniforms for a draw call being set so we didn't have predictable results.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Yes, I think we can accomplish this with a higher-level API that layers on top of this one.

template <> struct AttributeType<int32_t> : std::integral_constant<GLenum, GL_INT> {};
template <> struct AttributeType<uint32_t> : std::integral_constant<GLenum, GL_UNSIGNED_INT> {};
template <> struct AttributeType<float> : std::integral_constant<GLenum, GL_FLOAT> {};
template <> struct AttributeType<double> : std::integral_constant<GLenum, GL_DOUBLE> {};
Copy link
Contributor

@kkaefer kkaefer Sep 27, 2016

Choose a reason for hiding this comment

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

At some point, we should hide the GL_*, and gl* symbols so users of this library don't need to include gl.h.


using AttributeSources = std::vector<AttributeSource>;

namespace detail {
Copy link
Contributor

Choose a reason for hiding this comment

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

Ideally, we'd move all of the detail code that calls actual GL functions to an implementation file

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I think this is doable. It means that all polymorphic input types have to be wrapped in a variant at some point -- no full monomorphization via template methods. I think that's fine, it was already mostly that way.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Or did you just mean move them to an _impl.hpp file? I assumed you meant move them to a .cpp file.

Copy link
Contributor

Choose a reason for hiding this comment

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

Yeah, .cpp file.

bool r;
bool g;
bool b;
bool a;
Copy link
Contributor

Choose a reason for hiding this comment

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

For types like this, we should either default them, or write explicit initializers that require all parameters to prevent accidental misuse, which results in uninitialized values. Valgrind may catch them, but it's always better to have a compiler error slapped in the face.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

clang gives a warning if you omit parameters in brace initialization:

warning: missing field 'b' initializer [-Wmissing-field-initializers]
        return ColorMask { false, false };
                                        ^

This will be an error once we fix #6487.

Copy link
Member

Choose a reason for hiding this comment

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

For consecutive bool values we could save some memory space by using bit field e.g.:

bool r : 1;
bool g : 1;
bool b : 1;
bool a : 1;

@lucaswoj
Copy link
Contributor

This is excellent work @jfirebaugh! I am excited to iterate on GL Native and GL JS rendering architecture together as we work on mapbox/mapbox-gl-js#145.

@jfirebaugh jfirebaugh force-pushed the gl-bindings branch 2 times, most recently from ecc3ae0 to 3bf08b4 Compare September 28, 2016 14:43
@jfirebaugh jfirebaugh mentioned this pull request Sep 28, 2016
@kkaefer kkaefer added refactor Core The cross-platform C++ core, aka mbgl labels Sep 29, 2016
@jfirebaugh jfirebaugh force-pushed the gl-bindings branch 7 times, most recently from 2bc6035 to c0b3c9f Compare October 5, 2016 19:38
@jfirebaugh jfirebaugh force-pushed the gl-bindings branch 12 times, most recently from e2bd2e6 to 9d229b7 Compare October 10, 2016 20:51
@jfirebaugh
Copy link
Contributor Author

@kkaefer Rebased on top of #6596 and ready for re-review.

MBGL_CHECK_ERROR(glDrawArrays(GL_LINE_STRIP, 0, static_cast<GLsizei>(tileLineStripVertexBuffer.vertexCount)));
}

#ifndef NDEBUG
Copy link
Contributor Author

Choose a reason for hiding this comment

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

I removed Painter::renderClipMasks and Painter::renderDepthBuffer in an early step to avoid dealing with them at the time, and need to go back and convert them now.

@@ -27,10 +25,8 @@ class AttributeBinding {
type(DataTypeOf<T>::value),
count(N),
offset(O) {
static_assert(std::is_standard_layout<Vertex>::value, "vertex type must use standard layout");
Copy link
Contributor

Choose a reason for hiding this comment

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

Why are these removed?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

This one moved to the Drawable constructor. I just dropped "vertex type is too big", because it was kind of silly.

blend = true;
blendColor = color.blendColor;
apply_visitor([&] (const auto& blendFunction) {
// TODO: blendEquation = blendFunction.equation;
Copy link
Contributor

Choose a reason for hiding this comment

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

TODO

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Previous code didn't call glBlendEquation anywhere and we always want the default of GL_FUNC_ADD. But we should set it explicitly right? In case a custom layer changes it.


drawable.bindUniforms();

for (const auto& segment : drawable.segments) {
Copy link
Contributor

Choose a reason for hiding this comment

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

Is it possible to have zero drawable segments? In that case, we shouldn't set any of the state prior to this whatsoever.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I think it'll be short circuited prior to getting here, but I'll add an internal guard as well.

};

if (needAttributeBindings()) {
vertexBuffer.setDirty();
Copy link
Contributor

Choose a reason for hiding this comment

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

why setDirty here?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

It's for the case where a VAO is being initialized. VAOs don't inherit the existing buffer bindings; we need to ensure that glBindBuffer actually gets called. But it can be moved up into the needAttributeBindings lambda and commented. I'll do so.

TriangleStrip>;

} // namespace gl
} // namespace mbgl
Copy link
Contributor

Choose a reason for hiding this comment

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

Can we use a different name? Mode feels way too generic for this. I know that the parameter is called mode in OpenGL as well, but maybe we can use a name like PrimitiveType instead?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

PrimitiveMode?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Or:

  • ModeDrawMode
  • DepthDepthMode
  • StencilStencilMode
  • ColorColorMode

Copy link
Contributor

Choose a reason for hiding this comment

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

Either work for me. Maybe adding the Mode to all avoids ambiguity?

Mode mode;
Depth depth;
Stencil stencil;
Color color;
Copy link
Contributor

Choose a reason for hiding this comment

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

Rename to blendColor?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I chose Color for parallelism with Depth and Stencil -- each holds the configuration that affects the respective GL buffer attachment of that type. For Color, that's both blend settings and the color mask.

u_extrude_scale,
u_devicepixelratio> {};

} // namespace mbgl
Copy link
Contributor

Choose a reason for hiding this comment

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

Can we merge all of the _uniforms.hpp files into the shader headers? It seems like they are very short, and are always used together, so having them in the same file makes the code more readable. Maybe even make them a child class of the shader class?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

There's not a one-to-one relationship -- e.g. FillShader, FillPatternShader, FillOutlineShader, FillOutlinePatternShader. I think there's some consolidation to be done -- perhaps replacing explicit shader subclasses with a single Shader template -- but I'd like to defer it to a future PR.


namespace mbgl {

MBGL_DEFINE_UNIFORM_SCALAR(float, u_radius);
Copy link
Contributor

Choose a reason for hiding this comment

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

These are going to define global symbols, and I assume it's just a coincidence that we don't have any name clashes for different symbols. Instead we should define them inside the Shader class.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Where multiple shaders use the same uniform name, I've hoisted the definition to a shared file.

Copy link
Contributor

Choose a reason for hiding this comment

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

Yeah, I've seen that; I'm referring to the situation when two shaders use the same name for different uniform types.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Ok -- there aren't any cases of that currently, and I think we should avoid introducing any. Besides link errors, it would just be confusing to use the same uniform name for different types.

Copy link
Contributor

Choose a reason for hiding this comment

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

Okay; but it still feels weird to have them in the main mbgl namespace so we have symbols like mbgl::u_radius. Can we at least move them to another namespace?

VertexArrayID id = 0;
MBGL_CHECK_ERROR(gl::GenVertexArrays(1, &id));
vertexArrayObject = id;
vaos.emplace(vaoKey, UniqueVertexArray(std::move(id), { this }));
Copy link
Contributor

Choose a reason for hiding this comment

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

I'm not sure I like this design. Can we come up with a version that does the VAO management statically instead of during runtime?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Well, the static design is the previously existing one, where each buffer Segment holds a corresponding VAO, and binding is done explicitly. That approach seems to be error-prone.

@kkaefer
Copy link
Contributor

kkaefer commented Oct 29, 2016

Can you add the [core] prefix to the two last commits?

@jfirebaugh
Copy link
Contributor Author

Squashed one, prefixed the other.

I think it's prudent for this not to go into iOS v3.4.0 or Android v4.2.0, as it's an invasive change. iOS is on a release branch already and not planning any more merges from master. @mapbox/android, do you have plans to create a release branch soon, so that this PR can be merged to master?

@tobrun
Copy link
Member

tobrun commented Oct 29, 2016

@jfirebaugh feel free to merge to master, we have a release branch called release-android-v4.2.0.

@jfirebaugh jfirebaugh merged commit 39d5cb3 into master Oct 30, 2016
@jfirebaugh jfirebaugh deleted the gl-bindings branch October 30, 2016 02:25
@jfirebaugh jfirebaugh mentioned this pull request Oct 30, 2016
1 task
@kkaefer
Copy link
Contributor

kkaefer commented Oct 31, 2016

🎉

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 refactor
Projects
None yet
Development

Successfully merging this pull request may close these issues.

6 participants