June 8th, 2023
Let's take a look at how to render stuff with DirectX 11 inside a WinUI 3.0 window. In this first part, we'll be creating a bunch of DirectX devices used in the graphics pipeline later on. There's not going to be anything visually interesting right away, but at least it will be mentally interesting!
First thing we'll need is a SwapChainPanel
. It is a specialized container that serves as a bridge between the graphics processing and the display. It's like a window where the rendered images are presented and "swapped" with the previous frame on the screen. The term "swap chain" refers to the mechanism of displaying a sequence of images in a continuous and smooth manner. The SwapChainPanel
provides a surface for a SwapChain rendering graphics output and ensures that it's efficiently and seamlessly displayed on the screen.
<StackPanel Orientation="Horizontal" HorizontalAlignment="Center" VerticalAlignment="Center">
<SwapChainPanel x:Name="SwapChainCanvas" Width="500" Height="500"></SwapChainPanel>
</StackPanel>
Next up, let's create a DispatcherTimer
for our MainWindow class and set it to trigger 60 times per second, preparing to set the refresh rate of our SwapChain
to 60Hz.
using Vortice.Direct3D;
using Vortice.Direct3D11;
public sealed partial class MainWindow : Window
{
private DispatcherTimer timer;
public MainWindow()
{
this.InitializeComponent();
timer = new DispatcherTimer();
timer.Tick += Timer_Tick;
timer.Interval = TimeSpan.FromMilliseconds(1000 / 60);
}
private void Timer_Tick(object sender, object e)
{
// Drawing will happen here.
}
}
Direct3D is typically used in a C++ environment. Luckily, there's a C# wrapper called Vortice.Windows, made by Amer Koleci, that provides .NET bindings for working with DirectX 11. It tries to mirror the names and structures of DirectX APIs as closely as possible (although, not always). We're going to use quite a few of these packages, so fire up your NuGet.
Let's get down to business and create a new method for initializing some Direct3D devices. We can name this one InitializeDirectX() and call it inside our MainWindow. Right off the bat, we're going to need these three libraries:
Vortice.DirectX
: Vortice.DirectX is a module of the Vortice library that focuses on providing access to DirectX APIs. DirectX is a collection of APIs used for multimedia and gaming applications, particularly for rendering graphics and handling audio. Vortice.DirectX provides developers with the tools and capabilities to work with DirectX functionality in their applications.
Vortice.Direct3D
: Direct3D is a graphics API (Application Programming Interface) used for rendering 3D graphics. Vortice.Direct3D provides access to this API, enabling developers to create and interact with 3D objects, textures, and other elements in their applications.
Vortice.Direct3D11
: Direct3D11 is a specific version of Direct3D, and Vortice.Direct3D11 provides access to the Direct3D11 API. It allows developers to work with advanced 3D graphics features and take advantage of the capabilities offered by Direct3D11.
private ID3D11Device device;
private ID3D11DeviceContext deviceContext;
public MainWindow()
{
...
InitializeDirectX();
}
public void InitializeDirectX()
{
ID3D11Device tempDevice;
ID3D11DeviceContext tempContext;
D3D11.D3D11CreateDevice(
null,
DriverType.Hardware,
DeviceCreationFlags.BgraSupport | DeviceCreationFlags.Debug,
featureLevels,
out tempDevice,
out tempContext).CheckError();
device = tempDevice;
deviceContext = tempContext;
}
ID3D11Device
is like the conductor of an orchestra. It coordinates all the different parts of the graphics card to work together harmoniously. It receives instructions from your application on what to draw and how to draw it, and then it orchestrates the graphics card's resources and capabilities to bring those instructions to life on your screen. It manages the graphics pipeline, controls memory allocation, and ensures efficient communication between the application and the graphics hardware. Device focuses on managing and creating resources.
ID3D11DeviceContext
is a different story. It's an interface in Direct3D that represents the context in which you can issue drawing and rendering commands to the graphics device. It acts as a bridge between your application and the graphics device, allowing you to control various aspects of the rendering pipeline, such as setting up rendering states, drawing geometric primitives, and updating resources. It's like a command center where you can send instructions to the graphics device to perform specific rendering operations. DeviceContext is responsible for executing rendering commands and controlling the rendering pipeline. It provides a higher level of abstraction and allows you to interact with the graphics device in a more immediate and fine-grained manner. Think of ID3D11Device
as the manager of resources, and ID3D11DeviceContext
as the executor of rendering commands.
Both of these chumps are created with the same method. However, it requires quite a few parameters, so let's go through them.
null
: This parameter specifies the adapter or display adapter to use. Passing null
means that the system will automatically select the default adapter.DriverType.Hardware
: This parameter indicates the type of driver to use. Hardware means that the application will utilize the hardware acceleration capabilities of the graphics card.DeviceCreationFlags.BgraSupport | DeviceCreationFlags.Debug
: These flags specify additional options for creating the device. BgraSupport
indicates that the device should support BGRA (blue-green-red-alpha) color format, and Debug
enables debugging features for better troubleshooting during development (should be turned off for release).featureLevels
: This parameter is an array of feature levels that the device should support. Feature levels represent different versions and capabilities of DirectX. The array typically contains a list of feature levels in descending order, starting from the highest level that the application requires.out tempDevice
: This parameter is an output parameter where the created Direct3D device will be assigned to.out tempContext
: This parameter is an output parameter where the created device context (used for issuing rendering commands) will be assigned to.CheckError()
: This is a method that checks if any errors occurred during the device creation process and handles them accordingly.Since we mentioned errors, it might be a good idea to turn that functionality on in Visual Studio. Go to your Project Properties, and under Debug, select Open debug launch profiles UI. In the Launch Profiles window, you can select "Enable native code debugging", also known as mixed-mode debugging, and if DirectX has something to tell you, you'll find out in the Output window.
You'll notice that we make a temporary Direct3D device for the D3D11CreateDevice()
(tempDevice). The purpose of this is to validate the device's compatibility and feature support. It allows you to check if the device supports the required feature levels specified in the featureLevels
array, helping to avoid potential compatibility issues during the runtime of the application. Once the temporary device is created and validated, the reference to it (tempDevice) is assigned to the actual device variable (device) to be used throughout the application.
Speaking of the FeatureLevels
array, we didn't specify that, yet. Throw this piece of code at the beginning of the method:
FeatureLevel[] featureLevels = new FeatureLevel[]
{
FeatureLevel.Level_11_1,
FeatureLevel.Level_11_0,
FeatureLevel.Level_10_1,
FeatureLevel.Level_10_0,
FeatureLevel.Level_9_3,
FeatureLevel.Level_9_2,
FeatureLevel.Level_9_1
};
These parameters are like a menu of different versions of features that the Direct3D device can support. The code is saying, "Hey device, do you support these feature levels? If so, great, let's use the best one available. If not, let's try the next one until we find a compatible option." It's a way to ensure the device can handle the required features of the application.
Let's keep going, we're almost there.
using Vortice.DXGI;
private IDXGIDevice dxgiDevice;
public void InitializeDirectX()
{
...
dxgiDevice = device.QueryInterface<IDXGIDevice>();
}
By calling the QueryInterface
method, we're asking the device if it can provide an interface that specifically understands DXGI
(Microsoft DirectX Graphics Infrastructure). If the device supports DXGI, it will give us a reference to itself in the form of dxgiDevice
, which we can use for DXGI-related operations. It's like saying: "Hey device, can you tell me if you're a DXGI device? If you are, I want to get a special version of you that understands DXGI stuff." And for this one, we need another library.
Vortice.DXGI: DXGI is a component of DirectX that handles tasks related to displaying graphics on the screen. Vortice.DXGI provides access to DXGI functionality, such as creating a Swap Chain, accessing Device Information (e.g. query supported display formats or maximum texture size), and sharing Resources (like textures or buffers) between different graphics APIs.
A swap chain is a mechanism in Direct3D that manages the presentation of rendered images on the screen. It acts as a buffer that holds the rendered image until it's ready to be displayed on the screen. It's important because it helps eliminate flickering and provides smooth rendering. It ensures that the image being displayed on the screen is updated seamlessly, without any visible artifacts or glitches. We already created a SwapChainPanel
at the beginning, and that's where our Swap Chain will live.
Let's create another method for Initializing the SwapChain. We also want to have the IDXGISwapChain1 variable ready and waiting.
private IDXGISwapChain1 swapChain;
public void CreateSwapChain()
{
SwapChainDescription1 swapChainDesc = new SwapChainDescription1()
{
Stereo = false,
Width = (int)SwapChainCanvas.Width,
Height = (int)SwapChainCanvas.Height,
BufferCount = 2,
BufferUsage = Usage.RenderTargetOutput,
Format = Format.B8G8R8A8_UNorm,
SampleDescription = new SampleDescription(1, 0),
Scaling = Scaling.Stretch,
AlphaMode = Vortice.DXGI.AlphaMode.Premultiplied,
Flags = SwapChainFlags.None,
SwapEffect = SwapEffect.FlipSequential
};
}
Ok, this is a bit much again, but we need to create a description for a swap chain before we can create the SwapChain itself. Swap chains can have a ton of different features, so we need to make sure we're creating the right kind.
Stereo:
Whether the swap chain is for stereo (3D) rendering. In this case, it's set to false.Width and Height:
The dimensions (size) of the swap chain, which match the width and height of the SwapChainCanvas.BufferCount:
The number of buffers in the swap chain. Here, it's set to 2, meaning there are two images that can be swapped.BufferUsage:
How the swap chain buffers will be used. In this case, it's set for rendering to the screen.Format:
The pixel format of the swap chain buffers, which determines how the colors are represented. Here, it's set to B8G8R8A8_UNorm, which means each pixel has four channels: blue, green, red, and alpha (transparency).SampleDescription:
Specifies the number of multisamples (for anti-aliasing) per pixel and the quality level. Here, it's set to 1 sample with a quality level of 0, meaning no anti-aliasing.Scaling:
How the swap chain content is scaled to fit the size of the canvas. Here, it's set to stretch, meaning it will stretch the content to fill the canvas.AlphaMode:
The alpha blending mode used for the swap chain. Here, it's set to Premultiplied, which means the colors are already multiplied by the alpha value for smoother blending.Flags:
Any additional flags for the swap chain. Here, none are specified.SwapEffect:
The type of swap effect used when presenting the buffers. Here, it's set to FlipSequential, which provides the most efficient presentation mode.Now that we have described what kind of SwapChain we want, how do we create one? It's a bit of a trek. First of all, we need to get a reference to your graphics card, or in this context, an adapter.
IDXGIAdapter1 dxgiAdapter = dxgiDevice.GetParent<IDXGIAdapter1>();
IDXGIAdapter1
object allows you to interact with and access information about the specific graphics adapter being used for rendering graphics on the system. So, here we create a variable for IDXGIAdapter1
and get the DXGI Device to hand us a reference to our graphics adapter.
Next, we use the adapter object to create a DXGI factory, that's responsible for creating and managing graphics resources, such as swap chains.
IDXGIFactory2 dxgiFactory2 = dxgiAdapter.GetParent<IDXGIFactory2>();
And finally, we're able to create the IDXGISwapChain1
.
swapChain = dxgiFactory2.CreateSwapChainForComposition(device, swapChainDesc, null);
We hand it a reference to our ID3D11Device
, the description we just created, and the null parameter is optional, and actually already defaults to null. It's a IDXGIOutput
to which the swap chain is restricted. If provided, the swap chain will be created to be compatible with the specified output.
Alright, now that we've managed to create a whole bunch of stuff that doesn't give us any visual output yet, let's do a quick recap.
In the next article, we'll take a look at how buffers work.
Visual Studio project:
d3dwinui3pt1.zip (19 KB)
D3DWinUI3 part 1 in GitHub