DirectX 11 and WinUI 3.0

Setting Up a Swap Chain

Let's continue with our look at how to render stuff with DirectX 11 inside a WinUI 3.0 window. This time we'll be working towards creating a SwapChain and binging that to our WinUI SwapChainPanel.

Buffer Stuff

In DirectX programming, we'll be using Buffers a lot. So, here's a crash course on them. Buffer is a temporary storage area in memory. It is allocated for any given purpose, such as storing information before it is processed, transferred, or displayed. A buffer can refer to a data structure used to hold pixel values or geometry data for rendering images on a screen. Buffers are essential for efficient data handling.

Size and Memory Management: Buffers have a fixed size, allowing for efficient allocation of memory resources. They ensure that data fits within the allocated memory space, preventing overflow or underflow.

Data Alignment: Buffers can help with the arrangement of data in memory to improve efficiency and performance. By aligning the data properly, it allows for faster access and processing which can result in improved performance and reduced memory access overhead. Imagine a factory where products move along a conveyor belt for processing. If the items on the belt are randomly placed, workers would struggle to perform their tasks efficiently. However, if the products are aligned and spaced properly, workers can easily handle them, leading to smoother workflow and increased throughput.

Data Organization: Buffers are like organized containers that hold data. They have a specific structure and arrangement for storing information, which makes it easy to find and work with the data efficiently. It's like having a well-organized filing system where everything is in its right place for quick and easy access.

Data Transfer: Buffers are like messengers that help different parts of a system communicate by carrying data between them. They make sure the data is passed smoothly, arrives at the right place at the right time, and is handled in order, like people taking turns in a line.

Data Protection: Buffers can incorporate mechanisms for data validation and protection. For example, bounds checking can be implemented to prevent buffer overflows or other security vulnerabilities.

Performance Optimization: Buffers make data processing faster by keeping data organized. Accessing and changing data is quicker because it's all together and in a smooth sequence. It's like having all the pieces you need in one place, so you can work with them faster and more efficiently.

Back Buffer and Render Target View

We need a backbuffer. The backbuffer is not the final image itself, but rather a buffer that holds the rendered image temporarily. It acts as a working area for rendering operations. The swapchain manages the backbuffer and the process of presenting the final image on the screen. It ensures a smooth and efficient display by swapping the backbuffer with the front buffer (the buffer currently being displayed).

The render target view (RTV) is a resource that allows you to write or render graphics data onto a specific target, such as the backbuffer. During rendering, graphics data is written to the render target view, which updates the contents of the backbuffer. Eventually, we'll also make a viewport. The viewport defines the region of the screen where the rendered image will be displayed. It specifies the size, position, and depth range of the rendered output within that region.

So with that in mind, let's create a backbuffer and use it to make a render target view.

C#
private ID3D11Texture2D backBuffer;
private ID3D11RenderTargetView renderTargetView;

public void CreateSwapChain()
{
	...
	backBuffer = swapChain.GetBuffer<ID3D11Texture2D>(0);
	renderTargetView = device.CreateRenderTargetView(backBuffer);
}

By specifying (0) in the SwapChain's GetBuffer method, we are requesting the first buffer, that is often referred to as the primary buffer or back buffer. The backBuffer size is determined by the SwapChain, so it's based on the width and height it gets from the SwapChainPanel, the amount of buffers, the pixel format, and so on. Since the ID3D11Device is responsible for managing the graphics operations, we then ask it to create the RenderTargetView and hand it the backBuffer as a parameter to create the renderTargetView from the backBuffer. This render target view acts as a "window" into the back buffer. It allows you to manipulate and draw graphics specifically onto the back buffer.

Surfaces

Working directly with the back buffer may not always be possible or convenient because it is managed by the graphics system and may have certain limitations or optimizations that restrict direct access to its data. By converting the back buffer into an IDXGISurface, we gain a separate interface that allows us to perform operations such as reading, writing, or modifying the pixel values directly. The IDXGISurface interface exposes lower-level methods and properties that give us fine-grained access to the image data, which can be useful for advanced rendering techniques or custom modifications.

C#
IDXGISurface dxgiSurface = backBuffer.QueryInterface<IDXGISurface>();

Here, we create a IDXGISurface from the backBuffer, which is the target surface where the rendered graphics will be displayed. Okay, now we're finally ready to put the SwapChain into action and bind it to the SwapShainPanel we created at the beginning.

C#
private Vortice.WinUI.ISwapChainPanelNative swapChainPanel;

public void CreateSwapChain()
{
	...
	swapChainPanel.SetSwapChain(swapChain);
}

This requires the use of Vortice.WinUI which is a part of the Vortice library that provides integration with the WinUI framework. Vortice.WinUI allows us to leverage WinUI features and components in our application.

COM Objects and Interop

You might notice, that we're not setting the SwapChainCanvas anywhere. That's because it's the wrong format. We can't very well take a DirectX SwapChain and expect the Microsoft.UI.Xaml SwapChainPanel to have a method to just accept it. The ISwapChainPanelNative from Vortice.WinUI does have a method like that, but now we need to make these two to connect. And there is a way.

C#
public void CreateSwapChain()
{
	ComObject comObject = new ComObject(SwapChainCanvas);
	swapChainPanel = comObject.QueryInterfaceOrNull<Vortice.WinUI.ISwapChainPanelNative>();
	comObject.Dispose();
	...
}

Here, at the beginning of the method, we are checking if the SwapChainCanvas can be treated as ISwapChainPanelNative, using the ComObject and QueryInterfaceOrNull methods.

COM (Component Object Model) is a technology used for inter-component communication on Windows. ComObject provides a wrapper around a COM object instance (written in e.g. C++) and enables managed code (e.g. C#) to interact with it. In this case, it is used to create a COM object from the SwapChainCanvas instance.

A COM object can implement multiple interfaces, each representing a specific set of functionality. QueryInterfaceOrNull is a method that allows you to check if a COM object supports a particular interface and obtain a reference to that interface if it does, or null if the interface is not supported by the object. In this case, it is used to obtain a reference to the ISwapChainPanelNative interface from the COM object representing the SwapChainCanvas.

We are essentially performing a type conversion or interface adaptation between the WinUI SwapChainPanel and the ISwapChainPanelNative interface provided by Vortice. Since the Vortice library expects a specific interface, ISwapChainPanelNative, for interacting with the swap chain panel in the context of DirectX and Vortice's DirectX interop (interoperability, the ability of different software components or systems to work together and communicate seamlessly). By using the ComObject and QueryInterfaceOrNull methods, the code is attempting to obtain a reference to the ISwapChainPanelNative interface from the SwapChainCanvas object. If successful, it means that the SwapChainCanvas object can be treated as an ISwapChainPanelNative object by Vortice, allowing it to work with the swap chain panel in the Vortice-specific context.

Also, don't forget to dispose the comObject using the Dispose() method once we're done with it. The comObject is a wrapper around a native COM object, which typically requires explicit disposal to release system resources properly. Failing to dispose the object may result in memory leaks or other resource-related issues. It is recommended to dispose objects that implement the IDisposable interface to ensure proper cleanup.

Of course, the SwapChain initialization isn't called anywhere, yet. And therein lies another problem. The creation of a Microsoft.UI.Xaml.SwapChainPanel takes a little while. So, even though we are calling this.InitializeComponent() in the MainWindow constructor, if we immediately try to initialize the SwapChain, the panel simply won't be anywhere near ready. It'll hop on answering the doorbell with no pants on.

One way to make sure that the panel is ready for action is to initialize the SwapChain in the Panel's Loaded event. So let's do that. (Don't forget to add Loaded="SwapChainCanvas_Loaded" to the XAML element.)

C#
private void SwapChainCanvas_Loaded(object sender, RoutedEventArgs e)
{
	CreateSwapChain();
	timer.Start();
}

We can also start the DispatchTimer here, since it really won't be needed before this. And there you have it. We're almost done (stop saying that!).