Effect compilation and Content Pipeline automation in XNA Game Studio 4.0

Originally posted to Shawn Hargreaves Blog on MSDN, Friday, May 7, 2010

Previous XNA versions included an Effect.CompileEffectFromSource method. We do not support runtime shader compilation on Xbox, so this was only available in the Windows framework, which could compile shaders targeting either Windows or Xbox. This factoring was problematic because:

Game Studio 4.0 moves the shader compiler from the Windows framework to the Content Pipeline. Effect.CompileEffectFromSource is replaced by EffectProcessor.

There are several ways you can invoke the Content Pipeline to compile an effect:

To call into the Content Pipeline, you must add a reference to Microsoft.Xna.Framework.Content.Pipeline.dll. But when you open the Add Reference dialog, this will most likely not be listed. What gives?

.NET Framework 4 introduces the concept of a client profile, which is a subset of the full .NET Framework optimized for client applications (as opposed to server or developer tools), and which therefore has a smaller download size. The XNA Framework runtime works with the client profile, and our Windows Game project templates target it by default, but the Content Pipeline requires the full .NET framework. To reference the Content Pipeline assembly:

We're going to import a texture. First some using statements:

    using Microsoft.Xna.Framework.Content.Pipeline;
    using Microsoft.Xna.Framework.Content.Pipeline.Graphics;
    using Microsoft.Xna.Framework.Content.Pipeline.Processors;

Now we make a custom logger class. This version just throws away any warnings or messages, but a real app might want to display these somewhere:

    class MyLogger : ContentBuildLogger
    {
        public override void LogMessage(string message, params object[] messageArgs) { }
        public override void LogImportantMessage(string message, params object[] messageArgs) { }
        public override void LogWarning(string helpLink, ContentIdentity contentIdentity, string message, params object[] messageArgs) { }
    }

Finally we create a custom importer context, which allows the importer to communicate with whoever is hosting it:

    class MyImporterContext : ContentImporterContext
    {
        public override string IntermediateDirectory { get { return string.Empty; } }
        public override string OutputDirectory { get { return string.Empty; } }

        public override ContentBuildLogger Logger { get { return logger; } }
        ContentBuildLogger logger = new MyLogger();

        public override void AddDependency(string filename) { }
    }

Armed with these helper classes, it is easy to call into the TextureImporter:

    TextureImporter importer = new TextureImporter();

    TextureContent texture = importer.Import("cat.tga", new MyImporterContext());

Calling a processor is similar, except we need a custom processor context, which is more complex than the importer context we used before:

    class MyProcessorContext : ContentProcessorContext
    {
        public override TargetPlatform TargetPlatform { get { return TargetPlatform.Windows; } }
        public override GraphicsProfile TargetProfile { get { return GraphicsProfile.Reach; } }
        public override string BuildConfiguration { get { return string.Empty; } }
        public override string IntermediateDirectory { get { return string.Empty; } }
        public override string OutputDirectory { get { return string.Empty; } }
        public override string OutputFilename { get { return string.Empty; } }
        
        public override OpaqueDataDictionary Parameters { get { return parameters; } }
        OpaqueDataDictionary parameters = new OpaqueDataDictionary();
        
        public override ContentBuildLogger Logger { get { return logger; } }
        ContentBuildLogger logger = new MyLogger();

        public override void AddDependency(string filename) { }
        public override void AddOutputFile(string filename) { }

        public override TOutput Convert<TInput, TOutput>(TInput input, string processorName, OpaqueDataDictionary processorParameters) { throw new NotImplementedException(); }
        public override TOutput BuildAndLoadAsset<TInput, TOutput>(ExternalReference<TInput> sourceAsset, string processorName, OpaqueDataDictionary processorParameters, string importerName) { throw new NotImplementedException(); }
        public override ExternalReference<TOutput> BuildAsset<TInput, TOutput>(ExternalReference<TInput> sourceAsset, string processorName, OpaqueDataDictionary processorParameters, string importerName, string assetName) { throw new NotImplementedException(); }
    }

To build for a different platform, or for HiDef as opposed to Reach, change the TargetPlatform and TargetProfile properties.

You can use the AddDependency method to track additional files that are read by the importer or processor. For instance this is called any time the effect compiler encounters a #include statement in the effect source code. This information is useful if you want to implement incremental rebuild, so you can handle things like recompiling effects because their #include files have changed, even when the main .fx has not.

This simple example does not bother to implement the Convert, BuildAndLoadAsset, and BuildAsset methods. These are not neccessary when running simple standalone processors, but you will need to hook them up if you want to support more complex things like ModelProcessor, which uses them to call into the MaterialProcessor, EffectProcessor, and TextureProcessor.

Armed with our custom processor context, we can create an EffectContent object, set its EffectCode property to a string containing our HLSL source, then compile it with EffectProcessor:

    EffectContent effectSource = new EffectContent
    {
        Identity = new ContentIdentity { SourceFilename = "myshader.fx" },

        EffectCode =
        @"
            float4 MakeItPink() : COLOR0
            {
                return float4(1, 0, 1, 1);
            }

            technique Technique1
            {
                pass Pass1
                {
                    PixelShader = compile ps_2_0 MakeItPink();
                }
            }
        ",
    };

    EffectProcessor processor = new EffectProcessor();

    CompiledEffectContent compiledEffect = processor.Process(effectSource, new MyProcessorContext());

We can use processor parameters to adjust the compilation behavior:

    EffectProcessor processor = new EffectProcessor();

    processor.Defines = "ENABLE_FOG;NUM_LIGHTS=3";
    processor.DebugMode = EffectProcessorDebugMode.Optimize;

    CompiledEffectContent compiledEffect = processor.Process(effectSource, new MyProcessorContext());

Note that the Content Pipeline is not part of the XNA Framework redistributable, so this code will only work on computers that have a full Game Studio install.

Blog index   -   Back to my homepage