OpenGL render loops typically involve changing some render states followed by a draw call. For instance the app might change a few uniforms and invoke glDrawElements
:
for (const auto &obj : scene) { for (const auto &uni : obj.uniforms) { glUniform4fv(uni.loc, uni.data); } glDrawElements(GL_TRIANGLES, obj.eleCount, GL_UNSIGNED_SHORT, obj.eleOffset); }
Another update loop may change Texture and Vertex Array state before the draw:
for (const auto &obj : scene) { glBindBuffer(GL_ARRAY_BUFFER, obj.arrayBuffer); glBufferSubData(GL_ARRAY_BUFFER, obj.bufferOffset, obj.bufferSize, obj.bufferData); glVertexAttribPointer(obj.arrayIndex, obj.arraySize, GL_FLOAT, GL_FALSE, 0, nullptr); glBindTexture(GL_TEXTURE_2D, obj.texture); glDrawElements(GL_TRIANGLES, obj.eleCount, GL_UNSIGNED_SHORT, obj.eleOffset); }
Other update loops may change render states like the blending modes, the depth test, or Framebuffer attachments. In each case ANGLE needs to validate, track, and translate these state changes to the back-end as efficiently as possible.
Each OpenGL Context state value is stored in gl::State
. For instance the blending state, depth/stencil state, and current object bindings. Our problem is deciding how to notify the back-end when app changes front-end state. We decided to bundle changed state into bitsets. Each 1 bit indicates a specific changed state value. We call these bitsets “dirty bits”. See gl::State::DirtyBitType
.
Each back-end handles state changes in a syncState
implementation function that takes a dirty bitset. See examples in the GL back-end, D3D11 back-end and Vulkan back-end.
Container objects such as Vertex Array Objects and Framebuffers also have their own OpenGL front-end state. VAOs store vertex arrays and array buffer bindings. Framebuffers store attachment state and the active read and draw buffers. These containers also have internal dirty bits and syncState
methods. See gl::Framebuffer::DirtyBitType
and rx::FramebufferVk::syncState
for example.
Dirty bits allow us to efficiently process groups of state updates. We use fast instrinsic functions to scan the bitsets for 1 bits. See bitset_utils.h
for more information.
To optimize validation we cache many checks. See gl::StateCache
for examples. We need to refresh cached values on state changes. For instance, enabling a generic vertex array changes a cached mask of active vertex arrays. Changes to a texture‘s images could change a cached framebuffer’s completeness when the texture is bound as an attachment. And if the draw framebuffer becomes incomplete it changes a cached draw call validation check.
See a below example of a call to glTexImage2D
that can affect draw call validation:
We use the Observer pattern to implement cache invalidation notifications. See Observer.h
. In the example the Framebuffer
observes Texture
attachments via angle::ObserverBinding
. Framebuffer
implements angle::ObserverInterface::onSubjectStateChange
to receive a notification to update its completeness cache. The STORAGE_CHANGED
message triggers a call to gl::Context::onSubjectStateChange
which in turn calls gl::StateCache::updateBasicDrawStatesError
to re-validate the draw framebuffer's completeness. On subsequent draw calls we skip re-validation at minimal cost.
See the below diagram for the dependency relations between Subjects and Observers.
See Fast OpenGL State Transitions in Vulkan documents for additional information for how we implement state change optimization on the Vulkan back-end.