How Impeller Works Around The Lack of Uniform Buffers in Open GL ES 2.0.

The Impeller Renderer API allows callers to specify uniform data in a discrete buffer. Indeed, it is conventional for the higher level layers of the Impeller stack to specify all uniform data for a render pass in a single buffer. This jumbo buffer is allocated using a simple bump allocator on the host before being transferred over to VRAM. The ImpellerC reflection engine generates structs with the correct padding and alignment such that the caller can just populate uniform struct members and memcpy them into the jumbo buffer, or use placement-new. Placement-new is used in cases where device buffers can be memory mapped into the client address space.

This works extremely well when using a modern rendering backend like Metal. However, OpenGL ES 2.0 does not support uniform buffer objects. Instead, uniform data must be specified to GL from the client side using the glUniform* family of APIs. This poses a problem for the OpenGL backend implementation. From a view (an offset and range) into a buffer pointing to uniform data, it must infer the right uniform locations within a program object and bind uniform data at the right offsets within the buffer view.

Since command generation is strongly typed, a pointer to metadata about the uniform information is stashed along with the buffer view in the command stream. This metadata is generated by the offline reflection engine part of ImpellerC. The metadata is usually a collection of items the runtime would need to infer the right glUniform* calls. An item in this collection would look like the following:

struct ShaderStructMemberMetadata {
  ShaderType type; // the data type (bool, int, float, etc.)
  std::string name; // the uniform member name "frame_info.mvp"
  size_t offset;
  size_t size;
  size_t array_elements;
};

Using this mechanism, the runtime knows how to specify data from a buffer view to GL. But, this is still not sufficient as the buffer bindings are not known until after program link time.

To solve this issue, Impeller queries all active uniforms after program link time using glGet with GL_ACTIVE_UNIFORMS. It then iterates over these uniforms and notes their location using glGetUniformLocation. This uniform location in the program is mapped to the reflection engine's notion of the uniform location in the pipeline. This mapping is maintained in the pipeline state generated once during the Impeller runtime setup. In this way, even though there is no explicit notion of a pipeline state object in OpenGL ES, Impeller still maintains one for this backend.

Since all commands in the command stream reference the pipeline state object associated with the command, the render pass implementation in the OpenGL ES 2 backend can access the uniform bindings map and use that to bind uniforms using the pointer to metadata already located next to the commands' uniform buffer views.

And that’s it. This is convoluted in its implementation, but the higher levels of the tech stack don’t have to care about not having access to uniform buffer objects. Moreover, all the reflection happens offline and reflection information is specified in the command stream via just a pointer. The uniform bindings map is also generated just once during the setup of the faux pipeline state object. This makes the whole scheme extremely low overhead. Moreover, in a modern backend with uniform buffers, this mechanism is entirely irrelevant.