URP Shader Variant Auditor

Documentation

Finds what is driving your shader variant count, and what to change. A Unity editor tool by TechAthletes.

Contents

Why variant counts explode

A shader is not one program. Every #pragma multi_compile and #pragma shader_feature line declares a set of mutually exclusive keywords, and the compiler produces one program for every combination across every set, for every pass. Six sets of two states in one pass is 64 programs. Add a set of four and it is 256. URP's Lit shader carries a lot of these, because it has to cover every lighting, shadow, lightmap, fog and decal configuration a project might use.

Unity keeps that manageable by stripping. Its rule is roughly:

The second rule does the heavy lifting, and it depends entirely on Unity being able to see the materials. Anything that puts a shader into the build without a material attached removes that context, and the shader_feature sets go back to their full combinatorial size. That is the single largest cause of an unexpected variant count, and it is what URP001 checks.

How the count is produced

For each pass of each shader:

alwaysCompiled = product of multi_compile set sizes
feature        = product of shader_feature set sizes

Then per shader:

reached through materials  ->  sum over passes of  alwaysCompiled x (distinct keyword combinations the materials enable)
in Always Included Shaders ->  sum over passes of  alwaysCompiled x feature

The report shows both numbers for every shader. Where they differ, the difference is what a settings change would recover.

Shaders whose source cannot be read - ShaderGraph outputs, engine built-ins, binary shaders - are reported from the shader's keyword space as an upper bound (2^n over n keywords) and marked upper bound in the table. When Unity's own shader introspection is reachable, its figure is recorded in the JSON as unityReportedVariants for comparison.

Checking the estimate against a build

Unity prints its own ladder for every shader pass while it builds:

Compiling shader "Universal Render Pipeline/Lit" pass "ForwardLit" (fp)
    Full variant space:         4530751578
    After settings filtering:   56914
    After built-in stripping:   141
    After scriptable stripping: 54
    ... compiled 54 variants, skipped 0 variants

Point the audit at a build log and those real numbers appear beside the estimates:

Unity -batchmode -quit -nographics -projectPath <project> \
  -executeMethod TechAthletes.VariantAuditor.VariantAuditCli.Run \
  -auditOutput <dir> -auditBuildLog <path to a previous build's log>

In the editor window, the Build log... button does the same. On macOS the log is at ~/Library/Logs/Unity/Editor.log, on Windows at %LOCALAPPDATA%\Unity\Editor\Editor.log, or wherever -logFile sent it.

Unity's build log This report Comes from
Full variant space Declared keyword space every #pragma set multiplied out
After settings filtering After pipeline and Graphics settings the URP Asset, its renderers, Graphics settings
After built-in stripping After material keyword usage the keyword combinations your materials enable
After scriptable stripping not modelled IPreprocessShaders callbacks

How far the estimate sits from a build

The estimates are upper bounds, and the gap is large. A keyword set is reduced only when the model can name the reason, and Unity's settings filtering removes far more than any model outside the engine can account for.

Measured on a real URP project - Unity 6000.0.78f1, URP 17.0.4, 88 materials, macOS player, counting only the shaders this tool can parse:

Rung Measured in the build Estimated by this report
Full variant space 3.84B 7.0T
After settings filtering 64,652 3.9B
After built-in stripping 754 679,135
Compiled 609 -

So the estimate ran roughly three orders of magnitude above what the build compiled. That is the honest number and it is why the report says "estimated variant space" rather than "variants your build will compile".

What survives that gap, and what the tool is for:

Feed it a build log and the measured column is exact.

Reading the report

The header line is the comparison that matters:

842,000 variants on the current settings. 6,400 with stripping applied throughout.
Difference: 835,600.

A large difference means something is preventing stripping, and the critical findings say what. A small difference means stripping is working and the count is simply the shaders you use - in which case the lever is the keyword sets listed under URP003 and URP004, not the settings.

Each finding carries an estimated reduction. Those estimates are per finding and can overlap, so treat the project-level Difference as the total, not their sum.

Rules reference

URP001 - Render pipeline shaders in Always Included Shaders (critical). The pipeline's own shaders are the largest in the project. Listed here, they compile every keyword combination. See Always Included Shaders.

URP002 - Project shaders in Always Included Shaders (warning). Same mechanism on your own shaders. Smaller, still worth removing.

URP003 - Shaders above 20,000 variants (warning). A ranked list. Work down it rather than across the project.

URP004 - Keyword sets that multiply the count (info). For the three worst shaders, the keyword sets ordered by size, each marked as always compiled or strippable by material. This is the line-level attribution: a set of size 4 that you do not need accounts for three quarters of that shader's variants.

URP005 - Materials under Resources (info). Everything under Resources ships whether or not a scene references it. This is the intended way to keep a shader in a build without disabling stripping, so the finding is informational - it exists so you can confirm each one is deliberate.

URP006 - shader_feature keywords with no material to narrow them (warning). Raised when an always-included shader declares shader_feature sets. Those keywords exist so unused states can be dropped; through the Always Included list nothing drops them.

URP007 - Estimated variant space above 100,000 (warning, critical above 500,000). The overall verdict, with the space the project would have if stripping worked everywhere. When the two match, stripping is running and the finding points at URP003, URP004 and URP008 instead.

URP008 - Keyword sets still at full size (info). Sets the model could not reduce because the pipeline is configured to use the feature they belong to. Each is a switch on the URP Asset, a renderer, or Graphics settings that you could evaluate. Sets that were already reduced are not listed, so this is a list of live choices rather than a summary.

Command line and CI

Unity -batchmode -quit -nographics \
  -projectPath "$PROJECT" \
  -executeMethod TechAthletes.VariantAuditor.VariantAuditCli.Run \
  -auditOutput "$OUT" \
  -auditFailOn critical \
  -logFile "$OUT/unity.log"

Writes variant-audit.md and variant-audit.json into -auditOutput, logs a summary line per finding, and exits 2 when -auditFailOn is met.

One caveat that is Unity's, not this package's: -quit exits 0 when scripts fail to compile, because -executeMethod never runs. A CI step should check that the report file exists rather than trusting the exit code alone. ci-example.sh in this folder does that.

Where this came from

This package exists because of one build that would not finish. Adding URP's shaders to Always Included Shaders — to fix a magenta material — took a project with five materials to a reported 590,000 variants, with no error and no pre-build number to look at. The write-up of that incident, including the fix and the calibration of this tool's estimates against real build logs, is here: Chasing a magenta material took our Unity build to 590,000 shader variants.