Improve Effect Processor
From XNAWiki
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);
}
}