Improve Effect Processor

From XNAWiki
Jump to: navigation, search

The following was designed for use with XNA 3.1, and I am unsure what changes occurred in the effect pipeline that broke it for XNA 4.0.

This is an effect processor which handles compiling an effect using SlimDX to interact with the Direct3D Effect Compiler. It is significantly better than the compiler used by XNA 3.1, and I'm slowly coming to the same conclusion about XNA 4.0.

Features:

  • better error messages, especially for include files.
  • almost every error message takes you right to the line where it occurred, even if it's in an include file. Still working on improving this part to resolve pre-processor statements as well.
  • allow full use of the compiler hints (unroll, branch, etc).
  • handles multiple branches a lot better in release builds.
  • 100% compatible with the XNA Effect class.
[ContentProcessor(DisplayName = "Improved Effect Processor")]
public class XEffectProcessor : Microsoft.Xna.Framework.Content.Pipeline.Processors.EffectProcessor
{
    class IncludeHandler : Include
    {
        string m_directory;
        ContentProcessorContext m_context;
        ContentIdentity m_identity;
 
        public IncludeHandler(string directory, ContentProcessorContext context, ContentIdentity identity)
        {
            m_directory = directory;
            m_context = context;
            m_identity = identity;
        }
 
        #region Include Members
 
        public void Close(Stream stream)
        {
            // close the file stream, not good to leave them open
            if (stream != null)
            {
                stream.Close();
                stream.Dispose();
            }
        }
 
        public void Open(IncludeType type, string fileName, Stream parentStream, out Stream stream)
        {
            // resolve the path
            string fullPath = Path.GetFullPath(Path.Combine(m_directory, fileName));
 
            // add the file as a dependency and pray that visual studio watches it for changes
            m_context.AddDependency(fullPath);
 
            // open the include file, the compiler will handle integrating it automatically
            stream = File.OpenRead(fullPath);
        }
 
        #endregion
    }
 
    public override CompiledEffect Process(EffectContent input, ContentProcessorContext context)
    {
        if (context.TargetPlatform == TargetPlatform.Windows)
        {
            // it's cleaner to process the error messages manually
            SlimDX.Configuration.ThrowOnShaderCompileError = false;
 
            string compilerErrors = string.Empty;
            ShaderMacro[] preprocessorMacros = new ShaderMacro[0];
            Include includeHandler = new IncludeHandler(new FileInfo(input.Identity.SourceFilename).Directory.FullName, context, input.Identity);
            ShaderFlags shaderFlags = ShaderFlags.None;
 
            try
            {
                // use the compact compiler since all that is needed is the byte code
                ShaderBytecode shaderByteCode = ShaderBytecode.CompileFromFile(input.Identity.SourceFilename, "fx_2_0", shaderFlags, EffectFlags.None, preprocessorMacros, includeHandler, out compilerErrors);
                if (!string.IsNullOrEmpty(compilerErrors))
                {
                    ProcessErrorsAndWarnings(compilerErrors, input, context);
                    throw new InvalidContentException(compilerErrors, input.Identity);
                }
 
                // read back the compiled shader byte code
                byte[] byteCode = new byte[shaderByteCode.Data.Length];
                shaderByteCode.Data.Read(byteCode, 0, byteCode.Length);
 
                // output the results into the content pipeline
                return new CompiledEffect(byteCode, string.Empty);
            }
            catch (CompilationException e)
            {
                // this should never occur, but if it does be ready to process the error
                ProcessErrorsAndWarnings(e.Message, input, context);
                throw new InvalidContentException(e.Message, input.Identity);
            }
        }
 
        // when the target platform isn't windows, use the xna effect compiler
        return base.Process(input, context);
    }
 
    void ProcessErrorsAndWarnings(string errorsAndWarnings, EffectContent input, ContentProcessorContext context)
    {
        string[] errors = errorsAndWarnings.Split('\n');
        for (int i = 0; i < errors.Length; i++)
        {
            if (errors[i].StartsWith(Environment.NewLine))
                break;
 
            // find some unique characters in the error string
            int openIndex = errors[i].IndexOf('(');
            int closeIndex = errors[i].IndexOf(')');
 
            // can't process the message if it has no line counter
            if (openIndex == -1 || closeIndex == -1)
                continue;
 
            // find the error number, then move forward into the message
            int errorIndex = errors[i].IndexOf('X', closeIndex);
            if (errorIndex < 0)
                throw new InvalidContentException(errors[i], input.Identity);
 
            // trim out the data we need to feed the logger
            string fileName = errors[i].Remove(openIndex);
            string lineAndColumn = errors[i].Substring(openIndex + 1, closeIndex - openIndex - 1);
            string description = errors[i].Substring(errorIndex);
 
            // when the file name is not present, the error can be found in the root file
            if (string.IsNullOrEmpty(fileName))
                fileName = input.Identity.SourceFilename;
 
            // ensure that the file data points toward the correct file
            FileInfo fileInfo = new FileInfo(fileName);
            if (!fileInfo.Exists)
            {
                FileInfo parentFile = new FileInfo(input.Identity.SourceFilename);
                fileInfo = new FileInfo(Path.Combine(parentFile.Directory.FullName, fileName));
            }
            fileName = fileInfo.FullName;
 
            // construct the temporary content identity and file the error or warning
            ContentIdentity identity = new ContentIdentity(fileName, input.Identity.SourceTool, lineAndColumn);
            if (errors[i].Contains("warning"))
            {
                description = "A warning was generated when compiling effect.\n" + description;
                context.Logger.LogWarning(string.Empty, identity, description, string.Empty);
            }
            else if (errors[i].Contains("error"))
            {
                // handle the stupid non-conformant error messages
                if (description.StartsWith("ID3DXEffectCompiler") || description.StartsWith("XEffectCompiler"))
                {
                    errorIndex = errors[0].IndexOf('X');
                    int endOfErrorIndex = errors[0].IndexOf(';', errorIndex);
                    description = errors[0].Substring(errorIndex, endOfErrorIndex - errorIndex);
                }
 
                description = "Unable to compile effect.\n" + description;
                throw new InvalidContentException(description, identity);
            }
        }
 
        // if no exceptions were created in the above loop, generate a generic one here
        throw new InvalidContentException(errorsAndWarnings, input.Identity);
    }
}