Click here to Skip to main content
15,885,216 members
Articles / Multimedia / DirectX

DirectX Made Simple

Rate me:
Please Sign up or sign in to vote.
0.00/5 (No votes)
6 Jan 2013CPOL8 min read 15.9K   7  
DirectX made simple

With Windows 8, WinRT, C++/Cx I think the time to write an elegant C# / XAML app using some DirectX rendering in C++ has finally come! Thanks WinRT! :-)

Here, I just plan to describe my attempt at learning DirectX and C++ and integrate it nicely in a C# XAML app.

My first exercise was to attempt to create a simple DirectX “Context” as WinRT C++/Cx component that can target multiple DirectX hosts: SwapPanel, CoreWindow, ImageSource and render an independent scene and initialize and use it from C#.

Note: This is a metro app. It requires VS2012 and Windows 8.

First the appetizers, here is my simple scene:

SimpleSample

And it is created with the code below, mostly one giant C# (5, async inside!) object initializer:

C#
public class Universes
{
    public async static Task<Universe> CreateUniverse1(DXContext ctxt = null)
    {
        ctxt = ctxt ?? new DXContext();

        var cubetex = await CreateSceneTexture(ctxt);

        var earth = new BasicTexture(ctxt);
        await earth.Load("earth600.jpg");

        var cube = new BasicShape(ctxt);
        cube.CreateCube();
        var sphere = new BasicShape(ctxt);
        sphere.CreateSphere();

        var u = new Universe(ctxt)
        {
            Scene =
            {
                Background = Colors.Aquamarine,
                Camera =
                {
                    EyeLocation = dx.vector3(0, 0.0f, 0.0f),
                    LookDirection = dx.vector3(0, 0, 100),
                    UpDirection = dx.vector3(0, 1, 0),
                }
            },
            Items =
            {
                new SpaceBody(ctxt)
                {
                    FTransform = t => dx.identity().Scale(10, 10, 10).RotationY(36 * t),
                    FLocation = t => dx.vector3(0, 0, 50),
                    SceneItem =
                    {
                        Shape = cube,
                        Texture = cubetex,
                    }
                },
                new SpaceBody(ctxt)
                {
                    FTransform = t => dx.identity().Scale(8, 6, 8).RotationY(96 * t),
                    FLocation = t => 
                        new float3().Translate(15, 0, 0).RotationY(24 * t).Translate(0, 15, 50),
                    SceneItem =
                    {
                        Shape = sphere,
                        Texture = earth,
                    },
                    Items = 
                    {
                        new SpaceBody(ctxt)
                        {
                            FTransform = t => dx.identity().RotationY(84 * t),
                            FLocation = t => new float3().Translate(12, 0, 0).RotationY(24 * t),
                            SceneItem =
                            {
                                Shape = sphere,
                                Texture = earth,
                            }
                        }
                    },
                },
                new SpaceBody(ctxt)
                {
                    FTransform = t => dx.identity().Scale(6, 5, 6).RotationY(48 * t),
                    FLocation = t => new float3().Translate(-15, -15, 55),
                    SceneItem =
                    {
                        Shape = sphere,
                    }
                },
            },
        };

        return u;
    }

    public async static Task<BasicTexture> CreateSceneTexture(DXContext ctxt)
    {
        var tex = new BasicTexture(ctxt);
        tex.Create(300, 300);
        ctxt.SetTarget(tex);

        var scene = new Scene(ctxt);
        scene.Background = Windows.UI.Colors.DarkGoldenrod;
        scene.Add(new DXBase.Scenes.CubeRenderer());
        scene.Add(new DXBase.Scenes.HelloDWrite());
        await scene.LoadAsync().AsTask();
        scene.RenderFrame();
        return tex;
    }
}

There is much to say about this sample, but I won’t go into the detail of DirectX too much (this is a very basic sample as far as DirectX is concerned and the source code is available, at the top of this post), instead I will mostly speak about C++/Cx – C# communication.

1. The Main DirectX C++/Cx Components

1.1. DXContext

First there is the DirectX context, here is an extract of its important methods and properties:

C#
  public ref class DXContext sealed :  Windows::UI::Xaml::Data::INotifyPropertyChanged
  {
  public:
      DXContext();

      // Target the top level CoreWindow
      void SetTarget();
      // Target the argument top level SwapChainBackgroundPanel
      void SetTarget(Windows::UI::Xaml::Controls::SwapChainBackgroundPanel^ swapChainPanel);
      // Target the argument ImageSource
      void SetTarget(Windows::UI::Xaml::Media::Imaging::SurfaceImageSource^ image, int w, int h);
      // Target a texture
      void SetTarget(DXBase::Utils::BasicTexture^ texture);

      property float Dpi;
      property Windows::Foundation::Size Size;
      property Windows::Foundation::Rect Viewport;

      DXBase::Utils::BasicTexture^ Snapshot();

      // internal (shared) DirectX variables
  internal:
      // device independent resources
      Microsoft::WRL::ComPtr<ID2D1Factory1> m_d2dFactory;
      Microsoft::WRL::ComPtr<IDWriteFactory1> m_dwriteFactory;
      Microsoft::WRL::ComPtr<IWICImagingFactory2> m_wicFactory;

      // device resource
      D3D_FEATURE_LEVEL m_featureLevel;
      Microsoft::WRL::ComPtr<ID3D11Device1> m_d3dDevice;
      Microsoft::WRL::ComPtr<ID3D11DeviceContext1> m_d3dContext;
      Microsoft::WRL::ComPtr<ID2D1Device> m_d2dDevice;
      Microsoft::WRL::ComPtr<ID2D1DeviceContext> m_d2dContext;

      // target and size dependent resources
      DirectX::XMFLOAT4X4 mDisplayOrientation;
      Microsoft::WRL::ComPtr<ID3D11RenderTargetView> m_renderTargetView;
      Microsoft::WRL::ComPtr<ID3D11DepthStencilView> m_depthStencilView;
};

DXContext is a ‘public ref class’ meaning it’s a shared component (can be used by C#), it must be sealed (unfortunately… Except those inheriting from DependencyObject, all C++ public ref class must be sealed, as explained here, inheritance section).

All the public members are accessible from C#, the most important are the overloaded “SetTarget()” methods that will set the DirectX Rendering target. Can be changed anytime (although it seems to be an expensive operation, I think rendering on a Texture should probably be done another way, when I will know better).

Finally, it holds all DirectX device information as internal variables. These can’t be public or protected as they are not WinRT component. But, being internal, they can be accessed by other component in the library, it’s how the scene can render. I tried to trim the fat to the minimum number of DirectX variable that such an object should contain.

Note: Plain C++ doesn’t have the ‘internal’ visibility, this is a C++/Cx extension and it means the same thing as in C#, i.e., members are accessible by all code in the same library.

ComPtr<T> is a shared COM Pointer. Takes care of all reference counting for you.

DXContext implements INotifyPropertyChanged and can be observed by XAML component or data binding!
I also created a macro for the INotifyPropertyChanged implementation as it is repetitive and I had to write a long winded implementation due to some mysterious bug in the pure C++ sample.

It has a Snapshot() method to take a screen capture! And BasicTexture has a method to save to file.

1.2 Scene

My first attempt at using this DXContext was to create a Scene object which contains ISceneData object.

An ISceneData can be ripped of, more or less, verbatim from various DirectX samples around the web. And the Scene object will take care of initializing it and rendering it when the time is right. I have 2 ISceneData implementations: CubeRenderer, HelloDWrite.

1.3 BasicScene, BasicShape, BasicTexture

Unfortunately, all the samples on the web often have a lot variables, all mixed up and trying to sort out what does what takes some thinking.

So, I created a BasicScene which takes a list of shapes with texture and location (transform) and renders it.

C#
public ref class BasicSceneItem sealed
{
public:
    BasicSceneItem(DXContext^ ctxt);
    property DXContext^ Context;
    property DXBase::Utils::BasicShape^ Shape;
    property DXBase::Utils::BasicTexture^ Texture;
    property DXBase::Utils::float4x4 WorldTransform;
};

public ref class BasicScene sealed
{
public:
    BasicScene();
    BasicScene(DXContext^ ctxt);

    property PerformanceTimer^ Timer;
    property Windows::UI::Color Background;
    property DXBase::DXContext^ Context;
    property DXBase::Utils::BasicCamera^ Camera;

    property Windows::Foundation::Collections::IVectorView<BasicSceneItem^>^ Shapes;
    void Add(BasicSceneItem^ item);
    void Remove(BasicSceneItem^ item);
    void RemoveAt(int index);

    property bool IsLoaded;

    void RenderFrame();
    void RenderFrame(SceneRenderArgs^ args);
};

It also has some Background and a Camera, all WinRT components that can be controlled by C++.

The BasicShape contains point and index buffer for triangles and has various create methods that will populate the buffers.

The BasicTexture can load a file or be created directly in memory (and rendered to by using Context.SetTarget(texture)), and contains the texture and textureView used by the rendering process.

Each of these classes has very few DirectX specific variables making it relatively easy to understand what’s going on.

2. C++/Cx to C# Mapping

When C++/Cx components are called from C#, the .NET runtime does some type mapping for you. There is the obvious, the basic types (int, float, etc.) and value types (struct) are used as is. But there is more, mapping for exception and important interfaces (such as IEnumerable).

It’s worth having a look at this MSDN page which details the various mapping happening.

Also, to refresh my C++ skill, I found this interesting web site where most Google query lead to anytime I had a C++ syntax or STL issue!

3. Exception Across ABI

You can’t pass custom exception or exception’s message across ABI (C++ / C# / JavaScript boundary). All that can pass is an HRESULT, basically a number. Some special number will pass some special exception as explained on this MSDN page.

If you want to pass some specific exception, you have to use some unreserved HRESULT (as described here) and have some helper class to turn the HRESULT in a meaningful number.

Here comes the ExHelper class just for this purpose:

C#
// this range is free: 0x0200-0xFFFF
public enum class ErrorCodes;

// You can't throw custom exception with custom message across ABI
// This will help throw custom Exception with known HRESULT value
public ref class ExHelper sealed
{
public:
    static void Throw(ErrorCodes c);
    static ErrorCodes GetCode(Windows::Foundation::HResult ex);
    static Windows::Foundation::HResult CreateWinRTException(ErrorCodes c);
};

Note you can’t expose Platform::Exception publicly either (well maybe you can, but it was troublesome). But you can expose an HRESULT. The runtime will automatically turn it into a System.Exception when called from C#.

4. Reference Counting and Weak Pointer

C++/Cx is pure C++. There is no garbage collection happening when writing pure C++ app, even if one uses the C++/Cx extension. The hat (^) pointer is a ref counted pointer that can automatically be turned into a C# reference.

That can lead to a problem when 2 C++/Cx components reference each other as in the following (simplified) scenario.

C++
public ref class A sealed
{
    A^ other;
public:
    property A^ Other
    {
        A^ get() { return other; }
        void set(A^ value) { this.other = value; }
    }
};

{
    auto a1 = ref new A();
} // a1 is automatically destroyed

{
    auto a1 = ref new A();
    auto a2 = ref new A();
    a1.Other = a2;
    a2.Other = a1;
} // no automatic destruction takes place!

To solve such problem WinRT comes with a WeakReference. The class A can be modified as follow to not hold strong reference:

C++
public ref class A sealed
{
    WeakReference other;
public:
    property A^ Other
    {
        A^ get() { return other.Resolve<A>(); }
        void set(A^ value) 
        {
            if (value)
                this.other = value; 
            else
                this.other = WeakReference();
        }
    }
};

5. debugging / logging

Sometimes, logging is helpful for debugging. For example, I log creation and deletion of some items to be sure I don’t have any memory leak. However, printf, cout<<, System::Console::WriteLine won’t work in a metro app.

One has to use OutputDebugString, output will appear in Visual Studio output window.

6. IEnumerable, IList

If you use C#, you must love IEnumerable, IEnumerator, IList and LINQ. When writing a C++ component, you should make sure it plays nice with all that.

The .NET runtime does some automatic mapping when calling in C++/Cx component, as explained here.

6.1 IEnumerable

In C++, one shall expose Windows::Foundation::Collection::IIterable<T> to be consumed in C# a System.Collections.Generic.IEnumerable<T>.

IIterable has a single method First() that returns an IIterator. That will be mapped to an IEnumerator.

However, there is a little gotcha. Unlike C# IEnumerator which starts before the first element (one has to call bool MoveNext()) IIterator starts on the first element.

6.2 IList

One can return a Windows::Foundation::Collections::IVector<T> to be mapped to an IList<T>. There is already a class implementing it:

C++
Platform::Collections::Vector<T>

Or one can use vector->GetView() to return a Windows::Foundation::Collections::IVectorView<T> that will be mapped to an IReadonlyList<T>.

7. Function Pointers and Lambda

C++ 0x (or whatever is called the latest C++ standard) introduced lambda expression to create inline function, much like in C#.

There is a long description of on MSDN.

Basically it has the following syntax

[capture variable](parameters) –> optional return type specification { body }

It’s all quite intuitive except for the capture part. You have to specify which value you want to capture (this, local variable) and you can specify by value or reference (using the ‘&’ prefix), or all local variables and this with equal as in: ‘[=]’

In some instance, I had problem assigning lambda to a function pointer, for example the code below didn’t compile for me (maybe I missed something?)

C++
IAsyncOperation<bool>^ (*func)(Platform::Object^) = [] (Object^ ome) -> IAsyncOperation<bool>^ { ... };

Fortunately C++0x introduced “function object” which works fine.

C++
#include <functional>
//....
std::function<IAsyncOperation<bool>^(Object^)> func = [] 
(Object^ ome) -> IAsyncOperation<bool>^ { ... };

Remark: The function object will keep reference to the captured variable as long as it exists! Be careful with circular reference and WinRT component (hat pointers ‘^’).

8. Async

With Metro Async programming is an inescapable reality!

Of course, in your C++/Cx code, you can use the class from System.Threading.Tasks, but there is also some C++ native API just for that: task<T>.

One can create task from .NET IAsyncOperation or a simple C function:

C++
#include <ppltasks.h>
#include <ppl.h>

using namespace concurrency;
using namespace Windows::Foundation;

bool (*func)() = []()->bool { return true; };
task<bool> t1 = create_task(func);

IAsyncOperation<bool>^ loader = ...;
task<bool> t2 = create_task(loader);

Conversely, one can create .NET IAsyncOperation from task or C function with create_async, as in:

C++
#include <ppltasks.h>
#include <ppl.h>

using namespace concurrency;
using namespace Windows::Foundation;

bool (*func)() = []()->bool { return true; };
task<bool> t1 = create_task(func);
IAsyncOperation<bool>^ ao1 = create_async([t1] { return t1; });
IAsyncOperation<bool>^ ao2 = create_async(func);

Tasks can be chained with ‘then’ and one can wait on multiple task by adding them with ‘&&’ such as in:

C++
task<void> t1 = ...
task<void> t2 = ...

auto t3 = (t1 && t2).then([] -> void
{
    OutputDebugString(L"It is done");
});

Remark: Tasks can be chained with ‘then’ and one can wait on multiple task by adding them with ‘&&’ such as in:

C++
task<bool> theTask = ....
task<void> task = theTask.then([](concurrency::task<bool> t) -> void
{
    try
    {
        t.get();
        // success...
    }
    catch (Exception^ ex)
    {
        // failure
        auto msg = "Exception: " + ex->ToString() + "\r\n";
        OutputDebugString(msg->Data());
    }
});

Remark: Tasks are value type and start executing immediately once created (in another thread).

When chaining tasks with ‘then’, you can capture exception from previous task by taking a task<T> argument instead of T. And put a try/catch around task.get(). If you do not catch exception, it will eventually bring the program down.

C++
task<bool> theTask = ....
task<void> task = theTask.then([](concurrency::task<bool> t) -> void
{
    try
    {
        t.get();
        // success...
    }
    catch (Exception^ ex)
    {
        // failure
        auto msg = "Exception: " + ex->ToString() + "\r\n";
        OutputDebugString(msg->Data());
    }
});

9. Conclusion

It proved pleasantly surprisingly easy to have the C++ and C# work together with WinRT. Smooth and painless. C++ 11 was easier to use that my memory of C++ was telling me. And in the end, I mixed and matched them all with great fun. To boot my C# app starts real quick (like a plain C++ app)! It’s way better than C++ CLI!

A few frustrating points with C++/Cx still stand out though:

  • Microsoft value types (Windows::Foundation::Size for example) have custom constructors, methods and operator, yours cannot.
  • You can’t create a type hierarchy! (Can be worked around tediously with an interface hierarchy, but still!)

License

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


Written By
Software Developer (Senior) http://www.ansibleww.com.au
Australia Australia
The Australia born French man who went back to Australia later in life...
Finally got over life long (and mostly hopeless usually, yay!) chronic sicknesses.
Worked in Sydney, Brisbane, Darwin, Billinudgel, Darwin and Melbourne.

Comments and Discussions

 
-- There are no messages in this forum --