Precompiling shaders, without the pain

Everyone loves high-level shader languages. Well, most people anyway – it takes a certain kind of person to enjoy writing assembler language. They do have one disadvantage though – compiling them is not ‘free’, it does take an amount of time. For many things you might not notice, but if you have a lot of shaders, or particularly if you have long, complex shaders such as ‘uber shaders’, where you have one enormous set of code with precompiler options producing many variants, you might be surprised how long they might take to compile. I certainly was when I saw how long a particular HLSL program was taking to compile on a project I’m involved in now. The more sophisticated your shaders get, the more likely you are to start encountering this issue.

In these cases, precompiling shaders to assembler once, and then saving that assembler to be used directly afterwards is a sensible optimisation. It gets all the difficult bits done out of the way in advance, and if you’re smart you can do it in a just-in-time fashion so that you can continue to work primarily in a high-level language but use the precompiled assembler for repeat uses of unchanged shaders. Your startup sequence may well love you for it.

There was one main problem with this though – when you load the assembler version directly you have no access to the named parameter mappings that used to be in the original high-level source, because the assembler only knows about numbered constant registers. You had no choice but to switch to using indexed parameters, which was a bit awkward because you had to extract them manually from the compiled assembler, and also the rest of your code became less flexible since it was locked to a compiled output – if you changed the high-level source and recompiled, you risked having odd runtime errors because the constant layout changed. Ugh.

Well, never fear – now it’s better. You can now take named parameters and apply them to an assembler shader to make them easier & more consistent to use – you can either apply a GpuNamedConstants structure in-memory and apply it to an assembler program to describe the constants better, or you can save out the information from a high-level original program and have the assembler program read them back automatically. To do the latter, when defining an assembler shader, you use the new ‘manual_named_constants’ script option (also GpuProgram::setManualNamedConstantsFile) to specify a supplementary file containing saved information about named parameter definitions. This file can be created simply by calling ‘save’ on the GpuNamedConstants structure you can get from the original high-level program – and so this process could be automated by checking date/time stamps on the high-level & precompiled assembler files and saving out both the compiled assembler and the params definition if they’re out of date. Best of all, all the params code, material scripts etc that worked with the high-level program will work just the same on the assembler program, since all the parameters are the same, so you can generally switch back and forth without lots of awkward named/indexed parameter switching.

Using assembly shaders precompiled from the original high-level versions more seamlessly was an important issue for a project I’m involved with, where compile times were becoming a concern, so I’m pretty sure this will be useful to others too. Enjoy :)

  • pyro

    only two words: great stuff!

  • http://aras-p.info Aras Pranckevicius

    Now if only GLSL was able to do that! :)

  • http://zeuxcg.blogspot.com/ Arseny Kapoulkine

    Uhm, don’t you support precompiled shaders (to bytecode) for Direct3D? This kinda solves most problems (and gets as efficient as you can possibly on PC), and they have constant tables too, so no problem there.

  • http://www.stevestreeting.com Steve

    @Arseny: No – the difference between assembler and bytecode is minimal, certainly compared to the overhead of compiling the high-level shaders, and the inclusion of a const table in compiled output you mention is a DirectX-specific feature so we can’t rely on it anyway. This way works for all rendersystems / languages that have compilable shader output – unfortunately GLSL doesn’t have this option yet but you can use it with Cg on GL.