Last time, we talked about the basics of meshes. In my mind, that really completed the tour of the basics of Direct3D. Now that we have that foundation in place, I’d like to move on to cover some of the details that we glossed over in the introduction. This time, we’ll cover the topic of device loss.
Put simply, device loss is what happens whenever some external party takes control of the video hardware from us. Direct3D is all about interacting intimately with the video hardware, so there’s pretty much room for only one app at a time to do certain things. When Windows decides that a different app – or Windows itself – should have exclusive access to the graphics hardware, we get the boot and the device is “lost” to us.
The Direct3D device can become lost due to a variety of events - screen savers kicking in, computers being locked, and machines going into standby will all cause device loss. On my laptop, it even occurs when I close and then reopen the lid. Our primary indication that the device is has become lost comes when we attempt to call Device.Present. (You’ll recall Device.Present from our earlier discussions.)
When we call Device.Present after the device has become lost for whatever reason, Direct3D will throw aDeviceLostException. What’s interesting here is that Direct3D actually does a lot of work behind the scenes to ensure that the other APIs do not throw this exception. They won’t do what they’re supposed to – after all, the device has been lost – but they won’t fail, either. This allows us to localize our logic for handing device loss, rather than having to worry that a DeviceLostException might be thrown any time we do anything with Direct3D.
What do we do when the device becomes lost? Well, probably nothing! You see, whatever caused the device loss is probably still going on – the screensaver is still running, the user hasn’t unlocked the screen yet, or whatever. So what we really need to do is record the fact that the device has become lost. We could put the following code at the end of our Render method:
try { // Copy the back buffer to the display device.Present(); } catch (DeviceLostException) { // Indicate that the device has been lost deviceLost = true; // Spew a message into the output window of the debugger Debug.WriteLine("Device was lost"); }
Pretty much all this does is set deviceLost to true when DeviceLostException is thrown. deviceLost is just a private boolean field that I defined on the class, like this:
// Has the device been lost and not reset? private bool deviceLost;
Notice that I added a call to Debug.WriteLine, too. This is handy, because if you build your app with the DEBUG symbol defined (this should happen automatically when you build a VS.NET application in debug configuration), you’ll get a message out in the Output tab of the debugger.
One other thing we might want to do in this catch block is to pause the game/simulation/application. Since we’re not going to be able to draw to the display for a little while, it might make sense to pause until the device is recovered. The user might not be too happy if a monster walks over and eats their character because they accidentally locked their workstation and didn’t unlock it quickly enough!
OK. We know that the device has been lost. Now we want to know when it’s okay to start drawing again, and what to do when it is. The API that tells us when we can go ahead and start drawing again is Device.TestCooperativeLevel. We can call this method at the beginning of our Render method, and it will continue to throw DeviceLostException until the device is no longer lost. Once it stops throwing this exception, the device is no longer lost, and we’re almost ready to go.
There’s a slight twist at this point. Because the device was being used by another application (or possibly Windows itself) while it was lost, we have no idea what state the hardware is in. In fact, there’s no guarantee that the VertexBuffers that we allocated are even still around – they may have been discarded in the interim.
Direct3D knows that things might not be set up the way that we need them, and therefore won’t let us start rendering again until we reset the device. As it turns out, resetting the device is almost indistinguishable from setting it up the first time: we have to set the appropriate render states, set up the world, projection, and view matrices, re-create any vertex buffers that we want to use, etc. etc.
In fact, there are only two things we have to do differently. We need to clean up any resources that were allocated when the device was lost, and we need to call Device.Reset. Other than that, we can reuse the initialization code we already have.
To make it easy for us to recover from device loss, the Managed Direct3D API exposes a pair of events on the device object that we can hook up to. Both of these events get fired from within the implementation of Device.Reset. The first of them, DeviceLost, is fired early in the Reset method, making it a good place to do any cleanup, such as calling Dispose on any existing VertexBuffers. The second, DeviceReset, is called a little later, making it a good place to do the reallocation.
That’s a fair amount to keep straight, so let me show you what I mean. We’ve already seen the changes I made to the end of the Render method. Here are the changes that I’ve made to the beginning:
protected void Render() { if (deviceLost) { // Try to get the device back AttemptRecovery(); } // If we couldn't get the device back, don't try to render if (deviceLost) { return; }
The rest of the method is unchanged from what we’ve been using up to this point.
The changes we’ve made are pretty simple: if the device has been lost, attempt to recover it. If - after attempting to recover the device - the device remains lost, just return without rendering anything. Of course, I’ve buried the details of device recovery in the AttemptRecovery method. Here it is:
protected void AttemptRecovery() { try { device.TestCooperativeLevel(); } catch (DeviceLostException) { } catch (DeviceNotResetException) { try { device.Reset(pres); deviceLost = false; // Spew a message into the output window of the debugger Debug.WriteLine("Device successfully reset"); } catch (DeviceLostException) { // If it's still lost or lost again, just do // nothing } } }
AttemptRecovery is pretty straightforward. We call TestCooperativeLevel to see if it’s okay to try to reset the device. If the device is still lost, we’ll get a DeviceLostException, which we simply swallow. If, on the other hand, the device is no longer lost, but hasn’t yet been reset, we’ll get aDeviceNotResetException instead. In that case, we simply call Reset (passing in the samePresentParameters we used when we initially set up the device) and set deviceLost to false. Of course, the device might get lost again between the time we call TestCooperativeLevel and the time we call Reset, so we explicitly check for this case and discard the resulting DeviceLostException.
Now, remember that I said that not only do we have to call Reset on the device, but we also need to reallocate any VertexBuffers or other resources that we might have created. That is indeed happening, but it’s happening because when I created the device, I hooked the DeviceLost and DeviceReset events that I mentioned before. Here’s the altered InitializeGraphics method I used to do that:
protected bool InitializeGraphics() { // Set up our presentation parameters as usual pres.Windowed = true; pres.SwapEffect = SwapEffect.Discard; device = new Device(0, DeviceType.Hardware, this, CreateFlags.SoftwareVertexProcessing, pres); // Hook the DeviceReset event so OnDeviceReset will get called every // time we call device.Reset() device.DeviceReset += new EventHandler(this.OnDeviceReset); // Similarly, OnDeviceLost will get called every time we call // device.Reset(). The difference is that DeviceLost gets called // earlier, giving us a chance to do the cleanup that needs to // occur before we can call Reset() successfully device.DeviceLost += new EventHandler(this.OnDeviceLost); // Do the initial setup of our graphics objects SetupDevice(); return true; }
Notice the event registration code. It’s unfortunate that the designers of Managed Direct3D decided to use a delegate of type EventHandler rather than something more specific to Direct3D, but that’s just the way it is.
The call to SetupDevice is where we create the VertexBuffer and set render states, just like we’ve been doing all along. It doesn’t contain anything we haven’t talked about before, but here it is:
protected void SetupDevice() { // Set up the device's RenderStates device.RenderState.Lighting = false; device.RenderState.CullMode = Cull.None; // And create the VertexBuffer vertices = CreateVertexBuffer(device); }
CreateVertexBuffer is the same method we’ve been using for a while now to create the two-sided tricolor triangle.
We need to hook two different events because they get called at different times. They’re both called during the invocation of Device.Reset, but the DeviceLost event is invoked early, giving us a chance to free up any allocated resources before we allocate the new ones.
Here’s what our OnDeviceLost handler looks like:
protected void OnDeviceLost(object sender, EventArgs e) { // Clean up the VertexBuffer vertices.Dispose(); }
Since the only resource we’re managing in this sample is a single VertexBuffer, we only have one thing to clean up. If we were managing more things, we’d need to Dispose them as well.
The other event – DeviceReset – is called later in the Reset process, at which point it’s okay to recreate the resources we need. Like I said, this is basically the same thing that we do during initialization of the application, so our OnDeviceReset method just calls SetupDevice, like so:
protected void OnDeviceReset(object sender, EventArgs e) { // We use the same setup code to reset as we do for initial creation SetupDevice(); }
And that’s the story. Obviously, the more complex the application is, and the more resources you’re managing, the more complex the resource management code in the Reset cycle will be. But remember that you can register more than one event handler for a given event, so code like this:
device.DeviceLost += new EventHandler(object1.OnDeviceLost); device.DeviceLost += new EventHandler(object2.OnDeviceLost); device.DeviceLost += new EventHandler(object3.OnDeviceLost);
Might help you write cleaner code.
The code I’ve shown here works fine when running in windowed mode, but I’ve had a lot of people ask me about how to make this work when running the application in fullscreen mode. We haven’t talked about fullscreen mode yet, but I’m including the code you need here to save people some trouble. Just be sure to add this line to InitializeGraphics:
device.DeviceResizing += new System.ComponentModel.CancelEventHandler(this.CancelResize);
And then add this method to the class somewhere
protected void CancelResize(object sender, System.ComponentModel.CancelEventArgs e) { e.Cancel = true; }
We’ll talk about why this code is necessary in a future tutorial.
As usual, I’ve included a working application at the end of this article. Give it a try, and maybe compare it to one of the earlier applications that doesn’t have the device recovery code in it. See what happens when you lock the screen or suspend the computer.
Next time, we’ll talk about IndexBuffers, an interesting optimization used by Meshes to save wasted memory and speed up rendering.
using System; using System.Drawing; using System.Windows.Forms; using System.Diagnostics; using Microsoft.DirectX; using Microsoft.DirectX.Direct3D; namespace Craig.Direct3D { public class Game : System.Windows.Forms.Form { static void Main() { Game app = new Game(); app.InitializeGraphics(); app.Show(); while (app.Created) { app.Render(); Application.DoEvents(); } } private Device device; private VertexBuffer vertices; // Has the device been lost and not reset? private bool deviceLost; // We'll need these to Reset successfully, so hold them here private PresentParameters pres = new PresentParameters(); protected bool InitializeGraphics() { // Set up our presentation parameters as usual pres.Windowed = true; pres.SwapEffect = SwapEffect.Discard; device = new Device(0, DeviceType.Hardware, this, CreateFlags.SoftwareVertexProcessing, pres); // Hook the DeviceReset event so OnDeviceReset will get called every // time we call device.Reset() device.DeviceReset += new EventHandler(this.OnDeviceReset); // Similarly, OnDeviceLost will get called every time we call // device.Reset(). The difference is that DeviceLost gets called // earlier, giving us a chance to do the cleanup that needs to // occur before we can call Reset() successfully device.DeviceLost += new EventHandler(this.OnDeviceLost); // Do the initial setup of our graphics objects SetupDevice(); return true; } protected void OnDeviceReset(object sender, EventArgs e) { // We use the same setup code to reset as we do for initial creation SetupDevice(); } protected void OnDeviceLost(object sender, EventArgs e) { // Clean up the VertexBuffer vertices.Dispose(); } protected void SetupDevice() { // Set up the device's RenderStates device.RenderState.Lighting = false; device.RenderState.CullMode = Cull.None; // And create the VertexBuffer vertices = CreateVertexBuffer(device); } protected VertexBuffer CreateVertexBuffer(Device device) { VertexBuffer buf = new VertexBuffer( typeof(CustomVertex.PositionColored), // What type of vertices 3, // How many device, // The device 0, // Default usage CustomVertex.PositionColored.Format, // Vertex format Pool.Default); // Default pooling CustomVertex.PositionColored[] verts = (CustomVertex.PositionColored[]) buf.Lock(0, 0); int i = 0; verts[i++] = new CustomVertex.PositionColored( 0, 1, 0, Color.Red.ToArgb()); verts[i++] = new CustomVertex.PositionColored( -0.5F, 0, 0, Color.Green.ToArgb()); verts[i++] = new CustomVertex.PositionColored( 0.5F, 0, 0, Color.Blue.ToArgb()); buf.Unlock(); return buf; } protected void SetupMatrices() { float angle = Environment.TickCount / 500.0F; device.Transform.World = Matrix.RotationY(angle); device.Transform.View = Matrix.LookAtLH(new Vector3(0, 0.5F, -3), new Vector3(0, 0.5F, 0), new Vector3(0, 1, 0)); device.Transform.Projection = Matrix.PerspectiveFovLH((float)Math.PI/4.0F, 1.0F, 1.0F, 5.0F); } protected void Render() { if (deviceLost) { // Try to get the device back AttemptRecovery(); } // If we couldn't get the device back, don't try to render if (deviceLost) { return; } // Clear the back buffer device.Clear(ClearFlags.Target, Color.Bisque, 1.0F, 0); // Ready Direct3D to begin drawing device.BeginScene(); // Set the Matrices SetupMatrices(); // We're going to draw colored vertices in 3D device.VertexFormat = CustomVertex.PositionColored.Format; // Draw the scene - 3D Rendering calls go here device.SetStreamSource(0, vertices, 0); device.DrawPrimitives(PrimitiveType.TriangleList, 0, 1); // Indicate to Direct3D that we're done drawing device.EndScene(); try { // Copy the back buffer to the display device.Present(); } catch (DeviceLostException) { // Indicate that the device has been lost deviceLost = true; // Spew a message into the output window of the debugger Debug.WriteLine("Device was lost"); } } protected void AttemptRecovery() { try { device.TestCooperativeLevel(); } catch (DeviceLostException) { } catch (DeviceNotResetException) { try { device.Reset(pres); deviceLost = false; // Spew a message into the output window of the debugger Debug.WriteLine("Device successfully reset"); } catch (DeviceLostException) { // If it's still lost or lost again, just do // nothing } } } } }