Click here to Skip to main content
15,881,757 members
Articles / Multimedia / DirectX
Article

Shader Effects in WPF - The basics

Rate me:
Please Sign up or sign in to vote.
5.00/5 (5 votes)
10 Sep 2015CPOL23 min read 35.2K   745   10   4
Create HLSL files, compile them and use them in a WPF appliction. A light Shazzam Shader Editor tool for Visual Studio

Image 1

Introduction

If you have used Effects in WPF before, you have inadvertently also (hopefully) used GPU accelerated functions in WPF. These affects are called ShaderEffects and were introduced with the relese of .NET3.5SP1 (The old class BitmapEffects was obsolite from .NET 4.0, so don't use them). Prior to that, the Effects were done in the CPU, causing serious performance issues on computer intensive effects, like blur. Now WPF offers Effects that have all the calculation done in the GPU, so visual effects does not affect your CPU performance. The downside is that WPF only offer two effects in the class Media.Effects (the old BitmapEffects are obsolete, and should NEVER be used):

  • Blur Effect
  • DropShadowEffect

WPF also offers something quite interesting, namely a ShaderEffect class, and this is the class you can use ito communicate with custom created ShaderEffects.  These effect files, *.ps, are generated by the DirectX compiler from *.fx (HLSL) files. This article will go these steps:

  • Writing you own HLSL (.fx) file
  • How to compile the file into .ps (Pixel shader byte code)
  • Set up a class in either VB.NET or C# in WPF to communicate with the .ps file
  • Apply the effect to any WPF UIElement

If you read through this article my hope is that you will gain the ability to create your own custom pixel shader file and use it in your own WPF application. I should mention that most of the Shader files on the internet uses the shaders in the Silverlight language, but this article will only focus on WPF usages, but will contain material from programs original written using Silverlight and shaders.

This article relies heavily on the knowledge learned from reading the source code for Walt Ritscher for his program Shazzam Tool, and with the basis in his code I made some changes and improvements as his tool didn't run properly for me. Since the tool first was published there has also been some changes, especially on the Windows operation system, as the DirectX compiler and DLL's are now included by default. 

If you have a bought version of Visual Studio 2013 or above you could use the built in graphics debugger instead. This article is mainly written for Visual Studio Express users, allowing user to write fx files with Visual Studio highlight the HLSL, although you might pick up some hints and tips on the usage of pixel shaders. 

Generating HLSL files

The fx files or HLSL (an abbreviation for High Level Shading Language) files as they also are called. The files are programmed using a C like language, with a very limited number of functions that you can use. This makes it rather fast to learn and easy to use, but it do have some quirky details.

The fx files that are used by the ShaderEffect class in WPF always begin with a series of registered values that are stored in the GPU which you can communicate with using the ShaderEffect class (However, if you use the lib file in C++ you can access all of them). You can now see that the number of inputs that are availiable are fairly limited, there is also a maximum on how many you can use. 

https://msdn.microsoft.com/en-us/library/hh315751(v=vs.110).aspx

In the PixelShaders used in WPF this line of code below has to be included in the fx file. Its were the Image itself is stored in the GPU register, and the pointer to where it can be reached.

C++
sampler2D Input : register(S0);

This line of code actually specifies that the sampler2D value Input should be stored in the GPU register. It also specify in what register type it is in (S) but there are many more available, but the WPF application written in C# and VB can only communicate with the S and C register values. 

In the Pixel Shader you work on one Pixel at the time, and in the main the function call (entry point of the ps file), you get as an input the current pixel location in x and y coordinates. Well that not quite true, you do get the coordinates but they are between 0 to 1 (0,0 are upper left corner), that is why its a float2 variable.

C++
float4 main(float2 uv : TEXCOORD) : COLOR
{
 ...
}

you get the 4 values for the color of the given pixel by using the tex2 function, and the color is stored in a 4 item vector with float (again with values form 0  to 1, instead of the normal byte from 0 to 255) precision using the float4 deliberative field:

C++
float4 color = tex2D( Input, uv );

And to, let's say, invert the colors of the image, we simply add the line:

C++
float4 inverted_color = 1 - color ;

And to avoid the alpha (transparency factor) to be changed (there are more ways of getting the value), and  to return the inverted color.:   

inverted_color.a = 1;
return inverted_color;

This knowledge allows you to create all the effects that is calculated by one single pixel at the time. But most of the interesting stuff on images involves getting the neighboring pixels as well. Effects like a Gaussian filer, Sobel Filter etc all require calculations being done of several Neighboring pixels, so we need this function (the edge detector use a version of the derivative, which is actually available on the GPU as a function):

C++
float4 GetNeighborPixel(float2 uv, float2 pixelOffset)
{
    float x = pixelOffset.x / InputSize.x;
    float y = pixelOffset.y / InputSize.y;
    float2 NeighborPoint = { uv.x + x, uv.y + y };        
    return  tex2D(input, NeighborPoint);
};

There is one thing that is special about functions in the fx files, that is; each function must be declared before it is used:

C++
float TestFunc(float n)
{
     // ...
}

float main()
{
    // ...
}

or have a pointer declared before its used:

C++
float TestFunc(float);

float main()
{
    // ...
}

float TestFunc(float n)
{
    // ...
}

Assuming all is well, and you have defined the function properly, you should realize that you need the actual size of the picture. Since all the coordinates are on double precision format from 0 to 1, a pixel step is in double format.

C++
/// <summary>Explain the purpose of this variable.</summary>
/// <minValue>1, 1</minValue>
/// <maxValue>1024, 1024</maxValue>
/// <defaultValue>599, 124</defaultValue>
float2 InputSize : register(C0);  

In reality, you should now be able to write any PixelShader you want, just glace over the available functions from the documentation. Some cool example, that are pretty straight forward to follow, by Tamir Khason can be seen in these two links below:

There are also some old legacy tools for creating cool effects that might be worth taking a look at:

For more advanced shaders you might even want to use functions and class like structures in order to effectively program it. While the functions used are very useful for complex effects, you might need to compile the shader with a higher version than the standard version of 2.0 or 3.0.

Compiling HLSL files

Before I start to explain how to compile the fx files, I'd just want to say that this is only really needed if you have the Express editions of Visual Studio (of either 2012,  2013 or 2015), in the version you buy, you get to compiler as a resource. In the VS 2010 there is also a tool for compiling the fx file as a resource using a codeplex plugin. However, I think it would be useful to read the section anyway. I should also mention that there is yet another option to compile the fx file in VS 2010 and 2008 by installing a add-in to Visual Studio 

Using the fxc to compile

In previous Windows versions you needed to install the DirectX SDK in order to get the fxc.exe file that is needed to compile the fx files into machine codeed ps. From Windows 8 the SDK is deployed as standard, saving you several hundred MB download (although the fxc file, the only one you really need, is only about 250KB.).

To compile the fx file most seem to do a quick compile using the build event in VS 2013. The compilation (on Windows 8.1) would be the following in the post build command:

"C:\Program Files\Windows Kits\8.1\bin\x86\fxc.exe" /T ps_2_0 /E main /Fo"Watercolor.ps" "Watercolor.fx"

But as it turned out, the build event, isn't availiable in VB.NET! Well, that wouldn't have to be a problem, I would just have to build it in the Application.XAML file. However you must remember to implement the namespace in your application to accssess classes you have there:

VB.NET
Imports WpfAdventuresInPixelShader

Class Application

    ' Application-level events, such as Startup, Exit, and DispatcherUnhandledException
    ' can be handled in this file.

    Public Sub New()
      ' My code to build the fx files goes here...  
    End Sub  

End Class

Having this hurdle made me go into the details of how an fx file is compiled a lot earlier than I thought, and I quickly found out one critical issue. If you open the fx file in VS (any year really), it stores the text in UTM-8 by default. The funny, or not so funny depending one once perspective is that it wont compile it if it is not in the notepad ANSI code or the ASCII code. This was actually so annoying that I decided to open the file and store it using the ASCII encoding:

VB.NET
' Making sure the fx files are stored in an encoding
' that fxc understands. (It does not understand the default Visual studio encoding!)
For Each file As String In directoryEntries

    'Open the .fx file
    Dim OpenFxFileInTextFormat() As String = IO.File.ReadAllLines(file)

    'Makeign sure its stored in a format that the fxc compiler understands
    IO.File.WriteAllLines(file, OpenFxFileInTextFormat, Text.Encoding.ASCII)
Next

This little code snippet enabled me to write and edit files directly in the VS 2013 and still use the small fxc.exe compiler (There are other ways of achieving this by importing a DLL file to compile it in code, more on that later.). There is one more thing that you could do. If you run this code the VS 2013 compiler will detect changes in the file and will ask you if you want to reload it. So to save yourself some trouble, in the top menu go to:

TOOLS->OPTIONS...

A dialog will appear where you navigate to 

Environment->Documents

Tick off the Auto-Load changes, if saved here:

Image 2

I assume that you will have all the uncompiled fx files added as a resource in a folder, and that you want the compiled files to be contained in a folder where your exe file is compiled at. So I did the following:

VB.NET
'Get the folder where the exe file is currently compiled
        Dim ExeFileDirectory As String = AppDomain.CurrentDomain.BaseDirectory

        'The name of the folder that will be opened or created at the exe folder location
        Dim FolderNameForTheCompiledPsFiles As String = "ShaderFiles"

        ' Create the Directory were the shader files will be
        Dim FolderWithCompiledShaderFiles As String = ExeFileDirectory & FolderNameForTheCompiledPsFiles

        ' If it dosn't exitst creat it
        If (Not IO.Directory.Exists(FolderWithCompiledShaderFiles)) Then
            IO.Directory.CreateDirectory(FolderWithCompiledShaderFiles)
        End If

        'Find the resource folder where the uncompiled fx filer are
        Dim ShaderSourceFiles As String = IO.Path.Combine(GetParentDirectory(ExeFileDirectory, 2), "ShaderSourceFiles")

        ' Get all the uncopiled files in the folder
        Dim directoryEntries As String() = System.IO.Directory.GetFileSystemEntries(ShaderSourceFiles, "*.fx")

In the normal debug mode, the exe file is stored in a folder c:\ ... YouProject\bin\debug\YourExeFile.Exe so you need to back up two directories to get to the folder where the resource folder is located at. So I mad a short little function:

VB.NET
Private Function GetParentDirectory(ByVal FolderName As String, Optional ByVal ParentNumber As Integer = 1) As String

    If ParentNumber = 0 Then
        Return FolderName
    End If

    Dim result As IO.DirectoryInfo
    Dim CurrentFolderName As String = FolderName
    For i As Integer = 1 To ParentNumber + 1
        result = IO.Directory.GetParent(CurrentFolderName)
        CurrentFolderName = result.FullName
    Next
    Return CurrentFolderName
End Function

Now that we got the desired folders, one folder where we get the fx files and another where we will place the compiled files in. I decided to make two classes, one that hold the specific fx file properties and compilation settings for the file, and one that would do all the compiler stuff. Below is an exert from the HLSLFileHelperClass:

VB.NET
Public Class HLSLFileHelperClass

    ...

    Private pFileNameWithoutExtension As String
    Public Property FileNameWithoutExtension() As String
        Get
            Return pFileNameWithoutExtension
        End Get
        Set(ByVal value As String)
            pFileNameWithoutExtension = value
        End Set
    End Property

    ...

    Private pHLSLEntryPoint As String = "main"
    Public Property HLSLEntryPoint() As String
        Get
            Return pHLSLEntryPoint
        End Get
        Set(ByVal value As String)
            pHLSLEntryPoint = value
        End Set
    End Property

    Private pShaderCompilerVersion As ShaderVersion = ShaderVersion.ps_3_0
    Public Property ShaderCompilerVersion() As ShaderVersion
        Get
            Return pShaderCompilerVersion
        End Get
        Set(ByVal value As ShaderVersion)
            pShaderCompilerVersion = value
        End Set
    End Property

    Public Enum ShaderVersion
        ps_2_0
        ps_3_0
        ps_4_0
        ps_4_1
        ps_5_0
    End Enum


End Class

The Compiler helper would then hold a list of HLSLFileHelperClass that was going to be compiled, and the actual methods for the compiling. The compiler is done in a hidden cmd.exe window:

VB.NET
Sub Compile()
    Dim p As New Process()
    Dim info As New ProcessStartInfo()
    info.FileName = "cmd.exe"
    info.CreateNoWindow = True
    info.WindowStyle = ProcessWindowStyle.Hidden
    info.RedirectStandardInput = True
    info.UseShellExecute = False
    info.RedirectStandardOutput = True
    info.RedirectStandardError = True

    p.StartInfo = info

    p.Start()
    p.BeginOutputReadLine()
    p.BeginErrorReadLine()

    AddHandler p.OutputDataReceived, AddressOf NormalOutputHandler
    AddHandler p.ErrorDataReceived, AddressOf ErrorAndWarningOutputHandler

    Dim sw As IO.StreamWriter = p.StandardInput

    Dim result As String = ""
    For Each File As HLSLFileHelperClass In FilesToCompile
        CompileFile(sw, File)
    Next

    p.WaitForExit(1000)
End Sub

The error, warnings and compile completed messages are collected by the two handlers, and the information is stored in a String property that implements INotifiedChange interface.The CompileFile class just connects the necessary information and runs the command in cmd:

VB.NET
Sub CompileFile(ByVal sw As IO.StreamWriter, ByVal sender As HLSLFileHelperClass)
    If sw.BaseStream.CanWrite Then
        Dim s As String = """" & FXCFileLocation & """ /T " & sender.ShaderCompilerVersion.ToString & " /E " & sender.HLSLEntryPoint & " /Fo""" & sender.GetCompiledFileFullName & """ """ & sender.GetSourceFileFullName & """"
        sw.WriteLine(s)
    End If
End Sub

The different attributes in the command line is explained in the table below:

Attribute Description
/T Shader profile that will be used in compiling the file. ps_2_0 is simply Shader 2.0.
/E the entery function in the shader, like the console programs in the old days started with void main
/Fo the name and location of the file that is produced by the compiler (File Out)

These three are simply the must have to compile the file, but there are many more settings available.

I now have the equivalent to the build event, that is actually a bit better than it. It will run the compiler before the MainWindow starts, and you can bind the compiler results (either just have the error and warnings of all the compiled files, or the complete build store that will tell you if the file was compiled or not.) Lets face it, if you are a normal person, you will need the error massage sooner or later. The error (or warning!) will be given with a error code example: X3123, and the text that will follow is the corresponding to the list here. And lastly, it will give you the line number where the error or warning was thrown.

Using DLL's to compile the fx file

The fxc file compiler is not that bad, but it requires quite a lot of command line code and you would have to convert the file to ANSI format before you can compile the files. Both of these nusiances can be avoided if you instead compile the fx with an unmanaged (c++ compiled) dll call. 

Before I go one here I'm going to have to explain something first. The actual dll (32 bit system that is) file that has this function is located (on Windows 8.0, 8.1 and 10, just exchange the 10 with 8.1 or 8.1 to get the appropriate path for your version) here:

C:\Program Files\Windows Kits\10\bin\x86

If you have a keen eye, you will see that this is the same place that the fxc.exe file is located. However, if you wish to include a DLL in your program make sure to use the files that are meant for redistribution and are found here (for 32 bit again):

C:\Program Files\Windows Kits\10\Redist\D3D\x86

There could be differences on the two files, as one might be tailor made to fit your hardware. If you have any other versions of Windows you might want to have a look at this Codeproject article. It explains various ways of adding the DLL as a resource in a Visual studio project:

I decided to go an even easier way of to specify what DLL to use, as suggested by Jonathan Swift by employing LoadLibrary from the kernel32.dll:

VB.NET
Imports System.Runtime.InteropServices

Module NativeDLLMethods
    <DllImport("kernel32.dll")>
    Public Function LoadLibrary(dllToLoad As String) As IntPtr
    End Function

    <DllImport("kernel32.dll")>
    Public Function GetProcAddress(hModule As IntPtr, procedureName As String) As IntPtr
    End Function

    <DllImport("kernel32.dll")>
    Public Function FreeLibrary(hModule As IntPtr) As Boolean
    End Function
End Module

You can now load the DLL into memory, we will now take a closer look at how to implement the unmanaged code in C# or VB.NET. In the documentation the parameters for loading the DLL is given:

C++
HRESULT WINAPI D3DCompileFromFile(
  in      LPCWSTR pFileName,
  in_opt  const D3D_SHADER_MACRO pDefines,
  in_opt  ID3DInclude pInclude,
  in      LPCSTR pEntrypoint,
  in      LPCSTR pTarget,
  in      UINT Flags1,
  in      UINT Flags2,
  out     ID3DBlob ppCode,
  out_opt ID3DBlob ppErrorMsgs
);

When you define the entry point of the DLL, be sure to read the input types carefully, the strings especially. They have several different types of marshal. The Definition that finally worked looked like this:

VB.NET
<DllImport("d3dcompiler_47.dll", CharSet:=CharSet.Auto)> _
Public Function D3DCompileFromFile(<MarshalAs(UnmanagedType.LPWStr)> pFilename As String,
                                                      pDefines As IntPtr,
                                                      pInclude As IntPtr,
                                                      <MarshalAs(UnmanagedType.LPStr)> pEntrypoint As String,
                                                      <MarshalAs(UnmanagedType.LPStr)> pTarget As String,
                                                      flags1 As Integer,
                                                      flags2 As Integer,
                                                      ByRef ppCode As ID3DBlob,
                                                      ByRef ppErrorMsgs As ID3DBlob) As Integer

The two last elements , one that returns the compiled code and another that returns the possible error messages. It is given the name ID3DBlob, and its defined in the documentation. You also need the PreserveSig in this section, otherwise it wont work (The documentation).

VB.NET
<Guid("8BA5FB08-5195-40e2-AC58-0D989C3A0102"), InterfaceType(ComInterfaceType.InterfaceIsIUnknown)> _
Public Interface ID3DBlob
    <PreserveSig> _
    Function GetBufferPointer() As IntPtr
    <PreserveSig> _
    Function GetBufferSize() As Integer
End Interface

We now have all the code necessary for compiling the fx files with the DLL file instead of the tedious and complicated use of the fxc.exe file. The code blocks above that defined the dll function could be used as it is if you have Windows 8.0 or higher, as the location is known to the system.

To make sure that it will work on older versions of Windows, we need to specify the location and file we want to use. But to use the LoadLibrary function you need to define a delegate (that functions as a C++ style a pointer in this case) to define the function call you want to call within the DLL:

VB.NET
Imports System.Runtime.InteropServices

Module DLLForLoadLibraryUse

    Public Delegate Function D3DCompileFromFile(<MarshalAs(UnmanagedType.LPWStr)> pFilename As String,
                                                          pDefines As IntPtr,
                                                          pInclude As IntPtr,
                                                          <MarshalAs(UnmanagedType.LPStr)> pEntrypoint As String,
                                                          <MarshalAs(UnmanagedType.LPStr)> pTarget As String,
                                                          flags1 As Integer,
                                                          flags2 As Integer,
                                                          ByRef ppCode As ID3DBlob,
                                                          ByRef ppErrorMsgs As ID3DBlob) As Integer

    Public Delegate Function D3DCompile(<MarshalAs(UnmanagedType.LPStr)> pSrcData As String,
                                                         SrcDataSize As Integer,
                                                        <MarshalAs(UnmanagedType.LPStr)> pSourceName As String,
                                                         pDefines As IntPtr,
                                                         pInclude As IntPtr,
                                                        <MarshalAs(UnmanagedType.LPStr)> pEntrypoint As String,
                                                        <MarshalAs(UnmanagedType.LPStr)> pTarget As String,
                                                         flags1 As Integer,
                                                         flags2 As Integer,
                                                        ByRef ppCode As ID3DBlob,
                                                        ByRef ppErrorMsgs As ID3DBlob) As Integer

End Module

In the origianl post from Jonathan swift he recommended to use this calling convention line above the delegate:

VB.NET
<UnmanagedFunctionPointer(CallingConvention.Cdecl)>

It is only nessecery to use that if the function call contains a varargs, in fact; if you include this in the compiler for fx files it would give you an error, saying that the number of parameters in the delegate mismatch the numbers in the DLL function call. In fact Martin Costello suggest using the function call:

VB.NET
<UnmanagedFunctionPointer(CallingConvention.StdCall)>

It is not nessesary to include this line, as it is the default calling conavention. 

Since the DLL only needs to be loaded once, I decided to use shared variables to hold pointers to the position in memory. 

VB.NET
Public Shared FxDllCompiler As IntPtr
Public Shared pAddressOfFxByteCompiler As IntPtr
Public Shared pAddressOfFxBFileompiler As IntPtr
Public Shared pFxByteStreamCompilation As DLLForLoadLibraryUse.D3DCompile
Public Shared pFxFileCompilation As DLLForLoadLibraryUse.D3DCompileFromFile

Public Shared DllFilesLocation As String

Public Shared Sub FreeDlls()
    Dim result As Boolean = NativeDLLMethods.FreeLibrary(FxDllCompiler)
End Sub

Public Shared Sub SetUpDlls()

    If IntPtr.Size = 4 Then
        FxDllCompiler = NativeDLLMethods.LoadLibrary(IO.Path.Combine(DllFilesLocation, "d3dcompiler_47_32bit.dll"))
    Else
        FxDllCompiler = NativeDLLMethods.LoadLibrary(IO.Path.Combine(DllFilesLocation, "d3dcompiler_47_64bit.dll"))
    End If

    If FxDllCompiler = IntPtr.Zero Then
        MessageBox.Show("Could not load the DLL file")
    End If

    pAddressOfFxByteCompiler = NativeDLLMethods.GetProcAddress(FxDllCompiler, "D3DCompile")
    If pAddressOfFxByteCompiler = IntPtr.Zero Then
        MessageBox.Show("Could not locate the function D3DCompile in the DLL")
    End If

    pAddressOfFxBFileompiler = NativeDLLMethods.GetProcAddress(FxDllCompiler, "D3DCompileFromFile")
    If pAddressOfFxBFileompiler = IntPtr.Zero Then
        MessageBox.Show("Could not locate the function D3DCompileFromFile in the DLL")
    End If

    pFxByteStreamCompilation = Marshal.GetDelegateForFunctionPointer(pAddressOfFxByteCompiler, GetType(DLLForLoadLibraryUse.D3DCompile))
    pFxFileCompilation = Marshal.GetDelegateForFunctionPointer(pAddressOfFxBFileompiler, GetType(DLLForLoadLibraryUse.D3DCompileFromFile))
End Sub

All the entries are stored as shared members, as I only need to load the functions into memory once, and use this as an entry point for further calls. I can also call FreeLibary after I have finished calling the method, but I don't, so the DLL's get released from memory once the application is terminated. You should be careful not to call FreeLibrary if you arn't finished using the function, as it will throw an error if you try to relese it after each call. Mike Stall has created a wrapper for unmanaged calls to the kernel32.dll here that you could use instead.

The Entry point of the DLL's are kept in a module, so it has only one point to start it from. The compile code for a file is thereby given as:

VB.NET
Public Sub Compile(ByVal File As HLSLFileHelperClass)
    Dim pFilename As String = File.GetSourceFileFullName
    Dim pDefines As IntPtr = IntPtr.Zero
    Dim pInclude As IntPtr = IntPtr.Zero

    Dim pEntrypoint As String = File.HLSLEntryPoint
    Dim pTarget As String = File.ShaderCompilerVersion.ToString

    Dim flags1 As Integer = 0
    Dim flags2 As Integer = 0
    Dim ppCode As DLLEntryPointModule.ID3DBlob = Nothing
    Dim ppErrorMsgs As DLLEntryPointModule.ID3DBlob = Nothing

    Dim CompileResult As Integer = 0

    CompileResult = DLLEntryPointModule.D3DCompileFromFile(pFilename,
                                       pDefines,
                                       pInclude,
                                       pEntrypoint,
                                       pTarget,
                                       flags1,
                                       flags2,
                                       ppCode,
                                       ppErrorMsgs)

    If CompileResult <> 0 Then
        Dim errors As IntPtr = ppErrorMsgs.GetBufferPointer()
        Dim size As Integer = ppErrorMsgs.GetBufferSize()

        ErrorText = Marshal.PtrToStringAnsi(errors)

        IsCompiled = False
    Else

        IsCompiled = True
        Dim psPath = File.GetCompiledFileFullName
        Dim pCompiledPs As IntPtr = ppCode.GetBufferPointer()
        Dim compiledPsSize As Integer = ppCode.GetBufferSize()

        Dim compiledPs = New Byte(compiledPsSize - 1) {}
        Marshal.Copy(pCompiledPs, compiledPs, 0, compiledPs.Length)
        Using psFile = IO.File.Open(psPath, FileMode.Create, FileAccess.Write)
            psFile.Write(compiledPs, 0, compiledPs.Length)
        End Using
    End If

    If ppCode IsNot Nothing Then
        Marshal.ReleaseComObject(ppCode)
    End If
    ppCode = Nothing

    If ppErrorMsgs IsNot Nothing Then
        Marshal.ReleaseComObject(ppErrorMsgs)
    End If
    ppErrorMsgs = Nothing
End Sub

There is also one more advantage with compiling from this DLL, the files does not have to be in ANSI format, so you can edit the files directly in Visual Studio and you don't have to worry about the file format any more. 

C++
HRESULT WINAPI D3DCompile(
  in      LPCVOID pSrcData,
  in      SIZE_T SrcDataSize,
  in_opt  LPCSTR pSourceName,
  in_opt  const D3D_SHADER_MACRO pDefines,
  in_opt  ID3DInclude pInclude,
  in      LPCSTR pEntrypoint,
  in      LPCSTR pTarget,
  in      UINT Flags1,
  in      UINT Flags2,
  out     ID3DBlob ppCode,
  out_opt ID3DBlob ppErrorMsgs
);

The code is nearly exactly the same as CompileFromFile code, the only difference is how the data is read in:

VB.NET
Dim s As String = IO.File.ReadAllText(file.GetSourceFileFullName)

Dim pSrcData As String = s
Dim SrcDataSize As Integer = s.Length

Interact with *.ps files in VB.NET/C#

The simplest way that you have for a class to interacts with the values in the ps file, is to inherit the ShaderEffect class. When you use the ShaderEffect class in WPF you only get to communicate with two types of registers (S and C). However they can hold a number of different types, and they don't exactly correspond, in name type, that is. The list of type in HLSL and the correspondent values in .NET are listed below (from this site):

.NET type HLSL type
System.Boolean (C# keyword bool) Not Available
System.Int32 (C# keyword int) Not Available
System.Double (C# keyword double) float
System.Single (C# keyword float) float
System.Windows.Size float2
System.Windows.Point float2
System.Windows.Vector float2
System.Windows.Media.Media3D.Point3D float3
System.Windows.Media.Media3D.Vector3D float3
System.Windows.Media.Media3D.Point4D float4
System.Windows.Media.Color float4
You need to set up a dependency property for all the values that you want to interact with. There is however one value that you would always have to set up if you want to use the ShaderEffect class, and that is the Input property:
 
VB.NET
Inherits ShaderEffect

Public Shared ReadOnly InputProperty As DependencyProperty = ShaderEffect.RegisterPixelShaderSamplerProperty("Input", GetType(ShaderEffectBase), 0, SamplingMode.NearestNeighbor)

Protected Sub New()
    Me.UpdateShaderValue(InputProperty)
End Sub

''' <summary>
''' Gets, Sets the effect input.
''' </summary>
Public Property Input() As Brush
    Get
        Return TryCast(Me.GetValue(InputProperty), Brush)
    End Get
    Set(value As Brush)
        Me.SetValue(InputProperty, value)
    End Set
End Property

It has a custom build dependency property called RegisterPixelShaderSamplerProperty, which connects to values in the ps file named S0, S1 ... Sn where n is the index that is given in the Register of the DependencyProperty, 0 in the above code. If you have more than one Sampler property (ImageBrushes) a C1 will look like the one blow:

VB.NET
Public Shared ReadOnly Input2Property As DependencyProperty = ShaderEffect.RegisterPixelShaderSamplerProperty("Input2", GetType(BlendTwoPicturesEffect), 1, SamplingMode.NearestNeighbor)

Public Property Input2() As Brush
    Get
        Return TryCast(Me.GetValue(Input2Property), Brush)
    End Get
    Set(value As Brush)
        Me.SetValue(Input2Property, value)
    End Set
End Property

In the Effect is corresponds to the ImageBrush of the object that is going to be treated by this custom effect class. 

The second object type that cane be treated by the ShaderEffect class is the Cn values, given below as it communicates with the value C0 in the ps file (of float format):

VB.NET
Public Shared MixInAmountProperty As DependencyProperty = DependencyProperty.Register("MixInAmount", GetType(Double), GetType(BlendTwoPicturesEffect),
                New PropertyMetadata(0.5f, PixelShaderConstantCallback(0)))

Public Property MixInAmount As Double
    Get
        Return DirectCast(Me.GetValue(MixInAmountProperty), Double)
    End Get
    Set(value As Double)
        Me.SetValue(MixInAmountProperty, value)
    End Set
End Property
Be sure to give the default value (first value) in the PropertyMetaData a proper format, otherwise it wont recognize it as a float number and give you an error. I typically got this when I had 1 as the default value. I had to type 1.0f or 1.0d to make sure it got recognized.
 
The only thing that is missing to have a complete usable custom shader is the constructor:
VB.NET
Sub New()
    Dim s As String = AppDomain.CurrentDomain.BaseDirectory
    PixelShader = New PixelShader With {.UriSource = New Uri(s & "\ShaderFiles\BlendTwoPictures.ps")}
    Me.UpdateShaderValue(Input2Property)
    Me.UpdateShaderValue(MixInAmountProperty)
End Sub
It basically loading the ps file into the PixelShader, and sends a notification to update the shader effects with the new values. If you ever have the need to update a shader, that is actually possible if you add the code blow to the constructor:
VB.NET
Public Sub New()
     ...

    AddHandler CompositionTarget.Rendering, AddressOf CompositionTarget_Rendering
End Sub
This will start the function CompositionTarger_Rendering each time the render gets updated on the parent control. You can even adjust the time steps with a short addition of code in the function. This implementation will run once each second (give that the render updates more than once each second):
VB.NET
Dim LastTiumeRendered As Double = 0
Dim RenderPeriodInMS As Double = 1000
Private Sub CompositionTarget_Rendering(sender As Object, e As EventArgs)
    Dim rargs As RenderingEventArgs = DirectCast(e, RenderingEventArgs)
    If ((rargs.RenderingTime.TotalMilliseconds - LastTiumeRendered) > RenderPeriodInMS) Then
        ...
        LastTiumeRendered = rargs.RenderingTime.TotalMilliseconds
    End If
End Sub
The program made by Rene Schulte uses this approch together with a WritableBitmap to store some random numbers. I used a BitmapSource directly instead fro convinience.
 
And that is really all you need to know to implement shaders into a WPF application. In fact you might even think that it should be possible to generate the base code for any class given that you had the source code for the ps file (usually in fx format).  It is a fairly limited number of values that needs to be converted into WPF code, and luckily for me, Walt Ritscher has already done so in his Shazzam Shader Editor. Unfortunatly it did not compile for me, as it is dependent on a dll that is not included in the download. So I decided to strip out the needed bits and implement the code generator in VB.NET using the CodeDome from Shazzam.
 
His implementation is really a tree stage program, first he uses RegEx to find the type of values, the Default value and the range (if it is given) in the fx files. Secondly he stores the found properties in a new class, and from this he generates the code using CodeDom. The RegEx and the storing class is really straigth forward, at least if you use a handy tool like Expresso or similar.  My problem was that I really coun't find a good explanation for the CodeDome, so I thought I go through the implementation in some detail.
 

A simple walktrough of CodeDom 

CodeDom is a great tool for generation code, and if you avoid using expressions that could only be found in C# or only in VB.NET you have a generic code generating tool without having to resort to a code converter, and that's pretty neat. So, I'll start off from the point that is similar to both code languages in CodeDom. We then start off with the instance CodeCompileUnit:
VB.NET
Dim codeGraph As New CodeCompileUnit()
Having created the blueprint for the class, we usually need to import some namespaces to our class prior to the actual namespace our class will live in, and wrap it in a function:
VB.NET
Private Function AssignNamespacesToGraph(codeGraph As CodeCompileUnit, namespaceName As String) As CodeNamespace
    ' Add imports to the global (unnamed) namespace.
    Dim globalNamespace As New CodeNamespace()
    globalNamespace.[Imports].AddRange({New CodeNamespaceImport("System"),
                                        New CodeNamespaceImport("System.Windows"),
                                        New CodeNamespaceImport("System.Windows.Media"),
                                        New CodeNamespaceImport("System.Windows.Media.Effects"),
                                        New CodeNamespaceImport("System.Windows.Media.Media3D")})

    codeGraph.Namespaces.Add(globalNamespace)

    ' Create a named namespace.
    Dim ns As New CodeNamespace(namespaceName)
    codeGraph.Namespaces.Add(ns)
    Return ns
End Function

The next step is to actually declare the class with the name that we will use:

VB.NET
Dim shader As New CodeTypeDeclaration() With { _
      .Name = shaderModel.GeneratedClassName
  }

The class now needs to inherit the ShaderEffect class, and that is done by adding it to the the BaseTypes:

VB.NET
shader.BaseTypes.Add(New CodeTypeReference("ShaderEffect"))

If you wanted to add an interface instead you would do it in exactly the same method:

VB.NET
Dim iequatable As New CodeTypeReference("IEquatable", New CodeTypeReference(shader.Name))
shader.BaseTypes.Add(iequatable)

Just for the completeness you can implement the INotifiedChange logic as done in the following example:

VB.NET
Dim myCodeTypeDecl As New CodeTypeDeclaration() With { _
       .Name = "MyClass"
   }
    myCodeTypeDecl.BaseTypes.Add(GetType(System.ComponentModel.INotifyPropertyChanged))

    Dim myEvent As New CodeMemberEvent()
    With myEvent
        .Name = "PropertyChanged"
        .Type = New CodeTypeReference(GetType(System.ComponentModel.PropertyChangedEventHandler))
        .Attributes = MemberAttributes.Public Or MemberAttributes.Final
        .ImplementationTypes.Add(GetType(System.ComponentModel.INotifyPropertyChanged))
    End With

    myCodeTypeDecl.Members.Add(myEvent)

    Dim myMethod As New CodeMemberMethod

    With myMethod
        .Name = "OnPropertyChanged"
        .Parameters.Add(New CodeParameterDeclarationExpression(GetType(String), "pPropName"))
        .ReturnType = New CodeTypeReference(GetType(Void))
        .Statements.Add(New CodeExpressionStatement(
                        New CodeDelegateInvokeExpression(
                            New CodeEventReferenceExpression(
                                New CodeThisReferenceExpression(), "PropertyChanged"),
                            New CodeExpression() {
                                New CodeThisReferenceExpression(),
                                New CodeObjectCreateExpression(GetType(System.ComponentModel.PropertyChangedEventArgs),
                                                               New CodeArgumentReferenceExpression("pPropName"))})))

        .Attributes = MemberAttributes.FamilyOrAssembly
    End With

    myCodeTypeDecl.Members.Add(myMethod)

    Dim myProperty As New CodeMemberProperty

    With myProperty
        .Name = "fldItemNr"
        .Attributes = MemberAttributes.Public Or MemberAttributes.Final
        .Type = New CodeTypeReference(GetType(String))
        .SetStatements.Add(New CodeAssignStatement(New CodeVariableReferenceExpression("m_fldItemNr"), New CodePropertySetValueReferenceExpression))
        .SetStatements.Add(New CodeExpressionStatement(New CodeMethodInvokeExpression(New CodeMethodReferenceExpression(New CodeThisReferenceExpression(), "OnPropertyChanged"), New CodeExpression() {New CodePrimitiveExpression("fldItemNr")})))
        .GetStatements.Add(New CodeMethodReturnStatement(New CodeVariableReferenceExpression("m_fldItemNr")))
    End With

    myCodeTypeDecl.Members.Add(myProperty)

Back to the construction of the ps file ShaderEffect wrapper. We also need to implement the ps file in the constructor of the class, so we add logic for the creation of public constructor:

VB.NET
Dim constructor As New CodeConstructor() With { _
            .Attributes = MemberAttributes.[Public]
       }

We also need to create a relative Uri path to the location of the ps file, and set this Uri path to the PixelShader that is inherited from the ShaderEffect class:

VB.NET
Dim shaderRelativeUri As String = [String].Format("/{0};component/{1}.ps", shaderModel.GeneratedNamespace, shaderModel.GeneratedClassName)

Dim CreateUri As New CodeObjectCreateExpression
CreateUri.CreateType = New CodeTypeReference("Uri")
CreateUri.Parameters.AddRange({New CodePrimitiveExpression(shaderRelativeUri), New CodeFieldReferenceExpression(New CodeTypeReferenceExpression("UriKind"), "Relative")})

Dim ConnectUriSource As New CodeAssignStatement With {
        .Left = New CodeFieldReferenceExpression(New CodeThisReferenceExpression, "PixelShader.UriSource"),
        .Right = CreateUri}

Then we need to create a new instance of a PixelShader and add the code above to the constructor, and we also add an empty line at the end for some space:

VB.NET
constructor.Statements.AddRange({New CodeAssignStatement() With {.Left = New CodePropertyReferenceExpression(New CodeThisReferenceExpression(), "PixelShader"),
                                                                 .Right = New CodeObjectCreateExpression(New CodeTypeReference("PixelShader"))},
                                 ConnectUriSource,
                                 New CodeSnippetStatement("")})

In the constructor we also need to update the shader values by the command:

VB.NET
Dim result As New CodeMethodInvokeExpression() With { _
     .Method = New CodeMethodReferenceExpression(New CodeThisReferenceExpression(), "UpdateShaderValue")
}

result.Parameters.Add(New CodeVariableReferenceExpression(propertyName & "Property"))

This is done in a function as we need to do this for all the properties found in the ps file.

The creation of DependencyProperties is done in a function, please not that it only supports values of Cn as it communicates through the PixelShaderConstantCallback

VB.NET
Private Function CreateShaderRegisterDependencyProperty(shaderModel As ShaderModel, register As ShaderModelConstantRegister) As CodeMemberField

    Dim RegisterDependencyProperty As New CodeMethodInvokeExpression
    Dim RegisterMethod As New CodeMethodReferenceExpression
    RegisterMethod.TargetObject = New CodeTypeReferenceExpression("DependencyProperty")
    RegisterMethod.MethodName = "Register"

    RegisterDependencyProperty.Method = RegisterMethod

    Dim PropertyMetadataFunction As New CodeObjectCreateExpression
    PropertyMetadataFunction.CreateType = New CodeTypeReference("PropertyMetadata")
    PropertyMetadataFunction.Parameters.Add(CreateDefaultValue(register.DefaultValue))

    Dim PropertyMetadataCallback As New CodeMethodInvokeExpression
    PropertyMetadataCallback.Method = New CodeMethodReferenceExpression(Nothing, "PixelShaderConstantCallback")
    PropertyMetadataCallback.Parameters.Add(New CodePrimitiveExpression(register.RegisterNumber))
    PropertyMetadataFunction.Parameters.Add(PropertyMetadataCallback)

    RegisterDependencyProperty.Parameters.AddRange({New CodePrimitiveExpression(register.RegisterName), New CodeTypeOfExpression(register.RegisterType), New CodeTypeOfExpression(shaderModel.GeneratedClassName), PropertyMetadataFunction})

    Dim InitiateDependencyProperty As New CodeMemberField
    InitiateDependencyProperty.Type = New CodeTypeReference("DependencyProperty")
    InitiateDependencyProperty.Name = String.Format("{0}Property", register.RegisterName)
    InitiateDependencyProperty.Attributes = MemberAttributes.Public Or MemberAttributes.Static
    InitiateDependencyProperty.InitExpression = RegisterDependencyProperty

    Return InitiateDependencyProperty
End Function

To communicate with the sample register (Sn) you need a slightly different (and shorter) code:

VB.NET
Private Function CreateSamplerDependencyProperty(className As String, propertyName As String, ByVal RegisterNumber As Integer) As CodeMemberField

    Dim RegisterDependencyProperty As New CodeMethodInvokeExpression
    Dim RegisterMethod As New CodeMethodReferenceExpression
    RegisterMethod.TargetObject = New CodeTypeReferenceExpression("ShaderEffect")
    RegisterMethod.MethodName = "RegisterPixelShaderSamplerProperty"

    RegisterDependencyProperty.Method = RegisterMethod
    RegisterDependencyProperty.Parameters.AddRange({New CodePrimitiveExpression(propertyName), New CodeTypeOfExpression(className), New CodePrimitiveExpression(RegisterNumber)})

    Dim result As New CodeMemberField
    result.Type = New CodeTypeReference("DependencyProperty")
    result.Name = String.Format("{0}Property", propertyName)
    result.Attributes = MemberAttributes.Public Or MemberAttributes.Static
    result.InitExpression = RegisterDependencyProperty

    Return result
End Function

Now its just a matter of looping trough all the properties that one found in the fx file, and generate and add all the code:

VB.NET
' Add a dependency property and a CLR property for each of the shader's register variables
For Each register As ShaderModelConstantRegister In shaderModel.Registers
    If register.GPURegisterType.ToString.ToLower = "c" Then
        shader.Members.Add(CreateShaderRegisterDependencyProperty(shaderModel, register))
        shader.Members.Add(CreateCLRProperty(register.RegisterName, register.RegisterType, register.Description))
    Else
        shader.Members.Add(CreateSamplerDependencyProperty(shaderModel.GeneratedClassName, register.RegisterName, register.GPURegisterNumber))
        shader.Members.Add(CreateCLRProperty(register.RegisterName, GetType(Brush), Nothing))
    End If
Next

All the code that is needed to communicate with the ps file through ShaderEffect class in WPF is now included. Creating the VB or C# code is now really simple. This is indeed one of the true magic things with using CodeDom.

VB.NET
Private Function GenerateCode(provider As CodeDomProvider, compileUnit As CodeCompileUnit) As String
    ' Generate source code using the code generator.
    Using writer As New StringWriter()
        Dim indentString As String = ""
        Dim options As New CodeGeneratorOptions() With { _
             .IndentString = indentString, _
             .BlankLinesBetweenMembers = True _
        }
        provider.GenerateCodeFromCompileUnit(compileUnit, writer, options)
        Dim text As String = writer.ToString()
        ' Fix up code: make static DP fields readonly, and use triple-slash or triple-quote comments for XML doc comments.
        If provider.FileExtension = "cs" Then
            text = text.Replace("public static DependencyProperty", "public static readonly DependencyProperty")
            text = Regex.Replace(text, "// <(?!/?auto-generated)", "/// <")
        ElseIf provider.FileExtension = "vb" Then
            text = text.Replace("Public Shared ", "Public Shared ReadOnly ")
            text = text.Replace("'<", "'''<")
        End If
        Return text
    End Using
End Function

The CodeDomProvider can either be in VB.NET:

Dim provider As New Microsoft.VisualBasic.VBCodeProvider()

or C#:

VB.NET
Dim provider As New Microsoft.CSharp.CSharpCodeProvider()

Now you can quite simply provide the user with the code that he prefers.

Some advanced tips and tricks

Store a picture of the Effect

To actually store the control with the applied effect, you will have to result to RenderTargetBitmap function. This is a bit upsetting perhaps, as RenderTargetBitmap uses the CPU to proccess the UIElement. You can also get into trouble if you don't do it correctly as shown by Jamie Rodriguez in his blog. The code shown below is originally written by Adam Smith though.

VB.NET
Private Shared Function CaptureScreen(target As Visual, Optional dpiX As Double = 96, Optional dpiY As Double = 96) As BitmapSource
    If target Is Nothing Then
        Return Nothing
    End If
    Dim bounds As Rect = VisualTreeHelper.GetDescendantBounds(target)
    Dim rtb As New RenderTargetBitmap(CInt(bounds.Width * dpiX / 96.0), CInt(bounds.Height * dpiY / 96.0), dpiX, dpiY, PixelFormats.Pbgra32)
    Dim dv As New DrawingVisual()
    Using ctx As DrawingContext = dv.RenderOpen()
        Dim vb As New VisualBrush(target)
        ctx.DrawRectangle(vb, Nothing, New Rect(New Point(), bounds.Size))
    End Using
    rtb.Render(dv)
    Return rtb
End Function

Apply multiple effects to one control

When the old (and now outdated function) BitmapEffects were released they came with something called BitmapEffectGroup, which allowed you to apply several effects on one control. However, when the new Effect class that used the GPU instead of the CPU was created, the EffectGroup control was considered too difficult to implement directly. 

So if you still want to implement it there are two ways of doing it. The first one, suggested by Greg Schuster here, is to simply wrap a Border around the element you which to add an effect to, and add the second effect to the border as shown below.

HTML
<Border>
                <Border.Effect>
                    <local:GaussianEffect/>
                </Border.Effect>
                <Image>
                    <Image.Effect>
                        <local:ScratchedShader></local:ScratchedShader>
                    </Image.Effect>
                </Image>
            </Border>

Its fairly straight forward to implement in WPF without doing anything to the fx files.

There is however a better option, at least if you consider the computation time. All the fx files have an entry point function, which means that all of the functions of the different fx files could be united in one new fx file and compiled. You would of course also need to have the variables that WPF gives the input to the ps file with also.

I seems that the XNA framework have some means to load the ps files directly, but that is beond the scope of this article.

Apply effects only to a part of the control

A question that arises quite often is to apply the Effect on a part of an Image or Control. One way to do this is to call the Image.Clip and add an effect to that. 

HTML
<Rectangle VerticalAlignment="Top" HorizontalAlignment="Left" x:Name="BoundImageRect"  Height="50" Width="50" Panel.ZIndex="4" Stroke="Black" Fill="Transparent"  StrokeThickness="2" MouseDown="BoundImageRect_MouseDown" MouseMove="BoundImageRect_MouseMove" MouseUp="BoundImageRect_MouseUp" >
               <Rectangle.RenderTransform>
                   <TranslateTransform x:Name="Trans" X="0" Y="0"></TranslateTransform>
               </Rectangle.RenderTransform>
           </Rectangle>
           <Image  VerticalAlignment="Top" HorizontalAlignment="Left"  Width="Auto" Height="356" Source="{Binding ElementName=Img, Path=Source }" Panel.ZIndex="3">
               <Image.Clip>
                   <RectangleGeometry RadiusX="{Binding ElementName=BoundImageRect,Path=RadiusX}" RadiusY="{Binding ElementName=BoundImageRect,Path=RadiusY}" >
                       <RectangleGeometry.Rect>
                           <MultiBinding Converter="{StaticResource convertRect}">
                               <Binding ElementName="BoundImageRect" Path="Width"/>
                               <Binding ElementName="BoundImageRect" Path="Height"/>
                               <Binding ElementName="Trans" Path="X"/>
                               <Binding ElementName="Trans" Path="Y"/>
                           </MultiBinding>
                       </RectangleGeometry.Rect>
                   </RectangleGeometry>
               </Image.Clip>
               <Image.Effect>
                   <local:GaussianEffect></local:GaussianEffect>
               </Image.Effect>
           </Image>

However I actually experienced a memory leak while using this, so It might be a bit unstable. 

There are two other ways of chopping an element up. One is by adding an effect to a visual:

VB.NET
Public Class CoolDrawing
    Inherits FrameworkElement

    Implements System.ComponentModel.INotifyPropertyChanged

    ' Create a collection of child visual objects.
    Private _children As VisualCollection

    Public Sub New()
        _children = New VisualCollection(Me)

        Dim VisualImage As DrawingVisual
        VisualImage = CreateDrawingVisualCircle()
        VisualImage.Effect = New GaussianEffect
        _children.Add(VisualImage)

    End Sub

You can apply different effects on different DraingVisuals as you see fit. 

And as always there is a possibility to have the fx file itself only work on a small section, as is the case with the magnifier glass that is included. You don not have the opportunity (at least not without considerable work) to manipulate particular sections of the image with this approach. 

 

Points of interest

This wraps up part 1 of the series of adventures into shader effects, the beginning of the journey into shader effects, as this light Shazzam Editor tool will function as a toolbox for testing and debugging shader tools that you have created yourself.

I would also like to point out some of the references used in this article:

Shazzam Shader Editor - a fantastic tool written by Walt Ritscher (program wont run as it is though)

HLSL and Pixel Shaders for XAML Developers by Walt Ritscher

The blog Kodierer [Coder] by Rene Schulte, an MVP. He has some brilliant shadereffects articles for silverlight. 

Examples of shaders in action

Beond fanzy pictures

License

This article, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)


Written By
Chief Technology Officer
Norway Norway
This member has not yet provided a Biography. Assume it's interesting and varied, and probably something to do with programming.

Comments and Discussions

 
QuestionVisual studio 2015 is giving Build error for ShaderEffectLibrary. Pin
Member 1329311310-Oct-17 13:31
Member 1329311310-Oct-17 13:31 
AnswerRe: Visual studio 2015 is giving Build error for ShaderEffectLibrary. Pin
Kenneth Haugland11-Oct-17 7:37
mvaKenneth Haugland11-Oct-17 7:37 
QuestionCompiling NVidia Shader Library to ps file Pin
Roy Ben Shabat13-Mar-17 9:57
professionalRoy Ben Shabat13-Mar-17 9:57 
AnswerRe: Compiling NVidia Shader Library to ps file Pin
Kenneth Haugland13-Mar-17 15:44
mvaKenneth Haugland13-Mar-17 15:44 

General General    News News    Suggestion Suggestion    Question Question    Bug Bug    Answer Answer    Joke Joke    Praise Praise    Rant Rant    Admin Admin   

Use Ctrl+Left/Right to switch messages, Ctrl+Up/Down to switch threads, Ctrl+Shift+Left/Right to switch pages.