Skip to content

Commit

Permalink
GL: document async shader compilation and linking.
Browse files Browse the repository at this point in the history
  • Loading branch information
mosra committed Sep 6, 2022
1 parent 1f3c250 commit 4580c30
Show file tree
Hide file tree
Showing 5 changed files with 238 additions and 37 deletions.
28 changes: 28 additions & 0 deletions doc/shaders.dox
Original file line number Diff line number Diff line change
Expand Up @@ -287,6 +287,34 @@ While the primary use case of texture arrays is with uniform buffers and
multidraw, they work in the classic uniform workflow as well --- use
@relativeref{Shaders::PhongGL,setTextureLayer()} there instead.

@section shaders-async Async shader compilation and linking

By default, shaders are compiled and linked directly in their constructor.
While that's convenient and easy to use, applications using heavier shaders,
many shader combinations or running on platforms that translate GLSL to other
APIs such as HLSL or MSL, may spend a significant portion of their startup
time just on shader compilation and linking.

To mitigate this problem, shaders can be compiled in an asynchronous way.
Depending on the driver and system, this can mean that for example eight
shaders get compiled at the same time in eight parallel threads, instead of
sequentially one after another. To achieve such parallelism, the construction
needs to be broken into two parts --- first submitting compilation of all
shaders using @ref Shaders::FlatGL::compile() "Shaders::*GL::compile()",
forming temporary @ref Shaders::FlatGL::CompileState "Shaders::*GL::CompileState"
instances, then possibly doing other work until it's completed, and finally
constructing final shader instances out of the temporary state:

@snippet MagnumShaders-gl.cpp shaders-async

The above code will work correctly also on drivers that implement async
compilation partially or not at all --- there
@ref GL::AbstractShaderProgram::isLinkFinished() will implicitly return
@cpp true @ce, and the final construction will stall if it happens before a
(potentially async) compilation is finished. See also the
@ref GL-AbstractShaderProgram-async "GL::AbstractShaderProgram documentation"
for more information.

@section shaders-generic Generic vertex attributes and framebuffer attachments

Many shaders share the same vertex attribute definitions, such as positions,
Expand Down
69 changes: 69 additions & 0 deletions doc/snippets/MagnumGL.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -257,6 +257,59 @@ setTransformFeedbackOutputs({
};
#endif

#ifndef MAGNUM_TARGET_GLES
namespace Foo {

struct MyShader: GL::AbstractShaderProgram {
class CompileState;

MyShader(NoInitT);
MyShader(CompileState&&);
MyShader(int);

static CompileState compile(int);
};

/* [AbstractShaderProgram-async] */
class MyShader::CompileState: public MyShader {
friend MyShader;

explicit CompileState(MyShader&& shader, GL::Shader&& vert, GL::Shader&& frag):
MyShader{std::move(shader)}, _vert{std::move(vert)}, _frag{std::move(frag)} {}

GL::Shader _vert, _frag;
};

MyShader::CompileState MyShader::compile(DOXYGEN_ELLIPSIS(int)) {
GL::Shader vert{GL::Version::GL430, GL::Shader::Type::Vertex};
GL::Shader frag{GL::Version::GL430, GL::Shader::Type::Fragment};
DOXYGEN_ELLIPSIS()
vert.submitCompile();
frag.submitCompile();

MyShader out{NoInit};
DOXYGEN_ELLIPSIS()
out.attachShaders({vert, frag});
out.submitLink();

return CompileState{std::move(out), std::move(vert), std::move(frag)};
}

MyShader::MyShader(NoInitT) {}

MyShader::MyShader(CompileState&& state):
MyShader{static_cast<MyShader&&>(std::move(state))}
{
CORRADE_INTERNAL_ASSERT_OUTPUT(checkLink());
DOXYGEN_ELLIPSIS()
}

MyShader::MyShader(DOXYGEN_ELLIPSIS(int a)): MyShader{compile(DOXYGEN_ELLIPSIS(a))} {}
/* [AbstractShaderProgram-async] */

}
#endif

int main() {

#ifndef MAGNUM_TARGET_GLES2
Expand Down Expand Up @@ -432,6 +485,22 @@ shader.setTransformationMatrix(transformation)
}
#endif

#ifndef MAGNUM_TARGET_GLES
{
using Foo::MyShader;
/* [AbstractShaderProgram-async-usage] */
MyShader::CompileState state = MyShader::compile(DOXYGEN_ELLIPSIS(0));
// Other shaders to compile....

while(!state.isLinkFinished()) {
// Do other work...
}

MyShader shader{std::move(state)};
/* [AbstractShaderProgram-async-usage] */
}
#endif

{
GL::Framebuffer framebuffer{{}};
/* [AbstractFramebuffer-read1] */
Expand Down
119 changes: 98 additions & 21 deletions src/Magnum/GL/AbstractShaderProgram.h
Original file line number Diff line number Diff line change
Expand Up @@ -422,6 +422,74 @@ See also @ref Attribute::DataType enum for additional type options.
@ref Magnum::Matrix4x2 "Matrix4x2", @ref Magnum::Matrix3x4 "Matrix3x4" and
@ref Magnum::Matrix4x3 "Matrix4x3") are not available in WebGL 1.0.
@section GL-AbstractShaderProgram-async Asynchronous shader compilation and linking
The workflow described @ref GL-AbstractShaderProgram-subclassing "at the very top"
compiles and links the shader directly in a constructor. While that's fine for
many use cases, with heavier shaders, many shader combinations or on
platforms that translate GLSL to other APIs such as HLSL or MSL, the
compilation and linking can take a significant portion of application startup
time.
To mitigate this problem, nowadays drivers implement *asynchronous compilation*
--- when shader compilation or linking is requested, the driver offloads the
work to separate worker threads, and serializes it back to the application
thread only once the application wants to retrieve the result of the operation.
Which means, the ideal way to spread the operation over more CPU cores is to
first submit compilation & linking of several shaders at once and only then ask
for operation result. That allows the driver to perform compilation/linking of
multiple shaders at once. Furthermore, the
@gl_extension{KHR,parallel_shader_compile} extension adds a possibility to
query whether the operation was finished for a particular shader. That allows
the application to schedule other work in the meantime.
Async compilation and linking can be implemented by using
@ref Shader::submitCompile() and @ref submitLink(), followed by
@ref checkLink() (and optionally @ref Shader::checkCompile()), instead of
@ref Shader::compile() and @ref link(). Calling the submit functions will
trigger a (potentially async) compilation and linking, calling the check
functions will check the operation result, potentially stalling if the async
operation isn't finished yet.
The @ref Shader::isCompileFinished() and
@ref isLinkFinished() APIs then provide a way to query if the submitted
operation finished. If @gl_extension{KHR,parallel_shader_compile} is not
available, those two implicitly return @cpp true @ce, thus effectively causing
a stall if the operation isn't yet done at the time you call
@ref Shader::checkCompile() / @ref checkLink() --- but compared to the linear
workflow you still get the benefits from submitting multiple operations at
once.
A common way to equip an @ref AbstractShaderProgram subclass with async
creation capability while keeping also the simple constructor is the following:
1. An internal @ref NoInit constructor for the subclass is added, which only
creates the @ref AbstractShaderProgram base but does nothing else.
2. A @cpp CompileState @ce inner class is defined as a subclass of
@cpp MyShader @ce. Besides that it holds all temporary state needed to
finish the construction --- in particular all @ref Shader instances.
3. A @cpp static CompileState compile(…) @ce function does everything until
and including linking as the original constructor did, except that it calls
@ref Shader::submitCompile() and @ref submitLink() instead of
@ref Shader::compile() and @ref link(), and returns a populated
@cpp CompileState @ce instance.
4. A @cpp MyShader(CompileState&&) @ce constructor then takes over the base
of @cpp CompileState @ce by delegating it into the move constructor. Then
it calls @ref checkLink() (and if that fails also @ref Shader::checkCompile()
to provide more context) and then performs any remaining post-link steps
such as uniform setup.
5. The original @cpp MyShader(…) @ce constructor now only passes the result of
@cpp compile() @ce to @cpp MyShader(CompileState&&) @ce.
@snippet MagnumGL.cpp AbstractShaderProgram-async
Usage-wise, it can look for example like below, with the last line waiting for
linking to finish and making the shader ready to use. On drivers that don't
perform any async compilation this will behave the same as if the construction
was done the usual way.
@snippet MagnumGL.cpp AbstractShaderProgram-async-usage
@section GL-AbstractShaderProgram-performance-optimization Performance optimizations
The engine tracks currently used shader program to avoid unnecessary calls to
Expand Down Expand Up @@ -1259,14 +1327,19 @@ class MAGNUM_GL_EXPORT AbstractShaderProgram: public AbstractObject {
#endif

/**
* @brief Non-blocking linking status check
* @return @cpp true @ce if linking finished, @cpp false @ce otherwise
*
* On some drivers this might return false even after
* @ref checkLink() reported successful linking.
* @brief Whether a @ref submitLink() operation has finished
* @m_since_latest
*
* @see @fn_gl_keyword{GetProgram} with
* @def_gl_extension{COMPLETION_STATUS,KHR,parallel_shader_compile}
* Has to be called only if @ref submitLink() was called before, and
* before @ref checkLink(). If returns @cpp false @ce, a subsequent
* @ref checkLink() call will block until the linking is finished. If
* @gl_extension{KHR,parallel_shader_compile} is not available, the
* function always returns @cpp true @ce --- i.e., as if the linking
* was done synchronously. See @ref GL-AbstractShaderProgram-async for
* more information.
* @see @ref Shader::isCompileFinished(),
* @fn_gl_keyword{GetProgram} with
* @def_gl_extension{COMPLETION_STATUS,KHR,parallel_shader_compile}
*/
bool isLinkFinished();

Expand Down Expand Up @@ -1453,32 +1526,36 @@ class MAGNUM_GL_EXPORT AbstractShaderProgram: public AbstractObject {
/**
* @brief Link the shader
*
* Calls @ref submitLink(), then @ref checkLink().
* If possible, prefer to link multiple shaders at once using
* @ref link(std::initializer_list<Containers::Reference<AbstractShaderProgram>>)
* for improved performance, see its documentation for more
* information.
* Calls @ref submitLink(), immediately followed by @ref checkLink(),
* passing back its return value. See documentation of those two
* functions for details.
* @see @ref Shader::compile()
*/
bool link();

/**
* @brief Submit for linking
*
* The attached shaders must be compiled with @ref Shader::compile()
* or @ref Shader::submitCompile() before linking.
* @brief Submit the shader for linking
* @m_since_latest
*
* The attached shaders must be at least submitted for compilation
* with @ref Shader::submitCompile() or @ref Shader::compile() before
* linking. Call @ref isLinkFinished() or @ref checkLink() after, see
* @ref GL-AbstractShaderProgram-async for more information.
* @see @fn_gl_keyword{LinkProgram}
*/
void submitLink();

/**
* @brief Check link status and await completion
* @brief Check shader linking status and await completion
* @m_since_latest
*
* Has to be called only if @ref submitLink() was called before.
* Returns @cpp false @ce if linking failed, @cpp true @ce on success.
* Linker message (if any) is printed to error output. This function
* must be called only after @ref submitLink().
*
* @see @fn_gl_keyword{GetProgram} with
* Linker message (if any) is printed to error output. The function
* will stall until a (potentially async) linking operation finishes,
* you can use @ref isLinkFinished() to check the status instead. See
* @ref GL-AbstractShaderProgram-async for more information.
* @see @ref Shader::checkCompile(), @fn_gl_keyword{GetProgram} with
* @def_gl{LINK_STATUS} and @def_gl{INFO_LOG_LENGTH},
* @fn_gl_keyword{GetProgramInfoLog}
*/
Expand Down
2 changes: 1 addition & 1 deletion src/Magnum/GL/Shader.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -766,7 +766,7 @@ void Shader::submitCompile() {
glCompileShader(_id);
}

bool Shader::checkCompile() { /* After compilation phase, check status of all shaders */
bool Shader::checkCompile() {
GLint success, logLength;
glGetShaderiv(_id, GL_COMPILE_STATUS, &success);
glGetShaderiv(_id, GL_INFO_LOG_LENGTH, &logLength);
Expand Down
57 changes: 42 additions & 15 deletions src/Magnum/GL/Shader.h
Original file line number Diff line number Diff line change
Expand Up @@ -631,41 +631,68 @@ class MAGNUM_GL_EXPORT Shader: public AbstractObject {
Shader& addFile(const std::string& filename);

/**
* @brief Compile shader
* @brief Compile the shader
*
* Calls @ref submitCompile(), then @ref checkCompile().
* Prefer to compile multiple shaders at once using
* @ref compile(std::initializer_list<Containers::Reference<Shader>>)
* for improved performance, see its documentation for more
* information.
* Calls @ref submitCompile(), immediately followed by
* @ref checkCompile(), passing back its return value. See
* documentation of those two functions for details.
*/
bool compile();

/**
* @brief Submit shader for compilation
* @brief Submit the shader for compilation
* @m_since_latest
*
* You can call @ref isCompileFinished() or @ref checkCompile() after,
* but in most cases it's enough to defer that to after
* @ref AbstractShaderProgram::attachShader() and
* @relativeref{AbstractShaderProgram,submitLink()} were called, and
* then continuing with @relativeref{AbstractShaderProgram,isLinkFinished()}
* or @relativeref{AbstractShaderProgram,checkLink()} on the final
* program --- if compilation would fail, subsequent linking will as
* well. See @ref GL-AbstractShaderProgram-async for more information.
* @see @fn_gl_keyword{ShaderSource}, @fn_gl_keyword{CompileShader}
*/
void submitCompile();

/**
* @brief Check compilation status and await completion
*
* Returns @cpp false @ce if compilation of failed, @cpp true @ce on success.
* This function must be called only after @ref submitCompile().
* @brief Check shader compilation status and await completion
* @m_since_latest
*
* Has to be called only if @ref submitCompile() was called before. In
* most cases it's enough to defer this check to after
* @ref AbstractShaderProgram::attachShader() and
* @relativeref{AbstractShaderProgram,submitLink()} were called, and
* then continuing with @relativeref{AbstractShaderProgram,isLinkFinished()}
* or @relativeref{AbstractShaderProgram,checkLink()} on the final
* program --- if compilation would fail, subsequent linking will as
* well. See @ref GL-AbstractShaderProgram-async for more information.
* @see @fn_gl_keyword{GetShader} with @def_gl{COMPILE_STATUS} and
* @def_gl{INFO_LOG_LENGTH}, @fn_gl_keyword{GetShaderInfoLog}
*/
bool checkCompile();

/**
* @brief Non-blocking compilation status check
* @return @cpp true @ce if shader compilation finished, @cpp false @ce otherwise
* @brief Whether a @ref submitCompile() operation has finished
* @m_since_latest
*
* @see @fn_gl_keyword{GetProgram} with
* @def_gl_extension{COMPLETION_STATUS,KHR,parallel_shader_compile}
* Has to be called only if @ref submitCompile() was called before, and
* before @ref checkCompile(). If returns @cpp false @ce, a subsequent
* @ref checkCompile() call will block until the compilation is
* finished. If @gl_extension{KHR,parallel_shader_compile} is not
* available, the function always returns @cpp true @ce --- i.e., as if
* the compilation was done synchronously.
*
* In most cases it's enough to only wait for the final link to finish,
* and not for particular compilations --- i.e., right after
* @ref submitCompile() continue with
* @ref AbstractShaderProgram::attachShader() and
* @relativeref{AbstractShaderProgram,submitLink()}, and then check
* with @relativeref{AbstractShaderProgram,isLinkFinished()} on the
* final program. See @ref GL-AbstractShaderProgram-async for more
* information.
* @see @fn_gl_keyword{GetProgram} with
* @def_gl_extension{COMPLETION_STATUS,KHR,parallel_shader_compile}
*/
bool isCompileFinished();

Expand Down

0 comments on commit 4580c30

Please sign in to comment.