Shader permutation problem ( again )

Started by
13 comments, last by wolf 15 years, 5 months ago
Hi all: I have been reading a lot about shader permutation problem and I'm getting a little bit frustated because I really don't know which solution is better: - uber-shaders: I can't use it because I don't like it because the performance hit of dynamic branching and I must target low graphic cards. - Generate custom shaders on the fly: I have tried this solution but I really don't feel good with it. I have read the article of Shawn Hargreaves and did a little implementation of something similiar but I think is not the way to go ( I like to have more control of my code ) - Use ID3DXEffectCompiler enabling constants at compile time: I like it, but it doesn't seems the way to go neither for me. The main problem with it ( it happens with 'generatin custom shaders on the fly' ) is that I can't ( as far as I know ) save all shaders at disk in one effect and read it with one 'create' call ( I'm targetting to consoles so it matters to cut down loading time ). - Precompile all the possible permutations of the shader, even if I don't use a lot of them: I think this is the best, but I prefer to ask you. I like it because I can generate it totally offline without having to parse all the game data ( even parsing it, maybe the game needs some special combination that is not contemplated on game data ). Besides I can have only one ID3DXEffect ( per shader type ) with N techniques. The only thing the material has to compute is the technique name ( and even that can be computed offline and saved as an index when possible ). Even more, I feel like this solution is the most cross platform, because I can generate all the combinations for XBox360 and PC ( HLSL ), and PS3 ( it seems like I'll have to deal with it soon... ). What do you think? I appreciate every opinion from you. Thanks a lot in advance and excuse me for my english =) Carlos
Advertisement
my opinion in a nutshell is: it is not a technical problem but a workflow problem. I mentioned more details in some of the threads here that I also have on my blog.
In essence:
1. good naming convention for your < 20 meta-materials == *.fx files .. too many pixel shaders can be too much switch overhead
2. *.fxh include file libraries with the code that is called from the *.fx files
3. shaderlib directory that holds all the *.fxh files
Thank you, I've solved my problem =)

Carlos
Iv recently had the same kind of questions, and based on research ive done and getting professional opinions on it, an ubershader is the way to go (if your targeting recent hardware). It may seem like it would be pretty slow, but if used correctly, the compiler can actually turn your "dynamic branches" into static branches which are basically for free.

Furthermore, if the branching happens uniformly across the object and dynamic branching needs to be used, it will be fast because all of the pixels/verticies being processed will all be taking the same path, which is what the GPU is really good at.
Using an ubershader is a bad idea because you will get compile times of 30+ minutes for your shaders at some point with this. You do not want to let a team of graphics programmers wait everytime they change the shader for 30 minutes.
Quote:Original post by wolf
Using an ubershader is a bad idea because you will get compile times of 30+ minutes for your shaders at some point with this. You do not want to let a team of graphics programmers wait everytime they change the shader for 30 minutes.


Hey Wolf,

That seems just to me a build problem - if it is efficient for the game then use what ever is best for the game. If there are 30 minute build times then that needs to be addressed in the tools. Scattering the compiles across a build farm will solve the huge recompile. Also, while iterating on a shader the build could create a secondary shader and it only recompiles the one shader to display in game. When the graphics programmer is happy they can submit the shader to the build farm to recreate all the shader permutations.

With that said - I don't disagree with your approach though.

What we're implementing is very similiar to what you've said. We create a handful of ubershaders that represent different material types. Within each ubershader you can compile in and out different options(we don't do dynamic branching). Those options are implemented as functions in shared header files so all the different materials can reuse code.

This allows us to keep shaders at a reasonable size to make it easier to optimize and maintain.

-= Dave
Graphics Programmer - Ready At Dawn Studios
The problem is that you create all those shaders from one source file. This takes time. Whereas with a carefully selected shader setup you only build the shader you are currently working on.
You can't speed up the creation process of the uebershader because ... yep it is only one file that holds everything.
Quote:Original post by wolf
The problem is that you create all those shaders from one source file. This takes time. Whereas with a carefully selected shader setup you only build the shader you are currently working on.
You can't speed up the creation process of the uebershader because ... yep it is only one file that holds everything.


* It is a problem that is easily parallelised onto a build farm
* As I mentioned earlier when iterating on a shader source file you can have your tool use another source file that you iterate on. This will only spawn one build with one configuration (the current one the graphic programmers select). When the programmer is happy with his shader - the tool saves the source file over the original source file and spawns the parallel build that compiles all configurations

I think you should do that and on top of that have a 'carefully selected shader setup' as well.

-= Dave
Graphics Programmer - Ready At Dawn Studios
Quote:Original post by wolf
Using an ubershader is a bad idea because you will get compile times of 30+ minutes for your shaders at some point with this. You do not want to let a team of graphics programmers wait everytime they change the shader for 30 minutes.
and
Quote:The problem is that you create all those shaders from one source file. This takes time. Whereas with a carefully selected shader setup you only build the shader you are currently working on.
You can't speed up the creation process of the uebershader because ... yep it is only one file that holds everything.

Wolfgang, you are stating with utter certainty things that IMO are clearly wrong. David already explained to you the way of addressing the issue you mention: by using a smarter build system.

All asset processing can be sped up by (1) parallelizing/distributing the processing, and by (2) caching results (as well as temporaries) to avoid needless reprocessing. This neatly takes care of the issue you mention, and much much more!

Obviously, creating such a system is more work than just having separate .fx files for every shader than a single ubershader.fx file from which others are created, but (1) the above build system gives you pipeline performance benefits for all asset types, and (2) in your "simpler" system you have the problems of remembering to change every relevant shader file when you want to change e.g. lighting, plus you too would rebuild (nearly) all shaders if you touched something as general as lighting falloff, except you wouldn't have the benefit of the smarter build system to help you out in that case.

Plus you are missing out on other benefits that generated shaders buys you, such as the ability to fold constants at compile-time.

But what you suggest is certainly the simplest possible good solution, and simplicity is often -- but not always -- the right thing.
Quote:Original post by Christer Ericson
Obviously, creating such a system is more work than just having separate .fx files for every shader than a single ubershader.fx file from which others are created, but (1) the above build system gives you pipeline performance benefits for all asset types, and (2) in your "simpler" system you have the problems of remembering to change every relevant shader file when you want to change e.g. lighting, plus you too would rebuild (nearly) all shaders if you touched something as general as lighting falloff, except you wouldn't have the benefit of the smarter build system to help you out in that case.


You include all common elements/functions from shared seperate files, so, changing the lighting falloff only necessitates recompiling the shaders that use that particular lighting model. Easy to parse fx files for the #includes. This results in the optimal compilation time, as you are: compiling only exactly what you need to (any other way of setting up your shaders would require the same or worse), and you can easily parallelize compiling multiple fx files.

This topic is closed to new replies.

Advertisement