Chapter 25. Using Direct3D Swap Chains with MDI Applications

 

Mostly, when you see programmers, they aren’t doing anything. One of the attractive things about programmers is that you cannot tell whether or not they are working simply by looking at them. Very often they’re sitting there seemingly drinking coffee and gossiping, or just staring into space. What the programmer is trying to do is get a handle on all the individual and unrelated ideas that are scampering around in his head.

 
 --Charles M. Strauss

Almost every game displays itself in a single window, which is a single active device within Direct3D. Many tools, on the other hand, display multiple windows to the users so they may view multiple aspects of the game when designing content. The core purpose of Direct3D is to serve as a high-performance 3D API for real-time games, and because of this, it was designed to be most efficient rendering to a single device. Using a device for every display window in an editor or tool would be extremely inefficient and negatively affect performance.

The efficient way of rendering to multiple windows (or contexts) with a single Direct3D device is through the use of swap chains. Unfortunately, there are a scarce number of examples showing how to use them, and the SDK documentation is extremely vague. The purpose of this chapter is to fill the gap and provide you with extensive information about using swap chains within an MDI (or SDI) application.

What is a Swap Chain?

An application utilizing Direct3D to render real-time 3D graphics organizes an animated sequence into a series of frames that are stored in a collection of buffers, and renders them in the correct sequence. These buffers are grouped into swap chains that flip to the screen one after the other. A swap chain can render an upcoming frame in the background and present the frame to the screen when ready. This mechanism solves a common problem known as “tearing” and offers smoother animation.

Every Direct3D device that is created automatically instantiates a single implicit swap chain. When a surface flip is requested through the execution of Device.Present, the pointers for the front and back buffer(s) are swapped, and a new frame is presented to the viewer. If there is more than one back buffer in a swap chain, the pointers are swapped in a circular order.

Additional swap chains can be created within a given device, though a device can only contain a single full-screen swap chain. Each swap chain renders into a collection of buffers and can be presented to a different window from the main device. The back buffer for a swap chain can be accessed with SwapChain.GetBackBuffer.

Note

Before continuing, it is important to note that, by the term window, I am referring to any control. This association goes back to the unmanaged Win32 API.

A great benefit of using swap chains with a single device is the notion that resources, such as meshes and textures, are shared across all swap chains using a single location in memory.

Creating a swap chain is very easy to do, and the only prerequisite is that a valid Direct3D is already available. The first thing to do is to create a PresentParameters object and specify some rendering properties about the swap chain. Most of the properties are familiar from regular device settings, but the important ones to note are DeviceWindow, BackBufferWidth, and BackBufferHeight. All three refer to the handle, width, and height of the window (control) that the swap chain will be bound to for rendering. The variable of this control is called renderTarget and is of type System.Windows.Forms.Control.

The following code shows how to build present parameters for a swap chain.

PresentParameters presentParams = new PresentParameters();
presentParams.AutoDepthStencilFormat = DepthFormat.D16;
presentParams.Windowed = true;
presentParams.SwapEffect = SwapEffect.Discard;
presentParams.EnableAutoDepthStencil = true;
presentParams.DeviceWindow = renderTarget;
presentParams.BackBufferWidth = renderTarget.Width;
presentParams.BackBufferHeight = renderTarget.Height;

With the present parameters built, we can move on to building a swap chain object. The following code shows how to do this, where the first parameter is a reference to the Direct3D device, and the second parameter is a reference to the PresentParameters structure that we just built.

SwapChain swapChain = new SwapChain(device, presentParams);

The next important piece of code to show is the rendering logic that is executed each time a frame is rendered. This code is similar to a normal Direct3D application, except the back buffer must be set as a render target, and the swap chain presents the frame to the screen, not the device itself.

public void RenderSwapChain(SwapChain swapChain, Control renderTarget)
{
    using (Surface backBuffer = swapChain.GetBackBuffer(0,
                                                        BackBufferType.Mono))
    {
        swapChain.Device.SetRenderTarget(0, backBuffer);

        // Perform rendering here without calling Device.Present()

        swapChain.Present(renderTarget);
    }
}

Note

Rendering is performed as normal, except Present() is called on the swap chain rather than the device.

Now that the basics have been covered about how to create and render a Direct3D swap chain, it is time to expand on this topic and cover applicability towards MDI and SDI applications.

Thoughts for SDI and MDI Applicability

There are two common windowing modes for a Windows application: SDI and MDI. An SDI application (Single Document Interface) is typically used when you want to work with one data set at a time in a single window. A commonly known SDI application is Notepad.

An MDI application (Multiple Document Interface) has a primary window (parent) that contains a set of child windows within its client region. A child window is constrained to the boundaries of the parent window, and typically shares the menu bar, tool bar, and other common parts of the parent interface. MDI applications are commonly used in situations where the user wants to work on multiple data sets at the same time.

Swap chains are applicable to either windowing mode, but are more commonly used within MDI applications. An SDI application can use swap chains, but they should only be used when rendering to multiple controls when the problem cannot be solved with the use of viewports. Typically, the swap chains for an SDI application are created after the form is first opened and a Direct3D device is bound to it.

An MDI application has a few more issues to be taken into consideration when using swap chains within the child windows. Swap chains are only valid while the device is still active; the swap chains become invalid as soon as the device is lost or disposed. The device should be bound to the parent window since child windows cannot exist without it, and the swap chains should be created within each individual child window.

Multiple child windows will result in a system that must keep track of the swap chains at a much more intimate level and handle their creation, assignment, and release.

Before diving into the solution and implementation, it is important to discuss several “gotchas” and limitations that must be considered.

Common Pitfalls

An MDI application typically supports the resizing of child windows, so it is important to take this issue into consideration when using swap chains. Direct3D has a built-in mechanism to handle child window resizing, but the results may not be desirable. A stretch blit is used by default to present the frame buffer if the client area dimensions are not the same size as the frame buffer of the swap chain. This mechanism can lead to artifacts and aliasing unless the swap chain is re-created and the render target size is recalculated.

Another issue to take into consideration, which is more of a design concern, is the fact that a device automatically creates an implicit swap chain when it is created. Swap chains can be queried from a device by an indexer, where the implicit swap chain starts at 0 and the other swap chains increment by 1 thereafter. A common approach is to assign the swap chain indexer to the child window associated with it and release the swap chain when the window closes. The problem lies in assigning the implicit swap chain to a child window and trying to release it when the window closes. One solution to this problem is covered in the next section, “The Proposed Solution.”

As mentioned earlier, swap chains have the benefit of sharing data from a single device, requiring a single location in memory. While this feature can offer significant memory and performance gains, it can also lead to some headaches. Swap chains do not have their own collection of device settings, so each swap chain must be responsible for the management of settings, such as textures, view state, and render states. It is important that you remain careful and attentive when using swap chains so that you do not end up with settings that transfer over from one swap chain to another by forgetting to set new values.

There are increased render state changes that happen through the use of swap chains, so batching and minimization of changes are important so that performance is not impacted. Swap chains are still much more efficient than using multiple devices, so the performance issues go with the territory of rendering to multiple regions.

Multiple windows are hard to maintain and track, especially when swap chains are associated to them. Luckily, .NET makes MDI application development a breeze, so there is no real concern for this solution.

The Proposed Solution

In this chapter, I present a manager that handles the construction, usage, and destruction of swap chains within either an MDI or SDI application. The manager correctly handles the resizing of child windows to prevent artifacts and aliasing, and it transparently wraps a lot of the swap chain calls into a reusable and extensible framework.

Each child window within the MDI application will be responsible for handling its own rendering, but the swap chain manager must have a way to inform the child window that it should render a frame. The following interface is extremely simplistic but will provide a common mechanism that the manager can call, depending on which child it wants to render. The IRenderWindow interface will be implemented by all child windows to make the Render() method publicly accessible.

public interface IRenderWindow
{
    void Render();
}

The next section of code describes an associative container class that contains references to a swap chain, present parameters, back buffer, and render target control. This class also contains a unique identifier and makes the association of a swap chain to a child window extremely easy.

internal class SwapChainInstance
{
    private int _id;
    private SwapChain _swapChain;
    private PresentParameters _presentParameters;
    private Surface _backBuffer;
    private Control _renderTarget;

    public int Id
    {
        get { return _id; }
        set { _id = value; }
    }

    public SwapChain SwapChain
    {
        get { return _swapChain; }
        set { _swapChain = value; }
    }

    public PresentParameters PresentParameters
    {
        get { return _presentParameters; }
        set { _presentParameters = value; }
    }

    public Surface BackBuffer
    {
        get { return _backBuffer; }
        set { _backBuffer = value; }
   }
    public Control RenderTarget
    {
        get { return _renderTarget; }
        set { _renderTarget = value; }
    }

    public SwapChainInstance(int id,
                             SwapChain swapChain,
                             PresentParameters presentParameters)
    {
        this._id = id;
        this._swapChain = swapChain;
        this._presentParameters = presentParameters;
    }
}

As discussed earlier, there is an issue regarding the implicit swap chain of the device. Perhaps the best way to avoid any problems is to simply ignore the implicit swap chain. This approach is used for the solution, although an alternative approach had been tried with minor success prior to settling on this one.

The next class encompasses the bulk of the swap chain framework. The manager class is responsible for the construction, usage, and destruction of swap chains, and is also accountable for handling the association of a swap chain with a child window.

public sealed class SwapChainManager
{
    private List<SwapChainInstance> _swapChainList = new List<SwapChainInstance>();
    private SwapChainInstance _activeSwapChain;
    private int _idCounter;
    private Device _device;
    private bool _ready;
    private Mesh _teapotMesh;
    private Mesh _sphereMesh;

    public Device Device
    {
        get { return _device; }
    }

    public bool Ready
    {
       get { return _ready; }
    }

    public Mesh TeapotMesh
    {
        get { return _teapotMesh; }
    }

    public Mesh SphereMesh
    {
        get { return _sphereMesh; }
    }

The following method is a critical part of the manager. It is responsible for building present parameters and creating a swap chain object that becomes referenced by the manager with a unique identifier.

    public int CreateSwapChain(Control renderTarget)
    {
        _idCounter++;

        PresentParameters presentParams = new PresentParameters();

        presentParams.AutoDepthStencilFormat = DepthFormat.D16;
        presentParams.Windowed = true;
        presentParams.SwapEffect = SwapEffect.Discard;
        presentParams.EnableAutoDepthStencil = true;
        presentParams.DeviceWindow = renderTarget;
        presentParams.BackBufferWidth = renderTarget.Width;
        presentParams.BackBufferHeight = renderTarget.Height;

        if (renderTarget != null && _device != null)
        {
              SwapChain swapChain = new SwapChain(_device, presentParams);

              SwapChainInstance instance = new SwapChainInstance(_idCounter,
                                                                  swapChain,
                                                                  presentParams);
              instance.RenderTarget = renderTarget;

              _swapChainList.Add(instance);
        }
       return _idCounter;
    }

This method is fairly simple. It accepts a unique swap chain identifier, finds the associated object, and releases the swap chain object from the manager.

    public void DestroySwapChain(int id)
    {
        SwapChainInstance instance = FindSwapChainInstance(id);

        if (instance != null)
        {
            DestroySwapChain(instance.SwapChain);
            instance.SwapChain = null;
            _swapChainList.Remove(instance);
        }
    }

This method works very similarly to DestroySwapChain(), except instead of destroying the swap chain, it simply resets it. A specific use for this method is after a child window has been resized and the swap chain(s) must be reset to reflect the new render target region(s).

    public void ResetSwapChain(int id)
    {
        SwapChainInstance instance = FindSwapChainInstance(id);
        ResetSwapChain(instance);
    }

This method accepts a unique identifier, locates the referenced swap chain object in the manager, and returns a reference to it.

    private SwapChainInstance FindSwapChainInstance(int id)
    {
        foreach (SwapChainInstance instance in _swapChainList)
        {
            if (instance.Id.Equals(id))
                return instance;
        }

        return null;
    }

This method is used to re-create a swap chain after a device reset has occurred. First, the old swap chain is destroyed, and then a new swap chain with the new render target size is created.

     private void ResetSwapChain(SwapChainInstance instance)
     {
         if (instance != null)
         {
             DestroySwapChain(instance.SwapChain);

             instance.PresentParameters.BackBufferWidth =
                                          instance.RenderTarget.Width;
             instance.PresentParameters.BackBufferHeight =
                                          instance.RenderTarget.Height;
             instance.SwapChain = new SwapChain(_device,
                                                instance.PresentParameters);
        }
    }

This method is simply used to release the memory associated with a Direct3D swap chain.

    private void DestroySwapChain(SwapChain swapChain)
    {
        if (swapChain != null)
            swapChain.Dispose();
    }

This method is very important because it begins the rendering process for a specific swap chain that is referenced by a unique identifier. Notice the ready flag that breaks out of rendering if its value is set to false. This flag is used to prevent errors from occurring if the device is invalid.

    public void BeginSwapChainRender(int id)
    {
        if (!_ready)
            return;

        SwapChainInstance instance = FindSwapChainInstance(id);

        if (instance != null && instance.SwapChain != null)
        {
            _activeSwapChain = instance;

           instance.BackBuffer = instance.SwapChain.GetBackBuffer(0,
                                                            BackBufferType.Mono);

            if (instance.BackBuffer != null)
            {
                instance.SwapChain.Device.SetRenderTarget(0,
                                                          instance.BackBuffer);
            }
        }
    }

This method completes the rendering process for a specific swap chain that is referenced by a unique identifier.

    public void EndSwapChainRender(int id)
    {
        if (!_ready)
        return;

        SwapChainInstance instance = null;

        if (_activeSwapChain != null)
        {
            if (_activeSwapChain.Id == id)
                instance = _activeSwapChain;
            else
                _activeSwapChain = instance = FindSwapChainInstance(id);
        }

        if (instance != null)
        {
            if (instance.BackBuffer != null && instance.SwapChain != null)
            {
                using (instance.BackBuffer)
                {
                    instance.SwapChain.Present(instance.RenderTarget);
                }
                instance.BackBuffer = null;
            }
        }

        _activeSwapChain = null;
    }

The swap chains are obviously in need of a valid device to render with, and that is the responsibility of this method. A parent window is specified (either the MDI parent form or the SDI form), and the device is created and bound to this window.

    public void CreateDevice(Form containingWindow)
    {
        if (_device != null)
        {
            _device.Dispose();
            _device = null;
        }

        PresentParameters presentParams = new PresentParameters();

        presentParams.AutoDepthStencilFormat = DepthFormat.D16;
        presentParams.Windowed = true;
        presentParams.SwapEffect = SwapEffect.Discard;
        presentParams.PresentationInterval = PresentInterval.Immediate;
        presentParams.EnableAutoDepthStencil = true;

        _device = new Device(0,
                             DeviceType.Hardware,
                             containingWindow,
                             CreateFlags.SoftwareVertexProcessing,
                             presentParams);

        _device.DeviceLost + = new EventHandler(DeviceLost);
        _device.DeviceReset + = new EventHandler(DeviceReset);

        DeviceReset(null, null);

        _ready = true;
    }

The following method handles the device lost event. The only job of this method is to flip the ready flag to false so that errors do not occur when the application attempts to render with an invalid device.

    private void DeviceLost(object sender, EventArgs e)
    {
        _ready = false;
    }

The last method in our manager handles the device reset event. The purpose of this method is to re-create the swap chains with the recalculated render target size, and then re-create the resources that are shared across all swap chains. The ready flag is also flipped to true so that the application can begin rendering the scenes once again.

    private void DeviceReset(object sender, EventArgs e)
    {
        foreach (SwapChainInstance instance in _swapChainList)
            ResetSwapChain(instance);

        _teapotMesh = Mesh.Teapot(_device);
        _sphereMesh = Mesh.Sphere(_device, 1.0f, 30, 30);

        _device.Lights[0].Type = LightType.Directional;
        _device.Lights[0].Diffuse = System.Drawing.Color.White;
        _device.Lights[0].Enabled = true;

        _device.RenderState.Lighting = true;
        _device.RenderState.Ambient = Color.White;
        _device.RenderState.CullMode = Cull.CounterClockwise;
        _device.RenderState.ShadeMode = ShadeMode.Gouraud;

        Material material = new Material();
        material.Ambient = Color.ForestGreen;
        material.Diffuse = Color.Olive;

        _device.Material = material;

        _ready = true;
    }
}

The implementation of the swap chain manager is complete, so the discussion will now focus on using the manager. The following code insertions are methods and properties extracted directly from the example on the Companion Web site that should offer insight into using the solution if the interfaces alone are not enough. The code snippets are from the single context window that uses the entire window as a display context.

The first property is a unique identifier that references a SwapChainInstance object within the swap chain manager. It is initialized in the DeviceReset() method that is described later in this chapter.

private int _swapChain;

The next property is a reference to the swap chain manager instance that will typically be created in the parent form if the application uses an MDI windowing mode. In an SDI application, the manager can be instantiated with a device bound to the SDI window.

This example has the manager reference passed in through the child form constructor from the parent form.

private SwapChainManager _manager;

The next method is executed when the rendering device is lost or reset and the swap chain(s) must be re-created. You will notice that the methods requiring a control are passed a this keyword that references the entire Form. It is perfectly acceptable to pass a reference to a Control residing on the form if you want to target the rendering within a specific Control like a panel. The CreateSwapChain() method creates a swap chain for the entire window and returns a unique identifier back to the user. This unique identifier can be later used to return the swap chain object from the manager.

private void DeviceReset()
{
    _swapChain = _manager.CreateSwapChain(this);
}

Typically, a device is lost before it is reset, and the purpose of this method is to destroy an existing swap chain before the reset method is executed and a new swap chain is created.

private void DeviceLost()
{
    _manager.DestroySwapChain(_swapChain);
}

The following event is fired when the window is first loaded, resulting in the creation of a swap chain.

private void ContextWindow_Load(object sender, System.EventArgs e)
{
    DeviceReset();
}

One of the common pitfalls mentioned in this chapter are the aliasing and artifacts that result from a client region size not matching the size of the swap chain frame buffer. This normally occurs after the swap chain has been created and the associated window resizes. To account for this, there is a ResetSwapChain() method in the manager that will be executed when the window is resized using the event below.

private void ContextWindow_Resize(object sender, System.EventArgs e)
{
    _manager.ResetSwapChain(_swapChain);
}

Finally, we hit the interesting snippet, the Render() method. It is here that we first make sure the manager exists and is ready to render; if not, we skip the current frame. After that, it is important to set the render and view states for the swap chain in case they were altered by another swap chain in existence on the same device. The example does not employ many render state settings, but it is important to recalculate the projection, world, and view matrices so that the scene renders correctly.

Rendering is then initiated with a call to BeginSwapChainRender(), passing in the unique identifier for the swap chain created when the child window was first loaded. Rendering then proceeds as normal, except at the very end there is a call to EndSwapChainRender() instead of calling Present() on the device.

public void Render()
{
    if (_manager == null)
        return;

    if (!_manager.Ready)]
        return;

    CalculateProjection();

    _manager.Device.Transform.World = Matrix.Identity;
    Vector3 position = new Vector3(0.0f, 0.0f, -5.0f);
    Vector3 target = new Vector3(0.0f, 0.0f, 0.0f);
    Vector3 upVector = new Vector3(0.0f, 1.0f, 0.0f);
    _manager.Device.Transform.View = Matrix.LookAtLH(position,
                                                     target,
                                                     upVector);
    _manager.BeginSwapChainRender(_swapChain);
    _manager.Device.Clear(ClearFlags.Target | ClearFlags.ZBuffer,
                          unchecked((int)-8454144),
                          1.0F,
                          0);
    _manager.Device.BeginScene();

    _manager.SphereMesh.DrawSubset(0);

    _manager.Device.EndScene();
    _manager.EndSwapChainRender(_swapChain);
}

The following method was included in this topic for completeness, though it does not directly deal with the swap chain manager. This method recalculates the projection matrix after the render target client region is resized.

private void CalculateProjection()
{
    if (this.Height == 0)
        return;

    float aspect = (float)this.Width / this.Height;

    _manager.Device.Transform.Projection = Matrix.PerspectiveFovLH((float)Math.PI / 4,
                                                                    aspect,
                                                                    1.0f,
                                                                    60.0f);
}

The example provided on the Companion Web site is an MDI application that uses the swap chain manager described in this topic to render into multiple child windows and multiple controls within a child window. You can see two different types of child windows using the swap chain manager in Figure 25.1, and one of the child windows is maximized in Figure 25.2.

Variety of child windows using the swap chain manager.

Figure 25.1. Variety of child windows using the swap chain manager.

Maximized child window using the swap chain manager.

Figure 25.2. Maximized child window using the swap chain manager.

Both the sphere and teapot meshes are loaded into the single Direct3D device and are shared across all child windows in the example.

Conclusion

This chapter covered what a swap chain is, how to create them, applicability with MDI applications, and how to effectively create and manage swap chains within an MDI application.

Overall, the solution presented in this chapter is very flexible and extensible, although there are a couple of areas that could be refactored to improve performance or promote more reusability. For example, the device and swap chain creation could be routed through a virtual function that allows the user to specify settings and parameters on a per override basis. On another note, earlier it was mentioned that swap chains increase the number of render state changes, so it would be advantageous to implement a batching mechanism to reduce the total number of changes per frame.

The Companion Web site contains the full source code for the manager presented in this chapter, including a demo application that uses it within an MDI application in a variety of ways.

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset
3.144.9.124