As Unity’s Scriptable Render Pipeline (SRP)’s available feature set continues to grow, so does the amount of shader variants being processed and compiled at build time. Alongside ongoing support for additional graphics APIs and an ever-growing selection of target platforms, the SRP’s improvements continue to expand.
Shaders are compiled and cached after an initial (“clean”) build, thus accelerating further incremental (“warm”) builds. While clean builds usually take the longest, lengthy warm build times can be a common pain point during project development and iteration.
To address this problem, Unity’s Shader Management team has been hard at work to provide meaningful and scalable solutions. This has resulted in significantly reduced shader build times and runtime memory usage for projects created using Unity 2021 LTS and later versions.
To read more about these new optimizations, including affected versions, backports, and figures from our internal testing, skip directly to the sections covering shader variant prefiltering and dynamic shader loading. At the end of this blog post, we also address our future plans to further refine shader variant management as a whole – across project authoring, build, and runtime.
Before delving into the exciting improvements made to Unity’s shader system, let’s also take the opportunity to quickly review the concepts of conditional shader compilation, shader variants, and shader variant stripping.
Conditional shader features enable developers and artists to conveniently control and alter a shader’s functionality using scripts, material settings, as well as project and graphics settings. Such conditional features serve to simplify project authoring, allowing projects to efficiently scale by minimizing the number of shaders you’ll have to author and maintain.
Conditional shader features can be implemented in different ways:
While static branching avoids branching-related shader execution overhead at runtime, it’s evaluated and locked at compilation time and does not provide runtime control. Shader variant compilation, meanwhile, is a form of static branching that provides additional runtime control. This works by compiling a unique shader program (variant) for every possible combination of static branches, in order to maintain optimal GPU performance at runtime.
Such variants are created by conditionally declaring and evaluating shader functionality through shader_feature and multi_compile shader keywords. The correct shader variants are loaded at run time based on active keywords and runtime settings. Declaring and evaluating additional shader keywords can lead to an increase in build time, file size, and runtime memory usage.
At the same time, dynamic (uniform-based) branching entirely avoids the overhead of shader variant compilation, resulting in faster builds and both reduced file size and memory usage. This can bring forth smoother and faster iteration during development.
On the other hand, dynamic branching can have a strong impact on shader execution performance based on the shader’s complexity and the target device. Asymmetric branches, where one side of the branch is much more complex than the other, can negatively impact performance. This is because the execution of a simpler path can still incur the performance penalties of the more complex path.
When introducing conditional shader features in your own shaders, these approaches and trade-offs should be kept in mind. For more detailed information, see the shader conditionals, shader branching, and shader variants documentation.
To mitigate the increase in shader processing and compilation time, shader variant stripping is utilized. It aims to exclude unnecessary shader variants from compilation based on factors such as:
When enumerating shader variants, the Editor will automatically filter out any keywords declared with shader_feature that are not enabled by materials referenced and included in the build. As a result, these keywords will not generate any additional variants.
For example, if the Clear Coat material property is not enabled by any material using the Complex Lit URP Shader, all shader variants that implement the Clear Coat functionality will safely be stripped at build time.
In the meantime, multi_compile keywords prompt developers and players to freely control the shader’s functionality at runtime based on available Player settings and scripts. The flip side is that such keywords cannot automatically be stripped by the Editor to the same degree as shader_feature keywords. That’s why they generally produce a larger number of variants.
Scriptable stripping is a C# API that lets you exclude shader variants from compilation during build time via keywords and combinations not required at runtime. The render pipelines utilize scriptable stripping in order to strip unnecessary variants according to the project’s Render Pipeline settings and Quality Assets included in the build.
|Low quality||High quality||Variant multiplier|
|Main Light/Cast Shadows:||Off||On||2x|
|Main Light/Cast Shadows:||On||On||1x|
|Main Light/Cast Shadows:||Off||Off||1x|
In order to maximize the effects of the Editor’s shader variant stripping, we recommend disabling all graphics-related features and Render Pipeline settings not utilized at runtime. Please refer to the official documentation for more on shader variant stripping.
Shader variant stripping greatly reduces the amount of compiled shader variants, based on factors like the Render Pipeline Quality Assets in the build. However, stripping is currently performed at the end of the shader processing stage. Simply enumerating all the possible variants can still take a long time, regardless of compilation.
In order to reduce the shader variant processing (and project build) times, we are now introducing a significant optimization to the engine’s built-in shader variant stripping. With shader variant prefiltering, both clean and warm build times are significantly reduced.
The optimization works by introducing the early exclusion of multi_compile keywords, according to Prefiltering Attributes driven by Render Pipeline settings. This decreases the amount of variants being enumerated for potential stripping and compilation, which in turn, reduces shader processing time – with warm build times reduced by up to 90% in the most drastic examples.
Shader variant prefiltering first landed in 2023.1.0a14, and has been backported to 2022.2.0b15 and 2021.3.15f1.
Variant prefiltering also helps cut down initial/clean build times by applying the same principle.
Historically, the Unity runtime would front-load all shader objects from disk to CPU memory during scene and resource load. In most cases, a built project and scene includes many more shader variants than needed at any given moment during the application’s runtime. For projects using a large amount of shaders, this often results in high shader memory usage at runtime.
Dynamic shader loading addresses the issue by providing refined user control over shader loading behavior and memory usage. This optimization facilitates the streaming of shader data chunks into memory, as well as the eviction of shader data that is no longer needed at runtime, based on a user controlled memory budget. This allows you to significantly reduce shader memory usage on platforms with limited memory budgets.
New Shader Variant Loading Settings are now accessible from the Editor’s Player Settings. Use them to override the maximum number of shader chunks loaded and per-shader chunk size (MB).
With the following C# API now available, you can override the Shader Variant Loading Settings using Editor scripts, such as:
You can also override the maximum amount of loaded shader chunks at runtime using the C# API via Shader.maximumChunksOverride. This enables you to override the shader memory budget based on factors such as the total available system and graphics memory queried at runtime.
Dynamic shader loading landed in 2023.1.0a11 and has been backported to 2022.2.0b10, 2022.1.21f1, and 2021.3.12f. In the case of the Universal Render Pipeline (URP)’s Boat Attack, we observed a 78.8% reduction in runtime memory usage for shaders, from 315 MiB (default) to 66.8 MiB (dynamic loading). You can read more about this optimization in the official announcement.
Beyond the critical changes mentioned above, we are working to enhance the Universal Render Pipeline’s shader variant generation and stripping. We’re also investigating additional improvements to Unity’s shader variant management at large. The ultimate goal is to facilitate the engine’s increasing feature set, while ensuring minimal shader build and runtime overhead.
Some of our ongoing investigations involve the deduplication of shader resources across similar variants, as well as overall improvements to the shader keywords and Shader Variant Collection APIs. The aim is to provide more flexibility and control over shader variant processing and runtime performance.
Looking ahead, we are also exploring the possibility of in-Editor tooling for shader variant tracing and analysis to provide the following details on shader variant usage:
Your feedback has been instrumental so far as it helps us prioritize the most meaningful solutions. Please check out our public roadmap to vote on the features that best suit your needs. If there are additional changes you’d like to see, feel free to submit a feature request, or contact the team directly in this shader forum.