21/01/2021
Table Of Contents
- COM vs Nano-COM
- IUnknown
- Reference Counting
- PrivateDate and object names
- IIDs and CLSIDs
- QueryInterface
- Versioning
- HRESULTs
- COM pointer types
- Debugging COM leaks
COM vs Nano-COM
COM stands for Component Object Model, and is a programming interface standard made by Microsoft. It is complex and extends massively beyond what is relevant to DirectX, and we won't cover advanced COM topics (like
CoCreateInstanceand COM servers) here as they are irrelevant to DirectX. However, a solid understanding of nano-COM is essential to writing good DirectX code.
DirectX uses a interface model referred to casually as "Nano-COM", which utilises the ABI (application-binary interface - a topic for another article, but basically how different programs interact on a binary level, e.g parameter passing, errors, etc), as well as the error-code model of COM.
As anyone who has worked with COM or Win32 before will know, windows loves its typedefs. Not too many are essential to know, but a few are frequently used in COM code:
REFGUID- GUID pointer in C/C#, GUID reference in C++UINT/ULONG- 32 bit unsigned intLPVOID-void*HRESULT- 32 bit signed int used as error code (sign bit indicates failure (negative num) or success (positive num))
IUnknown
IUnknown is the base interface of all COM types. Everything derives from it, and DirectX interfaces are no exception. This interface has 3 methods, which allow reference counting and inheritance/aggregation, and are the core methods required to manage arbitrary COM objects. (All these methods must be thread safe)
UINT AddRef();
UINT Release();
HRESULT QueryInterface(REFGUID pIID, void** ppvObject);All these methods will soon be explained - for now, just understand that any COM interface is implicitly convertable to an IUnknown*. Similar to Java's Java.lang.Object type or .NET's System.Object type, it acts as the base of all types in the ecosystem.
Note: You always work through COM objects with pointers. Never ever derefence them (if your language allows it). You will get horrible object slicing and inevitable memory corruption. :(
Reference Counting
COM objects are memory-managed by reference counting, where the COM object stores how many references to it exist to control its lifetime, and then destroys itself when there are none left (never use delete on a COM interface!). AddRef and Release are the 2 methods used for manipulating reference counts. AddRef, as the name suggests, increments the object's internal reference counter, and then it returns the new reference count. Release decrements the COM object's internal counter, and then it also returns the new reference count (note that the documentation for IUnknown says these return values should only be relied upon for testing purposes, and not for general code (it isn't your business how many references a COM object has!)). If the call to Release results in the counter decrementing to 0, then the COM object's lifetime ends, and it is destroyed, roughly speaking. However, the DirectX runtime may keep objects alive even when their ref count appears to be 0 because they are still in use - and in D3D11 there are cases where because of this behaviour, some objects were revivable. However, from an app perspective, this is fairly irrelevant and shouldn't be a concern. Any further calls on the object are undefined behaviour (you can't "revive" it with an AddRef call). It is worth noting that the D3D12 debug layer frequently will warn you of double-releasing an object, but often will simply crash if you invoke other methods on a destroyed object.
PrivateDate and object names
Private data and object names are a DirectX concept, not a COM concept, but they are very much worth covering here. All D3D12 interfaces inherit from ID3D12Object (which in turn inherits from IUnknown), which contains the private data methods.
For completeness, all the DirectX "base" objects are:
- DXGI -
IDXGIObject - D3D11 -
ID3D11DeviceandID3D11DeviceChild(everything inherits from the latter except the former, which inherits fromIUnknown) - DML (Direct ML) -
IDMLObject
All of these types have 3 methods.
HRESULT GetPrivateData(REFGUID guid, UINT* pDataSize, void* data);
HRESULT SetPrivateData(REFGUID guid, UINT dataSize, const void* data);
HRESULT SetPrivateDataInterface(REFGUID guid, IUnknown* data);ID3D12Object and IDMLObject usefully have a fourth method:
HRESULT SetName(/* I hate this type name too. It is a WCHAR*. Just a UTF16 string I promise */ LPCWSTR Name);This is just shorthand for SetPrivateData with WKPDID_D3DDebugObjectNameW (WKPDID means Well-Known Pointer to Data ID) GUID. For ASCII strings, use WKPDID_D3DDebugObjectName and manual SetPrivateData.
These are effectively hashmap methods on every object, allowing you to store arbitrary data in them. The most common one by far is storing the object name, either via SetName or WKPDID_D3DDebugObjectNameW/WKPDID_D3DDebugObjectName, which debugging tools and the DirectX runtime recognise for error messages, which makes life easier. Just generate a new IID for the data you want to store and tada, you can attach it.
GUIDs are used for identifying the data, and are sometimes called DIDs (Data-IDs).
SetPrivateData sets data associated with a guid for retrieval later. Set dataSize to 0 and data to NULL to destroy associated data for a guid.
SetPrivateDataInterface allows you to associate other COM interfaces with an object, by passing it as an IUnknown. This way, each call to GetPrivateData will result in an AddRef so that reference counting still works. You use the standard GetPrivateData to retrieve it, with a size of void*, and then must Release the returned interface when finished with it. If the object you called SetPrivateDataInterface is destroyed, it calls Release on all its set interfaces, allowing you to associate lifetimes between objects.
GetPrivateData, when pData is NULL, writes the stored data size to the pDataSize parameter allowing you to determine the size if necessary (for an array or variable sized type). When pData is not NULL, if it is larger than the size of the stored data, the data will be written to data and then the amount of data written will be written to pDataSize.
E.g, to retrieve a fixed size structure and a name
template <class T>
T RetrieveNamedData(ID3D12Object* pObj, REFGUID did)
{
T val;
UINT size = sizeof(val);
if (FAILED(pObj->GetPrivateData(did, &size, &val)) || size != sizeof(val))
{
throw std::runtime_error(...);
}
return val;
}
std::wstring RetrieveName(ID3D12Object* pObj)
{
REFGUID did = WKPDID_D3DDebugObjectNameW;
UINT size = 0;
if (FAILED(pObj->GetPrivateData(did, &size, nullptr)) || size == 0)
{
return L"Unnamed object";
}
std::wstring name(0, size);
assert(SUCCEEDED(pObj->GetPrivateData(did, &size, name.data()))); // not thread safe example
return name;
}IIDs and CLSIDs
- GUID = Globally Unique IDentifier
- IID = Interface IDentifier
- CLSID = CLass Identifier
A GUID isn't a COM concept, and just means Globally-Unique ID. They are just a 128 bit value to identify something. Because 2^128 is big (really big. really really big. really really really big), you can generally assume any generated GUID is unique. Visual Studio has an inbuilt generator to create GUIDs for various formats, or you can use a website such as guidgenerator.
An IID is used to identify an interface programmatically, as a sort of crude RTTI system. For example, the IID of IUnknown is 00000000-0000-0000-C000-000000000046, and the IID of ID3D12Device is 189819F1-1DB6-4B57-BE54-1821339B85F7.
To retrieve an IID of an interface, in Visual C++ or C# (with TerraFX.Interop.Windows) you can use the __uuidof operator.
You will commonly find the combination of an IID and a void** being used to represent some sort of COM interface output. The IID represents which interface you want to receive, and the void** is the
actual pointer to be outputted to. For example, QueryInterface uses this pattern.
To simplify it, there is a macro called IID_PPV_ARGS in combaseapi.h
A CLSID is similar to an IID, except it represents a concrete instance of a type rather than an interface. These aren't used in core DirectX but they are used by the DirectX Shader Compiler and so are worth briefly covering.
DxcCreateInstance is a factory method used to create interfaces, and it takes two IIDs (unlike factory methods like D3D12CreateDevice). One IID, to indicate the type you are representing it as (e.g, DxcCompiler is a valid IDxcCompiler, IDxcCompiler2, and IDxcCompiler3), and one CLSID to indicate which type you want to create to implement that interface (currently there is only one option for each interface as far as I know, but this could change).
IDxcCompiler* compiler;
DxcCreateInstance(CLSID_DxcUtils, __uuidof(*compiler), CLSID_DxcCompiler);IUnknown* pUnk;
GUID unknown_iid = __uuidof(IUnknown);
// or
GUID unknown_iid = __uuidof(*pUnk);IUnknown* pUnk;
Guid unknownIid = __uuidof<IUnknown>();
// or
Guid unknownIid = __uuidof(*pUnk);You can also use the static IID definitions in the various DX header files, such as
GUID unknown_iid = IID_IUnknown;Doing this requires linking against dxguid.lib, unless you want your linker to kaboom.
QueryInterface, inheritance, and aggregation
QueryInterface is the method used for casting safely between COM interfaces.
It is analagous to a slightly more restrictive version of is/as in C#, instanceof in Java/Python, and dynamic_cast in C++.
To recap, the signature is
HRESULT QueryInterface(REFGUID iid, void** ppvObject);This IID + ppvObject pattern pops up a lot, so there is a macro to help you. IID_PPV_ARGS, which is defined as roughly (it actually uses a complex fancy template, but this gets the point across):
#define IID_PPV_ARGS(pObj) __uuidof(*(pObj)), (void **)(pObj)You use it as such:
IComInterfaceBlah* res = nullptr;
SomeIidPpvMethod(..., IID_PPV_ARGS(&res));You can find this macro in combaseapi.h as well as in the DirectX-Headers and DirectXTK12 repos on github.
COM has both inheritance and aggregation, and uses QueryInterface to encompass both.
Inheritance here meaning identity conversions (so your pointer to an ID3D12Device7 is actually also an ID3D12Device4), whereas aggregation simply means the interface may contain the desired interface as a field, meaning it can only be accessed through QueryInterface
(your ID3D12Devic4 might support ID3D12Device7, but it itself is not necessarily an ID3D12Device7).
The iid parameter is the IID of the interface you want. ppvObject is a pointer to the interface-pointer it will be outputted to.
Note: You can never safely downcast a COM pointer based on a QueryInterface result. Your ID3D12Device may implement ID3D12Device1, but not necessarily through the original pointer.
You can safely upcast where there is an explicit inheritance - e.g ID3D12Device1 inherits from ID3D12Device, so it can be safely converted to an ID3D12Device*. However, an arbitrary ID3D12Device
may have been created in some way where its child interfaces are exposed through aggregation rather than inheritance, so the returned object could be different. However, because that interface does directly inherit from ID3D12Device, you can safely upcast it. It's a little bit to wrap your head around.
C++ has a convenience overload of QueryInterface which takes a typed ppvObject and gets the IID for you.
template<class Q>
HRESULT QueryInterface(Q** pp)
{
return QueryInterface(__uuidof(Q), (void **)pp);
}QueryInterface can return 3 things. E_POINTER, meaning your ppvObject was NULL and so the call failed. E_NOINTERFACE, meaning the call succeeded but the interface could not be supplied (it isn't inherited or aggregated), and ppvObject has been set to NULL, or S_OK meaning it succeeded and ppvObject is set to the desired interface and had a reference added to it, and must be Released when it is finished being used.
Versioning
DirectX uses a very simple (but in my opinion, quite elegant) technique for versioning. Each new version of an interface just has an incrementing number attached to it. E.g ID3D12Device, ID3D12Device1, ..., all the way to ID3D12Device9 now. As a general rule, ID3D12FooN inherits from ID3D12Foo(N-1), and so you can safely upcast, but to get to ID3D12FooN from ID3D12Foo(N+1) you must QueryInterface. The exception is all the debug interfaces, (they all inherit from IUnknown instead! why!!! why!!!!. except ID3D12Debug3... which inherits from ID3D12Debug... just to make it more confusing).
This is one of the reasons most creation methods take an IID + ppvObject. Instead of doing
ID3D12Resource* pTmp;
ID3D12Resource4* pRes;
ThrowIfFailed(device->CreateResource(..., &pTmp));
ThrowIfFailed(pTmp->QueryInterface(IID_PPV_ARGS(&pRes)));
pTmp->Release();you can just do
ID3D12Resource4* pRes;
ThrowIfFailed(device->CreateResource(..., IID_PPV_ARGS(&pRes)));HRESULTs
A HRESULT is a 32 bit signed integer representing an error or success code. The reason for the slightly strange naming is that it was originally a result-handle, rather than an error code, (like a HWND is a window-handle, a HINSTANCE is an instance-handle, a HRESULT was a result-handle). But that was too heavy and deemed unnecessary early on, so it became a 32 bit signed integer error code instead.
A HRESULT has 3 elements - a severity (error or success), a facility (where it comes from), and a code (what it is). The severity is the sign bit, where a 1 (negative number) means an error and a 0 (positive number) means a success. To inspect this bit, there are 2 macros in winerror.h:
#define FAILED(hr) (((HRESULT)(hr)) < 0)
#define SUCCEEDED(hr) (((HRESULT)(hr)) >= 0)There are HRESULT_SEVERITY, HRESULT_FACILITY, HRESULT_CODE, and MAKE_HRESULT macros too that are relatively self explanatory, but generally not used for DirectX.
There are only two success codes used by DirectX:
S_OK- the value 0, the bog standard "yeah that worked" codeS_FALSE- success, but "alternative" success. Most frequently used in DirectX forCreateXXX(...)methods, where the output object (ppvObject) isNULLand so object creation is tested for success rather than created. In this case,S_FALSEis returned for success
Note: Every single
CreateXXXmethod (includingD3D12CreateDevice) can have a validIIDpassed and aNULLppvObjectto test creation. This can be useful.
The important error codes to know are:
-
E_POINTER- something isnullthat shouldn't be -
E_FAIL- something went wrong! -
E_INVALIDARG- ...pretty self explanatory -
E_NOINTERFACE- an interface could not be provided or wasn't supported -
E_OUTOFMEMORY- can either mean local (system) memory or device (GPU) memory is exhausted - local memory exhaustion is generally fatal, but device memory exhaustation often isn't -
D3D12_ERROR_ADAPTER_NOT_FOUND- you are trying to use a shader/pipeline cache from a different adapter -
D3D12_ERROR_DRIVER_VERSION_MISMATCH- you are trying to use a shader/pipeline cache (or serialized data) from a different driver version -
DXGI_ERROR_INVALID_CALL- returned sometimes by DXGI methods instead ofE_INVALIDARG -
DXGI_ERROR_WAS_STILL_DRAWING- very specifically returned byIDXGISwapChain::Presentwith flagDXGI_PRESENT_DO_NOT_WAIT, when the call without the flag would have resulted in a wait
All of the following indicate device removal:
DXGI_ERROR_DEVICE_REMOVED- device was removed, either physical removal or due to invalid API usageDXGI_ERROR_DEVICE_HUNG- the device was removed because it hung and wouldn't respondDXGI_ERROR_DEVICE_RESET- device was reset, e.g by a driver updateDXGI_ERROR_DRIVER_INTERNAL_ERROR- a driver error caused device loss. Contact your IHV and then cry furiously
COM pointer types
-
ATL
CComPtr- this type is old and nigh-on deprecated so honestly... ignore it -
WRL
ComPtr- the standard COM pointer type you see in most samples and tutorials. Get it from DirectXTK12 orwrl/client.h -
WinRt
com_ptr- as far as I know, the ""officially"" recommended one, but basically interchangeable with the other 2 in terms of quality -
WIL
com_ptr_t
These 3 types all have one primary role, which is to manage AddRef/Release for you, so they Release at the end of their lifetime, and AddRef when copied, etc
Some note-worthy points about these types:
- WinRt
com_ptrdoes not overload&to release the underlying address, soIID_PPV_ARGSdoesn't work, so it sucks. com_ptr_tandComPtrboth overload&(equivalent toReleaseAndGetAddressforComPtr, andputforcom_ptr_t) so that it releases the underlying pointer (if one is present), so thatIID_PPV_ARGSand using it as an output parameter works. IMPORTANTLY, do not use it as a single element array. E.g, I have seen
ComPtr<ID3D12DescriptorHeap> _resHeap;
cmdList->SetDescriptorHeaps(1, &_resHeap); // you just released the descriptor heap before passing it, and have now caused use-after-free. Use .GetAddressOf (or addressof for com_ptr_t instead)Debugging COM leaks
COM leaks are a real, real, real pain to debug. Thankfully, DirectX has a few helpers that make discovering and fixing leaks a lot easier.
Your first port of call should be ID3D12DebugDevice::ReportLiveDeviceObjects (QueryInterface the debug device off of the D3D12 device). I recommend using D3D12_RLDO_DETAIL | D3D12_RLDO_IGNORE_INTERNAL flags for noticing leaks - you get object types and names (when named), and ignore stuff being kept alive by the D3D12 runtime, which aren't your responsibility.
ID3DDestructionNotifier is also a useful type. QueryInterface for it off of you D3D12 object, and then call RegisterDestructionCallback that will be called when the object is released (the ref count equals 0). You can then log this, set a breakpoint, etc - just note you cannot safely access the object, as it may have started the destruction process and so the object's internal state is undefined.