Friday, November 16, 2012

Writing GPU Shaders in F#

I have been working lately on a library that allows one to write shaders in F#. It uses code quotations to translate these shaders to HLSL before being executed on the GPU. Check out SharpShaders on GitHub.

Monday, November 5, 2012

Auomatically enforcing HLSL Struct Layout with PostSharp

Anyone doing managed DirectX programming will at some point have to marshal data between the CPU and the GPU. In order to do this, the struct layout needs to adhere to some rules. The rules are:
  • Fields must start on a 4 byte boundary
  • If fields cross a 16 byte boundary, they have to start on a 16 byte boundary
  • Unfortunately in the .NET world, memory is not necessarily laid out in the same order that it was declared. In order to enforce a layout that adheres to the above rules, we have to declare each field offset explicitly. Doing this manually is time consuming and error prone. For example:
    [<Struct; StructLayout(LayoutKind.Explicit, Size=48)>]
    type SceneConstants_IfYouDontHavePostSharp =
        [<FieldOffset(0)>]  val Eye             : float3
        [<FieldOffset(16)>] val Light           : float3
        [<FieldOffset(32)>] val AmbientLight    : float3
        [<FieldOffset(44)>] val LightRangeSquared  : float32
        with
        new(eye, light, ambientLight, lightRange) = { 
            Eye = eye
            Light = light
            AmbientLight = ambientLight
            LightRangeSquared = lightRange }
    

    To avoid having to write all this boilerplate code every time we create a new shader, we can turn to aspect oriented programming.

    Using the PostSharp framework, one could create an aspect that will enforce the HLSL constant packing rules at compile time. It will actually weave in the necessary code into your existing class. For instance, the ConstantPacking aspect would be used as follows:
    [<Struct;ConstantPacking>]
    type SceneConstants(eye:float3, light:float3, ambientLight:float3, lightRangeSquared:float32) =
        member m.Eye = eye
        member m.Light = light
        member m.AmbientLight = ambientLight
        member m.LightRangeSquared = lightRangeSquared
    
    Isn't that neat? By just adding the magic ConstantPacking attribute, all the packing rules are enforced! The source code for this magic class can be found here.
    There is one downside to using PostSharp as your AOP framework. It is not free. However, its uses stretch far beyond enforcing layout rules. As programmers we put up with writing repetitive boilerplate code far more often than we need to. AOP helps to keep things DRY, not only when writing shaders. Considering how powerful this tool is and how much time it can save you, it is a small investment.