Developing a New XNA Framework Processor 

This topic describes the process of adding an importer, processor, and serializer to the XNA Framework Content Pipeline to provide support for a new pixel shader game asset type. It is assumed that the reader is familiar with standard processor usage and has read Standard Importers and Processors.

This topic discusses the steps needed to add a new importer, processor, and serializer for compiling HLSL source files into pixel shaders. This new functionality is similar to the EffectImporter and EffectProcessor classes that are already built in to the framework, but it processes individual pixel shaders rather than complete effects.

  • Creating the New Processor Project
  • Storing the Input Data
  • Importing the Pixel Shader Data
  • Compiling the Pixel Shader
  • Processing the Pixel Shader
  • Serializing the Pixel Shader
  • Accessing the Pixel Shader Processor from Your Game
  • Loading the Pixel Shader
  • Using the New Processor in Your Game

Creating the New Processor Project

The content pipeline is part of the build process and separate from your game code. Therefore, a new assembly needs to be created containing the code developed in this topic. Creating this new assembly project is the first step in developing a new processor.

Note

It is assumed that you have an existing game project which you will modify. For the purposes of this example, the game project is called MyGame.

To add the new processor project, load your main game project. From Solution Explorer, right-click the Solution node, point to Add, and then New Project. From this dialog box, select the Windows Game Library template and click OK.

Note

For the purposes of this example, the new processor project is called MyWinGamesLib.

The new project automatically contains a reference to the XNA Framework run-time assemblies. However, this project also needs to reference the design–time content pipeline assembly. To add this assembly, right-click the References node of the new processor project and click Add Reference. From the .NET tab of this dialog box, click Microsoft.Xna.Framework.Content.Pipeline and click OK.

Storing the Input Data

After adding the content pipeline reference, you can start to add functionality to the project with a simple class to hold your input data. For pixel shaders, the input data is a string containing the HLSL code. Add a new C# class to the processor project (called PixelShaderSourceCode), and add the following code to the class definition.

  public PixelShaderSourceCode( string sourceCode )
{
    this.sourceCode = sourceCode;
}

private string sourceCode;

public string SourceCode
{
    get { return sourceCode; }
}

Append the following code to the using list at the beginning of the new source file.

  using Microsoft.Xna.Framework.Content.Pipeline;

Importing the Pixel Shader Data

The next addition to the project is an importer class which reads data from disk into your source type. This class is derived from ContentImporter and implements the Import method. Add a new C# class to the project (called PixelShaderImporter) and replace the existing class definition code with the following:

  [ContentImporter( ".psh", DefaultProcessor = "PixelShaderProcessor" )]
class PixelShaderImporter : ContentImporter<PixelShaderSourceCode>
{
    public override PixelShaderSourceCode Import( string filename, ContentImporterContext context )
    {
        string sourceCode = File.ReadAllText( filename );

        return new PixelShaderSourceCode( sourceCode );
    }
}

The ContentImporter attribute (applied to the PixelShaderImporter class) provides some context for the user interface of XNA Game Studio Express. Since this importer supports files with a .psh extension, XNA Game Studio Express will automatically select the PixelShaderImporter importer when a .psh file is added to the project. In addition, the DefaultProcessor argument specifies the correct processor XNA Game Studio Express selects when a .psh file is added.

Append the following code to the using list at the beginning of the new source file.

  using Microsoft.Xna.Framework.Content.Pipeline;
using System.IO;

Compiling the Pixel Shader

At this point, the new processor project is able to read the pixel shader source code. However, a representation of the compiled result is needed. Because a compiled shader is stored as an array of bytes, the new class will be simple. Add a new C# class to the project (called CompiledPixelShader), and add the following code to the class definition.

  public CompiledPixelShader( byte[] compiledShader )
{
    this.compiledShader = compiledShader;
}

private byte[] compiledShader;

public byte[] CompiledShader
{
    get { return (byte[])compiledShader.Clone(); }
}

Processing the Pixel Shader

In this step, a new class (implementing the processor) is added. This new processor class converts a PixelShaderSourceCode object into a CompiledPixelShader object. Add a new C# class to the project (called PixelShaderProcessor), and replace the existing class definition code with the following code.

  [ContentProcessor]
class PixelShaderProcessor : ContentProcessor<PixelShaderSourceCode, CompiledPixelShader>
{
    public override CompiledPixelShader Process( PixelShaderSourceCode input, ContentProcessorContext context )
    {
        CompiledShader shader = ShaderCompiler.CompileFromSource( input.SourceCode, null, null, 
            CompilerOptions.None, "main", ShaderProfile.PS_2_0, context.TargetPlatform );

        if (!shader.Success)
            throw new InvalidContentException( shader.ErrorsAndWarnings );

        return new CompiledPixelShader( shader.GetShaderCode() );
    }
}

The shader compilation is done with the ShaderCompiler class. This class performs differently depending on the target platform of the game code. Therefore, the processor gets this information from the TargetPlatform property of the ContentProcessorContext argument. If the processor encounters any errors during compilation, an InvalidContentException is thrown. This exception appears in the Error List pane of XNA Game Studio Express.

Append the following code to the using list at the beginning of the new source file.

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

Serializing the Pixel Shader

The final design-time class to implement saves a compiled pixel shader into a binary .xnb file. This new class is implemented by deriving from ContentTypeWriter and implements the Write method. Add a new C# class to the project (called PixelShaderWriter), and replace the existing class definition code with the following code.

  [ContentTypeWriter]
class PixelShaderWriter : ContentTypeWriter<CompiledPixelShader>
{
    protected override void Write( ContentWriter output, CompiledPixelShader value )
    {
        output.Write( value.CompiledShader.Length );
        output.Write( value.CompiledShader );
    }

    public override string GetRuntimeType( TargetPlatform targetPlatform )
    {
        return typeof( PixelShader ).AssemblyQualifiedName;
    }

    public override string GetRuntimeReader( TargetPlatform targetPlatform )
    {
        return "MyWinGamesLib.PixelShaderReader, MyWinGamesLib, Version=1.0.0.0, Culture=neutral";
    }
}

Append the following code to the using list at the beginning of the new source file:

  using Microsoft.Xna.Framework;
using Microsoft.Xna.Framework.Content.Pipeline.Serialization.Compiler;
using Microsoft.Xna.Framework.Graphics;

The GetRuntimeType and GetRuntimeReader methods specify how to load the data into the game using the content pipeline. GetRuntimeReader returns the name of the worker class used to load it. This name is qualified with the name of the assembly in which that class can be found. This is usually the name of your game, so you will need to change the string to match your existing code.

The GetRuntimeType method is needed because the processor saves out from a different type than the one loaded into the game. At the end of the content pipeline, a CompiledPixelShader object is saved but in the game this data needs to be loaded as a PixelShader object. This type remapping is needed because a regular PixelShader object cannot be instantiated during the pipeline build process.

To illustrate the need for remapping type, consider the case of building content for the Xbox 360. Because the pixel shader is compiled into the Xbox 360 format, the Windows graphics drivers are unable to construct a PixelShader object that is formatted for the Xbox 360. The pipeline build process avoids this issue by not using the Windows graphics drivers. This means the content pipeline must use the CompiledPixelShader temporary type rather than the PixelShader type. Because the pipeline type is different than the runtime type, the GetRuntimeType method describes this difference. If the data did not differ between platforms, the design-time and run-time types could be the same, and you would not need to override GetRuntimeType.

Accessing the Pixel Shader Processor from Your Game

At this point, the pipeline code is complete. The next step builds the pipeline project and then uses it from the game. Double-click the Properties node of your game project and click the XNA Content Pipeline Assemblies tab from the Property Designer. Click the Add button, and browse for the new processor assembly DLL. It is commonly found in the ..\bin\Release folder of the new processor project.

It is a good idea to configure the project dependencies to make sure that the pipeline project is always built before the main game project. This isn't strictly necessary, but if you change any of the pipeline code, everything is automatically rebuilt in the correct order. To do this, right-click on your game project and click Project Dependencies. In this dialog box, select the Depends on check box next to the pipeline project and click OK.

At this point, when you add a file with a .psh extension to your game project, the new importer and processor are automatically selected as properties for that asset. When the game is built, the content pipeline uses the custom importer and processor you developed and produces an .xnb file containing the compiled shader code.

Loading the Pixel Shader

However, you need a new class that loads the compiled pixel shader into the game. The content pipeline uses this class to determine how the content must be loaded. Add a new C# class to your game project (called PixelShaderReader), and replace the existing class definition code with the following code.

  class PixelShaderReader : ContentTypeReader<PixelShader>
{
    protected override PixelShader Read(ContentReader input, PixelShader existingInstance)
    {
        int codeSize = input.ReadInt32();
        byte[] shaderCode = input.ReadBytes(codeSize);

        IGraphicsDeviceService graphicsDeviceService = (IGraphicsDeviceService)input.ContentManager.ServiceProvider.GetService(typeof(IGraphicsDeviceService));

        return new PixelShader(graphicsDeviceService.GraphicsDevice, shaderCode);
    }
}

This must be added to the main game project, not the new processor assembly that was recently created. In addition, the reader class must have the same name and namespace as specified by the GetRuntimeReader method of the PixelShaderWriter class.

Append the following code to the using list at the beginning of the new source file.

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

Using the New Processor in Your Game

Once you have successfully modified and compiled your solution, the new compiled pixel shader is accessed using the ContentManager::Load method. This is demonstrated with the following code (this example assumes there is an asset called myShader).

      PixelShader shader = content.Load<PixelShader>("myShader");
    

See Also

XNA Game Studio Express
Extending an XNA Framework Standard Processor