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:
And it is created with the code below, mostly one giant C# (5, async inside!) object initializer:
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:
public ref class DXContext sealed : Windows::UI::Xaml::Data::INotifyPropertyChanged
{
public:
DXContext();
void SetTarget();
void SetTarget(Windows::UI::Xaml::Controls::SwapChainBackgroundPanel^ swapChainPanel);
void SetTarget(Windows::UI::Xaml::Media::Imaging::SurfaceImageSource^ image, int w, int h);
void SetTarget(DXBase::Utils::BasicTexture^ texture);
property float Dpi;
property Windows::Foundation::Size Size;
property Windows::Foundation::Rect Viewport;
DXBase::Utils::BasicTexture^ Snapshot();
internal:
Microsoft::WRL::ComPtr<ID2D1Factory1> m_d2dFactory;
Microsoft::WRL::ComPtr<IDWriteFactory1> m_dwriteFactory;
Microsoft::WRL::ComPtr<IWICImagingFactory2> m_wicFactory;
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;
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.
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:
public enum class ErrorCodes;
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.
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();
}
{
auto a1 = ref new A();
auto a2 = ref new A();
a1.Other = a2;
a2.Other = a1;
}
To solve such problem WinRT comes with a WeakReference. The class A can be modified as follow to not hold strong reference:
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:
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?)
IAsyncOperation<bool>^ (*func)(Platform::Object^) = [] (Object^ ome) -> IAsyncOperation<bool>^ { ... };
Fortunately C++0x introduced “function object
” which works fine.
#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:
#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:
#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:
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:
task<bool> theTask = ....
task<void> task = theTask.then([](concurrency::task<bool> t) -> void
{
try
{
t.get();
}
catch (Exception^ ex)
{
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.
task<bool> theTask = ....
task<void> task = theTask.then([](concurrency::task<bool> t) -> void
{
try
{
t.get();
}
catch (Exception^ ex)
{
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!)
CodeProject