Shader Compilation in Unity 4.5

A story in several parts. 1) how shader compilation is done in upcoming Unity 4.5; and 2) how it was developed. First one is probably interesting to Unity users; whereas second one for the ones curious on how we work and develop stuff.

Short summary: Unity 4.5 will have a “wow, many shaders, much fast” shader importing and better error reporting.

Current state (Unity <=4.3)

When you create a new shader file (.shader) in Unity or edit existing one, we launch a “shader importer”. Just like for any other changed asset. That shader importer does some parsing, and then compiles the whole shader into all platform backends we support.

Typically when you create a simple surface shader, it internally expands into 50 or so internal shader variants (classic “preprocessor driven uber-shader” approach). And typically there 7 or so platform backends to compile into (d3d9, d3d11, opengl, gles, gles3, d3d11_9x, flash – more if you have console licenses). This means, each time you change anything in the shader, a couple hundred shaders are being compiled. And all that assuming you have a fairly simple shader – if you throw in some multi_compile directives, you’ll be looking at thousands or tens of thousands shaders being compiled. Each. And. Every. Time.

Does it make sense to do that? Not really.

Like most of “why are we doing this?” situations, this one also evolved organically, and can be explained with “it sounded like a good idea at the time” and “it does not fix itself unless someone works on it”.

A long time ago, Unity only had one or two shader platform backends (opengl and d3d9). And the amount of shader variants people were doing was much lower. With time, we got both more backends, and more variants; and it became very apparent that someone needs to solve this problem.

In addition to the above, there were other problems with shader compilation, for example:

Errors in shaders were reported, well, “in a funny way”. Sometimes the line numbers did not make any sense – which is quite confusing.

Errors in shaders are reported on correct lines; errors in shader include (.cginc) files are reported with the filename & line number correctly.

Was mostly “completely broken” before, especially when include files came into play.

On d3d11 backend we were reporting error column as the line, hah. At some point during d3dcompiler DLL upgrade it changed error printing syntax and we were parsing it wrong. Now added unit tests so hopefully it will never break again.

Surface shader debugging workflow is much better.

No more “add #pragma debug, open compiled shader, remove tons of assembly” nonsense. Just one button in inspector, “Show generated code”.

Generated surface shader code has some comments and better indentation. It is actually readable code now!

Shader inspector improvements:

Errors list has scrollview when it’s long; can double click on errors to open correct file/line; can copy error text via context click menu; each error clearly indicates which platform it happened for.

Investigating compiled shader is saner. One button to show compiled results for currently active platform; another button to show for all platforms.

Overview of how it works

Instead of compiling all shader variants for all possible platforms at import time:

Only do minimal processing of the shader (surface shader generation etc.).

Actually compile the shader variants only when needed.

Instead of typical work of compiling 100-1000 internal shaders at import time, this usually ends up compiling just a handful.

At player build time, compile all the shader variants for that target platform

Cache identical shaders under Library/ShaderCache.

So at player build time, only not-yet-ever-compiled shaders are compiled; and always only for the platforms that need them. If you never ever use Flash, for example, then none of shaders will be compiled for Flash (as opposed to 4.3, where all shaders are compiled to all platforms, even if you never ever need them).

Shader compiler (CgBatch) changes from being invoked for each shader import, into being run as a “service process”

Inter-process communication between compiler process & Unity; using same infrastructure as for VersionControl plugins integration.

At player build time, go wide and use all CPU cores to do shader compilation. Old compiler tried to internally multithread, but couldn’t due to some platforms not being thread-safe. Now, we just launch one compiler process per core and they can go fully parallel.

Helps with out-of-memory crashes as well, since shader compiler process never needs to hold bazillion of shader variants in memory all at once – what it sees is one variant at a time.

How it was developed

This was mostly a one-or-two person effort, and developed in several “sprints”. For this one we used our internal wiki for detailed task planning (Confluence “task lists”), but we could have just as well use Trello or something similar. Overall this was probably around two months of actual work – but spread out during much longer time. Initial sprint started in 2013 March, and landed in a “we think we can ship this tomorrow” state to 4.5 codebase just in time for 1st alpha build (2013 October). Minor tweaks and fixes were done during 4.5 alpha & beta period. Should ship anyday now, fingers crossed!

Surprisingly (or perhaps not), largest piece of work was around “how do you report errors in shaders?” area. Since now shader variants are imported only on demand, that means some errors can be discovered only “some time after initial import”. This is a by-design change, however – as the previous approach of “let’s compile all possible variants for all possible platforms” clearly does not scale in terms of iteration time. However, this “shader seemed like it did not have any errors, but whoops now it has” is clearly a potential downside. Oh well; as with almost everything there are upsides & downsides.

Most of development was done on a Unity 4.3-based branch, and after something was working we were sending off custom “4.3 + new shader importer” builds to the beta testing group. We were doing this before any 4.5 alpha even started to get early feedback. Perhaps the nicest feedback I ever got:

I’ve now used the build for about a week and I’m completely blown away with how it has changed how I work with shaders.

I can try out things way quicker.
I am no longer scared of making a typo in an include file.
These two combine into making me play around a LOT more when working.
Because of this I found out how to do fake HDR with filmic tonemapping [on my mobile target].

The thought of going back to regular beta without this [shader compiler] really scares me.

Anyhoo, here’s a dump of tasks from our wiki (all of them had little checkboxes that we’d tick off when done). As usual, “it basically works and is awesome!” was achieved after first week of work (1st sprint). What was left after that was “fix all the TODOs, do all the boring remaining work” etc.

2013 March Sprint:

Make CgBatch a DLL

Run unit tests

Import shaders from DLL

Don’t use temp files all over the place

Shader importer changes

Change surface shader part to only generate source code and not do any compilation

Make a “Open surface compiler output” button

At import time, do surface shader generation & cache the result (serialize in Shader, editor only)

Also process all CGINCLUDE blocks and actually do #includes at import time, and cache the result (after this, left with CGPROGRAM blocks, with no #include statements)

ShaderLab::Pass needs to know it will have yet-uncompiled programs inside, and able to find appropriate CGPROGRAM block:

Add syntax to shaderlab, something like Pass { GpuProgramID int }

Make CgBatch not do any compilation, just extract CGPROGRAM blocks, assign IDs to them, and replace them with “GpuProgramID xxx”

Change output of single shader compilation to not be in shaderlab program/subprogram/bindings syntax, but to produce data directly. Shader code as a string, some virtual interface that would report all uniforms/textures/… for the reflection data.

Compile shaders on demand

Data file format for gpu programs & their params

ShaderLab Pass has map: m_GpuProgramLookup (keywords -> GPUProgram).

GetMatchingSubProgram:

return one from m_GpuProgramLookup if found. Get from cache if found

Compile program snippet if not found

Write into cache

2013 July Sprint:

Pull and merge last 3 months of trunk

Player build pipeline

When building player/bundle, compile all shader snippets and include them

Error reporting: Figure out how to deal with late-discovered errors. If there’s bad syntax, typo etc.; effectively shader is “broken”. If a backend shader compiler reports an error:

Return pink “error shader” for all programs ­ i.e. if any of vertex/pixel/… had an error, we need to use the pink shaders for all of them.

Log the error to console.

Add error to the shader, so it’s displayed in the editor. Can’t serialize shader at that time, so add shaders to some database under Library (guid­>errors).

SQLite database with shader GUID -> set of errors.

Add shader to list of “shaders with errors”; after rendering loop is done go over them and make them use pink error shader. (Effectively this does not change current (4.2) behavior: if you have a syntax error, shader is pink).

Misc

Fix shader Fallback when it pulls in shader snippets

“Mesh components required by shader” part at build time – need to figure them out! Problem; needs to compile the variants to even know it.

2 shaders I use turned pink in the new system, Shield and XRayTransparent. Can you write a more in-depth guide to debugging old shaders? They worked fine till 4.5 said they had an error and turned them pink and unusable.

@Vladimir: never saw that problem. A bug report with any & all details would be appreciated (OS version? GPU? does it happen in all projects or some specific one? are UnityShaderCompiler processes using CPU at that time or not? etc. etc.)

PLEASE SOMEBODY ANSWER THIS!
Any Assets ( I’m talking about toolkit, gui’s and extensions, not about 3d models ) that are compatible with 4.3.4, do they have to get updated to work on Unity 4.5 or not??

Regarding ‘compile for all platforms’… the more I think about this, the more I’m persuaded that the current approach isn’t quite right.

Compile for *all* platforms isn’t right either – I really don’t care if my shader doesn’t compile for Flash, or Blackberry, or something. I don’t want to waste any time waiting for them (which I won’t, now) and I don’t want any errors from them cluttering up the console (which I would if you compiled in the background I guess).

However, compiling for only my current platform isn’t sufficient either, because I *am* targeting multiple platforms, and if my shader is going to fail to compile on any of them then I want to know while I’m still in a shader-writing mindset. Background compilation of them would be fine – so I can begin checking that the shader looks OK on at least the current platform ASAP – but you need to only be doing that background compilation for platforms that I’m targeting, not all platforms.

So I think you’re going to need, somewhere, a list of all the platforms you support, with checkboxes for me to indicate whether I want you to background-compile for that platform or not.

@Mark: Unity 5 is in very active development, about to finish alpha testing phase and start beta phase soon. How long that would take? Depends on how much feedback/issues will be found by beta testers. Definitely not “within weeks” :)

Thanks for the clarification, it now makes me almost wonder enough what the unimplemented DX11 hardware features are on this Intel HD Graphics chip that had Intel, MS and ASUS giving it the dropped HW support seal of disapproval.

I think this redesign of the shader compilations will fix the one bug that Unity has on my old MacMini with Intel 945 Graphics that would lock os X completely, unless ssh’ed in.

Generalizing the obscure arcana of direct access to OS APIs into the Unity API is why I like Unity so with that thanks & adieu.

@zimm: but that’s not true! DX10 class GPUs are supported just fine. There’s a difference between “graphics API” (DX9 and DX11) and “graphics capability level”. DX11 the API can run both on DX10 & DX11 capability GPUs.

So it does not really make any sense to also write code to run on DX10 API, since DX11 can do everything that DX10 can (including supporting DX10 GPUs just fine), and more.

@Aras Pranckevičius: I think he is asking why there is no DX10 support in Unity – just DX9 and DX11. This is totally unrelated to shader compilation in 4.5, though… But with my DX10-only videocard, that bugs me too.

DX10 is gone ASAP if MS has their way. Almost from the get go of buying my now ‘old’ PC (presented at a trade show for the 1st time in Jan 2010) MS, ASUS, and Intel stated Windows 8 and DX11 would not support it, even Apple doesn’t try to obsolete their hardware support that fast. It took almost a year after Windows 8 was released before one of those three buckled and wrote a video driver for the Intel HD Graphics that would run in Windows 8.

@Scott Richmond: “trololo” :) You could also say that UE4 had things ready over a year ago (since they did show things at GDC2013), but only released them this year! The same is true for any big software project; some things are made ready earlier than they are released.

There is so much information to learn from your each blog post! Kudos to all the explanations here and on your personal blog posts as well. Please keep sharing & writing technical details like this, its really helpful to folks like me who are newbees and willing to learn more.

@LARSBERTRAM / @10FINGERARMY: yes, there’s a button in shader inspector to compile all variants/platorms. At some point in the future maybe it would be nice to keep on compiling “not yet compiled” shader variants & platforms in the background (if the editor is not doing anything more important), but that’s not implemented yet.

@PETER: yeah, Unity 4.4 was a consoles-only release. So yeah, it pretty much does not exist in public; perhaps calling it 4.4 was not a terribly bright decision on our part. Sorry about that! That said, this isn’t the first time we’re “skipping” a number; we had 2.0 -> 2.1 -> 2.5 a while ago…

Why don’t you include an additional button in the shader inspector to “Compile to all platforms”. This would solve the late-error-detection issue for shader authors once they have a version they are happy with.

a lot of this is great news, but if i do you get right when i write a shader on osx it won’t get compiled for dx9 or dx11 unless i export a windows built?
that might be a bit painful i guess… as you won’t get error messages unless you built the built.