Formeln

Sunday, April 28, 2013

Resizing the RenderControl and Buffers

Problem

Up to now we did not the address resizing the buffer of the render target and depth/stencil buffer when resizing the control we are rendering on. This means the buffers stay the same when resizing the control and after a resize the rendered image looks pixelated and has the wrong aspect ratio which leads to a distorted image:





When resizing, we have to perform several steps:

  1. Dispose resources bound to the render target
  2. Dipose the render target itself
  3. Dispose the depth buffer
  4. Resize the buffers of the swap chain
  5. Create a new render target buffer from the swap chain
  6. Create a new render target
  7. Create a new depth/stencil buffer
  8. Create a new viewport and assign it to the rasterizer
  9. Set the render target buffer and the depth/stencil buffer in the output merger
  10. Take care, that the new aspect ratio is used, when creating the perspective matrix of the camera
As you can see, these are all critical resources when rendering. So we also have to take care that resizing is performed, while none of these resources are used during the rendering process. For example: disposing the render target while writing to it in the render thread is not a good idea and leads to an exception and might crash your application.


In order to prevent accessing these resources while rendering, I check if a resize is needed in every render cycle before the actual rendering happens.

Concept

There are mainly three objects involved for a resize operation:

  1. The RenderControl: the control I am rendering to
  2. DeviceManager: responsible for managing the device and its buffers
  3. RenderManager: responsible for rendering the scene and where the render thread is



If a resize of the RenderControl occurs, I set the resize variable in the RenderManager true. This resize variable is checked in every frame. If it is true, I call the Resize method of the DeviceManager and the buffers are resized there. The DeviceManager holds a reference to the RenderControl, so we know about the new width and height of the RenderControl.

The resize variable in the RenderManager is set back to false and we can continue rendering with  new buffers.

RenderControl

All we have to do in the RenderControl is to handle the resize event and to set the resize variable in the RenderManager from here to true:

private void RenderControl_Resize(object sender, EventArgs e)
{
  RenderManager.Instance.resize = true;
}

RenderManager

If the resize event in the RenderControl is triggered and the resize variable is set to true, we call the Resize method of the DeviceManager from here. This prevents disposing and creating critical resources while rendering, which would lead to an exception. The actual rendering is performed in the line Scene.Instance.render(), where I iterate through all objects of the scene and render them.

public bool resize = false;

public void RenderScene()
{
  while (true)
  {
    if (resize)
    {
      DeviceManager.Instance.Resize();
      resize = false;
    }

    fc.Count();

    DeviceManager dm = DeviceManager.Instance;
    dm.context.ClearDepthStencilView(dm.depthStencil,
      DepthStencilClearFlags.Depth | DepthStencilClearFlags.Stencil,
      1.0f,
      0);

    dm.context.ClearRenderTargetView(dm.renderTarget,
      new Color4(0.75f, 0.75f, 0.75f));

    Scene.Instance.render();

    dm.swapChain.Present(syncInterval, PresentFlags.None);
  }
}

DeviceManager

The Resize method of the DeviceManager executes the disposing of the buffers and resources and recreates them.

Texture2D resource;

internal void Resize()
{
  try
  {
    if (device == null)
      return;

    float aspectRatio = (float)form.Width / (float)form.Height;
    CameraManager.Instance.currentCamera.setPerspective((float)Math.PI / 4, aspectRatio, 0.1f, 1000.0f);

    // Dispose before resizing.
    if (renderTarget != null)
      renderTarget.Dispose();

    if (resource != null)
      resource.Dispose();

    if (depthStencil != null)
      depthStencil.Dispose();

    swapChain.ResizeBuffers(1,
      form.ClientSize.Width,
      form.ClientSize.Height,
      Format.R8G8B8A8_UNorm,
      SwapChainFlags.AllowModeSwitch);

    resource = Texture2D.FromSwapChain(swapChain, 0);
    renderTarget = new RenderTargetView(device, resource);

    CreateDepthStencilBuffer(form);

    viewport = new Viewport(0.0f, 0.0f, form.ClientSize.Width, form.ClientSize.Height);
    context.Rasterizer.SetViewports(viewport);
    context.OutputMerger.SetTargets(depthStencil, renderTarget);
  }
  catch (Exception ex)
  {
    Console.WriteLine(ex.ToString());
  }
}

I call the method CreateDepthStencilBuffer in the code above and paste it here, for the sake of completeness. Here the depth/stencil buffer is created again and the DepthStencilState is set again: 

public void CreateDepthStencilBuffer(System.Windows.Forms.Control form)
{
  Texture2D DSTexture = new Texture2D(
      device,
      new Texture2DDescription()
      {
        ArraySize = 1,
        MipLevels = 1,
        Format = Format.D32_Float,
        Width = form.ClientSize.Width,
        Height = form.ClientSize.Height,
        BindFlags = BindFlags.DepthStencil,
        CpuAccessFlags = CpuAccessFlags.None,
        SampleDescription = new SampleDescription(1, 0),
        Usage = ResourceUsage.Default
      }
  );

  depthStencil = new DepthStencilView(
     device,
     DSTexture,
     new DepthStencilViewDescription()
     {
       ArraySize = 0,
       FirstArraySlice = 0,
       MipSlice = 0,
       Format = Format.D32_Float,
       Dimension = DepthStencilViewDimension.Texture2D
     }
 );

  context.OutputMerger.DepthStencilState = DepthStencilState.FromDescription(
      device,
      new DepthStencilStateDescription()
      {
        DepthComparison = Comparison.Always,
        DepthWriteMask = DepthWriteMask.All,
        IsDepthEnabled = true,
        IsStencilEnabled = false
      }
  );

  context.OutputMerger.SetTargets(depthStencil, renderTarget);

  DepthStencilStateDescription dssd = new DepthStencilStateDescription
  {
    IsDepthEnabled = true,
    IsStencilEnabled = false,
    DepthWriteMask = DepthWriteMask.All,
    DepthComparison = Comparison.Less,
  };

  DepthStencilState depthStencilStateNormal;
  depthStencilStateNormal = DepthStencilState.FromDescription(DeviceManager.Instance.device, dssd);
  DeviceManager.Instance.context.OutputMerger.DepthStencilState = depthStencilStateNormal;
}


Result


The following two pictures show two windows with resized buffers, corresponding to the controls width and height.





You can download the source code for this tutorial here.

6 comments:

  1. Can I implement these codes to windows 8 development?

    ReplyDelete
    Replies
    1. Why not? SlimDX works under windows 8, so this should work ;)

      Delete
    2. No, I meant windows 8 metro environment. There is no documentation at all for slimdx and windows 8 metro.

      I love your blogs on these. I am implementing a 3-D modeling program for chemistry in the desktop environment. I will try to port it to metro soon.

      Delete
    3. Ah, now I understand. I just glanced over the development of metro apps and there is App Wizard for Metro Apps with DirectX and C++ in Visual Studio 2013. At the current state it seems to me, that it does not work with SlimDX.
      SharpDX http://www.sharpdx.org/download/ seems to have support for Windows Metro Apps.

      Delete
  2. Thank you for your reply.

    Unfortunately, sharpdx has almost no documentation. Even their samples are unusable because they're not really samples. You have to know how to incorporate them into your project. And unfortunately, there is zero documentation on how to do this. It is written primarily for professional developers, which I am not.

    If you have the time, is it possible for you to write a tutorial for sharpdx that doesn't assume the reader already knows how to use it?

    ReplyDelete
  3. The aspect ratio should use the client size the same as the buffer and viewport and should probably be applied to all cameras, not just the current one. Otherwise it takes the border into account and gives an incorrect ratio.

    ReplyDelete