Monday, December 28, 2009

Would you look at that sky, talk about blue...

I have been interested in functional programming lately and decided to start a blog in an attempt to document my experience in learning an exciting new language called F#. Actually it is not all that new. It has been in development for several years now but will only officially ship with Visual Studio 2010 as a first class language for the .NET CLR.

The other new technology that I am very interested in is DirectX 11. Since both the hardware and the software are very new, there are precious few tutorials or books out there on how to get started. While I am certainly no authority on either F# or DirectX, I hope my learning experience will help give others a head start.

While XNA already provides an excellent way for .NET programmers to play with game programming and computer graphics, it only supports DirectX 9 and Shader Model 3. Compute Shaders in DirectX 11 allows us to write more general purpose code on the GPU. Things like game physics and AI can now be calculated on the GPU without having to jump through all kinds of hoops.

In the absence of XNA there are two options to create DirectX applications for .NET.
- Windows API Code Pack
- SlimDX
I have chosen the latter since it provides access to several other APIs like DirectInput in addition to the graphics wrappers.

I hope to create a series of tutorials over the next couple of weeks. The first will focus on how to get up and running with DirectX 11 and highlight a couple of cool F# features along the way. Today we will create a basic render window that renders a blue screen. In order for this example to work, you will need a DirectX 11 graphics card. At the moment the ATI Radeon 5800 series are the only cards that support this.

Setting up your project
-Create a new F# Application. In Visual Studio 2010 Beta 2 this will be a console application by default. Change the Ouput type to 'Windows Application' under Properties->Application tab.
- Download SlimDX and reference the assembly in your F# project.
- Add references to System.Windows.Forms and System.Drawing
- Set the target framework to .NET 4.0 (Not the client framework)

The code
To prevent having to write the fully qualified class names we will use a couple of import declarations. In F# we use the 'open' keyword.

open System
open System.Windows.Forms
open System.Drawing

open SlimDX
open SlimDX.Direct3D11
open SlimDX.DXGI

The first thing we will need is a device to access the graphics hardware. We will also need a swap chain which will allow us to present the front buffer after drawing a frame.

let deviceAndSwapChain windowHandle width height =
let modeDescription = new ModeDescription(
Width = width,
Height = height,
Format = Format.R8G8B8A8_UNorm,
RefreshRate = new Rational(60,1))

let swapChainDescription = new SwapChainDescription(
BufferCount = 1,
ModeDescription = modeDescription,
Usage = Usage.RenderTargetOutput,
OutputHandle = windowHandle,
SampleDescription = new SampleDescription(Count = 1),
IsWindowed = true,
SwapEffect = SwapEffect.Discard)

let _, device, swapChain = Device.CreateWithSwapChain(
null,
DriverType.Hardware,
DeviceCreationFlags.None,
swapChainDescription )
device, swapChain

There are a couple of interesting things about this first function.
- We defined a function that takes three parameters: a window handle, the screen width and the screen height. Yet we did not specify any types. One of F#'s strengths is that even though it is a statically typed language, you rarely need to specify any types. Most of the time they can be inferred by the compiler. This removes a lot of the clutter that you are forced to write in other languages like C# or C++ without sacrificing type safety.
- The arguments to a function can be separated by spaces. This is by personal choice. It could have been written as:

let deviceAndSwapChain(windowHandle, width, height)

The first form also allows you to do a funky thing called currying, which I will not get into now.
- In F# there are no need for curly braces. Statement blocks are implied by the tabbing.
- Note how we can initialize the ModeDescription and SwapChainDescription structures by specifying the values in what looks like the constructor, even though there is no such constructor. This is some F# syntactical sugar to avoid the much clumsier classical way of initializing a struct:

let deviceAndSwapChain windowHandle width height =
let modeDescription = new ModeDescription()
modeDescription.Width <- width
modeDescription.Height <- height
modeDescription.Format <- Format.R8G8B8A8_UNorm
modeDescription.RefreshRate <- new Rational(60,1)

- There is no 'return' statement at the end of the function definition. It is implicit.
- We can return multiple values. These are wrapped as a tuple.

Next up is our main function. We have to apply the EntryPoint attribute to indicate that this is where the application should start. We first create our form in order to get a valid window handle. We then use this handle with the screen dimensions to create our device and swap chain.

[<STAThread>]
[<EntryPoint>]
let main(args) =
let width, height = 800, 600
let form = new Form(Text = "Flow Edit", Width = width, Height = height)
let device, swapChain = deviceAndSwapChain form.Handle width height

We need a RenderTargetView in order to gain access the back buffer. This will be cleared at the start of each frame during the the callback to 'paint'. We register to the Paint event by adding our own handler that clears the back buffer and flips it using the swap chain. Note that since we do not use the PaintEventArgs in the callback, we simply name the argument with an underscore, which happens to be a valid identifier in F#. The struct initialization syntax again comes in handy. This time there is actually a constructor available that takes 4 float arguments. When inspecting the code however, new Color4(1.0f,0.0f,0.0f,1.0f) would be a bit ambiguous. Is this ARGB (blue) or RGBA (red)? By providing named parameters we avoid any confusion.

let renderTargetView =
use renderTarget = Resource.FromSwapChain(swapChain, 0)
new RenderTargetView(device, renderTarget)

let paint _ =
do device.ImmediateContext.ClearRenderTargetView(
renderTargetView,
new Color4(Alpha = 1.0f, Red = 0.0f, Green = 0.0f, Blue = 1.0f))
do swapChain.Present(0, PresentFlags.None) |> ignore

form.Paint.Add(paint)

When we run our form, lo and behold, we have a window rendering a glorious blue sky. ;-)

do Application.Run(form)


When the form is closed, we need to dispose of any SlimDX objects. For this simple example, I just rely on the ObjectTable to make sure that all objects are disposed.

for item in ObjectTable.Objects do
item.Dispose()
0

Below is the full source code listing. You can find the Visual Studio 2010 solution at Google Code

open System
open System.Windows.Forms
open System.Drawing

open SlimDX
open SlimDX.Direct3D11
open SlimDX.DXGI

let deviceAndSwapChain windowHandle width height =
let modeDescription = new ModeDescription(
Width = width,
Height = height,
Format = Format.R8G8B8A8_UNorm,
RefreshRate = new Rational(60,1))

let swapChainDescription = new SwapChainDescription(
BufferCount = 1,
ModeDescription = modeDescription,
Usage = Usage.RenderTargetOutput,
OutputHandle = windowHandle,
SampleDescription = new SampleDescription(Count = 1),
IsWindowed = true,
SwapEffect = SwapEffect.Discard)

let _, device, swapChain = Device.CreateWithSwapChain(
null,
DriverType.Hardware,
DeviceCreationFlags.None,
swapChainDescription )
device, swapChain

//-----------------------------------------------------------------------------------
[<STAThread>]
[<EntryPoint>]
let main(args) =
let width, height = 400, 300
let form = new Form(Text = "Blue skies", Width = width, Height = height)
let device, swapChain = deviceAndSwapChain form.Handle width height
let renderTargetView =
use renderTarget = Resource.FromSwapChain(swapChain, 0)
new RenderTargetView(device, renderTarget)

let paint _ =
do device.ImmediateContext.ClearRenderTargetView(
renderTargetView,
new Color4(Alpha = 1.0f, Red = 0.0f, Green = 0.0f, Blue = 1.0f))
do swapChain.Present(0, PresentFlags.None) |> ignore

form.Paint.Add(paint)

do Application.Run(form)

for item in ObjectTable.Objects do
item.Dispose()
0

1 comment:

  1. Hello! I'm trying run this sample, but take
    Exception: "Method 'SlimDX.Direct3D11.Resource.FromPointerReflectionThunk' not found."
    at line: use renderTarget = Resource.FromSwapChain(swapChain, 0). Need add <Texture2D>: use renderTarget = Resource.FromSwapChain<Texture2D>(swapChain, 0)
    P.S.: Sorry for my bad English:)

    ReplyDelete