June 12th, 2023
There's one elephant in the cubicle we haven't addressed yet. When you close your application, tell me, does your Output window go something like...
D3D11 WARNING: Process is terminating. Using simple reporting. Please call ReportLiveObjects() at runtime for standard reporting. [ STATE_CREATION WARNING #0: UNKNOWN]
D3D11 WARNING: Live Producer at 0x150BD3E4, Refcount: 11. [ STATE_CREATION WARNING #0: UNKNOWN]
D3D11 WARNING: Live Object at 0x15131060, Refcount: 1. [ STATE_CREATION WARNING #0: UNKNOWN]
...
DXGI WARNING: Live Object at 0x05DFC250, Refcount: 4. [ STATE_CREATION WARNING #0: ]
DXGI WARNING: Live Object at 0x175D2020, Refcount: 1. [ STATE_CREATION WARNING #0: ]
DXGI WARNING: Live Object : 2 [ STATE_CREATION WARNING #0: ]
If not, then you either were smart and disposed of everything you were using, or didn't enable mixed-mode debugging when we set the DeviceCreationFlags for the ID3D11Device way back in part 1.
The warning messages you are seeing are indications that we are not disposing Direct3D and DXGI objects when they are no longer needed. Each time something references one of these objects, a reference count is increased, and when the object referencing it is disposed, the reference count is decreased. If all objects aren't properly disposed of before the application is terminated, the reference count for these objects will not be zero, which will trigger these warning messages. Well, some of them, like the ID3D11Device, don't have to be exactly zero. You'll know when the warnings disappear.
In general, if an object implements the IDisposable interface, you should call the Dispose method to free any resources it is holding when you are done using it. If you don't, the garbage collector will eventually finalize the object and free the resources, but this might take a while and can lead to performance problems. If these objects are not properly disposed of when they are no longer needed, they will continue to use up memory. This can lead to increased memory usage over time, which can slow down your program and potentially cause it to crash if it ends up using all available memory.
Of course, some of these resources we need for the whole duration of the application runtime, and when a .NET application is closed, the process is terminated, and all memory allocated to that process is returned to the operating system. So even if you do not manually dispose of all objects, when your application closes, the memory that it was using is freed up. But if you're like me and just hate warnings, we can take care of this.
Let's go back to our InitializeDirectX method and scramble things around a bit.
private ID3D11Debug iD3D11Debug;
public void InitializeDirectX()
{
...
ID3D11Device tempDevice;
ID3D11DeviceContext tempContext;
DeviceCreationFlags deviceCreationFlags = DeviceCreationFlags.BgraSupport;
#if DEBUG
deviceCreationFlags |= DeviceCreationFlags.Debug;
#endif
D3D11.D3D11CreateDevice(null, DriverType.Hardware, deviceCreationFlags, featureLevels, out tempDevice, out tempContext).CheckError();
device = tempDevice;
deviceContext = tempContext;
iD3D11Debug = device.QueryInterfaceOrNull<ID3D11Debug>();
...
}
The DeviceCreationFlags.Debug really isn't something you want to keep around in the release version of your application since it can have a serious performance impact. So, in the above code snippet, we make a DeviceCreationFlags variable for the flags we want to use. Since we definitely will want BgraSupport, we can add that in right away.
But then we're using a compilation directive #if DEBUG. Visual Studio automatically defines the DEBUG symbol for debug builds. When you create a new project in Visual Studio, it generates different build configurations, including Debug and Release. The Debug configuration is typically set up to include the DEBUG symbol, while the Release configuration excludes it. So, now we can add DeviceCreationFlags.Debug into our variable inside the #if #endif block and it'll only be used in Debug builds. Also, remember to change the third parameter of D3D11CreateDevice to use our variable.
Let's not stop there. ID3D11Debug is a debugging interface in DirectX 11. It is used for debugging and analyzing the behavior of Direct3D objects during runtime. The device.QueryInterfaceOrNull<ID3D11Debug>() call is attempting to obtain an instance of the ID3D11Debug interface from the device object. If one exists, it's placed in iD3D11Debug variable. Once we have acquired the ID3D11Debug interface, we can use its methods and properties to perform various debugging operations, such as reporting live objects, validating objects, or enabling debug layer functionality. We'll be using it in just a moment.
Read more about ID3D11Debug in this great article:
Direct3D 11 Debug API Tricks by Sean Middleditch from Blizzard Entertainment.
The problem is, we don't really know what these undisposed live objects are. A warning like D3D11 WARNING: Live Object at 0x150BD3E4, Refcount: 4 tells us that there is something at this memory address and 4 objects are referencing it at the time of app termination. But there is a way to see the names of these objects.
private void Window_Closed(object sender, WindowEventArgs args)
{
iD3D11Debug.ReportLiveDeviceObjects(
ReportLiveDeviceObjectFlags.Detail |
ReportLiveDeviceObjectFlags.IgnoreInternal);
iD3D11Debug.Dispose(); // Don't forget to dispose of the interface when you're done
}
Here, I've added a Window_Closed event for the MainWindow and I'm using the ID3D11Debug interface to give us a more detailed report of the Live Device Objects. You can naturally do this at any time, but I want to compare this detailed report to what we're getting when the application closes. We are passing it a couple of flags.
ReportLiveDeviceObjectFlags.Detail: This flag indicates that detailed information about each live device object should be reported, including information about its interfaces, reference count, and object name, if available. This is great, because then we'll get the actual object names.
ReportLiveDeviceObjectFlags.IgnoreInternal: This flag specifies that any internal device objects, which are typically managed internally by the DirectX runtime, should be ignored and not included in the report. You might notice lines with a Refcount: 0, but an IntRef: 1. These do not interest us, so we're leaving them out.
After this, running and closing the application gives us a lot more informative list of warnings (in addition to the useless list).
D3D11 WARNING: Live ID3D11Device at 0x16388C6C, Refcount: 11 [ STATE_CREATION WARNING #441: LIVE_DEVICE]
D3D11 WARNING: Live ID3D11Context at 0x162EE060, Refcount: 1, IntRef: 1 [ STATE_CREATION WARNING #2097226: LIVE_CONTEXT]
D3D11 WARNING: Live IDXGISwapChain at 0x1831F6D8, Refcount: 3 [ STATE_CREATION WARNING #442: LIVE_SWAPCHAIN]
D3D11 WARNING: Live ID3D11Texture2D at 0x1836C804, Refcount: 1, IntRef: 1 [ STATE_CREATION WARNING #425: LIVE_TEXTURE2D]
...
Alright, but what do we do about the warnings? Well, we have to dispose of every reference to the objects that implement the IDisposable interface.
private void Window_Closed(object sender, WindowEventArgs args)
{
deviceContext.ClearState();
deviceContext.Flush();
device.Dispose();
deviceContext.Dispose();
swapChain.Dispose();
backBuffer.Dispose();
renderTargetView.Dispose();
vertexShader.Dispose();
pixelShader.Dispose();
vertexBuffer.Dispose();
indexBuffer.Dispose();
swapChainPanel.Dispose();
...
}
ClearState() and Flush() methods ensure that the device and device context are not currently in use when they are disposed of. After that, you can just start calling Dispose on everything. If a type doesn't implement the IDisposable interface, it won't have the Dispose method, so you'll know by the error message.
This list helps a little bit, but there's still a lot of live objects in our code that reference these objects. We have to be more careful about disposing objects we're not using anymore. Here are a couple of examples.
public void CreateSwapChain()
{
...
IDXGIAdapter1 dxgiAdapter = dxgiDevice.GetParent();
IDXGIFactory2 dxgiFactory2 = dxgiAdapter.GetParent();
swapChain = dxgiFactory2.CreateSwapChainForComposition(device, swapChainDesc, null);
dxgiAdapter.Dispose();
dxgiFactory2.Dispose();
dxgiDevice.Dispose();
...
}
When we've successfully created the SwapChain, we never use the IDXGIAdapter1, the IDXGIFactory2, or the IDXGIDevice anymore. So, there's no reason to have them hanging around. Let's Dispose of them!
public void SetRenderState()
{
...
deviceContext.IASetInputLayout(inputLayout);
inputLayout.Dispose();
...
}
Once the input layout is set for the input assembler stage, the object has served its purpose. It's time to call Dispose.
I'll leave the rest for you to hunt down. When everything is disposed of correctly, even if the ID3D11Device still reports a small refcount, the useless list doesn't pop up anymore. Beautiful!
While we're talking about debugging, let's also mention a couple of tools.
PIX is a profiling and debugging tool developed by Microsoft specifically for graphics programming on Xbox consoles. It provides a wide range of features for analyzing and optimizing the performance of DirectX applications. With PIX, developers can capture and inspect frame-by-frame rendering data, analyze GPU utilization, profile shader performance, visualize GPU events, debug shader issues, and more. PIX is designed to work seamlessly with Xbox development kits and offers advanced capabilities for optimizing graphics performance on Xbox platforms. Note: For a WinUI 3.0 project, you need to build the project in Packaged mode to launch it in PIX.
Download PIX at Microsoft.com.
RenderDoc is a cross-platform graphics debugger and profiler that supports various graphics APIs, including DirectX, Vulkan, OpenGL, and Metal. It is widely used in game development and other graphics-intensive applications. RenderDoc allows developers to capture and analyze frames, inspect render targets and buffers, debug shaders, analyze GPU events and draw calls, and perform pixel history debugging, among other capabilities. RenderDoc provides a user-friendly interface and can be used on Windows, Linux, and Android platforms, making it a versatile tool for graphics debugging across multiple APIs and platforms.
Download RenderDoc at RenderDoc.org.
Note that if you want to use these tools, you'll need to compile the project for the x64 platform. But since that's what we'll probably want to use anyway, we might as well make the switch.
Visual Studio project:
d3dwinui3pt5.zip (21 KB)
D3DWinUI3 part 5 in GitHub