In Direct3D9, direct CPU access to a resource is obtained by obtaining a lock on the resource. Every time you obtain the lock, you must make sure that you release it. In Direct3D10 and Direct3D11, a similar situation is faced with resources, but the terminology has changed to mapping the resource. Using a helper class to handle the lock and unlock operations on a resource helps you create exception safe code in C++.
Exception Handling With Locked Resources
So why should we bother using a helper class? Why not just call Lock
and Unlock
directly? Consider the following code:
void FillIndexBuffer(IDirect3DIndexBuffer9 *indices) { void *ptr = 0; THR(indices->Lock(0, 0, &ptr, D3DLOCK_DISCARD)); WORD *indexData = static_cast(ptr); ReadIndicesFromFile(indexData); THR(indices->Unlock()); }
(THR
turns a failed HRESULT
into a C++ exception.) What would happen if ReadIndicesFromFile
threw some sort of exception? The call stack would be unwound until we found an appropriate exception handler and we would leave FillIndexBuffer
without unlocking the index buffer. A similar problem can arise if FillIndexBuffer
is a long method and has an alternate return path in the middle where we might forget to unlock the buffer. Even worse, you may have already called Unlock
and still attempt to use the indexData
or ptr
variables because they are still in scope. If you’re lucky, this will give you an access violation right away. If you’re unlucky, strange things will start to happen and you’ll have no idea why.
If we treat the buffer lock as a resource and use the Resource Acquisition Is Initialization (RAII) pattern, then we can make sure that no matter how we leave the execution context of FillIndexBuffer
that we have unlocked the buffer before we return.
There’s another bit of ugliness up there that we will end up repeating every time we call Lock
manually on an index buffer. I’m talking about the whole void *
and casting business. I generally consider the appearance of void *
in my own code to be a code smell. We can push this casting smell into our helper class that performs the lock for us.
A Locking Helper Class
A simple class that satisfies these requirements looks like this:
template <typename Index> class index_lock { public: // lock in constructor, unlock in destructor index_lock(IDirect3DIndexBuffer9 *ib, DWORD flags = 0, UINT offset = 0, UINT size = 0) : m_ib(ib) { void *data = 0; THR(m_ib->Lock(offset, size, &data, flags)); m_data = static_cast<Index *>(data); } ~index_lock() { const HRESULT hr = m_ib->Unlock(); assert(SUCCEEDED(hr)); } // type safe accessors to vertex data const Index *data() const { return m_data; } Index *data() { return m_data; } private: IDirect3DIndexBuffer9Ptr m_ib; Index *m_data; };
The constructor for this helper performs the lock and stashes away the index buffer in a _com_ptr
style smart pointer. (The declaration of IDirect3DIndexBuffer9Ptr
is provided by the d3d9.h
header file when _COM_SMARTPTR_TYPEDEF
is defined.) The flags
argument was moved so that the more common case of locking the entire buffer doesn’t require extra arguments. Attempting to construct the smart lock index_lock
will fail if the call to Lock
fails, since THR
will throw on failure. The casting business is hidden from consumers of index_lock
and the saved data pointer is appropriately typed.
All of that is fairly straightforward. The destructor is a little more interesting. You don’t want to throw an exception from a destructor. Instead, we assert that the Unlock
succeeded. Its important to have the assert as a separate statement because in release builds, the argument to assert
may be removed entirely from the source. The data
members give us access to the underlying data pointer in a typesafe manner.
Example Revisited
Our little example from above would now look like this:
void FillIndexBuffer(IDirect3DIndexBuffer9 *indices) { ReadIndicesFromFile(index_lock<WORD>(indices, D3DLOCK_DISCARD).data()); }
Wow, that’s quite a bit shorter! But is it really safe? Let’s look closely at what happens. First, an instance of the index_lock
class is constructed while evaluating the arguments to ReadIndicesFromFile
. That will call Lock
on the index buffer and obtain the data pointer. If Lock
fails, an exception will be thrown and we will unwind the call stack, starting with FillIndexBuffer
(ReadIndicesFromFile
hasn’t been called yet). When the lock succeeds, we have an instance of index_lock<WORD>
and we call the data
method on that to obtain the data pointer which is passed to ReadIndicesFromFile
.
ReadIndicesFromFile
does its work and returns. What happens to the instance of index_lock
? C++ guarantees that the temporary instance created by the compiler persists until ReadIndicesFromFile
returns or the stack is unwound due to an exception. In the normal case, ReadIndicesFromFile
returns, the temporary instance is destroyed. This invokes the destructor for index_lock
and the index buffer is unlocked. Since we didn’t keep around a pointer to the locked data in a variable, its impossible for us to misuse this pointer after the lock has been released. If ReadIndicesFromFile
throws an exception, then the stack will be unwound and the temporary created by the compiler will be destroyed. Again, this will unlock the index buffer before the stack is unwound any further.
By using a helper class for resource locking, we get cleaner code that is easier to maintain. We also get the benefit of cleaning up the resource lock properly when an exception is thrown.
22-July-2009 at 1:56 am
Interesting read, I am also a big fan of using RAII to do techniques such as this, allows for very tidy implementations and systems with minimum fuss.
In the second paragraph threw is mispelt, and RAII is spelt RIAA in bracks where you link to wiki.
Anyway, informative read. Thanks!
LikeLike
22-July-2009 at 11:57 am
Thanks for the sharp eye! I have corrected both spelling mistakes.
LikeLike
4-September-2009 at 8:26 am
[…] from Direct3D: locking an index buffer gives you a typeless pointer to the locked data. Using a smart resource lock class hides the typeless pointer inside the resource […]
LikeLike
9-October-2009 at 7:17 am
Wouldn’t this index_lock class leak your Index* whenever it gets deleted? Just wondering why that pointer doesn’t get cleaned up in this class.
Next to that if you are doing this for a game you will most like not want to use exceptions as they are still considered to be to slow.
LikeLike
24-November-2009 at 6:18 pm
No, the Index pointer is not leaked because it is cleaned up by calling Unlock.
Exceptions are for error handling. Error handling is not a time critical path. Obviously handling errors costs more than not handling errors. Also obviously, not handling detectable errors makes for code that is difficult to debug.
Talking about “X is too slow for games” is meaningless without context. Not every game needs every single CPU cycle dedicated exclusively to their game. Do you think a game like “SceneIt!” is CPU bound? Is a game like Hexic CPU bound? Nonsense. Premature optimization is the root of all evil.
Yes, there are some games that might be penalized by using exceptions for error handling, but it is my feeling that for all the games out there, it is very few. Certainly if you are new to game programming you should be focused on creating correct code first. You can profile for execution speed later. Again, we’re talking about error handling here, the exceptional case, not the typical case.
LikeLike