diff --git a/doc/shaders.dox b/doc/shaders.dox index 36ab1fd2b6..9489b5853f 100644 --- a/doc/shaders.dox +++ b/doc/shaders.dox @@ -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, diff --git a/doc/snippets/MagnumGL.cpp b/doc/snippets/MagnumGL.cpp index 98b995c736..59d6273812 100644 --- a/doc/snippets/MagnumGL.cpp +++ b/doc/snippets/MagnumGL.cpp @@ -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(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 @@ -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] */ diff --git a/src/Magnum/GL/AbstractShaderProgram.h b/src/Magnum/GL/AbstractShaderProgram.h index a7ad8c5e12..682f46c700 100644 --- a/src/Magnum/GL/AbstractShaderProgram.h +++ b/src/Magnum/GL/AbstractShaderProgram.h @@ -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 @@ -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(); @@ -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>) - * 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} */ diff --git a/src/Magnum/GL/Shader.cpp b/src/Magnum/GL/Shader.cpp index 8b7bbb98bf..d45b304f77 100644 --- a/src/Magnum/GL/Shader.cpp +++ b/src/Magnum/GL/Shader.cpp @@ -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); diff --git a/src/Magnum/GL/Shader.h b/src/Magnum/GL/Shader.h index ee3ded5330..f42eee3c6a 100644 --- a/src/Magnum/GL/Shader.h +++ b/src/Magnum/GL/Shader.h @@ -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>) - * 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();